diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7a3d9f0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +.js \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..d161a9d --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,10 @@ +module.exports = { + tabWidth: 2, + bracketSpacing: true, + useTabs: false, + bracketSameLine: false, + singleQuote: true, + trailingComma: 'all', + arrowParens: 'avoid', + quoteProps: 'preserve', +}; diff --git a/docs/plugin-template.ts b/docs/plugin-template.ts index 2f63dc1..cfdc283 100644 --- a/docs/plugin-template.ts +++ b/docs/plugin-template.ts @@ -1,90 +1,90 @@ -import { fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; -import { load as loadCheerio } from "cheerio"; -import { defaultCover } from "@libs/defaultCover"; -import { NovelStatus } from "@libs/novelStatus"; -// import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; -// import { parseMadaraDate } from "@libs/parseMadaraDate"; - -class TemplatePlugin implements Plugin.PluginBase { - id = ""; - name = ""; - icon = ""; - site = "https://example.com"; - version = "1.0.0"; - filters: Filters | undefined = undefined; - - async popularNovels( - pageNo: number, - { - showLatestNovels, - filters, - }: Plugin.PopularNovelsOptions - ): Promise { - const novels: Plugin.NovelItem[] = []; - - /** Add your fetching code here */ - novels.push({ - name: "Novel1", - path: "/novels/1", - cover: defaultCover, - }); - return novels; - } - async parseNovel(novelPath: string): Promise { - const novel: Plugin.SourceNovel = { - path: novelPath, - name: 'Untitled' - }; - - // TODO: get here data from the site and - // un-comment and fill-in the relevant fields - - // novel.name = ""; - // novel.artist = ""; - // novel.author = ""; - novel.cover = defaultCover; - // novel.genres = ""; - // novel.status = NovelStatus.Completed; - // novel.summary = ""; - - let chapters: Plugin.ChapterItem[] = []; - - // TODO: here parse the chapter list - - // TODO: add each chapter to the list using - const chapter: Plugin.ChapterItem = { - name: "", - path: "", - releaseTime: "", - chapterNumber: 0, - }; - chapters.push(chapter); - - novel.chapters = chapters; - return novel; - } - async parseChapter(chapterPath: string): Promise { - // parse chapter text here - const chapterText = ""; - return chapterText; - } - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - let novels: Plugin.NovelItem[] = []; - - // get novels using the search term - - return novels; - } - async fetchImage(url: string): Promise { - // if your plugin has images and they won't load - // this is the function to fiddle with - return fetchFile(url); - } -} - -export default new TemplatePlugin(); +import { fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +// import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; +// import { parseMadaraDate } from "@libs/parseMadaraDate"; + +class TemplatePlugin implements Plugin.PluginBase { + id = ''; + name = ''; + icon = ''; + site = 'https://example.com'; + version = '1.0.0'; + filters: Filters | undefined = undefined; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + const novels: Plugin.NovelItem[] = []; + + /** Add your fetching code here */ + novels.push({ + name: 'Novel1', + path: '/novels/1', + cover: defaultCover, + }); + return novels; + } + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Untitled', + }; + + // TODO: get here data from the site and + // un-comment and fill-in the relevant fields + + // novel.name = ""; + // novel.artist = ""; + // novel.author = ""; + novel.cover = defaultCover; + // novel.genres = ""; + // novel.status = NovelStatus.Completed; + // novel.summary = ""; + + let chapters: Plugin.ChapterItem[] = []; + + // TODO: here parse the chapter list + + // TODO: add each chapter to the list using + const chapter: Plugin.ChapterItem = { + name: '', + path: '', + releaseTime: '', + chapterNumber: 0, + }; + chapters.push(chapter); + + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise { + // parse chapter text here + const chapterText = ''; + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + let novels: Plugin.NovelItem[] = []; + + // get novels using the search term + + return novels; + } + async fetchImage(url: string): Promise { + // if your plugin has images and they won't load + // this is the function to fiddle with + return fetchFile(url); + } +} + +export default new TemplatePlugin(); diff --git a/index.ts b/index.ts index 6133cef..782cb38 100644 --- a/index.ts +++ b/index.ts @@ -1,181 +1,175 @@ -require("module-alias/register"); +require('module-alias/register'); -import path from "path"; -import dayjs from "dayjs"; -import express from "express"; -import bodyParser from "body-parser"; -import * as pluginApi from "./test_web/api/plugins"; -import localizedFormat from "dayjs/plugin/localizedFormat"; +import path from 'path'; +import dayjs from 'dayjs'; +import express from 'express'; +import bodyParser from 'body-parser'; +import * as pluginApi from './test_web/api/plugins'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; const app = express(); const port = Number(process.argv[2]); if (!port) { - process.exit(0); + process.exit(0); } -const host = "localhost"; +const host = 'localhost'; -const dirname = path.join(__dirname, ".."); +const dirname = path.join(__dirname, '..'); export const getHeaders = () => { - return pluginApi.fetchHeaders; + return pluginApi.fetchHeaders; }; app.use((req, res, next) => { - const xHeaders = req.headers["x-custom-headers"]; - if (xHeaders && typeof xHeaders === "string") { - for (const [headerName, headerValue] of Object.entries( - pluginApi.fetchHeaders - )) { - if ( - typeof headerName === "string" && - typeof headerValue === "string" - ) - delete pluginApi.fetchHeaders[headerName]; - } - const headers = JSON.parse(xHeaders); - for (const [headerName, headerValue] of Object.entries(headers)) { - if ( - typeof headerName === "string" && - typeof headerValue === "string" - ) - pluginApi.fetchHeaders[headerName] = headerValue; - } + const xHeaders = req.headers['x-custom-headers']; + if (xHeaders && typeof xHeaders === 'string') { + for (const [headerName, headerValue] of Object.entries( + pluginApi.fetchHeaders, + )) { + if (typeof headerName === 'string' && typeof headerValue === 'string') + delete pluginApi.fetchHeaders[headerName]; } - if (pluginApi.fetchHeaders.userAgent) - console.log("Custom headers: ", pluginApi.fetchHeaders); - next(); + const headers = JSON.parse(xHeaders); + for (const [headerName, headerValue] of Object.entries(headers)) { + if (typeof headerName === 'string' && typeof headerValue === 'string') + pluginApi.fetchHeaders[headerName] = headerValue; + } + } + if (pluginApi.fetchHeaders.userAgent) + console.log('Custom headers: ', pluginApi.fetchHeaders); + next(); }); app.use(bodyParser.json()); -app.use("/static", express.static(path.join(dirname, "test_web", "static"))); -app.use("/icons", express.static(path.join(dirname, "icons"))); -app.get("/all_plugins", (req, res) => { - const allPlugins = pluginApi.all_plugins(); - res.json(allPlugins); +app.use('/static', express.static(path.join(dirname, 'test_web', 'static'))); +app.use('/icons', express.static(path.join(dirname, 'icons'))); +app.get('/all_plugins', (req, res) => { + const allPlugins = pluginApi.all_plugins(); + res.json(allPlugins); }); -app.post("/filters", async (req, res) => { - const filters = await pluginApi.getFilter(req.body["pluginRequirePath"]); - res.json(filters || []); +app.post('/filters', async (req, res) => { + const filters = await pluginApi.getFilter(req.body['pluginRequirePath']); + res.json(filters || []); }); -app.post("/popularNovels/", async (req, res) => { - const requirePath = req.body["pluginRequirePath"]; - const showLatestNovels = req.body["showLatestNovels"] || false; - const defaultFilters = await pluginApi.getFilter(requirePath); - const filters = showLatestNovels - ? defaultFilters - : req.body["filters"] || defaultFilters; - const page = parseInt(req.body["page"]); - try { - const novels = await pluginApi.popularNovels(requirePath, page || 1, { - showLatestNovels, - filters, - }); - res.json(novels); - } catch (err: unknown) { - res.json({ error: String(err) }); - } +app.post('/popularNovels/', async (req, res) => { + const requirePath = req.body['pluginRequirePath']; + const showLatestNovels = req.body['showLatestNovels'] || false; + const defaultFilters = await pluginApi.getFilter(requirePath); + const filters = showLatestNovels + ? defaultFilters + : req.body['filters'] || defaultFilters; + const page = parseInt(req.body['page']); + try { + const novels = await pluginApi.popularNovels(requirePath, page || 1, { + showLatestNovels, + filters, + }); + res.json(novels); + } catch (err: unknown) { + res.json({ error: String(err) }); + } }); -app.post("/searchNovels/", async (req, res) => { - const page = parseInt(req.body["page"]) || 1; - try { - const novels = await pluginApi.searchNovels( - req.body["pluginRequirePath"], - page, - req.body["searchTerm"] - ); - res.json(novels); - } catch (err: unknown) { - res.json({ error: String(err) }); - } +app.post('/searchNovels/', async (req, res) => { + const page = parseInt(req.body['page']) || 1; + try { + const novels = await pluginApi.searchNovels( + req.body['pluginRequirePath'], + page, + req.body['searchTerm'], + ); + res.json(novels); + } catch (err: unknown) { + res.json({ error: String(err) }); + } }); -app.post("/parseNovel/", async (req, res) => { - try { - const sourceNovel = await pluginApi.parseNovel( - req.body["pluginRequirePath"], - req.body["novelPath"] - ); - res.json(sourceNovel); - } catch (err: unknown) { - res.json({ error: String(err) }); - } +app.post('/parseNovel/', async (req, res) => { + try { + const sourceNovel = await pluginApi.parseNovel( + req.body['pluginRequirePath'], + req.body['novelPath'], + ); + res.json(sourceNovel); + } catch (err: unknown) { + res.json({ error: String(err) }); + } }); -app.post("/hasParsePage", async (req, res) => { - try { - const parsePage = await pluginApi.hasParsePage( - req.body["pluginRequirePath"] - ) - res.json({ hasParsePage: parsePage !== undefined }); - } catch (err: unknown) { - res.json({ hasParsePage: false }); - } +app.post('/hasParsePage', async (req, res) => { + try { + const parsePage = await pluginApi.hasParsePage( + req.body['pluginRequirePath'], + ); + res.json({ hasParsePage: parsePage !== undefined }); + } catch (err: unknown) { + res.json({ hasParsePage: false }); + } }); -app.post("/parsePage/", async (req, res) => { - try { - const sourcePage = await pluginApi.parsePage( - req.body["pluginRequirePath"], - req.body["novelPath"], - req.body["page"] - ); - res.json(sourcePage); - } catch (err: unknown) { - res.json({ error: String(err) }); - } +app.post('/parsePage/', async (req, res) => { + try { + const sourcePage = await pluginApi.parsePage( + req.body['pluginRequirePath'], + req.body['novelPath'], + req.body['page'], + ); + res.json(sourcePage); + } catch (err: unknown) { + res.json({ error: String(err) }); + } }); -app.post("/parseChapter/", async (req, res) => { - try { - const chapterText = await pluginApi.parseChapter( - req.body["pluginRequirePath"], - req.body["chapterPath"] - ); - res.send(chapterText); - } catch (err: unknown) { - res.json({ error: String(err) }); - } +app.post('/parseChapter/', async (req, res) => { + try { + const chapterText = await pluginApi.parseChapter( + req.body['pluginRequirePath'], + req.body['chapterPath'], + ); + res.send(chapterText); + } catch (err: unknown) { + res.json({ error: String(err) }); + } }); -app.post("/fetchImage/", async (req, res) => { - try { - const base64 = await pluginApi.fetchImage( - req.body["pluginRequirePath"], - req.body["url"] - ); - res.send(base64); - } catch (err: unknown) { - res.json({ error: String(err) }); - } +app.post('/fetchImage/', async (req, res) => { + try { + const base64 = await pluginApi.fetchImage( + req.body['pluginRequirePath'], + req.body['url'], + ); + res.send(base64); + } catch (err: unknown) { + res.json({ error: String(err) }); + } }); -app.post("/resolveUrl/", async (req, res) => { - try { - const fullURL = await pluginApi.resolveUrl( - req.body["pluginRequirePath"], - req.body["path"], - req.body["isNovel"], - ); - res.send(fullURL); - } catch (err: unknown) { - res.json({ error: String(err) }); - } +app.post('/resolveUrl/', async (req, res) => { + try { + const fullURL = await pluginApi.resolveUrl( + req.body['pluginRequirePath'], + req.body['path'], + req.body['isNovel'], + ); + res.send(fullURL); + } catch (err: unknown) { + res.json({ error: String(err) }); + } }); -app.get("/", (req, res) => { - res.sendFile(path.join(dirname, "test_web", "index.html")); +app.get('/', (req, res) => { + res.sendFile(path.join(dirname, 'test_web', 'index.html')); }); app.listen(port, host, () => { - console.log(`Testing plugins web listening on http://localhost:${port}`); + console.log(`Testing plugins web listening on http://localhost:${port}`); }); //Dayjs localization -const language = Intl?.DateTimeFormat()?.resolvedOptions()?.locale || "en"; +const language = Intl?.DateTimeFormat()?.resolvedOptions()?.locale || 'en'; -require("dayjs/locale/ar"); -require("dayjs/locale/de"); -require("dayjs/locale/es"); -require("dayjs/locale/it"); -require("dayjs/locale/pt"); -require("dayjs/locale/ru"); -require("dayjs/locale/tr"); -require("dayjs/locale/uk"); -require("dayjs/locale/zh"); +require('dayjs/locale/ar'); +require('dayjs/locale/de'); +require('dayjs/locale/es'); +require('dayjs/locale/it'); +require('dayjs/locale/pt'); +require('dayjs/locale/ru'); +require('dayjs/locale/tr'); +require('dayjs/locale/uk'); +require('dayjs/locale/zh'); dayjs.locale(language); dayjs.extend(localizedFormat); diff --git a/libs/defaultCover.ts b/libs/defaultCover.ts index 2fe0459..bbc5754 100644 --- a/libs/defaultCover.ts +++ b/libs/defaultCover.ts @@ -1,2 +1,2 @@ export const defaultCover = - "https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true"; + 'https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true'; diff --git a/libs/fetch.ts b/libs/fetch.ts index b92bbdf..8e4675a 100644 --- a/libs/fetch.ts +++ b/libs/fetch.ts @@ -1,53 +1,53 @@ import { parse as parseProto } from 'protobufjs'; type FetchInit = { - headers?: Record | Headers; - method?: string; - body?: FormData | string; - [x: string]: - | string - | Record - | undefined - | FormData - | Headers; + headers?: Record | Headers; + method?: string; + body?: FormData | string; + [x: string]: + | string + | Record + | undefined + | FormData + | Headers; }; const makeInit = async (init?: FetchInit) => { - let defaultHeaders: Record = { - 'Connection': 'keep-alive', - 'Accept': '*/*', - 'Accept-Language': '*', - 'Sec-Fetch-Mode': 'cors', - 'Accept-Encoding': 'gzip, deflate', - 'Cache-Control': 'max-age=0', + let defaultHeaders: Record = { + 'Connection': 'keep-alive', + 'Accept': '*/*', + 'Accept-Language': '*', + 'Sec-Fetch-Mode': 'cors', + 'Accept-Encoding': 'gzip, deflate', + 'Cache-Control': 'max-age=0', + }; + try { + const { getHeaders } = await import('../index.js'); + defaultHeaders = { + ...defaultHeaders, + ...getHeaders(), }; - try { - const { getHeaders } = await import("../index.js"); - defaultHeaders = { - ...defaultHeaders, - ...getHeaders() - }; - } catch { - // nothing to do - } - if (init?.headers) { - if (init.headers instanceof Headers) { - for (const [name, value] of Object.entries(defaultHeaders)) { - if (!init.headers.get(name)) init.headers.set(name, value); - } - } else { - init.headers = { - ...defaultHeaders, - ...init.headers, - }; - } + } catch { + // nothing to do + } + if (init?.headers) { + if (init.headers instanceof Headers) { + for (const [name, value] of Object.entries(defaultHeaders)) { + if (!init.headers.get(name)) init.headers.set(name, value); + } } else { - init = { - ...init, - headers: defaultHeaders, - }; + init.headers = { + ...defaultHeaders, + ...init.headers, + }; } - return init; + } else { + init = { + ...init, + headers: defaultHeaders, + }; + } + return init; }; /** @@ -57,10 +57,10 @@ const makeInit = async (init?: FetchInit) => { * @returns response as normal fetch */ export async function fetchApi(url: string, init?: FetchInit) { - init = await makeInit(init); - // console.log(init.headers); - // console.log(url, init); - return await fetch(url, init as RequestInit); + init = await makeInit(init); + // console.log(init.headers); + // console.log(url, init); + return await fetch(url, init as RequestInit); } /** @@ -71,16 +71,16 @@ export async function fetchApi(url: string, init?: FetchInit) { * @example fetchFile('https://avatars.githubusercontent.com/u/81222734?s=48&v=4'); */ export const fetchFile = async function (url: string, init?: FetchInit) { - init = await makeInit(init); - console.log(url, init); - try { - const res = await fetch(url, init as RequestInit); - if (!res.ok) return ""; - const arrayBuffer = await res.arrayBuffer(); - return Buffer.from(arrayBuffer).toString("base64"); - } catch (e) { - return ""; - } + init = await makeInit(init); + console.log(url, init); + try { + const res = await fetch(url, init as RequestInit); + if (!res.ok) return ''; + const arrayBuffer = await res.arrayBuffer(); + return Buffer.from(arrayBuffer).toString('base64'); + } catch (e) { + return ''; + } }; /** @@ -92,67 +92,74 @@ export const fetchFile = async function (url: string, init?: FetchInit) { * @example fetchText('https://github.com/LNReader/lnreader', {}, 'gbk'); */ export const fetchText = async function ( - url: string, - init?: FetchInit, - encoding?: string + url: string, + init?: FetchInit, + encoding?: string, ): Promise { - init = await makeInit(init); - console.log(url, init); - try { - const res = await fetch(url, init as RequestInit); - if (!res.ok) return ""; - const arrayBuffer = await res.arrayBuffer(); - const decoder = new TextDecoder(encoding); - return decoder.decode(arrayBuffer); - } catch (e) { - return ""; - } + init = await makeInit(init); + console.log(url, init); + try { + const res = await fetch(url, init as RequestInit); + if (!res.ok) return ''; + const arrayBuffer = await res.arrayBuffer(); + const decoder = new TextDecoder(encoding); + return decoder.decode(arrayBuffer); + } catch (e) { + return ''; + } }; interface ProtoRequestInit { - // merged .proto file - proto: string; - requestType: string; - requestData?: any; - responseType: string; + // merged .proto file + proto: string; + requestType: string; + requestData?: any; + responseType: string; } const BYTE_MARK = BigInt((1 << 8) - 1); -export const fetchProto = async function ( - protoInit: ProtoRequestInit, - url: string, - init?: FetchInit, +export const fetchProto = async function ( + protoInit: ProtoRequestInit, + url: string, + init?: FetchInit, ) { - const protoRoot = parseProto(protoInit.proto).root; - const RequestMessge = protoRoot.lookupType(protoInit.requestType); - if (RequestMessge.verify(protoInit.requestData)){ - throw new Error('Invalid Proto'); - } - // encode request data - const encodedrequest = RequestMessge.encode(protoInit.requestData).finish(); - const requestLength = BigInt(encodedrequest.length); - const headers = new Uint8Array( - Array(5).fill(0).map((v, idx) => { - if(idx === 0) return 0; - return Number((requestLength >> BigInt(8 * (5 - idx - 1))) & BYTE_MARK); - }) - ); - init = await makeInit(init); - const bodyArray = new Uint8Array(headers.length + encodedrequest.length) - bodyArray.set(headers, 0) - bodyArray.set(encodedrequest, headers.length) - return fetch(url, { - method: 'POST', - ...init, - body: bodyArray - } as RequestInit - ).then(r => r.arrayBuffer()) + const protoRoot = parseProto(protoInit.proto).root; + const RequestMessge = protoRoot.lookupType(protoInit.requestType); + if (RequestMessge.verify(protoInit.requestData)) { + throw new Error('Invalid Proto'); + } + // encode request data + const encodedrequest = RequestMessge.encode(protoInit.requestData).finish(); + const requestLength = BigInt(encodedrequest.length); + const headers = new Uint8Array( + Array(5) + .fill(0) + .map((v, idx) => { + if (idx === 0) return 0; + return Number((requestLength >> BigInt(8 * (5 - idx - 1))) & BYTE_MARK); + }), + ); + init = await makeInit(init); + const bodyArray = new Uint8Array(headers.length + encodedrequest.length); + bodyArray.set(headers, 0); + bodyArray.set(encodedrequest, headers.length); + return fetch(url, { + method: 'POST', + ...init, + body: bodyArray, + } as RequestInit) + .then(r => r.arrayBuffer()) .then(arr => { - // decode response data - const payload = new Uint8Array(arr); - const length = Number(BigInt(payload[1] << 24) | BigInt(payload[2] << 16) | BigInt(payload[3] << 8) | BigInt(payload[4])); - const ResponseMessage = protoRoot.lookupType(protoInit.responseType); - return ResponseMessage.decode(payload.slice(5, 5 + length)); - }) as ReturnType -} + // decode response data + const payload = new Uint8Array(arr); + const length = Number( + BigInt(payload[1] << 24) | + BigInt(payload[2] << 16) | + BigInt(payload[3] << 8) | + BigInt(payload[4]), + ); + const ResponseMessage = protoRoot.lookupType(protoInit.responseType); + return ResponseMessage.decode(payload.slice(5, 5 + length)); + }) as ReturnType; +}; diff --git a/libs/filterInputs.ts b/libs/filterInputs.ts index e1580bf..7023987 100644 --- a/libs/filterInputs.ts +++ b/libs/filterInputs.ts @@ -1,78 +1,78 @@ export interface FilterOption { - readonly label: string; - readonly value: string; + readonly label: string; + readonly value: string; } export enum FilterTypes { - TextInput = "Text", - Picker = "Picker", - CheckboxGroup = "Checkbox", - Switch = "Switch", - ExcludableCheckboxGroup = "XCheckbox", + TextInput = 'Text', + Picker = 'Picker', + CheckboxGroup = 'Checkbox', + Switch = 'Switch', + ExcludableCheckboxGroup = 'XCheckbox', } type SwitchFilter = { - type: FilterTypes.Switch; - /** Default value */ - value: boolean; + type: FilterTypes.Switch; + /** Default value */ + value: boolean; }; type TextFilter = { - type: FilterTypes.TextInput; - /** Default value */ - value: string; + type: FilterTypes.TextInput; + /** Default value */ + value: string; }; type CheckboxFilter = { - type: FilterTypes.CheckboxGroup; - options: readonly FilterOption[]; - /** Default value */ - value: string[]; + type: FilterTypes.CheckboxGroup; + options: readonly FilterOption[]; + /** Default value */ + value: string[]; }; type PickerFilter = { - type: FilterTypes.Picker; - options: readonly FilterOption[]; - /** Default value */ - value: string; + type: FilterTypes.Picker; + options: readonly FilterOption[]; + /** Default value */ + value: string; }; type ExcludableCheckboxFilter = { - type: FilterTypes.ExcludableCheckboxGroup; - options: readonly FilterOption[]; - value: { include?: string[]; exclude?: string[] }; + type: FilterTypes.ExcludableCheckboxGroup; + options: readonly FilterOption[]; + value: { include?: string[]; exclude?: string[] }; }; export type Filters = Record< - string, - | { label: string } & ( - | PickerFilter - | CheckboxFilter - | TextFilter - | SwitchFilter - | ExcludableCheckboxFilter - ) + string, + { label: string } & ( + | PickerFilter + | CheckboxFilter + | TextFilter + | SwitchFilter + | ExcludableCheckboxFilter + ) >; export type FilterToValues< - T extends Record | undefined + T extends Record | undefined, > = T extends undefined - ? undefined - : { - [K in keyof T]: Omit< - { type: NonNullable[K]["type"] } & Filters[string], - "label" | "options" - >; - }; + ? undefined + : { + [K in keyof T]: Omit< + { type: NonNullable[K]['type'] } & Filters[string], + 'label' | 'options' + >; + }; export type ValueOfFilter = - T extends FilterTypes.CheckboxGroup - ? CheckboxFilter["value"] - : T extends FilterTypes.Picker - ? PickerFilter["value"] - : T extends FilterTypes.Switch - ? SwitchFilter["value"] + T extends FilterTypes.CheckboxGroup + ? CheckboxFilter['value'] + : T extends FilterTypes.Picker + ? PickerFilter['value'] + : T extends FilterTypes.Switch + ? SwitchFilter['value'] : T extends FilterTypes.TextInput - ? TextFilter["value"] - : T extends FilterTypes.ExcludableCheckboxGroup - ? ExcludableCheckboxFilter["value"] - : never; + ? TextFilter['value'] + : T extends FilterTypes.ExcludableCheckboxGroup + ? ExcludableCheckboxFilter['value'] + : never; diff --git a/libs/isAbsoluteUrl.ts b/libs/isAbsoluteUrl.ts index d082f6e..5f47249 100644 --- a/libs/isAbsoluteUrl.ts +++ b/libs/isAbsoluteUrl.ts @@ -1,23 +1,23 @@ export const isUrlAbsolute = (url: string) => { - if (url) { - if (url.indexOf("//") === 0) { - return true; - } // URL is protocol-relative (= absolute) - if (url.indexOf("://") === -1) { - return false; - } // URL has no protocol (= relative) - if (url.indexOf(".") === -1) { - return false; - } // URL does not contain a dot, i.e. no TLD (= relative, possibly REST) - if (url.indexOf("/") === -1) { - return false; - } // URL does not contain a single slash (= relative) - if (url.indexOf(":") > url.indexOf("/")) { - return false; - } // The first colon comes after the first slash (= relative) - if (url.indexOf("://") < url.indexOf(".")) { - return true; - } // Protocol is defined before first dot (= absolute) - } - return false; // Anything else must be relative + if (url) { + if (url.indexOf('//') === 0) { + return true; + } // URL is protocol-relative (= absolute) + if (url.indexOf('://') === -1) { + return false; + } // URL has no protocol (= relative) + if (url.indexOf('.') === -1) { + return false; + } // URL does not contain a dot, i.e. no TLD (= relative, possibly REST) + if (url.indexOf('/') === -1) { + return false; + } // URL does not contain a single slash (= relative) + if (url.indexOf(':') > url.indexOf('/')) { + return false; + } // The first colon comes after the first slash (= relative) + if (url.indexOf('://') < url.indexOf('.')) { + return true; + } // Protocol is defined before first dot (= absolute) + } + return false; // Anything else must be relative }; diff --git a/libs/languages.ts b/libs/languages.ts index abc6e63..e94d447 100644 --- a/libs/languages.ts +++ b/libs/languages.ts @@ -1,17 +1,17 @@ export const languages = { - Arabic: "العربية", - Chinese: "中文, 汉语, 漢語", - English: "English", - French: "Français", - Indonesian: "Bahasa Indonesia", - Japanese: "日本語", - Korean: "조선말, 한국어", - Polish: "Polski", - Portuguese: "Português", - Russian: "Русский", - Spanish: "Español", - Thai: "ไทย", - Turkish: "Türkçe", - Ukrainian: "Українська", - Vietnamese: "Tiếng Việt", + Arabic: 'العربية', + Chinese: '中文, 汉语, 漢語', + English: 'English', + French: 'Français', + Indonesian: 'Bahasa Indonesia', + Japanese: '日本語', + Korean: '조선말, 한국어', + Polish: 'Polski', + Portuguese: 'Português', + Russian: 'Русский', + Spanish: 'Español', + Thai: 'ไทย', + Turkish: 'Türkçe', + Ukrainian: 'Українська', + Vietnamese: 'Tiếng Việt', } as const; diff --git a/libs/novelStatus.ts b/libs/novelStatus.ts index 280a647..b638dd3 100644 --- a/libs/novelStatus.ts +++ b/libs/novelStatus.ts @@ -1,9 +1,9 @@ export const NovelStatus = { - Unknown: "Unknown", - Ongoing: "Ongoing", - Completed: "Completed", - Licensed: "Licensed", - PublishingFinished: "Publishing Finished", - Cancelled: "Cancelled", - OnHiatus: "On Hiatus", -} as const; \ No newline at end of file + Unknown: 'Unknown', + Ongoing: 'Ongoing', + Completed: 'Completed', + Licensed: 'Licensed', + PublishingFinished: 'Publishing Finished', + Cancelled: 'Cancelled', + OnHiatus: 'On Hiatus', +} as const; diff --git a/package-lock.json b/package-lock.json index 8c02ee9..8c04ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,9 @@ "@types/express": "^4.17.21", "@types/jquery": "^3.5.29", "@types/node": "^20.11.19", - "pre-commit": "^1.2.2", + "husky": "^9.0.11", + "lint-staged": "^15.2.2", + "prettier": "3.2.5", "tsc-watch": "^6.0.4" } }, @@ -367,6 +369,45 @@ "node": ">=0.4.0" } }, + "node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -781,6 +822,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -867,6 +920,43 @@ "node": ">=0.10" } }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combine-source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", @@ -1012,17 +1102,6 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, - "node_modules/cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", - "dev": true, - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, "node_modules/crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -1331,6 +1410,12 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, + "node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -1454,6 +1539,12 @@ "through": "~2.3.1" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -1471,6 +1562,79 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -1719,6 +1883,18 @@ "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -1733,6 +1909,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1959,6 +2147,30 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2120,6 +2332,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -2166,6 +2390,18 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-typed-array": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", @@ -2262,26 +2498,151 @@ "node": ">=0.10.0" } }, + "node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/lint-staged": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz", + "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "3.0.0", + "listr2": "8.0.1", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/listr2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", + "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.3.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/lodash.memoize": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==" }, + "node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, - "node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, "node_modules/lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -2352,6 +2713,12 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2427,6 +2794,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -2584,6 +2963,33 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -2647,20 +3053,26 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" }, - "node_modules/os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -2818,6 +3230,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -2862,16 +3286,19 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/pre-commit": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", - "integrity": "sha512-qokTiqxD6GjODy5ETAIgzsRgnBWWQHQH2ghy86PU7mIn/wuWeTwF3otyNQZxWBwVn8XNr8Tdzj/QfUXpH+gRZA==", + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, - "hasInstallScript": true, - "dependencies": { - "cross-spawn": "^5.0.1", - "spawn-sync": "^1.0.15", - "which": "1.2.x" + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/process": { @@ -2943,12 +3370,6 @@ "node": ">= 0.10" } }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true - }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -3111,6 +3532,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3120,6 +3587,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -3292,27 +3765,6 @@ "fast-safe-stringify": "^2.0.7" } }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/shell-quote": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", @@ -3334,6 +3786,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3361,6 +3825,22 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3394,17 +3874,6 @@ "node": ">=0.10.0" } }, - "node_modules/spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, "node_modules/split": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", @@ -3520,6 +3989,50 @@ "node": ">=0.6.19" } }, + "node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/subarg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", @@ -3766,6 +4279,18 @@ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3902,18 +4427,6 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, - "node_modules/which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha512-16uPglFkRPzgiUXYMi1Jf8Z5EzN1iB4V0ZtMXcHZnwsBtQhhHeCqoWw7tsUY42hJGNDWtUsVLTjakIa5BgAxCw==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/which-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", @@ -3932,6 +4445,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3945,11 +4475,14 @@ "node": ">=0.4" } }, - "node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } }, "node_modules/yn": { "version": "3.1.1", @@ -4254,6 +4787,27 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" }, + "ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "requires": { + "type-fest": "^3.0.0" + } + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -4613,6 +5167,12 @@ "set-function-length": "^1.1.1" } }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, "cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -4676,6 +5236,31 @@ "timers-ext": "^0.1.7" } }, + "cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "requires": { + "restore-cursor": "^4.0.0" + } + }, + "cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + } + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "combine-source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", @@ -4805,17 +5390,6 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -5057,6 +5631,12 @@ } } }, + "emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -5155,6 +5735,12 @@ "through": "~2.3.1" } }, + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -5169,6 +5755,60 @@ "safe-buffer": "^5.1.1" } }, + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -5382,6 +6022,12 @@ "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" }, + "get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true + }, "get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -5393,6 +6039,12 @@ "hasown": "^2.0.0" } }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5557,6 +6209,18 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" }, + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, + "husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5665,6 +6329,12 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, + "is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true + }, "is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -5696,6 +6366,12 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, "is-typed-array": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", @@ -5768,26 +6444,111 @@ } } }, + "lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true + }, + "lint-staged": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz", + "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", + "dev": true, + "requires": { + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "3.0.0", + "listr2": "8.0.1", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.4" + }, + "dependencies": { + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "listr2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", + "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "dev": true, + "requires": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.3.0", + "wrap-ansi": "^9.0.0" + } + }, "lodash.memoize": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==" }, + "log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "requires": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.0.0" + } + }, + "slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + } + } + } + }, "long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, "lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -5852,6 +6613,12 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5905,6 +6672,12 @@ "mime-db": "1.52.0" } }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -6018,6 +6791,23 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + }, + "dependencies": { + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + } + } + }, "nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -6063,17 +6853,20 @@ "wrappy": "1" } }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" }, - "os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==", - "dev": true - }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -6198,6 +6991,12 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, + "pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -6222,16 +7021,11 @@ "source-map-js": "^1.0.2" } }, - "pre-commit": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", - "integrity": "sha512-qokTiqxD6GjODy5ETAIgzsRgnBWWQHQH2ghy86PU7mIn/wuWeTwF3otyNQZxWBwVn8XNr8Tdzj/QfUXpH+gRZA==", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "spawn-sync": "^1.0.15", - "which": "1.2.x" - } + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true }, "process": { "version": "0.11.10", @@ -6286,12 +7080,6 @@ "event-stream": "=3.3.4" } }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true - }, "public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -6420,11 +7208,50 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + } + } + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, + "rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, "ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -6558,21 +7385,6 @@ "fast-safe-stringify": "^2.0.7" } }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true - }, "shell-quote": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", @@ -6588,6 +7400,12 @@ "object-inspect": "^1.9.0" } }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, "simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -6598,6 +7416,16 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6624,16 +7452,6 @@ } } }, - "spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==", - "dev": true, - "requires": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, "split": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", @@ -6740,6 +7558,32 @@ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true }, + "string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, "subarg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", @@ -6920,6 +7764,12 @@ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, + "type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -7030,15 +7880,6 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, - "which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha512-16uPglFkRPzgiUXYMi1Jf8Z5EzN1iB4V0ZtMXcHZnwsBtQhhHeCqoWw7tsUY42hJGNDWtUsVLTjakIa5BgAxCw==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "which-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", @@ -7051,6 +7892,17 @@ "has-tostringtag": "^1.0.0" } }, + "wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7061,10 +7913,10 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dev": true }, "yn": { diff --git a/package.json b/package.json index 774c90d..02d823c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "clearMultisrc": "find . -type f -wholename \"*plugins*\\[*\\]*.[t,j]s\" -delete", "less": "npx less ./test_web/static/css/index.less ./test_web/static/css/index.css", "host-linux": "chmod +x ./host.sh && ./host.sh", - "host-windows": "powershell ./host.ps1" + "host-windows": "powershell ./host.ps1", + "prettier": "prettier --write \"./**/*.{js,ts}\"", + "prepare": "husky install" }, "author": "LNReader", "license": "MIT", @@ -44,7 +46,12 @@ "@types/express": "^4.17.21", "@types/jquery": "^3.5.29", "@types/node": "^20.11.19", - "pre-commit": "^1.2.2", + "husky": "^9.0.11", + "lint-staged": "^15.2.2", + "prettier": "3.2.5", "tsc-watch": "^6.0.4" + }, + "lint-staged": { + "**/*.{js,ts}": "prettier --write" } } diff --git a/plugins/chinese/69shu.ts b/plugins/chinese/69shu.ts index 0668063..1d1e885 100644 --- a/plugins/chinese/69shu.ts +++ b/plugins/chinese/69shu.ts @@ -1,219 +1,223 @@ -import { load as parseHTML } from "cheerio"; -import { fetchText, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; +import { load as parseHTML } from 'cheerio'; +import { fetchText, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { NovelStatus } from '@libs/novelStatus'; class Shu69 implements Plugin.PluginBase { - id = "69shu"; - name = "69书吧"; - icon = "src/cn/69shu/icon.png"; - site = "https://www.69shu.xyz"; - version = "0.2.0"; + id = '69shu'; + name = '69书吧'; + icon = 'src/cn/69shu/icon.png'; + site = 'https://www.69shu.xyz'; + version = '0.2.0'; - async popularNovels( - pageNo: number, - { - showLatestNovels, - filters, - }: Plugin.PopularNovelsOptions - ): Promise { - let url: string; - if (showLatestNovels) { - url = `${this.site}/rank/lastupdate/${pageNo}.html`; - } else if (filters.sort.value === 'none') { - url = `${this.site}/rank/${filters.rank.value}/${pageNo}.html`; - } else { - url = `${this.site}/sort/${filters.sort.value}/${pageNo}.html`; - } - - const body = await fetchText(url); - if (body === '') throw Error('无法获取小说列表,请检查网络'); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio('div.book-coverlist').each((i, el) => { - let url = loadedCheerio(el).find('a.cover').attr('href'); - - const novelName = loadedCheerio(el).find('h4.name').text().trim(); - const novelCover = loadedCheerio(el).find('a.cover > img').attr('src'); - if (!url) return; - - const novel = { - name: novelName, - cover: novelCover, - path: url, - }; - - novels.push(novel); - }); - - return novels; + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + let url: string; + if (showLatestNovels) { + url = `${this.site}/rank/lastupdate/${pageNo}.html`; + } else if (filters.sort.value === 'none') { + url = `${this.site}/rank/${filters.rank.value}/${pageNo}.html`; + } else { + url = `${this.site}/sort/${filters.sort.value}/${pageNo}.html`; } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; + const body = await fetchText(url); + if (body === '') throw Error('无法获取小说列表,请检查网络'); - const body = await fetchText(url); - if (body === '') throw Error('无法获取小说内容,请检查网络'); + const loadedCheerio = parseHTML(body); - let loadedCheerio = parseHTML(body); + const novels: Plugin.NovelItem[] = []; - const novel: Plugin.SourceNovel = { - path: novelPath, - chapters: [], - name: loadedCheerio('h1').text().trim() - }; + loadedCheerio('div.book-coverlist').each((i, el) => { + let url = loadedCheerio(el).find('a.cover').attr('href'); - novel.cover = loadedCheerio('div.cover > img').attr('src'); + const novelName = loadedCheerio(el).find('h4.name').text().trim(); + const novelCover = loadedCheerio(el).find('a.cover > img').attr('src'); + if (!url) return; - novel.summary = loadedCheerio('#bookIntro').text().trim(); + const novel = { + name: novelName, + cover: novelCover, + path: url, + }; - const bookInfo = loadedCheerio('div.caption-bookinfo > p') + novels.push(novel); + }); - novel.author = bookInfo.find("a").attr('title'); + return novels; + } - novel.artist = undefined; + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; - novel.status = bookInfo.text().includes('连载') ? NovelStatus.Ongoing : NovelStatus.Completed; + const body = await fetchText(url); + if (body === '') throw Error('无法获取小说内容,请检查网络'); - novel.genres = ''; + let loadedCheerio = parseHTML(body); - // Table of Content is on a different page than the summary page - let chapters: Plugin.ChapterItem[] = []; - - const allUrl = loadedCheerio('dd.all > a').attr('href') - if (allUrl) { - const chaptersUrl = this.site + allUrl; - const chaptersBody = await fetchText(chaptersUrl); - - const chaptersLoadedCheerio = parseHTML(chaptersBody); - - chaptersLoadedCheerio('dd').each((i, el) => { - const chapterUrl = chaptersLoadedCheerio(el).find('a').attr('href'); - const chapterName = chaptersLoadedCheerio(el).find('a').text().trim(); - if(chapterUrl){ - chapters.push({ - name: chapterName, - path: chapterUrl, - }); - } - }); - } else { - loadedCheerio('div.panel.hidden-xs > dl.panel-chapterlist:nth-child(2) > dd').each((i, el) => { - const chapterUrl = loadedCheerio(el).find('a').attr('href'); - const chapterName = loadedCheerio(el).find('a').text().trim(); - if(chapterUrl){ - chapters.push({ - name: chapterName, - path: chapterUrl, - }); - } - }); - } - - novel.chapters = chapters; - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const body = await fetchText(this.site + chapterPath); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio('#chaptercontent p') - .map((i, el) => loadedCheerio(el).text()) - .get() - // remove empty lines and 69shu ads - .map((line: string) => line.trim()) - .filter((line: string) => line !== '' && !line.includes('69书吧')) - .map((line: string) => `

${line}

`) - .join('\n'); - - return chapterText; + const novel: Plugin.SourceNovel = { + path: novelPath, + chapters: [], + name: loadedCheerio('h1').text().trim(), }; - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - if (pageNo > 1) return []; + novel.cover = loadedCheerio('div.cover > img').attr('src'); - const searchUrl = `${this.site}/search`; - const formData = new FormData(); - formData.append('searchkey', searchTerm); + novel.summary = loadedCheerio('#bookIntro').text().trim(); - const body = await fetchText(searchUrl, { - method: 'post', - body: formData, - }); - if (body === '') throw Error('无法获取搜索结果,请检查网络'); + const bookInfo = loadedCheerio('div.caption-bookinfo > p'); - let loadedCheerio = parseHTML(body); + novel.author = bookInfo.find('a').attr('title'); - const novels: Plugin.NovelItem[] = []; + novel.artist = undefined; - loadedCheerio('div.book-coverlist').each((i, el) => { - let url = loadedCheerio(el).find('a.cover').attr('href'); + novel.status = bookInfo.text().includes('连载') + ? NovelStatus.Ongoing + : NovelStatus.Completed; - const novelName = loadedCheerio(el).find('h4.name').text().trim(); - const novelCover = loadedCheerio(el).find('a.cover > img').attr('src'); + novel.genres = ''; - if (!url) return; + // Table of Content is on a different page than the summary page + let chapters: Plugin.ChapterItem[] = []; - const novel = { - name: novelName, - cover: novelCover, - path: url, - }; + const allUrl = loadedCheerio('dd.all > a').attr('href'); + if (allUrl) { + const chaptersUrl = this.site + allUrl; + const chaptersBody = await fetchText(chaptersUrl); - novels.push(novel); - }); + const chaptersLoadedCheerio = parseHTML(chaptersBody); - return novels; + chaptersLoadedCheerio('dd').each((i, el) => { + const chapterUrl = chaptersLoadedCheerio(el).find('a').attr('href'); + const chapterName = chaptersLoadedCheerio(el).find('a').text().trim(); + if (chapterUrl) { + chapters.push({ + name: chapterName, + path: chapterUrl, + }); + } + }); + } else { + loadedCheerio( + 'div.panel.hidden-xs > dl.panel-chapterlist:nth-child(2) > dd', + ).each((i, el) => { + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + const chapterName = loadedCheerio(el).find('a').text().trim(); + if (chapterUrl) { + chapters.push({ + name: chapterName, + path: chapterUrl, + }); + } + }); } - fetchImage = fetchFile; + novel.chapters = chapters; - filters = { - rank: { - label: "排行榜", - value: "allvisit", - options: [ - { label: "总排行榜", value: "allvisit" }, - { label: "月排行榜", value: "monthvisit" }, - { label: "周排行榜", value: "weekvisit" }, - { label: "日排行榜", value: "dayvisit" }, - { label: "收藏榜", value: "goodnum" }, - { label: "字数榜", value: "words" }, - { label: "推荐榜", value: "allvote" }, - { label: "新书榜", value: "postdate" }, - { label: "更新榜", value: "lastupdate" }, - ], - type: FilterTypes.Picker, - }, - sort: { - label: "分类", - value: "none", - options: [ - { label: "无", value: "none" }, - { label: "全部", value: "all" }, - { label: "玄幻", value: "xuanhuan" }, - { label: "仙侠", value: "xianxia" }, - { label: "都市", value: "dushi" }, - { label: "历史", value: "lishi" }, - { label: "游戏", value: "youxi" }, - { label: "科幻", value: "kehuan" }, - { label: "灵异", value: "kongbu" }, - { label: "言情", value: "nvsheng" }, - { label: "其它", value: "qita" }, - ], - type: FilterTypes.Picker, - } - } satisfies Filters; + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchText(this.site + chapterPath); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#chaptercontent p') + .map((i, el) => loadedCheerio(el).text()) + .get() + // remove empty lines and 69shu ads + .map((line: string) => line.trim()) + .filter((line: string) => line !== '' && !line.includes('69书吧')) + .map((line: string) => `

${line}

`) + .join('\n'); + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo > 1) return []; + + const searchUrl = `${this.site}/search`; + const formData = new FormData(); + formData.append('searchkey', searchTerm); + + const body = await fetchText(searchUrl, { + method: 'post', + body: formData, + }); + if (body === '') throw Error('无法获取搜索结果,请检查网络'); + + let loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.book-coverlist').each((i, el) => { + let url = loadedCheerio(el).find('a.cover').attr('href'); + + const novelName = loadedCheerio(el).find('h4.name').text().trim(); + const novelCover = loadedCheerio(el).find('a.cover > img').attr('src'); + + if (!url) return; + + const novel = { + name: novelName, + cover: novelCover, + path: url, + }; + + novels.push(novel); + }); + + return novels; + } + + fetchImage = fetchFile; + + filters = { + rank: { + label: '排行榜', + value: 'allvisit', + options: [ + { label: '总排行榜', value: 'allvisit' }, + { label: '月排行榜', value: 'monthvisit' }, + { label: '周排行榜', value: 'weekvisit' }, + { label: '日排行榜', value: 'dayvisit' }, + { label: '收藏榜', value: 'goodnum' }, + { label: '字数榜', value: 'words' }, + { label: '推荐榜', value: 'allvote' }, + { label: '新书榜', value: 'postdate' }, + { label: '更新榜', value: 'lastupdate' }, + ], + type: FilterTypes.Picker, + }, + sort: { + label: '分类', + value: 'none', + options: [ + { label: '无', value: 'none' }, + { label: '全部', value: 'all' }, + { label: '玄幻', value: 'xuanhuan' }, + { label: '仙侠', value: 'xianxia' }, + { label: '都市', value: 'dushi' }, + { label: '历史', value: 'lishi' }, + { label: '游戏', value: 'youxi' }, + { label: '科幻', value: 'kehuan' }, + { label: '灵异', value: 'kongbu' }, + { label: '言情', value: 'nvsheng' }, + { label: '其它', value: 'qita' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new Shu69(); diff --git a/plugins/chinese/69xinshu.ts b/plugins/chinese/69xinshu.ts index 078fc13..2e410cf 100644 --- a/plugins/chinese/69xinshu.ts +++ b/plugins/chinese/69xinshu.ts @@ -1,199 +1,210 @@ -import { load as parseHTML } from "cheerio"; -import { fetchText, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { encode } from "urlencode"; -import { NovelStatus } from "@libs/novelStatus"; +import { load as parseHTML } from 'cheerio'; +import { fetchText, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { encode } from 'urlencode'; +import { NovelStatus } from '@libs/novelStatus'; class XinShu69 implements Plugin.PluginBase { - id = "69xinshu"; - name = "69书吧"; - icon = "src/cn/69xinshu/icon.png"; - site = "https://www.69shu.pro"; - version = "0.1.1"; + id = '69xinshu'; + name = '69书吧'; + icon = 'src/cn/69xinshu/icon.png'; + site = 'https://www.69shu.pro'; + version = '0.1.1'; - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let url: string; - if (pageNo === 1) { - url = `${this.site}/novels/class/${filters.class.value}.htm`; - } else { - url = `${this.site}/ajax_novels/class/${filters.class.value}/${pageNo}.htm`; - } - - const body = await fetchText(url, {}, 'gbk'); - if (body === '') throw Error('无法获取小说列表,请检查网络'); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - const novelsList = pageNo === 1 ? loadedCheerio('div.newbox > ul > li') : loadedCheerio('li'); - novelsList.each((i, e) => { - const novelUrl = loadedCheerio(e).find('li > div > h3 > a:nth-child(2)').attr('href'); - - if (novelUrl) { - const novelName = loadedCheerio(e).find('li > div > h3 > a:nth-child(2)').text(); - const novelCover = loadedCheerio(e) - .find('li > a > img') - .attr('data-src'); - - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; - - novels.push(novel); - } - }); - - return novels; + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let url: string; + if (pageNo === 1) { + url = `${this.site}/novels/class/${filters.class.value}.htm`; + } else { + url = `${this.site}/ajax_novels/class/${filters.class.value}/${pageNo}.htm`; } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; + const body = await fetchText(url, {}, 'gbk'); + if (body === '') throw Error('无法获取小说列表,请检查网络'); - const body = await fetchText(url, {}, 'gbk'); - if (body === '') throw Error('无法获取小说内容,请检查网络'); + const loadedCheerio = parseHTML(body); - let loadedCheerio = parseHTML(body); + const novels: Plugin.NovelItem[] = []; - const novel: Plugin.SourceNovel = { - path: novelPath, - chapters: [], - name: loadedCheerio('h1 > a').text() + const novelsList = + pageNo === 1 + ? loadedCheerio('div.newbox > ul > li') + : loadedCheerio('li'); + novelsList.each((i, e) => { + const novelUrl = loadedCheerio(e) + .find('li > div > h3 > a:nth-child(2)') + .attr('href'); + + if (novelUrl) { + const novelName = loadedCheerio(e) + .find('li > div > h3 > a:nth-child(2)') + .text(); + const novelCover = loadedCheerio(e) + .find('li > a > img') + .attr('data-src'); + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), }; - novel.cover = loadedCheerio('div.bookimg2 > img').attr( - 'src', - ); + novels.push(novel); + } + }); - novel.summary = loadedCheerio('div.navtxt').text().trim(); + return novels; + } - const bookInfo = loadedCheerio('div.booknav2').text() + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; - const authorMatch = bookInfo.match(/作者:(.*)/); - if (authorMatch) { - novel.author = authorMatch[1]; - } + const body = await fetchText(url, {}, 'gbk'); + if (body === '') throw Error('无法获取小说内容,请检查网络'); - novel.artist = undefined; + let loadedCheerio = parseHTML(body); - novel.status = bookInfo.includes('连载') ? NovelStatus.Ongoing : NovelStatus.Completed; - - const genresMatch = bookInfo.match(/分类:(.*)/); - if (genresMatch) { - novel.genres = genresMatch[1]; - } - - // Table of Content is on a different page than the summary page - let chapters: Plugin.ChapterItem[] = []; - - const chaptersUrl = loadedCheerio('a.more-btn').attr('href'); - if (chaptersUrl) { - const chaptersBody = await fetchText(chaptersUrl, {}, 'gbk'); - - const chaptersLoadedCheerio = parseHTML(chaptersBody); - - chaptersLoadedCheerio('li').each((i, e) => { - const chapterUrl = chaptersLoadedCheerio(e).find('a').attr('href'); - - if (chapterUrl?.startsWith("https://")) { - const chapterName = chaptersLoadedCheerio(e).find('a').text().trim(); - - chapters.push({ - name: chapterName, - path: chapterUrl.replace(this.site, ''), - }); - } - }); - } - - novel.chapters = chapters; - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const body = await fetchText(this.site + chapterPath, {}, 'gbk'); - - const loadedCheerio = parseHTML(body); - - const chapterText = (loadedCheerio('div.txtnav').prop('innerText') ?? '') - .split('\n') - // remove empty lines - .map((line: string) => line.trim()) - .filter((line: string) => line !== '') - // remove the first two lines which are the chapter name and author name - .slice(2) - .map((line: string) => `

${line}

`) - .join('\n'); - - return chapterText; + const novel: Plugin.SourceNovel = { + path: novelPath, + chapters: [], + name: loadedCheerio('h1 > a').text(), }; - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - if (pageNo > 1) return []; + novel.cover = loadedCheerio('div.bookimg2 > img').attr('src'); - const searchUrl = `${this.site}/modules/article/search.php`; - const formData = new FormData(); - formData.append('searchkey', encode(searchTerm, 'gbk')); - formData.append('searchtype', 'all'); + novel.summary = loadedCheerio('div.navtxt').text().trim(); - const body = await fetchText(searchUrl, { method: 'post', body: formData, }, 'gbk'); - if (body === '') throw Error('无法获取搜索结果,请检查网络'); + const bookInfo = loadedCheerio('div.booknav2').text(); - let loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio('div.newbox > ul > li').each((i, e) => { - const novelUrl = loadedCheerio(e).find('a').attr('href')!; - const novelName = loadedCheerio(e).find('div.newnav > h3').text(); - const novelCover = loadedCheerio(e).find('img').attr('data-src'); - - const novel = { - name: novelName, - path: novelUrl.replace(this.site, ''), - cover: novelCover, - }; - - novels.push(novel); - }); - - return novels; + const authorMatch = bookInfo.match(/作者:(.*)/); + if (authorMatch) { + novel.author = authorMatch[1]; } - fetchImage = fetchFile; + novel.artist = undefined; - filters = { - class: { - label: "分类", - value: "0", - options: [ - { label: "全部分类", value: "0" }, - { label: "言情小说", value: "3" }, - { label: "玄幻魔法", value: "1" }, - { label: "修真武侠", value: "2" }, - { label: "穿越时空", value: "11" }, - { label: "都市小说", value: "9" }, - { label: "历史军事", value: "4" }, - { label: "游戏竞技", value: "5" }, - { label: "科幻空间", value: "6" }, - { label: "悬疑惊悚", value: "7" }, - { label: "同人小说", value: "8" }, - { label: "官场职场", value: "10" }, - { label: "青春校园", value: "12" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + novel.status = bookInfo.includes('连载') + ? NovelStatus.Ongoing + : NovelStatus.Completed; + + const genresMatch = bookInfo.match(/分类:(.*)/); + if (genresMatch) { + novel.genres = genresMatch[1]; + } + + // Table of Content is on a different page than the summary page + let chapters: Plugin.ChapterItem[] = []; + + const chaptersUrl = loadedCheerio('a.more-btn').attr('href'); + if (chaptersUrl) { + const chaptersBody = await fetchText(chaptersUrl, {}, 'gbk'); + + const chaptersLoadedCheerio = parseHTML(chaptersBody); + + chaptersLoadedCheerio('li').each((i, e) => { + const chapterUrl = chaptersLoadedCheerio(e).find('a').attr('href'); + + if (chapterUrl?.startsWith('https://')) { + const chapterName = chaptersLoadedCheerio(e).find('a').text().trim(); + + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + } + }); + } + + novel.chapters = chapters; + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchText(this.site + chapterPath, {}, 'gbk'); + + const loadedCheerio = parseHTML(body); + + const chapterText = (loadedCheerio('div.txtnav').prop('innerText') ?? '') + .split('\n') + // remove empty lines + .map((line: string) => line.trim()) + .filter((line: string) => line !== '') + // remove the first two lines which are the chapter name and author name + .slice(2) + .map((line: string) => `

${line}

`) + .join('\n'); + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo > 1) return []; + + const searchUrl = `${this.site}/modules/article/search.php`; + const formData = new FormData(); + formData.append('searchkey', encode(searchTerm, 'gbk')); + formData.append('searchtype', 'all'); + + const body = await fetchText( + searchUrl, + { method: 'post', body: formData }, + 'gbk', + ); + if (body === '') throw Error('无法获取搜索结果,请检查网络'); + + let loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.newbox > ul > li').each((i, e) => { + const novelUrl = loadedCheerio(e).find('a').attr('href')!; + const novelName = loadedCheerio(e).find('div.newnav > h3').text(); + const novelCover = loadedCheerio(e).find('img').attr('data-src'); + + const novel = { + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }; + + novels.push(novel); + }); + + return novels; + } + + fetchImage = fetchFile; + + filters = { + class: { + label: '分类', + value: '0', + options: [ + { label: '全部分类', value: '0' }, + { label: '言情小说', value: '3' }, + { label: '玄幻魔法', value: '1' }, + { label: '修真武侠', value: '2' }, + { label: '穿越时空', value: '11' }, + { label: '都市小说', value: '9' }, + { label: '历史军事', value: '4' }, + { label: '游戏竞技', value: '5' }, + { label: '科幻空间', value: '6' }, + { label: '悬疑惊悚', value: '7' }, + { label: '同人小说', value: '8' }, + { label: '官场职场', value: '10' }, + { label: '青春校园', value: '12' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new XinShu69(); diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index ac38281..368d59c 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -1,561 +1,551 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchText, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchText, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { NovelStatus } from '@libs/novelStatus'; class Linovelib implements Plugin.PluginBase { - id = "linovelib"; - name = "Linovelib"; - icon = "src/cn/linovelib/icon.png"; - site = "https://www.bilinovel.com"; - version = "1.1.1"; + id = 'linovelib'; + name = 'Linovelib'; + icon = 'src/cn/linovelib/icon.png'; + site = 'https://www.bilinovel.com'; + version = '1.1.1'; - async popularNovels( - pageNo: number, - { - showLatestNovels, - filters, - }: Plugin.PopularNovelsOptions - ): Promise { - const rank = showLatestNovels ? 'lastupdate' : filters.rank.value; - const url = `${this.site}/top/${rank}/${pageNo}.html`; + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + const rank = showLatestNovels ? 'lastupdate' : filters.rank.value; + const url = `${this.site}/top/${rank}/${pageNo}.html`; - const body = await fetchText(url); - if (body === '') throw Error('无法获取小说列表,请检查网络'); + const body = await fetchText(url); + if (body === '') throw Error('无法获取小说列表,请检查网络'); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - const novels: Plugin.NovelItem[] = []; + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".module-rank-booklist .book-layout").each((i, el) => { - let url = loadedCheerio(el).attr("href"); + loadedCheerio('.module-rank-booklist .book-layout').each((i, el) => { + let url = loadedCheerio(el).attr('href'); - const novelName = loadedCheerio(el).find(".book-title").text(); - const novelCover = loadedCheerio(el) - .find("div.book-cover > img") - .attr("data-src"); - if (!url) return; + const novelName = loadedCheerio(el).find('.book-title').text(); + const novelCover = loadedCheerio(el) + .find('div.book-cover > img') + .attr('data-src'); + if (!url) return; - const novel = { - name: novelName, - cover: novelCover, - path: url, - }; + const novel = { + name: novelName, + cover: novelCover, + path: url, + }; - novels.push(novel); + novels.push(novel); + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + + const body = await fetchText(url); + if (body === '') throw Error('无法获取小说内容,请检查网络'); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + chapters: [], + name: loadedCheerio('#bookDetailWrapper .book-title').text(), + }; + + novel.cover = loadedCheerio('#bookDetailWrapper img.book-cover').attr( + 'src', + ); + + novel.summary = loadedCheerio('#bookSummary content').text(); + + novel.author = loadedCheerio('#bookDetailWrapper .book-rand-a a').text(); + + const meta = loadedCheerio('#bookDetailWrapper .book-meta').text(); + novel.status = meta.includes('完结') + ? NovelStatus.Completed + : NovelStatus.Ongoing; + + novel.genres = loadedCheerio('.tag-small.red') + .children('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + // Table of Content is on a different page than the summary page + let chapter: Plugin.ChapterItem[] = []; + + const idPattern = /\/(\d+)\.html/; + const novelId = url.match(idPattern)?.[1]; + + const chaptersUrl = this.site + loadedCheerio('#btnReadBook').attr('href'); + const chaptersBody = await fetchText(chaptersUrl); + + const chaptersLoadedCheerio = parseHTML(chaptersBody); + + let volumeName: string, chapterId: number; + + chaptersLoadedCheerio('#volumes .chapter-li:not(.volume-cover)').each( + (i, el) => { + if (chaptersLoadedCheerio(el).hasClass('chapter-bar')) { + volumeName = chaptersLoadedCheerio(el).text(); + return; + } else { + const urlPart = chaptersLoadedCheerio(el) + .find('.chapter-li-a') + .attr('href'); + const chapterIdMatch = urlPart?.match(idPattern); + + // Sometimes the href attribute does not contain the url, but javascript:cid(0). + // Increment the previous chapter ID should result in the right URL + if (chapterIdMatch) { + chapterId = +chapterIdMatch[1]; + } else { + chapterId++; + } + } + + const chapterUrl = `/novel/${novelId}/${chapterId}.html`; + const chapterName = + volumeName + + ' — ' + + chaptersLoadedCheerio(el).find('.chapter-index').text().trim(); + const releaseDate = null; + + if (!chapterId) return; + + chapter.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl, + }); + }, + ); + + novel.chapters = chapter; + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + let chapterName, + chapterText = '', + hasNextPage, + pageHasNextPage, + pageText = ''; + let pageNumber = 1; + + /* + * TODO: Maybe there are other ways to get the translation table + * It is embed and encrypted inside readtool.js + * UPDATE: Decrypted, see skillgg + */ + // const mapping_dict = { + // '“': '「', + // '’': '』', + // '': '是', + // '': '不', + // '': '好', + // '': '个', + // '': '开', + // '': '样', + // '': '想', + // '': '说', + // '': '年', + // '': '那', + // '': '她', + // '': '美', + // '': '自', + // '': '家', + // '': '而', + // '': '去', + // '': '都', + // '': '于', + // '': '舔', + // '': '他', + // '': '只', + // '': '看', + // '': '来', + // '': '用', + // '': '道', + // '': '得', + // '': '乳', + // '': '茎', + // '': '肉', + // '': '胸', + // '': '淫', + // '': '性', + // '': '骚', + // '”': '」', + // '': '的', + // '': '当', + // '': '人', + // '': '有', + // '': '上', + // '': '到', + // '': '地', + // '': '中', + // '': '生', + // '': '着', + // '': '和', + // '': '起', + // '': '交', + // '': '以', + // '': '可', + // '': '过', + // '': '能', + // '': '多', + // '': '心', + // '': '小', + // '': '成', + // '': '了', + // '': '把', + // '': '发', + // '': '第', + // '': '子', + // '': '事', + // '': '阴', + // '': '欲', + // '': '里', + // '': '私', + // '': '臀', + // '': '脱', + // '': '唇', + // '‘': '『', + // '': '一', + // '': '我', + // '': '在', + // '': '这', + // '': '们', + // '': '时', + // '': '为', + // '': '你', + // '': '国', + // '': '就', + // '': '要', + // '': '也', + // '': '后', + // '': '没', + // '': '下', + // '': '天', + // '': '对', + // '': '然', + // '': '学', + // '': '之', + // '': '出', + // '': '没', + // '': '如', + // '': '还', + // '': '大', + // '': '作', + // '': '种', + // '': '液', + // '': '呻', + // '': '射', + // '': '穴', + // '': '么', + // '': '裸', + // }; + const skillgg: Record = { + '\u201c': '\u300c', + '\u201d': '\u300d', + '\u2018': '\u300e', + '\u2019': '\u300f', + '\ue82c': '\u7684', + '\ue852': '\u4e00', + '\ue82d': '\u662f', + '\ue819': '\u4e86', + '\ue856': '\u6211', + '\ue857': '\u4e0d', + '\ue816': '\u4eba', + '\ue83c': '\u5728', + '\ue830': '\u4ed6', + '\ue82e': '\u6709', + '\ue836': '\u8fd9', + '\ue859': '\u4e2a', + '\ue80a': '\u4e0a', + '\ue855': '\u4eec', + '\ue842': '\u6765', + '\ue858': '\u5230', + '\ue80b': '\u65f6', + '\ue81f': '\u5927', + '\ue84a': '\u5730', + '\ue853': '\u4e3a', + '\ue81e': '\u5b50', + '\ue822': '\u4e2d', + '\ue813': '\u4f60', + '\ue85b': '\u8bf4', + '\ue807': '\u751f', + '\ue818': '\u56fd', + '\ue810': '\u5e74', + '\ue812': '\u7740', + '\ue851': '\u5c31', + '\ue801': '\u90a3', + '\ue80c': '\u548c', + '\ue815': '\u8981', + '\ue84c': '\u5979', + '\ue840': '\u51fa', + '\ue848': '\u4e5f', + '\ue835': '\u5f97', + '\ue800': '\u91cc', + '\ue826': '\u540e', + '\ue863': '\u81ea', + '\ue861': '\u4ee5', + '\ue854': '\u4f1a', + '\ue827': '\u5bb6', + '\ue83b': '\u53ef', + '\ue85d': '\u4e0b', + '\ue84d': '\u800c', + '\ue862': '\u8fc7', + '\ue81c': '\u5929', + '\ue81d': '\u53bb', + '\ue860': '\u80fd', + '\ue843': '\u5bf9', + '\ue82f': '\u5c0f', + '\ue802': '\u591a', + '\ue831': '\u7136', + '\ue84b': '\u4e8e', + '\ue837': '\u5fc3', + '\ue829': '\u5b66', + '\ue85e': '\u4e48', + '\ue83a': '\u4e4b', + '\ue832': '\u90fd', + '\ue808': '\u597d', + '\ue841': '\u770b', + '\ue821': '\u8d77', + '\ue845': '\u53d1', + '\ue803': '\u5f53', + '\ue828': '\u6ca1', + '\ue81b': '\u6210', + '\ue83e': '\u53ea', + '\ue820': '\u5982', + '\ue84e': '\u4e8b', + '\ue85a': '\u628a', + '\ue806': '\u8fd8', + '\ue83f': '\u7528', + '\ue833': '\u7b2c', + '\ue811': '\u6837', + '\ue804': '\u9053', + '\ue814': '\u60f3', + '\ue80f': '\u4f5c', + '\ue84f': '\u79cd', + '\ue80e': '\u5f00', + '\ue823': '\u7f8e', + '\ue849': '\u4e73', + '\ue805': '\u9634', + '\ue809': '\u6db2', + '\ue81a': '\u830e', + '\ue844': '\u6b32', + '\ue847': '\u547b', + '\ue850': '\u8089', + '\ue824': '\u4ea4', + '\ue85f': '\u6027', + '\ue817': '\u80f8', + '\ue85c': '\u79c1', + '\ue838': '\u7a74', + '\ue82a': '\u6deb', + '\ue83d': '\u81c0', + '\ue82b': '\u8214', + '\ue80d': '\u5c04', + '\ue839': '\u8131', + '\ue834': '\u88f8', + '\ue846': '\u9a9a', + '\ue825': '\u5507', + }; + const addPage = async (pageCheerio: CheerioAPI) => { + const formatPage = async () => { + // Remove JS + pageCheerio('#acontentz .adsbygoogle').remove(); + pageCheerio('#acontentz script').remove(); + + // Load lazyloaded images + pageCheerio('#acontentz img.imagecontent').each((i, el) => { + // Sometimes images are either in data-src or src + const imgSrc = + pageCheerio(el).attr('data-src') || pageCheerio(el).attr('src'); + if (imgSrc) { + // Clean up img element + pageCheerio(el) + .attr('src', imgSrc) + .removeAttr('data-src') + .removeClass('lazyload'); + } }); - return novels; - } + // Recover the original character + pageText = pageCheerio('#acontentz').html() || ''; + pageText = pageText.replace(/./g, char => skillgg[char] || char); - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; + return Promise.resolve(); + }; - const body = await fetchText(url); - if (body === '') throw Error('无法获取小说内容,请检查网络'); + await formatPage(); + chapterName = + pageCheerio('#atitle + h3').text() + + ' — ' + + pageCheerio('#atitle').text(); + if (chapterText === '') { + chapterText = '

' + chapterName + '

'; + } + chapterText += pageText; + }; - let loadedCheerio = parseHTML(body); + const loadPage = async (url: string) => { + const headers = { + 'Accept': + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': + 'zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en;q=0.6,en-GB;q=0.5,en-US;q=0.4', + 'Cache-Control': 'no-cache', + }; - const novel: Plugin.SourceNovel = { - path: novelPath, - chapters: [], - name: loadedCheerio("#bookDetailWrapper .book-title").text() - }; + const body = await fetchText(url, { headers }); + const pageCheerio = parseHTML(body); + await addPage(pageCheerio); + pageHasNextPage = + pageCheerio('#footlink a:last').text() === '下一页' ? true : false; + return { pageCheerio, pageHasNextPage }; + }; - novel.cover = loadedCheerio("#bookDetailWrapper img.book-cover").attr( - "src" - ); + let url = this.site + chapterPath; + do { + const page = await loadPage(url); + hasNextPage = page.pageHasNextPage; + if (hasNextPage === true) { + pageNumber++; + url = url.replace(/\.html/gi, `_${pageNumber}` + '.html'); + } + } while (hasNextPage === true); - novel.summary = loadedCheerio("#bookSummary content").text(); + return chapterText; + } - novel.author = loadedCheerio( - "#bookDetailWrapper .book-rand-a a" - ).text(); + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}/search/${encodeURI(searchTerm)}_${pageNo}.html`; - const meta = loadedCheerio("#bookDetailWrapper .book-meta").text() - novel.status = meta.includes("完结") ? NovelStatus.Completed : NovelStatus.Ongoing; + const body = await fetchText(url); + if (body === '') throw Error('无法获取搜索结果,请检查网络'); - novel.genres = loadedCheerio(".tag-small.red") - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); + const pageCheerio = parseHTML(body); - // Table of Content is on a different page than the summary page - let chapter: Plugin.ChapterItem[] = []; + const novels: Plugin.NovelItem[] = []; - const idPattern = /\/(\d+)\.html/; - const novelId = url.match(idPattern)?.[1]; + // const addPage = async (pageCheerio: CheerioAPI, redirect: string) => { + // const loadSearchResults = () => { + // pageCheerio(".book-ol .book-layout").each((i, el) => { + // let nUrl = pageCheerio(el).attr("href"); - const chaptersUrl = - this.site + loadedCheerio("#btnReadBook").attr("href"); - const chaptersBody = await fetchText(chaptersUrl); + // const novelName = pageCheerio(el) + // .find(".book-title") + // .text(); + // const novelCover = pageCheerio(el) + // .find("div.book-cover > img") + // .attr("data-src"); + // const novelUrl = this.site + nUrl; - const chaptersLoadedCheerio = parseHTML(chaptersBody); + // if (!nUrl) return; - let volumeName: string, chapterId: number; + // novels.push({ + // name: novelName, + // url: novelUrl, + // cover: novelCover, + // }); + // }); + // }; - chaptersLoadedCheerio("#volumes .chapter-li:not(.volume-cover)").each((i, el) => { - if (chaptersLoadedCheerio(el).hasClass("chapter-bar")) { - volumeName = chaptersLoadedCheerio(el).text(); - return; - } else { - const urlPart = chaptersLoadedCheerio(el) - .find(".chapter-li-a") - .attr("href"); - const chapterIdMatch = urlPart?.match(idPattern); + // const novelResults = pageCheerio(".book-ol a.book-layout"); + // if (novelResults.length === 0) { + // } else { + // loadSearchResults(); + // } - // Sometimes the href attribute does not contain the url, but javascript:cid(0). - // Increment the previous chapter ID should result in the right URL - if (chapterIdMatch) { - chapterId = +chapterIdMatch[1]; - } else { - chapterId++; - } - } + // if (redirect.length) { + // novels.length = 0; + // const novelName = pageCheerio( + // "#bookDetailWrapper .book-title" + // ).text(); - const chapterUrl = `/novel/${novelId}/${chapterId}.html`; - const chapterName = - volumeName + - " — " + - chaptersLoadedCheerio(el).find(".chapter-index").text().trim(); - const releaseDate = null; + // const novelCover = pageCheerio( + // "#bookDetailWrapper div.book-cover > img" + // ).attr("src"); + // const novelUrl = + // this.site + + // pageCheerio("#btnReadBook").attr("href")?.slice(0, -8) + + // ".html"; + // novels.push({ + // name: novelName, + // url: novelUrl, + // cover: novelCover, + // }); + // } + // }; - if (!chapterId) return; + // NOTE: don't know redirect is for what, comment out for now - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - path: chapterUrl, - }); - }); + // const redirect = pageCheerio("div.book-layout").text(); + // await addPage(pageCheerio, redirect); - novel.chapters = chapter; + pageCheerio('.book-ol .book-layout').each((i, el) => { + let nUrl = pageCheerio(el).attr('href'); - return novel; - } + const novelName = pageCheerio(el).find('.book-title').text(); + const novelCover = pageCheerio(el) + .find('div.book-cover > img') + .attr('data-src'); - async parseChapter(chapterPath: string): Promise { - let chapterName, - chapterText = "", - hasNextPage, - pageHasNextPage, - pageText = ""; - let pageNumber = 1; + if (!nUrl) return; - /* - * TODO: Maybe there are other ways to get the translation table - * It is embed and encrypted inside readtool.js - * UPDATE: Decrypted, see skillgg - */ - // const mapping_dict = { - // '“': '「', - // '’': '』', - // '': '是', - // '': '不', - // '': '好', - // '': '个', - // '': '开', - // '': '样', - // '': '想', - // '': '说', - // '': '年', - // '': '那', - // '': '她', - // '': '美', - // '': '自', - // '': '家', - // '': '而', - // '': '去', - // '': '都', - // '': '于', - // '': '舔', - // '': '他', - // '': '只', - // '': '看', - // '': '来', - // '': '用', - // '': '道', - // '': '得', - // '': '乳', - // '': '茎', - // '': '肉', - // '': '胸', - // '': '淫', - // '': '性', - // '': '骚', - // '”': '」', - // '': '的', - // '': '当', - // '': '人', - // '': '有', - // '': '上', - // '': '到', - // '': '地', - // '': '中', - // '': '生', - // '': '着', - // '': '和', - // '': '起', - // '': '交', - // '': '以', - // '': '可', - // '': '过', - // '': '能', - // '': '多', - // '': '心', - // '': '小', - // '': '成', - // '': '了', - // '': '把', - // '': '发', - // '': '第', - // '': '子', - // '': '事', - // '': '阴', - // '': '欲', - // '': '里', - // '': '私', - // '': '臀', - // '': '脱', - // '': '唇', - // '‘': '『', - // '': '一', - // '': '我', - // '': '在', - // '': '这', - // '': '们', - // '': '时', - // '': '为', - // '': '你', - // '': '国', - // '': '就', - // '': '要', - // '': '也', - // '': '后', - // '': '没', - // '': '下', - // '': '天', - // '': '对', - // '': '然', - // '': '学', - // '': '之', - // '': '出', - // '': '没', - // '': '如', - // '': '还', - // '': '大', - // '': '作', - // '': '种', - // '': '液', - // '': '呻', - // '': '射', - // '': '穴', - // '': '么', - // '': '裸', - // }; - const skillgg: Record = { - "\u201c": "\u300c", - "\u201d": "\u300d", - "\u2018": "\u300e", - "\u2019": "\u300f", - "\ue82c": "\u7684", - "\ue852": "\u4e00", - "\ue82d": "\u662f", - "\ue819": "\u4e86", - "\ue856": "\u6211", - "\ue857": "\u4e0d", - "\ue816": "\u4eba", - "\ue83c": "\u5728", - "\ue830": "\u4ed6", - "\ue82e": "\u6709", - "\ue836": "\u8fd9", - "\ue859": "\u4e2a", - "\ue80a": "\u4e0a", - "\ue855": "\u4eec", - "\ue842": "\u6765", - "\ue858": "\u5230", - "\ue80b": "\u65f6", - "\ue81f": "\u5927", - "\ue84a": "\u5730", - "\ue853": "\u4e3a", - "\ue81e": "\u5b50", - "\ue822": "\u4e2d", - "\ue813": "\u4f60", - "\ue85b": "\u8bf4", - "\ue807": "\u751f", - "\ue818": "\u56fd", - "\ue810": "\u5e74", - "\ue812": "\u7740", - "\ue851": "\u5c31", - "\ue801": "\u90a3", - "\ue80c": "\u548c", - "\ue815": "\u8981", - "\ue84c": "\u5979", - "\ue840": "\u51fa", - "\ue848": "\u4e5f", - "\ue835": "\u5f97", - "\ue800": "\u91cc", - "\ue826": "\u540e", - "\ue863": "\u81ea", - "\ue861": "\u4ee5", - "\ue854": "\u4f1a", - "\ue827": "\u5bb6", - "\ue83b": "\u53ef", - "\ue85d": "\u4e0b", - "\ue84d": "\u800c", - "\ue862": "\u8fc7", - "\ue81c": "\u5929", - "\ue81d": "\u53bb", - "\ue860": "\u80fd", - "\ue843": "\u5bf9", - "\ue82f": "\u5c0f", - "\ue802": "\u591a", - "\ue831": "\u7136", - "\ue84b": "\u4e8e", - "\ue837": "\u5fc3", - "\ue829": "\u5b66", - "\ue85e": "\u4e48", - "\ue83a": "\u4e4b", - "\ue832": "\u90fd", - "\ue808": "\u597d", - "\ue841": "\u770b", - "\ue821": "\u8d77", - "\ue845": "\u53d1", - "\ue803": "\u5f53", - "\ue828": "\u6ca1", - "\ue81b": "\u6210", - "\ue83e": "\u53ea", - "\ue820": "\u5982", - "\ue84e": "\u4e8b", - "\ue85a": "\u628a", - "\ue806": "\u8fd8", - "\ue83f": "\u7528", - "\ue833": "\u7b2c", - "\ue811": "\u6837", - "\ue804": "\u9053", - "\ue814": "\u60f3", - "\ue80f": "\u4f5c", - "\ue84f": "\u79cd", - "\ue80e": "\u5f00", - "\ue823": "\u7f8e", - "\ue849": "\u4e73", - "\ue805": "\u9634", - "\ue809": "\u6db2", - "\ue81a": "\u830e", - "\ue844": "\u6b32", - "\ue847": "\u547b", - "\ue850": "\u8089", - "\ue824": "\u4ea4", - "\ue85f": "\u6027", - "\ue817": "\u80f8", - "\ue85c": "\u79c1", - "\ue838": "\u7a74", - "\ue82a": "\u6deb", - "\ue83d": "\u81c0", - "\ue82b": "\u8214", - "\ue80d": "\u5c04", - "\ue839": "\u8131", - "\ue834": "\u88f8", - "\ue846": "\u9a9a", - "\ue825": "\u5507", - }; - const addPage = async (pageCheerio: CheerioAPI) => { - const formatPage = async () => { - // Remove JS - pageCheerio("#acontentz .adsbygoogle").remove(); - pageCheerio("#acontentz script").remove(); + novels.push({ + name: novelName, + path: nUrl, + cover: novelCover, + }); + }); - // Load lazyloaded images - pageCheerio("#acontentz img.imagecontent").each((i, el) => { - // Sometimes images are either in data-src or src - const imgSrc = - pageCheerio(el).attr("data-src") || - pageCheerio(el).attr("src"); - if (imgSrc) { - // Clean up img element - pageCheerio(el) - .attr("src", imgSrc) - .removeAttr("data-src") - .removeClass("lazyload"); - } - }); + return novels; + } - // Recover the original character - pageText = pageCheerio("#acontentz").html() || ""; - pageText = pageText.replace( - /./g, - (char) => skillgg[char] || char - ); + async fetchImage(url: string): Promise { + const headers = new Headers({ + 'Referer': `${this.site}/`, + }); + return await fetchFile(url, { headers }); + } - return Promise.resolve(); - }; - - await formatPage(); - chapterName = - pageCheerio("#atitle + h3").text() + - " — " + - pageCheerio("#atitle").text(); - if (chapterText === "") { - chapterText = "

" + chapterName + "

"; - } - chapterText += pageText; - }; - - const loadPage = async (url: string) => { - const headers = { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "Accept-Language": "zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en;q=0.6,en-GB;q=0.5,en-US;q=0.4", - "Cache-Control": "no-cache", - } - - const body = await fetchText(url, { headers }); - const pageCheerio = parseHTML(body); - await addPage(pageCheerio); - pageHasNextPage = - pageCheerio("#footlink a:last").text() === "下一页" - ? true - : false; - return { pageCheerio, pageHasNextPage }; - }; - - let url = this.site + chapterPath; - do { - const page = await loadPage(url); - hasNextPage = page.pageHasNextPage; - if (hasNextPage === true) { - pageNumber++; - url = url.replace( - /\.html/gi, - `_${pageNumber}` + ".html" - ); - } - } while (hasNextPage === true); - - return chapterText; - } - - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - const url = `${this.site}/search/${encodeURI(searchTerm)}_${pageNo}.html`; - - const body = await fetchText(url); - if (body === '') throw Error('无法获取搜索结果,请检查网络'); - - const pageCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - // const addPage = async (pageCheerio: CheerioAPI, redirect: string) => { - // const loadSearchResults = () => { - // pageCheerio(".book-ol .book-layout").each((i, el) => { - // let nUrl = pageCheerio(el).attr("href"); - - // const novelName = pageCheerio(el) - // .find(".book-title") - // .text(); - // const novelCover = pageCheerio(el) - // .find("div.book-cover > img") - // .attr("data-src"); - // const novelUrl = this.site + nUrl; - - // if (!nUrl) return; - - // novels.push({ - // name: novelName, - // url: novelUrl, - // cover: novelCover, - // }); - // }); - // }; - - // const novelResults = pageCheerio(".book-ol a.book-layout"); - // if (novelResults.length === 0) { - // } else { - // loadSearchResults(); - // } - - // if (redirect.length) { - // novels.length = 0; - // const novelName = pageCheerio( - // "#bookDetailWrapper .book-title" - // ).text(); - - // const novelCover = pageCheerio( - // "#bookDetailWrapper div.book-cover > img" - // ).attr("src"); - // const novelUrl = - // this.site + - // pageCheerio("#btnReadBook").attr("href")?.slice(0, -8) + - // ".html"; - // novels.push({ - // name: novelName, - // url: novelUrl, - // cover: novelCover, - // }); - // } - // }; - - // NOTE: don't know redirect is for what, comment out for now - - // const redirect = pageCheerio("div.book-layout").text(); - // await addPage(pageCheerio, redirect); - - pageCheerio(".book-ol .book-layout").each((i, el) => { - let nUrl = pageCheerio(el).attr("href"); - - const novelName = pageCheerio(el) - .find(".book-title") - .text(); - const novelCover = pageCheerio(el) - .find("div.book-cover > img") - .attr("data-src"); - - if (!nUrl) return; - - novels.push({ - name: novelName, - path: nUrl, - cover: novelCover, - }); - }); - - return novels; - } - - async fetchImage(url: string): Promise { - const headers = new Headers( - { - "Referer": `${this.site}/`, - } - ) - return await fetchFile(url, { headers }); - } - - filters = { - rank: { - label: "排行榜", - value: "monthvisit", - options: [ - { label: "月点击榜", value: "monthvisit" }, - { label: "周点击榜", value: "weekvisit" }, - { label: "月推荐榜", value: "monthvote" }, - { label: "周推荐榜", value: "weekvote" }, - { label: "月鲜花榜", value: "monthflower" }, - { label: "周鲜花榜", value: "weekflower" }, - { label: "月鸡蛋榜", value: "monthegg" }, - { label: "周鸡蛋榜", value: "weekegg" }, - { label: "最近更新", value: "lastupdate" }, - { label: "最新入库", value: "postdate" }, - { label: "收藏榜", value: "goodnum" }, - { label: "新书榜", value: "newhot" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + rank: { + label: '排行榜', + value: 'monthvisit', + options: [ + { label: '月点击榜', value: 'monthvisit' }, + { label: '周点击榜', value: 'weekvisit' }, + { label: '月推荐榜', value: 'monthvote' }, + { label: '周推荐榜', value: 'weekvote' }, + { label: '月鲜花榜', value: 'monthflower' }, + { label: '周鲜花榜', value: 'weekflower' }, + { label: '月鸡蛋榜', value: 'monthegg' }, + { label: '周鸡蛋榜', value: 'weekegg' }, + { label: '最近更新', value: 'lastupdate' }, + { label: '最新入库', value: 'postdate' }, + { label: '收藏榜', value: 'goodnum' }, + { label: '新书榜', value: 'newhot' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new Linovelib(); diff --git a/plugins/english/NovelOnline.ts b/plugins/english/NovelOnline.ts index fbdf410..fe48e5d 100644 --- a/plugins/english/NovelOnline.ts +++ b/plugins/english/NovelOnline.ts @@ -1,204 +1,209 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import qs from "qs"; +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import qs from 'qs'; class NovelsOnline implements Plugin.PluginBase { - id = "NO.net"; - name = "novelsOnline"; - site = "https://novelsonline.net"; - icon = "src/en/novelsonline/icon.png"; - version = "1.0.0"; + id = 'NO.net'; + name = 'novelsOnline'; + site = 'https://novelsonline.net'; + icon = 'src/en/novelsonline/icon.png'; + version = '1.0.0'; - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = this.site; - if (filters.genre.value !== '') - link += `/category/${filters.genre.value}/`; - else - link += `/top-novel/`; - - link += page - link += `?change_type=${filters.sort.value}` + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site; + if (filters.genre.value !== '') link += `/category/${filters.genre.value}/`; + else link += `/top-novel/`; - const body = await fetchApi(link).then((r) => r.text()); + link += page; + link += `?change_type=${filters.sort.value}`; - const loadedCheerio = parseHTML(body); - const novel: Plugin.NovelItem[] = []; - - loadedCheerio('.top-novel-block').each((i,el) => { - const novelName = loadedCheerio(el).find('h2').text(); - const novelCover = loadedCheerio(el).find('.top-novel-cover img').attr('src'); - const novelUrl = loadedCheerio(el).find('h2 a').attr('href'); - if (!novelUrl) return; + const body = await fetchApi(link).then(r => r.text()); - novel.push({ - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }) - }) - return novel; - } + const loadedCheerio = parseHTML(body); + const novel: Plugin.NovelItem[] = []; - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath).then((res) => res.text()); - let $ = parseHTML(result); + loadedCheerio('.top-novel-block').each((i, el) => { + const novelName = loadedCheerio(el).find('h2').text(); + const novelCover = loadedCheerio(el) + .find('.top-novel-cover img') + .attr('src'); + const novelUrl = loadedCheerio(el).find('h2 a').attr('href'); + if (!novelUrl) return; - const novel: Plugin.SourceNovel = { - path: novelPath, - name: $("h1").text() || 'Untitled', - cover: $(".novel-cover").find("a > img").attr("src"), - chapters: [], - }; + novel.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + }); + return novel; + } - $('.novel-detail-item').each((i,el) => { - let detailName = $(el).find('h6').text(); - let detail = $(el).find('.novel-detail-body'); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath).then(res => + res.text(), + ); + let $ = parseHTML(result); - switch(detailName){ - case "Description": - novel.summary = detail.text(); - break; - case "Genre": - novel.genres = detail.find('li') - .map((_, el) => $(el).text()) - .get() - .join(', '); - break; - case "Author(s)": - novel.author = detail.find('li') - .map((_, el) => $(el).text()) - .get() - .join(', '); - break; - case "Status": - novel.status = detail.text().trim(); - break; - } - }) + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $('h1').text() || 'Untitled', + cover: $('.novel-cover').find('a > img').attr('src'), + chapters: [], + }; - novel.chapters = $("ul.chapter-chs > li > a") - .map((_, el) => { - const chapterUrl = $(el).attr("href"); - const chapterName = $(el).text(); + $('.novel-detail-item').each((i, el) => { + let detailName = $(el).find('h6').text(); + let detail = $(el).find('.novel-detail-body'); - return { - name: chapterName, - path: chapterUrl?.replace(this.site, ''), - } as Plugin.ChapterItem; - }) - .get(); - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath).then((res) => res.text()); - const loadedCheerio = parseHTML(result); - - const chapterText = loadedCheerio("#contentall").html() || ""; - - return chapterText; - } - - async searchNovels(searchTerm: string, pageNo: number): Promise { - const result = await fetchApi( - "https://novelsonline.net/sResults.php", - { - headers: { - Accept: "*/*", - "Accept-Language": "pl,en-US;q=0.7,en;q=0.3", - "Content-Type": - "application/x-www-form-urlencoded; charset=UTF-8", - }, - method: "POST", - body: qs.stringify({ q: searchTerm }), - }, - ).then((res) => res.text()); - - const $ = parseHTML(result); - - const headers = $("li"); - return headers - .map((i, h) => { - const novelName = $(h).text(); - const novelUrl = $(h).find("a").attr("href"); - const novelCover = $(h).find("img").attr("src"); - - if (!novelUrl) return - - return { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; - }) + switch (detailName) { + case 'Description': + novel.summary = detail.text(); + break; + case 'Genre': + novel.genres = detail + .find('li') + .map((_, el) => $(el).text()) .get() - .filter((sr) => sr !== null) - } + .join(', '); + break; + case 'Author(s)': + novel.author = detail + .find('li') + .map((_, el) => $(el).text()) + .get() + .join(', '); + break; + case 'Status': + novel.status = detail.text().trim(); + break; + } + }); - async fetchImage(url: string): Promise { - return fetchFile(url); - } + novel.chapters = $('ul.chapter-chs > li > a') + .map((_, el) => { + const chapterUrl = $(el).attr('href'); + const chapterName = $(el).text(); - filters = { - sort: { - value: "top_rated", - label: "Sort by", - options: [ - { label: "Top Rated", value: "top_rated" }, - { label: "Most Viewed", value: "view" }, - ], - type: FilterTypes.Picker, - }, - genre: { - value: "", - label: "Category", - options: [ - { label: "None", value: "" }, - { label: "Action", value: "action" }, - { label: "Adventure", value: "adventure" }, - { label: "Celebrity", value: "celebrity" }, - { label: "Comedy", value: "comedy" }, - { label: "Drama", value: "drama" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Gender Bender", value: "gender-bender" }, - { label: "Harem", value: "harem" }, - { label: "Historical", value: "historical" }, - { label: "Horror", value: "horror" }, - { label: "Josei", value: "josei" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Mecha", value: "mecha" }, - { label: "Mystery", value: "mystery" }, - { label: "Psychological", value: "psychological" }, - { label: "Romance", value: "romance" }, - { label: "School Life", value: "school-life" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Seinen", value: "seinen" }, - { label: "Shotacon", value: "shotacon" }, - { label: "Shoujo", value: "shoujo" }, - { label: "Shoujo Ai", value: "shoujo-ai" }, - { label: "Shounen", value: "shounen" }, - { label: "Shounen Ai", value: "shounen-ai" }, - { label: "Slice of Life", value: "slice-of-life" }, - { label: "Sports", value: "sports" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Xianxia", value: "xianxia" }, - { label: "Xuanhuan", value: "xuanhuan" }, - { label: "Yaoi", value: "yaoi" }, - { label: "Yuri", value: "yuri" } - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + return { + name: chapterName, + path: chapterUrl?.replace(this.site, ''), + } as Plugin.ChapterItem; + }) + .get(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(result); + + const chapterText = loadedCheerio('#contentall').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const result = await fetchApi('https://novelsonline.net/sResults.php', { + headers: { + Accept: '*/*', + 'Accept-Language': 'pl,en-US;q=0.7,en;q=0.3', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + method: 'POST', + body: qs.stringify({ q: searchTerm }), + }).then(res => res.text()); + + const $ = parseHTML(result); + + const headers = $('li'); + return headers + .map((i, h) => { + const novelName = $(h).text(); + const novelUrl = $(h).find('a').attr('href'); + const novelCover = $(h).find('img').attr('src'); + + if (!novelUrl) return; + + return { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + }) + .get() + .filter(sr => sr !== null); + } + + async fetchImage(url: string): Promise { + return fetchFile(url); + } + + filters = { + sort: { + value: 'top_rated', + label: 'Sort by', + options: [ + { label: 'Top Rated', value: 'top_rated' }, + { label: 'Most Viewed', value: 'view' }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: '', + label: 'Category', + options: [ + { label: 'None', value: '' }, + { label: 'Action', value: 'action' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Celebrity', value: 'celebrity' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Josei', value: 'josei' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Romance', value: 'romance' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shotacon', value: 'shotacon' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shoujo Ai', value: 'shoujo-ai' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Shounen Ai', value: 'shounen-ai' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Sports', value: 'sports' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new NovelsOnline(); diff --git a/plugins/english/StorySeedling.ts b/plugins/english/StorySeedling.ts index 60ae400..9dd258f 100644 --- a/plugins/english/StorySeedling.ts +++ b/plugins/english/StorySeedling.ts @@ -1,140 +1,164 @@ -import { Plugin } from "@typings/plugin"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { load as parseHTML } from "cheerio"; -import { defaultCover } from "@libs/defaultCover"; +import { Plugin } from '@typings/plugin'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; class StorySeedlingPlugin implements Plugin.PluginBase { - id = "storyseedling"; - name = "StorySeedling"; - icon = "src/en/storyseedling/icon.png"; - site = "https://storyseedling.com/"; - version = "1.0.0"; + id = 'storyseedling'; + name = 'StorySeedling'; + icon = 'src/en/storyseedling/icon.png'; + site = 'https://storyseedling.com/'; + version = '1.0.0'; - async popularNovels(pageNo: number): Promise { - let novels: Plugin.NovelItem[] = []; - let url = "https://storyseedling.com/ajax"; + async popularNovels(pageNo: number): Promise { + let novels: Plugin.NovelItem[] = []; + let url = 'https://storyseedling.com/ajax'; - const postUrl = `https://storyseedling.com/browse/`; - const body = await fetchApi(postUrl).then((r) => r.text()); + const postUrl = `https://storyseedling.com/browse/`; + const body = await fetchApi(postUrl).then(r => r.text()); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - var postValue = loadedCheerio("div[ax-load][x-data]").attr("x-data")?.replace("browse('","").replace("')", "") as string; + var postValue = loadedCheerio('div[ax-load][x-data]') + .attr('x-data') + ?.replace("browse('", '') + .replace("')", '') as string; - var data = new FormData(); - data.set("search", ""); - data.set("orderBy", "recent"); - data.set("curpage", pageNo.toString()); - data.set("post", postValue); - data.set("action", "fetch_browse"); + var data = new FormData(); + data.set('search', ''); + data.set('orderBy', 'recent'); + data.set('curpage', pageNo.toString()); + data.set('post', postValue); + data.set('action', 'fetch_browse'); - var response = await(await fetchApi(url, {body: data, method: "POST"})).json(); + var response = await ( + await fetchApi(url, { body: data, method: 'POST' }) + ).json(); - response.data.posts.forEach((element: any) => { - const novel = { - name: element.title, - cover: element.thumbnail, - path: element.permalink.replace(this.site, ""), - }; - novels.push(novel); - }); + response.data.posts.forEach((element: any) => { + const novel = { + name: element.title, + cover: element.thumbnail, + path: element.permalink.replace(this.site, ''), + }; + novels.push(novel); + }); - return novels; + return novels; + } + + async parseNovel(novelPath: string): Promise { + var site = this.site; + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + }; + + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + novel.name = loadedCheerio('h1').text().trim(); + + //novel.artist = ""; + //novel.author = ""; + novel.cover = loadedCheerio( + 'img[x-ref="art"].w-full.rounded.shadow-md', + ).attr('src'); + if (!novel.cover) { + novel.cover = defaultCover; } + let genres: string[] = []; + loadedCheerio( + 'section[x-data="{ tab: location.hash.substr(1) || \'chapters\' }"].relative > div > div > div.flex.flex-wrap > a', + ).each(function () { + genres.push(loadedCheerio(this).text().trim()); + }); + novel.genres = genres.join(', '); + // novel.status = NovelStatus.Completed; + novel.summary = loadedCheerio('div.mb-4.order-2:not(.lg\\:grid-in-buttons)') + .text() + .trim() + .replace(/(\r\n|\n|\r)/gm, ''); - async parseNovel(novelPath: string): Promise { - var site = this.site; - const novel: Plugin.SourceNovel = { - path: novelPath, - name: "" - }; + let chapters: Plugin.ChapterItem[] = []; - const body = await fetchApi(this.site+novelPath).then((r) => r.text()); + loadedCheerio( + 'div.grid.w-full.grid-cols-1.gap-4.md\\:grid-cols-2 > a', + ).each(function () { + if (loadedCheerio(this).find('> div').length == 2) { + return; + } + let name = loadedCheerio(this).find('.truncate').text().trim(); + let url = loadedCheerio(this).attr('href') as string; + let releaseTime = loadedCheerio(this) + .find('div > div > small') + .text() + .trim(); + let chapterNumber = name.split('-')[0].trim().split(' ')[1]; + const chapter: Plugin.ChapterItem = { + name: name, + path: url.replace(site, ''), + releaseTime: releaseTime, + chapterNumber: parseInt(chapterNumber), + }; + chapters.push(chapter); + }); + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + let t = loadedCheerio('div.justify-center > div.mb-4'); + let chapterText = t.html() || ''; - const loadedCheerio = parseHTML(body); - - novel.name = loadedCheerio("h1").text().trim(); + return chapterText; + } - //novel.artist = ""; - //novel.author = ""; - novel.cover = loadedCheerio('img[x-ref="art"].w-full.rounded.shadow-md').attr("src"); - if (!novel.cover) { - novel.cover = defaultCover; - } - let genres:string[] = []; - loadedCheerio('section[x-data="{ tab: location.hash.substr(1) || \'chapters\' }"].relative > div > div > div.flex.flex-wrap > a').each(function(){ - genres.push(loadedCheerio(this).text().trim()); - }); - novel.genres = genres.join(", "); - // novel.status = NovelStatus.Completed; - novel.summary = loadedCheerio("div.mb-4.order-2:not(.lg\\:grid-in-buttons)").text().trim().replace(/(\r\n|\n|\r)/gm, ""); + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + let novels: Plugin.NovelItem[] = []; + let url = 'https://storyseedling.com/ajax'; - let chapters: Plugin.ChapterItem[] = []; + const postUrl = `https://storyseedling.com/browse/`; + const body = await fetchApi(postUrl).then(r => r.text()); - loadedCheerio("div.grid.w-full.grid-cols-1.gap-4.md\\:grid-cols-2 > a").each(function() { - if(loadedCheerio(this).find("> div").length == 2){ - return; - } - let name = loadedCheerio(this).find(".truncate").text().trim(); - let url = loadedCheerio(this).attr("href") as string; - let releaseTime = loadedCheerio(this).find("div > div > small").text().trim(); - let chapterNumber = name.split("-")[0].trim().split(" ")[1]; - const chapter: Plugin.ChapterItem = { - name: name, - path: url.replace(site, ""), - releaseTime: releaseTime, - chapterNumber: parseInt(chapterNumber), - }; - chapters.push(chapter); - }); - novel.chapters = chapters; - return novel; - } - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site+chapterPath).then((r) => r.text()); + const loadedCheerio = parseHTML(body); - const loadedCheerio = parseHTML(body); - let t = loadedCheerio("div.justify-center > div.mb-4"); - let chapterText = t.html() || ''; + var postValue = loadedCheerio('div[ax-load][x-data]') + .attr('x-data') + ?.replace("browse('", '') + .replace("')", '') as string; - return chapterText; - } - - async searchNovels(searchTerm: string, pageNo: number): Promise { - let novels: Plugin.NovelItem[] = []; - let url = "https://storyseedling.com/ajax"; + var data = new FormData(); + data.set('search', searchTerm); + data.set('orderBy', 'recent'); + data.set('curpage', pageNo.toString()); + data.set('post', postValue); + data.set('action', 'fetch_browse'); - const postUrl = `https://storyseedling.com/browse/`; - const body = await fetchApi(postUrl).then((r) => r.text()); + var response = await ( + await fetchApi(url, { body: data, method: 'POST' }) + ).json(); - const loadedCheerio = parseHTML(body); + response.data.posts.forEach((element: any) => { + const novel = { + name: element.title, + cover: element.thumbnail, + path: element.permalink.replace(this.site, ''), + }; + novels.push(novel); + }); - var postValue = loadedCheerio("div[ax-load][x-data]").attr("x-data")?.replace("browse('","").replace("')", "") as string; - - var data = new FormData(); - data.set("search", searchTerm); - data.set("orderBy", "recent"); - data.set("curpage", pageNo.toString()); - data.set("post", postValue); - data.set("action", "fetch_browse"); - - var response = await(await fetchApi(url, {body: data, method: "POST"})).json(); - - response.data.posts.forEach((element: any) => { - const novel = { - name: element.title, - cover: element.thumbnail, - path: element.permalink.replace(this.site, ""), - }; - novels.push(novel); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new StorySeedlingPlugin(); diff --git a/plugins/english/allnovelfull.ts b/plugins/english/allnovelfull.ts index 2c341f8..2981af3 100644 --- a/plugins/english/allnovelfull.ts +++ b/plugins/english/allnovelfull.ts @@ -1,215 +1,214 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; class AllNovelFullPlugin implements Plugin.PluginBase { - id = "anf.net"; - name = "AllNovelFull"; - icon = "src/en/allnovelfull/icon.png"; - site = "https://allnovelfull.net"; - version = "1.0.0"; + id = 'anf.net'; + name = 'AllNovelFull'; + icon = 'src/en/allnovelfull/icon.png'; + site = 'https://allnovelfull.net'; + version = '1.0.0'; - parseNovels(loadedCheerio: CheerioAPI) { - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".col-truyen-main .list-truyen .row").each((i,el) => { - const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href"); - if (!novelUrl) return; + loadedCheerio('.col-truyen-main .list-truyen .row').each((i, el) => { + const novelUrl = loadedCheerio(el) + .find('h3.truyen-title > a') + .attr('href'); + if (!novelUrl) return; - const novelName = loadedCheerio(el) - .find("h3.truyen-title > a") - .text(); + const novelName = loadedCheerio(el).find('h3.truyen-title > a').text(); - let novelCover = loadedCheerio(el).find("img.cover").attr("src"); - if (novelCover && !isUrlAbsolute(novelCover)){ - novelCover = this.site + novelCover; - } + let novelCover = loadedCheerio(el).find('img.cover').attr('src'); + if (novelCover && !isUrlAbsolute(novelCover)) { + novelCover = this.site + novelCover; + } - const novel = { - path: novelUrl, - name: novelName, - cover: novelCover, - }; - novels.push(novel); + const novel = { + path: novelUrl, + name: novelName, + cover: novelCover, + }; + novels.push(novel); + }); + return novels; + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site; + + if (filters.genres.value.length) link += filters.genres.value; + else link += filters.order.value; + + link += `?page=${pageNo}`; + + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + const result = await fetchApi(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.book > img').attr('alt') || 'Untitled', + cover: this.site + loadedCheerio('.book > img').attr('src'), + summary: loadedCheerio('.desc-text').text().trim(), + chapters: [], + }; + + loadedCheerio('.info > div').each((i, el) => { + const detailName = loadedCheerio(el).find('h3').text(); + const detail = loadedCheerio(el) + .find('a') + .map((a, ex) => loadedCheerio(ex).text()) + .toArray() + .join(','); + + switch (detailName) { + case 'Author:': + novel.author = detail; + break; + case 'Status:': + novel.status = detail; + break; + case 'Genre:': + novel.genres = detail; + break; + } + }); + + const novelId = loadedCheerio('#rating').attr('data-novel-id'); + + const getChapters = async (id: string) => { + const chaptersUrl = this.site + '/ajax/chapter-option?novelId=' + id; + + const data = await fetchApi(chaptersUrl); + const chapters = await data.text(); + + loadedCheerio = parseHTML(chapters); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('select > option').each((i, el) => { + const chapterName = loadedCheerio(el).text(); + const chapterUrl = loadedCheerio(el).attr('value'); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl, }); - return novels; + }); + + return chapter; }; - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = this.site; - - if (filters.genres.value.length) - link += filters.genres.value; - else - link += filters.order.value; - - link += `?page=${pageNo}`; - - const result = await fetchApi(link); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); + if (novelId) { + novel.chapters = await getChapters(novelId); } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - const result = await fetchApi(url); - const body = await result.text(); + return novel; + } - let loadedCheerio = parseHTML(body); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".book > img").attr("alt") || 'Untitled', - cover: this.site + loadedCheerio(".book > img").attr("src"), - summary: loadedCheerio(".desc-text").text().trim(), - chapters: [], - }; + const loadedCheerio = parseHTML(body); - loadedCheerio(".info > div").each((i,el) => { - const detailName = loadedCheerio(el).find("h3").text(); - const detail = loadedCheerio(el) - .find('a') - .map((a,ex) => loadedCheerio(ex).text()) - .toArray() - .join(','); + const chapterText = loadedCheerio('#chapter-content').html() || ''; - switch (detailName) { - case 'Author:': - novel.author = detail; - break; - case 'Status:': - novel.status = detail; - break; - case 'Genre:': - novel.genres = detail; - break; - }}); + return chapterText; + } - const novelId = loadedCheerio("#rating").attr("data-novel-id"); + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const url = `${this.site}/search?keyword=${searchTerm}&page=${page}`; + const result = await fetchApi(url); + const body = await result.text(); - const getChapters = async (id: string) => { - const chaptersUrl = this.site + "/ajax/chapter-option?novelId=" + id; + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - const data = await fetchApi(chaptersUrl); - const chapters = await data.text(); + async fetchImage(url: string): Promise { + return await fetchFile(url); + } - loadedCheerio = parseHTML(chapters); - - const chapter: Plugin.ChapterItem[] = []; - - loadedCheerio("select > option").each((i,el) => { - const chapterName = loadedCheerio(el).text(); - const chapterUrl = loadedCheerio(el).attr("value"); - - if (!chapterUrl ) return; - - chapter.push({ - name: chapterName, - path: chapterUrl, - }); - }); - - return chapter; - } - - if (novelId) { - novel.chapters = await getChapters(novelId); - } - - return novel; - }; - - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio("#chapter-content").html() || ""; - - return chapterText; - }; - - async searchNovels( - searchTerm: string, - page: number - ): Promise { - const url = `${this.site}/search?keyword=${searchTerm}&page=${page}`; - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - }; - - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - order: { - value: "/most-popular", - label: "Order by", - options: [ - { label: "Latest Release", value: "/latest-release-novel" }, - { label: "Hot Novel", value: "/hot-novel" }, - { label: "Completed Novel", value: "/completed-novel" }, - { label: "Most Popular", value: "/most-popular" }, - ], - type: FilterTypes.Picker, - }, - genres: { - value: "", - label: "Genre", - options: [ - { label: "None", value: "" }, - { label: "Shounen", value: "/genre/Shounen" }, - { label: "Harem", value: "/genre/Harem" }, - { label: "Comedy", value: "/genre/Comedy" }, - { label: "Martial Arts", value: "/genre/Martial+Arts" }, - { label: "School Life", value: "/genre/School+Life" }, - { label: "Mystery", value: "/genre/Mystery" }, - { label: "Shoujo", value: "/genre/Shoujo" }, - { label: "Romance", value: "/genre/Romance" }, - { label: "Sci-fi", value: "/genre/Sci-fi" }, - { label: "Gender Bender", value: "/genre/Gender+Bender" }, - { label: "Mature", value: "/genre/Mature" }, - { label: "Fantasy", value: "/genre/Fantasy" }, - { label: "Horror", value: "/genre/Horror" }, - { label: "Drama", value: "/genre/Drama" }, - { label: "Tragedy", value: "/genre/Tragedy" }, - { label: "Supernatural", value: "/genre/Supernatural" }, - { label: "Ecchi", value: "/genre/Ecchi" }, - { label: "Xuanhuan", value: "/genre/Xuanhuan" }, - { label: "Adventure", value: "/genre/Adventure" }, - { label: "Action", value: "/genre/Action" }, - { label: "Psychological", value: "/genre/Psychological" }, - { label: "Xianxia", value: "/genre/Xianxia" }, - { label: "Wuxia", value: "/genre/Wuxia" }, - { label: "Historical", value: "/genre/Historical" }, - { label: "Slice of Life", value: "/genre/Slice+of+Life" }, - { label: "Seinen", value: "/genre/Seinen" }, - { label: "Lolicon", value: "/genre/Lolicon" }, - { label: "Adult", value: "/genre/Adult" }, - { label: "Josei", value: "/genre/Josei" }, - { label: "Sports", value: "/genre/Sports" }, - { label: "Smut", value: "/genre/Smut" }, - { label: "Mecha", value: "/genre/Mecha" }, - { label: "Yaoi", value: "/genre/Yaoi" }, - { label: "Shounen Ai", value: "/genre/Shounen+Ai" }, - { label: "Magical Realism", value: "/genre/Magical+Realism" }, - { label: "Video Games", value: "/genre/Video+Games" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + order: { + value: '/most-popular', + label: 'Order by', + options: [ + { label: 'Latest Release', value: '/latest-release-novel' }, + { label: 'Hot Novel', value: '/hot-novel' }, + { label: 'Completed Novel', value: '/completed-novel' }, + { label: 'Most Popular', value: '/most-popular' }, + ], + type: FilterTypes.Picker, + }, + genres: { + value: '', + label: 'Genre', + options: [ + { label: 'None', value: '' }, + { label: 'Shounen', value: '/genre/Shounen' }, + { label: 'Harem', value: '/genre/Harem' }, + { label: 'Comedy', value: '/genre/Comedy' }, + { label: 'Martial Arts', value: '/genre/Martial+Arts' }, + { label: 'School Life', value: '/genre/School+Life' }, + { label: 'Mystery', value: '/genre/Mystery' }, + { label: 'Shoujo', value: '/genre/Shoujo' }, + { label: 'Romance', value: '/genre/Romance' }, + { label: 'Sci-fi', value: '/genre/Sci-fi' }, + { label: 'Gender Bender', value: '/genre/Gender+Bender' }, + { label: 'Mature', value: '/genre/Mature' }, + { label: 'Fantasy', value: '/genre/Fantasy' }, + { label: 'Horror', value: '/genre/Horror' }, + { label: 'Drama', value: '/genre/Drama' }, + { label: 'Tragedy', value: '/genre/Tragedy' }, + { label: 'Supernatural', value: '/genre/Supernatural' }, + { label: 'Ecchi', value: '/genre/Ecchi' }, + { label: 'Xuanhuan', value: '/genre/Xuanhuan' }, + { label: 'Adventure', value: '/genre/Adventure' }, + { label: 'Action', value: '/genre/Action' }, + { label: 'Psychological', value: '/genre/Psychological' }, + { label: 'Xianxia', value: '/genre/Xianxia' }, + { label: 'Wuxia', value: '/genre/Wuxia' }, + { label: 'Historical', value: '/genre/Historical' }, + { label: 'Slice of Life', value: '/genre/Slice+of+Life' }, + { label: 'Seinen', value: '/genre/Seinen' }, + { label: 'Lolicon', value: '/genre/Lolicon' }, + { label: 'Adult', value: '/genre/Adult' }, + { label: 'Josei', value: '/genre/Josei' }, + { label: 'Sports', value: '/genre/Sports' }, + { label: 'Smut', value: '/genre/Smut' }, + { label: 'Mecha', value: '/genre/Mecha' }, + { label: 'Yaoi', value: '/genre/Yaoi' }, + { label: 'Shounen Ai', value: '/genre/Shounen+Ai' }, + { label: 'Magical Realism', value: '/genre/Magical+Realism' }, + { label: 'Video Games', value: '/genre/Video+Games' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new AllNovelFullPlugin(); diff --git a/plugins/english/bestlightnovel.ts b/plugins/english/bestlightnovel.ts index 8ba7f0e..cab25bb 100644 --- a/plugins/english/bestlightnovel.ts +++ b/plugins/english/bestlightnovel.ts @@ -1,279 +1,276 @@ -import { CheerioAPI, load as parseHTML} from "cheerio"; -import { Plugin } from "@typings/plugin"; -import { defaultCover } from "@libs/defaultCover"; -import { fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { FilterTypes, Filters } from "@libs/filterInputs"; - -class BLN implements Plugin.PluginBase { - id = "BLN"; - name = "BestLightNovel"; - icon = "src/en/bestlightnovel/icon.png"; - site = "https://bestlightnovel.com/"; - version = "1.0.0"; - - parseNovels(loadedCheerio: CheerioAPI) { - const novels: Plugin.NovelItem[] = []; - - loadedCheerio(".update_item.list_category").each((i,el) => { - const novelUrl = loadedCheerio(el).find("h3 > a").attr("href"); - if (!novelUrl) { - // TODO: Handle error - console.error("No novel url!"); - return; - } - - const novelName = loadedCheerio(el).find("h3 > a").text(); - const novelCover = loadedCheerio(el).find("img").attr("src"); - - const novel = { - name: novelName, - path: novelUrl.replace(this.site, ''), - cover: novelCover, - }; - - novels.push(novel); - }); - - return novels; - } - - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = this.site + "novel_list?" - link += 'type=' + filters.type.value; - link += '&category=' + filters.category.value; - link += '&state=' + filters.status.value; - link += '&page=' + page; - - const result = await fetch(link); - if (!result.ok) { - console.error(await result.text()); - // TODO: Cloudflare protection or other error - return []; - } - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async parseNovel(novelPath: string): Promise { - const result = await fetch(this.site + novelPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".truyen_info_right h1").text().trim() || "", - cover: loadedCheerio(".info_image img").attr("src") || defaultCover, - summary: loadedCheerio("#noidungm").text()?.trim() || "", - chapters: [], - }; - loadedCheerio('ul.truyen_info_right > li').each(function() { - const detailName = loadedCheerio(this).find('span').text(); - const detail = loadedCheerio(this) - .find('a') - .map((a,ex) => loadedCheerio(ex).text()) - .toArray() - .join(', '); - - switch(detailName) { - case "Author(s): ": - novel.author = detail; - break; - case "GENRES: ": - novel.genres = detail; - break; - case "STATUS : ": - novel.status = - detail === "Ongoing" - ? NovelStatus.Ongoing - : detail === "Completed" - ? NovelStatus.Completed - : NovelStatus.Unknown; - } - }) - - const chapter: Plugin.ChapterItem[] = []; - - loadedCheerio(".chapter-list div.row").each((i, el) => { - const chapterName = loadedCheerio(el).find("a").text().trim(); - const releaseDate = loadedCheerio(el) - .find("span:last") - .text() - .trim(); - - const months = [ - "jan", - "feb", - "mar", - "apr", - "may", - "jun", - "jul", - "aug", - "sep", - "oct", - "nov", - "dec", - ].join("|"); - const rx = new RegExp(`(${months})-(\\d{1,2})-(\\d{2})`,'i') - .exec(releaseDate); - if (!rx) return; - const year = 2000 + +rx[3]; - const month = months.indexOf(rx[1].toLowerCase()); - const day = +rx[2]; - - const chapterUrl = loadedCheerio(el).find("a").attr("href"); - if (!chapterUrl) { - // TODO: Handle error - console.error("No chapter url!"); - return; - } - - chapter.push({ - name: chapterName, - releaseTime: new Date(year, month, day).toISOString(), - path: chapterUrl.replace(this.site, ''), - }); - }); - - novel.chapters = chapter.reverse(); - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const result = await fetch(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio("#vung_doc").html() || ""; - - return chapterText; - } - - async searchNovels( - searchTerm: string, - page: number - ): Promise { - const url = `${this.site}search_novels/${searchTerm}?page=${page}`; - - const result = await fetch(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - status: { - label: "Status", - value: "all", - options: [ - { label: "ALL", value: "all" }, - { label: "Completed", value: "completed" }, - { label: "Ongoing", value: "ongoing" }, - ], - type: FilterTypes.Picker, - }, - type: { - value: "topview", - label: "Type", - options: [ - { label: "Recently updated", value: "latest" }, - { label: "Newest", value: "newest" }, - { label: "Top view", value: "topview" }, - ], - type: FilterTypes.Picker, - }, - category: { - label: "Category", - value: "all", - options: [ - { label: "ALL", value: "all" }, - { label: "Action", value: "1" }, - { label: "Adventure", value: "2" }, - { label: "Animals", value: "65" }, - { label: "Arts", value: "40" }, - { label: "Biographies", value: "41" }, - { label: "Business", value: "42" }, - { label: "Chinese", value: "3" }, - { label: "Comedy", value: "4" }, - { label: "Computers", value: "43" }, - { label: "Crafts, Hobbies", value: "45" }, - { label: "Drama", value: "5" }, - { label: "Education", value: "46" }, - { label: "English", value: "6" }, - { label: "Entertainment", value: "47" }, - { label: "Fantasy", value: "7" }, - { label: "Fiction", value: "48" }, - { label: "Gender Bender", value: "8" }, - { label: "Harem", value: "9" }, - { label: "Historical", value: "10" }, - { label: "History", value: "49" }, - { label: "Home", value: "50" }, - { label: "Horror", value: "11" }, - { label: "Humor", value: "51" }, - { label: "Investing", value: "52" }, - { label: "Josei", value: "12" }, - { label: "Korean", value: "13" }, - { label: "Literature", value: "53" }, - { label: "Lolicon", value: "14" }, - { label: "Martial Arts", value: "15" }, - { label: "Mature", value: "16" }, - { label: "Mecha", value: "17" }, - { label: "Memoirs", value: "54" }, - { label: "Mystery", value: "18" }, - { label: "Original", value: "19" }, - { label: "Other Books", value: "66" }, - { label: "Philosophy", value: "55" }, - { label: "Photography", value: "56" }, - { label: "Politics", value: "57" }, - { label: "Professional", value: "58" }, - { label: "Psychological", value: "20" }, - { label: "Reference", value: "59" }, - { label: "Reincarnation", value: "21" }, - { label: "Religion", value: "60" }, - { label: "Romance", value: "22" }, - { label: "School Life", value: "23" }, - { label: "School Stories", value: "67" }, - { label: "Sci-Fi", value: "24" }, - { label: "Seinen", value: "25" }, - { label: "Short Stories", value: "68" }, - { label: "Shotacon", value: "26" }, - { label: "Shoujo", value: "27" }, - { label: "Shoujo Ai", value: "28" }, - { label: "Shounen", value: "29" }, - { label: "Shounen Ai", value: "30" }, - { label: "Slice Of Life", value: "31" }, - { label: "Smut", value: "32" }, - { label: "Social Science", value: "61" }, - { label: "Spirituality", value: "62" }, - { label: "Sports", value: "33" }, - { label: "Supernatural", value: "34" }, - { label: "Teaser", value: "69" }, - { label: "Technical", value: "63" }, - { label: "Technology", value: "64" }, - { label: "Tragedy", value: "35" }, - { label: "Virtual Reality", value: "36" }, - { label: "Wuxia", value: "37" }, - { label: "Xianxia", value: "38" }, - { label: "Xuanhuan", value: "39" }, - - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; -} - -export default new BLN(); +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { Plugin } from '@typings/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { FilterTypes, Filters } from '@libs/filterInputs'; + +class BLN implements Plugin.PluginBase { + id = 'BLN'; + name = 'BestLightNovel'; + icon = 'src/en/bestlightnovel/icon.png'; + site = 'https://bestlightnovel.com/'; + version = '1.0.0'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.update_item.list_category').each((i, el) => { + const novelUrl = loadedCheerio(el).find('h3 > a').attr('href'); + if (!novelUrl) { + // TODO: Handle error + console.error('No novel url!'); + return; + } + + const novelName = loadedCheerio(el).find('h3 > a').text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + + const novel = { + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }; + + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site + 'novel_list?'; + link += 'type=' + filters.type.value; + link += '&category=' + filters.category.value; + link += '&state=' + filters.status.value; + link += '&page=' + page; + + const result = await fetch(link); + if (!result.ok) { + console.error(await result.text()); + // TODO: Cloudflare protection or other error + return []; + } + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const result = await fetch(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.truyen_info_right h1').text().trim() || '', + cover: loadedCheerio('.info_image img').attr('src') || defaultCover, + summary: loadedCheerio('#noidungm').text()?.trim() || '', + chapters: [], + }; + loadedCheerio('ul.truyen_info_right > li').each(function () { + const detailName = loadedCheerio(this).find('span').text(); + const detail = loadedCheerio(this) + .find('a') + .map((a, ex) => loadedCheerio(ex).text()) + .toArray() + .join(', '); + + switch (detailName) { + case 'Author(s): ': + novel.author = detail; + break; + case 'GENRES: ': + novel.genres = detail; + break; + case 'STATUS : ': + novel.status = + detail === 'Ongoing' + ? NovelStatus.Ongoing + : detail === 'Completed' + ? NovelStatus.Completed + : NovelStatus.Unknown; + } + }); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('.chapter-list div.row').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text().trim(); + const releaseDate = loadedCheerio(el).find('span:last').text().trim(); + + const months = [ + 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'jun', + 'jul', + 'aug', + 'sep', + 'oct', + 'nov', + 'dec', + ].join('|'); + const rx = new RegExp(`(${months})-(\\d{1,2})-(\\d{2})`, 'i').exec( + releaseDate, + ); + if (!rx) return; + const year = 2000 + +rx[3]; + const month = months.indexOf(rx[1].toLowerCase()); + const day = +rx[2]; + + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + if (!chapterUrl) { + // TODO: Handle error + console.error('No chapter url!'); + return; + } + + chapter.push({ + name: chapterName, + releaseTime: new Date(year, month, day).toISOString(), + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#vung_doc').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const url = `${this.site}search_novels/${searchTerm}?page=${page}`; + + const result = await fetch(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async fetchImage(url: string): Promise { + return await fetchFile(url); + } + + filters = { + status: { + label: 'Status', + value: 'all', + options: [ + { label: 'ALL', value: 'all' }, + { label: 'Completed', value: 'completed' }, + { label: 'Ongoing', value: 'ongoing' }, + ], + type: FilterTypes.Picker, + }, + type: { + value: 'topview', + label: 'Type', + options: [ + { label: 'Recently updated', value: 'latest' }, + { label: 'Newest', value: 'newest' }, + { label: 'Top view', value: 'topview' }, + ], + type: FilterTypes.Picker, + }, + category: { + label: 'Category', + value: 'all', + options: [ + { label: 'ALL', value: 'all' }, + { label: 'Action', value: '1' }, + { label: 'Adventure', value: '2' }, + { label: 'Animals', value: '65' }, + { label: 'Arts', value: '40' }, + { label: 'Biographies', value: '41' }, + { label: 'Business', value: '42' }, + { label: 'Chinese', value: '3' }, + { label: 'Comedy', value: '4' }, + { label: 'Computers', value: '43' }, + { label: 'Crafts, Hobbies', value: '45' }, + { label: 'Drama', value: '5' }, + { label: 'Education', value: '46' }, + { label: 'English', value: '6' }, + { label: 'Entertainment', value: '47' }, + { label: 'Fantasy', value: '7' }, + { label: 'Fiction', value: '48' }, + { label: 'Gender Bender', value: '8' }, + { label: 'Harem', value: '9' }, + { label: 'Historical', value: '10' }, + { label: 'History', value: '49' }, + { label: 'Home', value: '50' }, + { label: 'Horror', value: '11' }, + { label: 'Humor', value: '51' }, + { label: 'Investing', value: '52' }, + { label: 'Josei', value: '12' }, + { label: 'Korean', value: '13' }, + { label: 'Literature', value: '53' }, + { label: 'Lolicon', value: '14' }, + { label: 'Martial Arts', value: '15' }, + { label: 'Mature', value: '16' }, + { label: 'Mecha', value: '17' }, + { label: 'Memoirs', value: '54' }, + { label: 'Mystery', value: '18' }, + { label: 'Original', value: '19' }, + { label: 'Other Books', value: '66' }, + { label: 'Philosophy', value: '55' }, + { label: 'Photography', value: '56' }, + { label: 'Politics', value: '57' }, + { label: 'Professional', value: '58' }, + { label: 'Psychological', value: '20' }, + { label: 'Reference', value: '59' }, + { label: 'Reincarnation', value: '21' }, + { label: 'Religion', value: '60' }, + { label: 'Romance', value: '22' }, + { label: 'School Life', value: '23' }, + { label: 'School Stories', value: '67' }, + { label: 'Sci-Fi', value: '24' }, + { label: 'Seinen', value: '25' }, + { label: 'Short Stories', value: '68' }, + { label: 'Shotacon', value: '26' }, + { label: 'Shoujo', value: '27' }, + { label: 'Shoujo Ai', value: '28' }, + { label: 'Shounen', value: '29' }, + { label: 'Shounen Ai', value: '30' }, + { label: 'Slice Of Life', value: '31' }, + { label: 'Smut', value: '32' }, + { label: 'Social Science', value: '61' }, + { label: 'Spirituality', value: '62' }, + { label: 'Sports', value: '33' }, + { label: 'Supernatural', value: '34' }, + { label: 'Teaser', value: '69' }, + { label: 'Technical', value: '63' }, + { label: 'Technology', value: '64' }, + { label: 'Tragedy', value: '35' }, + { label: 'Virtual Reality', value: '36' }, + { label: 'Wuxia', value: '37' }, + { label: 'Xianxia', value: '38' }, + { label: 'Xuanhuan', value: '39' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new BLN(); diff --git a/plugins/english/comrademao.broken.ts b/plugins/english/comrademao.broken.ts index cc8ef35..4826097 100644 --- a/plugins/english/comrademao.broken.ts +++ b/plugins/english/comrademao.broken.ts @@ -1,184 +1,180 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import dayjs from "dayjs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import dayjs from 'dayjs'; class ComradeMaoPlugin implements Plugin.PluginBase { - id = "comrademao"; - name = "Comrade Mao"; - site = "https://comrademao.com/"; - version = "1.0.0"; - icon = "src/en/comrademao/icon.png"; + id = 'comrademao'; + name = 'Comrade Mao'; + site = 'https://comrademao.com/'; + version = '1.0.0'; + icon = 'src/en/comrademao/icon.png'; - parseNovels(loadedCheerio: CheerioAPI) { - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".listupd") - .find("div.bs") - .each(function () { - const novelName = loadedCheerio(this).find(".tt").text().trim(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("a").attr("href"); + loadedCheerio('.listupd') + .find('div.bs') + .each(function () { + const novelName = loadedCheerio(this).find('.tt').text().trim(); + const novelCover = loadedCheerio(this).find('img').attr('src'); + const novelUrl = loadedCheerio(this).find('a').attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - novels.push(novel); - }); - return novels; - } - - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions, - ): Promise { - let link = this.site; - - link += filters.category.value; - - link += "page/" + pageNo + "/?post_type=novel"; - - const body = await fetchApi(link).then((result) => result.text()); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async parseNovelAndChapters(novelUrl: string): Promise { - const body = await fetchApi(novelUrl).then((result) => result.text()); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url: novelUrl, - chapters: [], + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, }; + novels.push(novel); + }); + return novels; + } - novel.name = loadedCheerio(".entry-title").text().trim(); + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site; - novel.cover = loadedCheerio("div.thumbook > div > img").attr("src"); + link += filters.category.value; - loadedCheerio(".infox .wd-full").each((i, el) => { - const detailName = loadedCheerio(el).find("b").text(); - const detail = loadedCheerio(el).find("span").text(); + link += 'page/' + pageNo + '/?post_type=novel'; - switch (detailName) { - case "Publisher: ": - novel.author = detail.trim(); - break; - case "Status: ": - novel.status = detail.trim(); - break; - case "Genres:": - novel.genres = detail.trim().replace(/ , /g, ","); - break; - case "Description: ": - novel.summary = detail.trim(); - break; + const body = await fetchApi(link).then(result => result.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovelAndChapters(novelUrl: string): Promise { + const body = await fetchApi(novelUrl).then(result => result.text()); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + url: novelUrl, + chapters: [], + }; + + novel.name = loadedCheerio('.entry-title').text().trim(); + + novel.cover = loadedCheerio('div.thumbook > div > img').attr('src'); + + loadedCheerio('.infox .wd-full').each((i, el) => { + const detailName = loadedCheerio(el).find('b').text(); + const detail = loadedCheerio(el).find('span').text(); + + switch (detailName) { + case 'Publisher: ': + novel.author = detail.trim(); + break; + case 'Status: ': + novel.status = detail.trim(); + break; + case 'Genres:': + novel.genres = detail.trim().replace(/ , /g, ','); + break; + case 'Description: ': + novel.summary = detail.trim(); + break; + } + }); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('#chapterlist') + .find('li') + .each(function () { + const chapterUrl = loadedCheerio(this).find('a').attr('href'); + if (!chapterUrl) return; + + const chapterName = loadedCheerio(this).find('.chapternum').text(); + let releaseTime = loadedCheerio(this).find('.chapterdate').text(); + + if (releaseTime?.includes?.('ago')) { + const timeAgo = releaseTime.match(/\d+/)?.[0] || '0'; + const timeAgoInt = parseInt(timeAgo, 10); + + if (timeAgoInt) { + const dayJSDate = dayjs(); // today + if ( + releaseTime.includes('hours ago') || + releaseTime.includes('hour ago') + ) { + dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours } + + if ( + releaseTime.includes('days ago') || + releaseTime.includes('day ago') + ) { + dayJSDate.subtract(timeAgoInt, 'days'); // go back N days + } + + if ( + releaseTime.includes('months ago') || + releaseTime.includes('month ago') + ) { + dayJSDate.subtract(timeAgoInt, 'months'); // go back N months + } + + releaseTime = dayJSDate.format('LL'); + } + } + chapter.push({ + name: chapterName, + url: chapterUrl, + releaseTime, }); + }); - const chapter: Plugin.ChapterItem[] = []; + novel.chapters = chapter.reverse(); - loadedCheerio("#chapterlist") - .find("li") - .each(function () { - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - if (!chapterUrl) return; + return novel; + } - const chapterName = loadedCheerio(this) - .find(".chapternum") - .text(); - let releaseTime = loadedCheerio(this) - .find(".chapterdate") - .text(); + async parseChapter(chapterUrl: string): Promise { + const body = await fetchApi(chapterUrl).then(result => result.text()); + const loadedCheerio = parseHTML(body); - if (releaseTime?.includes?.("ago")) { - const timeAgo = releaseTime.match(/\d+/)?.[0] || "0"; - const timeAgoInt = parseInt(timeAgo, 10); + const chapterText = loadedCheerio('#chaptercontent').html()?.trim() || ''; - if (timeAgoInt) { - const dayJSDate = dayjs(); // today - if ( - releaseTime.includes("hours ago") || - releaseTime.includes("hour ago") - ) { - dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours - } + return chapterText; + } - if ( - releaseTime.includes("days ago") || - releaseTime.includes("day ago") - ) { - dayJSDate.subtract(timeAgoInt, "days"); // go back N days - } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`; - if ( - releaseTime.includes("months ago") || - releaseTime.includes("month ago") - ) { - dayJSDate.subtract(timeAgoInt, "months"); // go back N months - } + const body = await fetchApi(url).then(result => result.text()); - releaseTime = dayJSDate.format("LL"); - } - } - chapter.push({ - name: chapterName, - url: chapterUrl, - releaseTime, - }); - }); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - novel.chapters = chapter.reverse(); + async fetchImage(url: string): Promise { + return await fetchFile(url); + } - return novel; - } - - async parseChapter(chapterUrl: string): Promise { - const body = await fetchApi(chapterUrl).then((result) => result.text()); - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio("#chaptercontent").html()?.trim() || ""; - - return chapterText; - } - - async searchNovels( - searchTerm: string, - pageNo: number, - ): Promise { - const url = `${this.site}page/${pageNo}/?s=${searchTerm}&post_type=novel`; - - const body = await fetchApi(url).then((result) => result.text()); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - category: { - value: "", - label: "Type", - options: [ - { label: "None", value: "" }, - { label: "Chinese", value: "/mtype/chinese/" }, - { label: "Japanese", value: "/mtype/japanese/" }, - { label: "Korean", value: "/mtype/korean/" }, - { label: "Manhua", value: "/mtype/manhua/" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + category: { + value: '', + label: 'Type', + options: [ + { label: 'None', value: '' }, + { label: 'Chinese', value: '/mtype/chinese/' }, + { label: 'Japanese', value: '/mtype/japanese/' }, + { label: 'Korean', value: '/mtype/korean/' }, + { label: 'Manhua', value: '/mtype/manhua/' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new ComradeMaoPlugin(); diff --git a/plugins/english/divinedaolibrary.ts b/plugins/english/divinedaolibrary.ts index 3a0fab5..b80a3da 100644 --- a/plugins/english/divinedaolibrary.ts +++ b/plugins/english/divinedaolibrary.ts @@ -1,138 +1,137 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { defaultCover } from "@libs/defaultCover"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { defaultCover } from '@libs/defaultCover'; class DDLPlugin implements Plugin.PluginBase { - id = "DDL.com"; - name = "Divine Dao Library"; - site = "https://www.divinedaolibrary.com/"; - version = "1.0.1"; - icon = "src/en/divinedaolibrary/icon.png"; - filters?: undefined; + id = 'DDL.com'; + name = 'Divine Dao Library'; + site = 'https://www.divinedaolibrary.com/'; + version = '1.0.1'; + icon = 'src/en/divinedaolibrary/icon.png'; + filters?: undefined; - parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string){ - let novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) { + let novels: Plugin.NovelItem[] = []; - loadedCheerio("#main") - .find("li") - .each((i,el) => { - const novelName = loadedCheerio(el).find("a").text(); - const novelCover = defaultCover; - const novelUrl = loadedCheerio(el).find("a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; - - novels.push(novel); - }); - - if (searchTerm) { - novels = novels.filter((novel) => - novel.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - } - return novels; - }; + loadedCheerio('#main') + .find('li') + .each((i, el) => { + const novelName = loadedCheerio(el).find('a').text(); + const novelCover = defaultCover; + const novelUrl = loadedCheerio(el).find('a').attr('href'); - async popularNovels( - pageNo: number, - options: Plugin.PopularNovelsOptions - ): Promise { - let link = this.site + "novels"; + if (!novelUrl) return; - const body = await fetchApi(link).then((res) => res.text()); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio("h1.entry-title").text().trim() || "Untitled", - cover: - loadedCheerio(".entry-content").find("img").attr("data-ezsrc") || - defaultCover, - chapters: [], + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), }; - novel.summary = loadedCheerio("#main > article > div > p:nth-child(6)") - .text() - .trim(); + novels.push(novel); + }); - novel.author = loadedCheerio("#main > article > div > h3:nth-child(2)") - .text() - .replace(/Author:/g, "") - .trim(); - - const chapter: Plugin.ChapterItem[] = []; - - loadedCheerio("#main") - .find("li > span > a") - .each((i, el) => { - const chapterName = loadedCheerio(el).text().trim(); - const chapterUrl = loadedCheerio(el).attr("href"); - - if (!chapterUrl) return; - - chapter.push({ - name: chapterName, - path: chapterUrl.replace(this.site, ''), - }); - }); - - novel.chapters = chapter; - - return novel; - }; - - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const chapterName = loadedCheerio(".entry-title").text().trim(); - - let chapterText = loadedCheerio(".entry-content").html(); - - if (!chapterText) { - chapterText = loadedCheerio(".page-header").html(); - } - - chapterText = `

${chapterName}

` + chapterText; - - return chapterText; - }; - - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - let url = this.site + "novels"; - - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio, searchTerm); - }; - - - async fetchImage(url: string): Promise { - return await fetchFile(url); + if (searchTerm) { + novels = novels.filter(novel => + novel.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); } + return novels; + } + + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site + 'novels'; + + const body = await fetchApi(link).then(res => res.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.entry-title').text().trim() || 'Untitled', + cover: + loadedCheerio('.entry-content').find('img').attr('data-ezsrc') || + defaultCover, + chapters: [], + }; + + novel.summary = loadedCheerio('#main > article > div > p:nth-child(6)') + .text() + .trim(); + + novel.author = loadedCheerio('#main > article > div > h3:nth-child(2)') + .text() + .replace(/Author:/g, '') + .trim(); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('#main') + .find('li > span > a') + .each((i, el) => { + const chapterName = loadedCheerio(el).text().trim(); + const chapterUrl = loadedCheerio(el).attr('href'); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapter; + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterName = loadedCheerio('.entry-title').text().trim(); + + let chapterText = loadedCheerio('.entry-content').html(); + + if (!chapterText) { + chapterText = loadedCheerio('.page-header').html(); + } + + chapterText = `

${chapterName}

` + chapterText; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + let url = this.site + 'novels'; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio, searchTerm); + } + + async fetchImage(url: string): Promise { + return await fetchFile(url); + } } -export default new DDLPlugin(); \ No newline at end of file +export default new DDLPlugin(); diff --git a/plugins/english/earlynovel.ts b/plugins/english/earlynovel.ts index c7f52b5..caa1a6b 100644 --- a/plugins/english/earlynovel.ts +++ b/plugins/english/earlynovel.ts @@ -1,213 +1,206 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; class EarlyNovelPlugin implements Plugin.PagePlugin { - id = "earlynovel"; - name = "Early Novel"; - version = "1.0.0"; - icon = "multisrc/madara/latestnovel.png"; - site = "https://earlynovel.net/"; + id = 'earlynovel'; + name = 'Early Novel'; + version = '1.0.0'; + icon = 'multisrc/madara/latestnovel.png'; + site = 'https://earlynovel.net/'; - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".col-truyen-main > .list-truyen > .row").each( - (i, el) => { - const novelUrl = loadedCheerio(el).find("h3.truyen-title > a").attr("href"); - const novelName = loadedCheerio(el) - .find("h3.truyen-title > a") - .text(); - const novelCover = loadedCheerio(el).find('.lazyimg').attr("data-image"); - - if (!novelUrl) return; - novels.push({ - path: novelUrl, - name: novelName, - cover: novelCover, - }); - } - ); - return novels; + loadedCheerio('.col-truyen-main > .list-truyen > .row').each((i, el) => { + const novelUrl = loadedCheerio(el) + .find('h3.truyen-title > a') + .attr('href'); + const novelName = loadedCheerio(el).find('h3.truyen-title > a').text(); + const novelCover = loadedCheerio(el).find('.lazyimg').attr('data-image'); + + if (!novelUrl) return; + novels.push({ + path: novelUrl, + name: novelName, + cover: novelCover, + }); + }); + return novels; + } + + parseChapters(loadedCheerio: CheerioAPI) { + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('ul.list-chapter > li').each((i, el) => { + const chapterName = loadedCheerio(el).find('.chapter-text').text().trim(); + const chapterUrl = loadedCheerio(el).find('a').attr('href')?.slice(1); + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl, + }); + }); + return chapter; + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site; + + if (filters.genres.value.length) link += filters.genres.value; + else link += filters.order.value; + + link += `?page=${pageNo}`; + + const body = await fetchApi(link).then(res => res.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel( + novelPath: string, + ): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + loadedCheerio('.glyphicon-menu-right').closest('li').remove(); + const pagenav = loadedCheerio('.page-nav').prev().find('a'); + const lastPageStr = pagenav.attr('title')?.match(/(\d+)/); + const lastPage = Number(lastPageStr?.[1] || '0'); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('.book > img').attr('alt') || 'Untitled', + cover: loadedCheerio('.book > img').attr('src'), + summary: loadedCheerio('.desc-text').text().trim(), + chapters: [], + totalPages: lastPage, }; - parseChapters(loadedCheerio:CheerioAPI){ - const chapter: Plugin.ChapterItem[] = []; - loadedCheerio("ul.list-chapter > li").each((i,el) => { - const chapterName = loadedCheerio(el) - .find(".chapter-text") - .text() - .trim(); - const chapterUrl = loadedCheerio(el) - .find("a") - .attr("href") - ?.slice(1); - if (!chapterUrl) return; + loadedCheerio('.info > div > h3').each(function () { + let detailName = loadedCheerio(this).text(); + let detail = loadedCheerio(this) + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); - chapter.push({ - name: chapterName, - path: chapterUrl - })}); - return chapter; - } + switch (detailName) { + case 'Author:': + novel.author = detail; + break; + case 'Status:': + novel.status = detail; + break; + case 'Genre:': + novel.genres = detail; + break; + } + }); - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = this.site; + novel.chapters = this.parseChapters(loadedCheerio); + return novel; + } - if (filters.genres.value.length) - link += filters.genres.value; - else - link += filters.order.value; + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath + '?page=' + page; + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + const chapters = this.parseChapters(loadedCheerio); + return { chapters }; + } - link += `?page=${pageNo}`; + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - const body = await fetchApi(link).then((res) => res.text()); + const loadedCheerio = parseHTML(body); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const chapterText = loadedCheerio('#chapter-c').html() || ''; - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + return chapterText; + } - let loadedCheerio = parseHTML(body); - loadedCheerio('.glyphicon-menu-right').closest('li').remove(); - const pagenav = loadedCheerio('.page-nav').prev().find('a'); - const lastPageStr = pagenav.attr('title')?.match(/(\d+)/); - const lastPage = Number(lastPageStr?.[1] || "0"); + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}search?keyword=${searchTerm}`; + const result = await fetchApi(url); + const body = await result.text(); - const novel: Plugin.SourceNovel & {totalPages: number}= { - path: novelPath, - name: loadedCheerio(".book > img").attr("alt") || "Untitled", - cover: loadedCheerio(".book > img").attr("src"), - summary: loadedCheerio(".desc-text").text().trim(), - chapters: [], - totalPages: lastPage - }; + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - loadedCheerio(".info > div > h3").each(function () { - let detailName = loadedCheerio(this).text(); - let detail = loadedCheerio(this) - .siblings() - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); + async fetchImage(url: string): Promise { + return await fetchFile(url); + } - switch (detailName) { - case "Author:": - novel.author = detail; - break; - case "Status:": - novel.status = detail; - break; - case "Genre:": - novel.genres = detail; - break; - } - }); - - novel.chapters = this.parseChapters(loadedCheerio); - return novel; - } - - async parsePage(novelPath: string, page: string): Promise { - const url = this.site + novelPath + '?page=' + page; - const body = await fetchApi(url).then((res) => res.text()); - const loadedCheerio = parseHTML(body); - const chapters = this.parseChapters(loadedCheerio); - return {chapters} - } - - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio("#chapter-c").html() || ""; - - return chapterText; - }; - - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - const url = `${this.site}search?keyword=${searchTerm}`; - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - }; - - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - order: { - value: "/most-popular", - label: "Order by", - options: [ - { label: "Latest Release", value: "/latest-release-novel" }, - { label: "Hot Novel", value: "/hot-novel" }, - { label: "Completed Novel", value: "/completed-novel" }, - { label: "Most Popular", value: "/most-popular" }, - ], - type: FilterTypes.Picker, - }, - genres: { - value: "", - label: "Genre", - options: [ - { label: "None", value: "" }, - { label: "Action", value: "/genre/action-1" }, - { label: "Adult", value: "/genre/adult-2" }, - { label: "Adventure", value: "/genre/adventure-3" }, - { label: "Comedy", value: "/genre/comedy-4" }, - { label: "Drama", value: "/genre/drama-5" }, - { label: "Ecchi", value: "/genre/ecchi-6" }, - { label: "Fantasy", value: "/genre/fantasy-7" }, - { label: "Gender Bender", value: "/genre/gender-bender-8" }, - { label: "Harem", value: "/genre/harem-9" }, - { label: "Historical", value: "/genre/historical-10" }, - { label: "Horror", value: "/genre/horror-11" }, - { label: "Josei", value: "/genre/josei-12" }, - { label: "Martial Arts", value: "/genre/martial-arts-13" }, - { label: "Mature", value: "/genre/mature-14" }, - { label: "Mecha", value: "/genre/mecha-15" }, - { label: "Mystery", value: "/genre/mystery-16" }, - { label: "Psychological", value: "/genre/psychological-17" }, - { label: "Romance", value: "/genre/romance-18" }, - { label: "School Life", value: "/genre/school-life-19" }, - { label: "Sci-fi", value: "/genre/sci-fi-20" }, - { label: "Seinen", value: "/genre/seinen-21" }, - { label: "Shoujo", value: "/genre/shoujo-22" }, - { label: "Shoujo Ai", value: "/genre/shoujo-ai-23" }, - { label: "Shounen", value: "/genre/shounen-24" }, - { label: "Shounen Ai", value: "/genre/shounen-ai-25" }, - { label: "Slice of Life", value: "/genre/slice-of-life-26" }, - { label: "Smut", value: "/genre/smut-27" }, - { label: "Sports", value: "/genre/sports-28" }, - { label: "Supernatural", value: "/genre/supernatural-29" }, - { label: "Tragedy", value: "/genre/tragedy-30" }, - { label: "Wuxia", value: "/genre/wuxia-31" }, - { label: "Xianxia", value: "/genre/xianxia-32" }, - { label: "Xuanhuan", value: "/genre/xuanhuan-33" }, - { label: "Yaoi", value: "/genre/yaoi-34" }, - { label: "Yuri", value: "/genre/yuri-35" }, - { label: "Video Games", value: "/genre/video-games-36" }, - { label: "Magical Realism", value: "/genre/magical-realism-37" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + order: { + value: '/most-popular', + label: 'Order by', + options: [ + { label: 'Latest Release', value: '/latest-release-novel' }, + { label: 'Hot Novel', value: '/hot-novel' }, + { label: 'Completed Novel', value: '/completed-novel' }, + { label: 'Most Popular', value: '/most-popular' }, + ], + type: FilterTypes.Picker, + }, + genres: { + value: '', + label: 'Genre', + options: [ + { label: 'None', value: '' }, + { label: 'Action', value: '/genre/action-1' }, + { label: 'Adult', value: '/genre/adult-2' }, + { label: 'Adventure', value: '/genre/adventure-3' }, + { label: 'Comedy', value: '/genre/comedy-4' }, + { label: 'Drama', value: '/genre/drama-5' }, + { label: 'Ecchi', value: '/genre/ecchi-6' }, + { label: 'Fantasy', value: '/genre/fantasy-7' }, + { label: 'Gender Bender', value: '/genre/gender-bender-8' }, + { label: 'Harem', value: '/genre/harem-9' }, + { label: 'Historical', value: '/genre/historical-10' }, + { label: 'Horror', value: '/genre/horror-11' }, + { label: 'Josei', value: '/genre/josei-12' }, + { label: 'Martial Arts', value: '/genre/martial-arts-13' }, + { label: 'Mature', value: '/genre/mature-14' }, + { label: 'Mecha', value: '/genre/mecha-15' }, + { label: 'Mystery', value: '/genre/mystery-16' }, + { label: 'Psychological', value: '/genre/psychological-17' }, + { label: 'Romance', value: '/genre/romance-18' }, + { label: 'School Life', value: '/genre/school-life-19' }, + { label: 'Sci-fi', value: '/genre/sci-fi-20' }, + { label: 'Seinen', value: '/genre/seinen-21' }, + { label: 'Shoujo', value: '/genre/shoujo-22' }, + { label: 'Shoujo Ai', value: '/genre/shoujo-ai-23' }, + { label: 'Shounen', value: '/genre/shounen-24' }, + { label: 'Shounen Ai', value: '/genre/shounen-ai-25' }, + { label: 'Slice of Life', value: '/genre/slice-of-life-26' }, + { label: 'Smut', value: '/genre/smut-27' }, + { label: 'Sports', value: '/genre/sports-28' }, + { label: 'Supernatural', value: '/genre/supernatural-29' }, + { label: 'Tragedy', value: '/genre/tragedy-30' }, + { label: 'Wuxia', value: '/genre/wuxia-31' }, + { label: 'Xianxia', value: '/genre/xianxia-32' }, + { label: 'Xuanhuan', value: '/genre/xuanhuan-33' }, + { label: 'Yaoi', value: '/genre/yaoi-34' }, + { label: 'Yuri', value: '/genre/yuri-35' }, + { label: 'Video Games', value: '/genre/video-games-36' }, + { label: 'Magical Realism', value: '/genre/magical-realism-37' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } -export default new EarlyNovelPlugin(); \ No newline at end of file +export default new EarlyNovelPlugin(); diff --git a/plugins/english/faqwikius.ts b/plugins/english/faqwikius.ts index 92f79d5..9eb5be0 100644 --- a/plugins/english/faqwikius.ts +++ b/plugins/english/faqwikius.ts @@ -1,142 +1,168 @@ -import { Plugin } from "@typings/plugin"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { NovelStatus } from "@libs/novelStatus"; +import { Plugin } from '@typings/plugin'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; class FaqWikiUs implements Plugin.PluginBase { - id = "FWK.US"; - name = "Faq Wiki"; - site = "https://faqwiki.us/"; - version = "1.1.1"; - icon = "src/en/faqwikius/icon.png"; + id = 'FWK.US'; + name = 'Faq Wiki'; + site = 'https://faqwiki.us/'; + version = '1.1.1'; + icon = 'src/en/faqwikius/icon.png'; - parseNovels(loadedCheerio:CheerioAPI, searchTerm?: string) { - let novels: Plugin.NovelItem[] = []; - - loadedCheerio("figure.wp-block-image").each((index, element) => { - const name = loadedCheerio(element).text(); - let cover = loadedCheerio(element).find("img").attr("data-ezsrc"); + parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) { + let novels: Plugin.NovelItem[] = []; - // Remove the appended query string - if (cover) { - cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! - } else { - cover = loadedCheerio(element).find("img").attr("src"); - } + loadedCheerio('figure.wp-block-image').each((index, element) => { + const name = loadedCheerio(element).text(); + let cover = loadedCheerio(element).find('img').attr('data-ezsrc'); - const path = loadedCheerio(element).find("a").attr("href")?.replace('tp:', 'tps:')?.slice(this.site.length); - if (!path) return; + // Remove the appended query string + if (cover) { + cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! + } else { + cover = loadedCheerio(element).find('img').attr('src'); + } - novels.push({ name, cover, path }); - }) + const path = loadedCheerio(element) + .find('a') + .attr('href') + ?.replace('tp:', 'tps:') + ?.slice(this.site.length); + if (!path) return; - if (searchTerm) { - novels = novels.filter((novel) => - novel.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + novels.push({ name, cover, path }); + }); + + if (searchTerm) { + novels = novels.filter(novel => + novel.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + } + + return novels; + } + + async popularNovels( + page: number, + { showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + const body = await fetchApi(this.site).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio); + } + + async parseNovel(path: string): Promise { + const body = await fetchApi(this.site + path).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path, + name: '', + }; + + novel.name = loadedCheerio('.entry-title') + .text() + .replace('Novel – All Chapters', '') + .trim(); + + novel.cover = loadedCheerio('.wp-block-image') + .find('img') + .attr('data-ezsrc') + ?.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! + + const status = loadedCheerio( + "#lcp_instance_0 +:icontains('complete')", + ).text(); + novel.status = status ? NovelStatus.Completed : NovelStatus.Ongoing; + + const div = loadedCheerio('.book-review-block__meta-item-value'); + + div.html(div.html()?.replace(/(?<=>)([^<]+)(?=)/g, '

$1

')!); + + loadedCheerio('.book-review-block__meta-item-value strong').each( + (i, el) => { + const key = loadedCheerio(el) + .text() + .replace(':', '') + .trim() + .toLowerCase(); + const value = loadedCheerio(el).next().text().replace(':', '').trim(); + + switch (key) { + case 'description': + novel.summary = value; + break; + case 'author(s)': + novel.author = value; + break; + case 'genre': + novel.genres = value.split(' ').join(','); } + }, + ); - return novels; - } + const chapters: Plugin.ChapterItem[] = loadedCheerio( + '.lcp_catlist > li > a', + ) + .map((chapterIndex, element) => ({ + name: loadedCheerio(element) + .text() + .replace(novel.name + '', '') + .replace('Novel' + '', '') + .trim(), + path: '' + loadedCheerio(element).attr('href')?.slice(this.site.length), + chapterNumber: chapterIndex + 1, + })) + .get(); - async popularNovels( - page: number, - { showLatestNovels }: Plugin.PopularNovelsOptions, - ): Promise { - - const body = await fetchApi(this.site).then((res) => res.text()); - const loadedCheerio = parseHTML(body); + novel.chapters = chapters; + return novel; + } - return this.parseNovels(loadedCheerio); - } + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(body); + loadedCheerio('span').remove(); - async parseNovel(path: string): Promise { - const body = await fetchApi(this.site + path).then((res) => res.text()); - const loadedCheerio = parseHTML(body); + const chapterParagraphs = loadedCheerio('.entry-content p'); - const novel: Plugin.SourceNovel = { - path, - name: "" - }; + let chapterContent; // Variable to store the result - novel.name = loadedCheerio(".entry-title").text().replace("Novel – All Chapters", "").trim(); - - novel.cover = loadedCheerio(".wp-block-image") - .find("img") - .attr("data-ezsrc") - ?.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! - - const status = loadedCheerio("#lcp_instance_0 +:icontains('complete')").text(); - novel.status = (status ? NovelStatus.Completed : NovelStatus.Ongoing) - - const div = loadedCheerio(".book-review-block__meta-item-value"); - - div.html(div.html()?.replace(/(?<=>)([^<]+)(?=)/g, '

$1

')!); - - loadedCheerio(".book-review-block__meta-item-value strong").each((i,el) => { - const key = loadedCheerio(el).text().replace(':', '').trim().toLowerCase(); - const value = loadedCheerio(el).next().text().replace(':', '').trim(); - - switch(key){ - case "description": - novel.summary = value; - break; - case "author(s)": - novel.author = value; - break; - case "genre": - novel.genres = value.split(' ').join(','); - } + if (chapterParagraphs.length < 5) { + //some chapter in this site store their whole text in 1-4

, + chapterContent = chapterParagraphs + .map((index, element) => { + const text = loadedCheerio(element).html(); + return text; }) - - const chapters: Plugin.ChapterItem[] = loadedCheerio(".lcp_catlist > li > a") - .map((chapterIndex, element) => ({ - name: loadedCheerio(element).text().replace(novel.name + "", "").replace("Novel" + "", "").trim(), - path: ""+loadedCheerio(element).attr("href")?.slice(this.site.length), - chapterNumber: chapterIndex + 1, - })) - .get(); - - novel.chapters = chapters; - return novel; + .get() + .join('\n\n'); + } else { + // Multiple paragraphs case + chapterContent = chapterParagraphs + .map((index, element) => { + const text = loadedCheerio(element).text().trim(); + return `

${text}

`; + }) + .get() + .join('\n\n'); } - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); - const loadedCheerio = parseHTML(body); - loadedCheerio("span").remove(); + return chapterContent; + } - const chapterParagraphs = loadedCheerio(".entry-content p"); + async searchNovels(searchTerm: string): Promise { + const body = await fetchApi(this.site).then(res => res.text()); + const loadedCheerio = parseHTML(body); - let chapterContent; // Variable to store the result + return this.parseNovels(loadedCheerio, searchTerm); + } - if (chapterParagraphs.length < 5) { //some chapter in this site store their whole text in 1-4

, - chapterContent = chapterParagraphs.map((index, element) => { - const text = loadedCheerio(element).html() - return text; - }).get().join('\n\n'); - } - - else { - // Multiple paragraphs case - chapterContent = chapterParagraphs.map((index, element) => { - const text = loadedCheerio(element).text().trim(); - return `

${text}

`; - }).get().join('\n\n'); - } - - return chapterContent; - } - - - async searchNovels(searchTerm: string): Promise { - const body = await fetchApi(this.site).then((res) => res.text()); - const loadedCheerio = parseHTML(body); - - return this.parseNovels(loadedCheerio, searchTerm); - } - - fetchImage = fetchFile; + fetchImage = fetchFile; } export default new FaqWikiUs(); diff --git a/plugins/english/freewebnovel.ts b/plugins/english/freewebnovel.ts index cd184eb..9fa214f 100644 --- a/plugins/english/freewebnovel.ts +++ b/plugins/english/freewebnovel.ts @@ -1,73 +1,78 @@ -import { Plugin } from "@typings/plugin"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import qs from "qs"; +import { Plugin } from '@typings/plugin'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import qs from 'qs'; class FreeWebNovel implements Plugin.PluginBase { - id = "FWN.com"; - name = "Free Web Novel"; - site = "https://freewebnovel.com"; - version = "1.0.0"; - icon = "src/en/freewebnovel/icon.png"; + id = 'FWN.com'; + name = 'Free Web Novel'; + site = 'https://freewebnovel.com'; + version = '1.0.0'; + icon = 'src/en/freewebnovel/icon.png'; async popularNovels( page: number, { showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { const sort = showLatestNovels - ? "/latest-release-novels/" - : "/completed-novels/"; + ? '/latest-release-novels/' + : '/completed-novels/'; - const body = await fetchApi(this.site + sort + page).then((res) => res.text()); + const body = await fetchApi(this.site + sort + page).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); - const novels: Plugin.NovelItem[] = loadedCheerio(".li-row") + const novels: Plugin.NovelItem[] = loadedCheerio('.li-row') .map((index, element) => ({ - name: loadedCheerio(element).find(".tit").text() || "", - cover: loadedCheerio(element).find("img").attr("src"), - path: loadedCheerio(element).find("h3 > a").attr("href") || "", + name: loadedCheerio(element).find('.tit').text() || '', + cover: loadedCheerio(element).find('img').attr('src'), + path: loadedCheerio(element).find('h3 > a').attr('href') || '', })) .get() - .filter((novel) => novel.name && novel.path); + .filter(novel => novel.name && novel.path); return novels; } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const body = await fetchApi(this.site + novelPath).then(res => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, - name: loadedCheerio("h1.tit").text(), - cover: loadedCheerio(".pic > img").attr("src"), - summary: loadedCheerio(".inner").text().trim(), + name: loadedCheerio('h1.tit').text(), + cover: loadedCheerio('.pic > img').attr('src'), + summary: loadedCheerio('.inner').text().trim(), }; - novel.genres = loadedCheerio("[title=Genre]") + novel.genres = loadedCheerio('[title=Genre]') .next() .text() - .replace(/[\t\n]/g, ""); + .replace(/[\t\n]/g, ''); - novel.author = loadedCheerio("[title=Author]") + novel.author = loadedCheerio('[title=Author]') .next() .text() - .replace(/[\t\n]/g, ""); + .replace(/[\t\n]/g, ''); - novel.status = loadedCheerio("[title=Status]") + novel.status = loadedCheerio('[title=Status]') .next() .text() - .replace(/[\t\n]/g, ""); + .replace(/[\t\n]/g, ''); - const chapters: Plugin.ChapterItem[] = loadedCheerio("#idData > li > a") + const chapters: Plugin.ChapterItem[] = loadedCheerio('#idData > li > a') .map((chapterIndex, element) => ({ name: - loadedCheerio(element).attr("title") || - "Chapter " + (chapterIndex + 1), + loadedCheerio(element).attr('title') || + 'Chapter ' + (chapterIndex + 1), path: - loadedCheerio(element).attr("href") || - novelPath.replace(".html", "/chapter-" + (chapterIndex + 1) + ".html"), + loadedCheerio(element).attr('href') || + novelPath.replace( + '.html', + '/chapter-' + (chapterIndex + 1) + '.html', + ), releaseTime: null, chapterNumber: chapterIndex + 1, })) @@ -78,33 +83,35 @@ class FreeWebNovel implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); - const chapterText = loadedCheerio("div.txt").html() || ""; + const chapterText = loadedCheerio('div.txt').html() || ''; return chapterText; } async searchNovels(searchTerm: string): Promise { - const body = await fetchApi(this.site + "/search/", { + const body = await fetchApi(this.site + '/search/', { headers: { - "Content-Type": "application/x-www-form-urlencoded", + 'Content-Type': 'application/x-www-form-urlencoded', Referer: this.site, Origin: this.site, }, - method: "POST", + method: 'POST', body: qs.stringify({ searchkey: searchTerm }), - }).then((res) => res.text()); + }).then(res => res.text()); const loadedCheerio = parseHTML(body); - const novels: Plugin.NovelItem[] = loadedCheerio(".li-row > .li > .con") + const novels: Plugin.NovelItem[] = loadedCheerio('.li-row > .li > .con') .map((index, element) => ({ - name: loadedCheerio(element).find(".tit").text() || "", - cover: loadedCheerio(element).find(".pic > a > img").attr("src"), - path: loadedCheerio(element).find("h3 > a").attr("href") || "", + name: loadedCheerio(element).find('.tit').text() || '', + cover: loadedCheerio(element).find('.pic > a > img').attr('src'), + path: loadedCheerio(element).find('h3 > a').attr('href') || '', })) .get() - .filter((novel) => novel.name && novel.path); + .filter(novel => novel.name && novel.path); return novels; } diff --git a/plugins/english/lightnovelpub.ts b/plugins/english/lightnovelpub.ts index cd7de8c..58932fd 100644 --- a/plugins/english/lightnovelpub.ts +++ b/plugins/english/lightnovelpub.ts @@ -1,247 +1,248 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import dayjs from "dayjs"; +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import dayjs from 'dayjs'; class LightNovelPub implements Plugin.PagePlugin { - id = "lightnovelpub"; - name = "LightNovelPub"; - version = "1.0.1"; - icon = "src/en/lightnovelpub/icon.png"; - site = "https://www.lightnovelpub.com/"; - headers = { - Accept: "application/json", - "Content-Type": "application/json", + id = 'lightnovelpub'; + name = 'LightNovelPub'; + version = '1.0.1'; + icon = 'src/en/lightnovelpub/icon.png'; + site = 'https://www.lightnovelpub.com/'; + headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}browse/`; + link += `${filters.genres.value}/`; + link += `${filters.order.value}/`; + link += `${filters.status.value}/`; + link += page; + + const body = await fetchApi(link).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.novel-item.ads').remove(); + + loadedCheerio('.novel-item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.novel-title').text().trim(); + const novelCover = loadedCheerio(ele).find('img').attr('data-src'); + const novelUrl = loadedCheerio(ele) + .find('.novel-title > a') + .attr('href') + ?.substring(1); + + if (!novelUrl) return; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel( + novelPath: string, + ): Promise { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + const totalChapters = parseInt( + loadedCheerio('.header-stats span:first strong').text(), + 10, + ); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('h1.novel-title').text().trim() || 'Untitled', + cover: loadedCheerio('figure.cover > img').attr('data-src'), + author: loadedCheerio('.author > a > span').text(), + summary: loadedCheerio('.summary > .content').text().trim(), + status: loadedCheerio('.header-stats span:last strong').text(), + totalPages: Math.ceil(totalChapters / 100), + chapters: [], }; - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}browse/`; - link += `${filters.genres.value}/`; - link += `${filters.order.value}/`; - link += `${filters.status.value}/`; - link += page; + novel.genres = loadedCheerio('.categories ul li') + .map((a, ex) => loadedCheerio(ex).text().trim()) + .toArray() + .join(','); - const body = await fetchApi(link).then((r) => r.text()); + return novel; + } - const loadedCheerio = parseHTML(body); + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath + '/chapters/page-' + page; + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('.chapter-list li').each(function () { + const chapterName = + 'Chapter ' + + loadedCheerio(this).find('.chapter-no').text().trim() + + ' - ' + + loadedCheerio(this).find('.chapter-title').text().trim(); - const novels: Plugin.NovelItem[] = []; + const releaseDate = loadedCheerio(this) + .find('.chapter-update') + .attr('datetime'); - loadedCheerio(".novel-item.ads").remove(); + const chapterUrl = loadedCheerio(this) + .find('a') + .attr('href') + ?.substring(1); + if (!chapterUrl) return; - loadedCheerio(".novel-item").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find(".novel-title") - .text() - .trim(); - const novelCover = loadedCheerio(ele).find("img").attr("data-src"); - const novelUrl = - loadedCheerio(ele) - .find(".novel-title > a") - .attr("href") - ?.substring(1); + chapter.push({ + name: chapterName, + path: chapterUrl, + releaseTime: dayjs(releaseDate).toISOString(), + }); + }); + const chapters = chapter; + return { chapters }; + } - if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl, - }; + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); - novels.push(novel); - }); + const loadedCheerio = parseHTML(body); - return novels; - } + let chapterText = loadedCheerio('#chapter-container').html() || ''; - async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((r) => r.text()); + return chapterText; + } - const loadedCheerio = parseHTML(body); - const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10); + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}lnsearchlive`; + const link = `${this.site}search`; + const response = await fetchApi(link).then(r => r.text()); + const token = parseHTML(response); + let verifytoken = token('#novelSearchForm > input').attr('value'); - const novel: Plugin.SourceNovel & { totalPages: number } = { - path: novelPath, - name: loadedCheerio("h1.novel-title").text().trim() || "Untitled", - cover: loadedCheerio("figure.cover > img").attr("data-src"), - author: loadedCheerio(".author > a > span").text(), - summary: loadedCheerio(".summary > .content").text().trim(), - status: loadedCheerio(".header-stats span:last strong").text(), - totalPages: Math.ceil(totalChapters / 100), - chapters: [], - }; + let formData = new FormData(); + formData.append('inputContent', searchTerm); - novel.genres = loadedCheerio('.categories ul li') - .map((a, ex) => loadedCheerio(ex).text().trim()) - .toArray() - .join(','); + const body = await fetchApi(url, { + method: 'POST', + headers: { LNRequestVerifyToken: verifytoken! }, + body: formData, + }).then(r => r.text()); - return novel; - } + let loadedCheerio = parseHTML(body); - async parsePage(novelPath: string, page: string): Promise { - const url = this.site + novelPath + '/chapters/page-' + page; - const body = await fetchApi(url).then((res) => res.text()); - const loadedCheerio = parseHTML(body); - const chapter: Plugin.ChapterItem[] = []; - loadedCheerio('.chapter-list li').each(function () { - const chapterName = - 'Chapter ' + - loadedCheerio(this).find('.chapter-no').text().trim() + - ' - ' + - loadedCheerio(this).find('.chapter-title').text().trim(); + let novels: Plugin.NovelItem[] = []; - const releaseDate = loadedCheerio(this) - .find('.chapter-update') - .attr('datetime'); + let results = JSON.parse(loadedCheerio('body').text()); - const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1); - if (!chapterUrl) return; + loadedCheerio = parseHTML(results.resultview); - chapter.push({ - name: chapterName, - path: chapterUrl, - releaseTime: dayjs(releaseDate).toISOString(), - }) - }); - const chapters = chapter; - return { chapters }; - } + loadedCheerio('.novel-item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h4.novel-title').text().trim(); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + const novelUrl = loadedCheerio(ele).find('a').attr('href')?.substring(1); + if (!novelUrl) return; + novels.push({ + name: novelName, + path: novelUrl, + cover: novelCover, + }); + }); - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); + return novels; + } - const loadedCheerio = parseHTML(body); + async fetchImage(url: string): Promise { + return fetchFile(url, { headers: this.headers }); + } - let chapterText = loadedCheerio("#chapter-container").html() || ''; - - return chapterText; - } - - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.site}lnsearchlive`; - const link = `${this.site}search`; - const response = await fetchApi(link).then((r) => r.text()); - const token = parseHTML(response); - let verifytoken = token("#novelSearchForm > input").attr("value"); - - let formData = new FormData(); - formData.append("inputContent", searchTerm); - - const body = await fetchApi( - url, - { - method: "POST", - headers: { LNRequestVerifyToken: verifytoken! }, - body: formData, - }, - ).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - let results = JSON.parse(loadedCheerio("body").text()); - - loadedCheerio = parseHTML(results.resultview); - - loadedCheerio(".novel-item").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find("h4.novel-title") - .text() - .trim(); - const novelCover = loadedCheerio(ele).find("img").attr("src"); - const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1); - if (!novelUrl) return; - novels.push({ - name: novelName, - path: novelUrl, - cover: novelCover, - }); - }); - - return novels; - } - - async fetchImage(url: string): Promise { - return fetchFile(url, { headers: this.headers }); - } - - filters = { - order: { - value: "popular", - label: "Order by", - options: [ - { label: "New", value: "new" }, - { label: "Popular", value: "popular" }, - { label: "Updates", value: "updated" }, - ], - type: FilterTypes.Picker, - }, - status: { - value: "all", - label: "Status", - options: [ - { label: "All", value: "all" }, - { label: "Completed", value: "completed" }, - { label: "Ongoing", value: "ongoing" } - ], - type: FilterTypes.Picker, - }, - genres: { - value: "all", - label: "Genre", - options: [ - { label: "All", value: "all" }, - { label: "Action", value: "action" }, - { label: "Adventure", value: "adventure" }, - { label: "Drama", value: "drama" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Harem", value: "harem" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Romance", value: "romance" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Xuanhuan", value: "xuanhuan" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Comedy", value: "comedy" }, - { label: "Slice of Life", value: "slice-of-life" }, - { label: "Mystery", value: "mystery" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Psychological", value: "psychological" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Xianxia", value: "xianxia" }, - { label: "School Life", value: "school-life" }, - { label: "Josei", value: "josei" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Shounen", value: "shounen" }, - { label: "Horror", value: "horror" }, - { label: "Mecha", value: "mecha" }, - { label: "Historical", value: "historical" }, - { label: "Shoujo", value: "shoujo" }, - { label: "Adult", value: "adult" }, - { label: "Seinen", value: "seinen" }, - { label: "Sports", value: "sports" }, - { label: "Lolicon", value: "lolicon" }, - { label: "Gender Bender", value: "gender-bender" }, - { label: "Shounen Ai", value: "shounen-ai" }, - { label: "Yaoi", value: "yaoi" }, - { label: "Video Games", value: "video-games" }, - { label: "Smut", value: "smut" }, - { label: "Magical Realism", value: "magical-realism" }, - { label: "Eastern Fantasy", value: "eastern-fantasy" }, - { label: "Contemporary Romance", value: "contemporary-romance" }, - { label: "Fantasy Romance", value: "fantasy-romance" }, - { label: "Shoujo Ai", value: "shoujo-ai" }, - { label: "Yuri", value: "yuri" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + order: { + value: 'popular', + label: 'Order by', + options: [ + { label: 'New', value: 'new' }, + { label: 'Popular', value: 'popular' }, + { label: 'Updates', value: 'updated' }, + ], + type: FilterTypes.Picker, + }, + status: { + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Completed', value: 'completed' }, + { label: 'Ongoing', value: 'ongoing' }, + ], + type: FilterTypes.Picker, + }, + genres: { + value: 'all', + label: 'Genre', + options: [ + { label: 'All', value: 'all' }, + { label: 'Action', value: 'action' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Drama', value: 'drama' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Harem', value: 'harem' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Romance', value: 'romance' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Josei', value: 'josei' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Horror', value: 'horror' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Historical', value: 'historical' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Adult', value: 'adult' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Sports', value: 'sports' }, + { label: 'Lolicon', value: 'lolicon' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Shounen Ai', value: 'shounen-ai' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Video Games', value: 'video-games' }, + { label: 'Smut', value: 'smut' }, + { label: 'Magical Realism', value: 'magical-realism' }, + { label: 'Eastern Fantasy', value: 'eastern-fantasy' }, + { label: 'Contemporary Romance', value: 'contemporary-romance' }, + { label: 'Fantasy Romance', value: 'fantasy-romance' }, + { label: 'Shoujo Ai', value: 'shoujo-ai' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } -export default new LightNovelPub(); \ No newline at end of file +export default new LightNovelPub(); diff --git a/plugins/english/lightnovelworld.ts b/plugins/english/lightnovelworld.ts index 55e4226..51a4717 100644 --- a/plugins/english/lightnovelworld.ts +++ b/plugins/english/lightnovelworld.ts @@ -1,247 +1,248 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import dayjs from "dayjs"; +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import dayjs from 'dayjs'; class LightNovelWorld implements Plugin.PagePlugin { - id = "lightnovelworld"; - name = "LightNovelWorld"; - site = "https://www.webnovelworld.org/"; - version = "1.0.1"; - icon = "src/en/lightnovelworld/icon.png"; - headers = { - Accept: "application/json", - "Content-Type": "application/json", + id = 'lightnovelworld'; + name = 'LightNovelWorld'; + site = 'https://www.webnovelworld.org/'; + version = '1.0.1'; + icon = 'src/en/lightnovelworld/icon.png'; + headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}browse/`; + link += `${filters.genres.value}/`; + link += `${filters.order.value}/`; + link += `${filters.status.value}/`; + link += page; + + const body = await fetchApi(link).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.novel-item.ads').remove(); + + loadedCheerio('.novel-item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.novel-title').text().trim(); + const novelCover = loadedCheerio(ele).find('img').attr('data-src'); + const novelUrl = loadedCheerio(ele) + .find('.novel-title > a') + .attr('href') + ?.substring(1); + + if (!novelUrl) return; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel( + novelPath: string, + ): Promise { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + const totalChapters = parseInt( + loadedCheerio('.header-stats span:first strong').text(), + 10, + ); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('h1.novel-title').text().trim() || 'Untitled', + cover: loadedCheerio('figure.cover > img').attr('data-src'), + author: loadedCheerio('.author > a > span').text(), + summary: loadedCheerio('.summary > .content').text().trim(), + status: loadedCheerio('.header-stats span:last strong').text(), + totalPages: Math.ceil(totalChapters / 100), + chapters: [], }; - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}browse/`; - link += `${filters.genres.value}/`; - link += `${filters.order.value}/`; - link += `${filters.status.value}/`; - link += page; + novel.genres = loadedCheerio('.categories ul li') + .map((a, ex) => loadedCheerio(ex).text().trim()) + .toArray() + .join(','); - const body = await fetchApi(link).then((r) => r.text()); + return novel; + } - const loadedCheerio = parseHTML(body); + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath + '/chapters/page-' + page; + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('.chapter-list li').each(function () { + const chapterName = + 'Chapter ' + + loadedCheerio(this).find('.chapter-no').text().trim() + + ' - ' + + loadedCheerio(this).find('.chapter-title').text().trim(); - const novels: Plugin.NovelItem[] = []; + const releaseDate = loadedCheerio(this) + .find('.chapter-update') + .attr('datetime'); - loadedCheerio(".novel-item.ads").remove(); + const chapterUrl = loadedCheerio(this) + .find('a') + .attr('href') + ?.substring(1); + if (!chapterUrl) return; - loadedCheerio(".novel-item").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find(".novel-title") - .text() - .trim(); - const novelCover = loadedCheerio(ele).find("img").attr("data-src"); - const novelUrl = - loadedCheerio(ele) - .find(".novel-title > a") - .attr("href") - ?.substring(1); + chapter.push({ + name: chapterName, + path: chapterUrl, + releaseTime: dayjs(releaseDate).toISOString(), + }); + }); + const chapters = chapter; + return { chapters }; + } - if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl, - }; + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); - novels.push(novel); - }); + const loadedCheerio = parseHTML(body); - return novels; - } + let chapterText = loadedCheerio('#chapter-container').html() || ''; - async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((r) => r.text()); + return chapterText; + } - const loadedCheerio = parseHTML(body); - const totalChapters = parseInt(loadedCheerio(".header-stats span:first strong").text(), 10); + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}lnsearchlive`; + const link = `${this.site}search`; + const response = await fetchApi(link).then(r => r.text()); + const token = parseHTML(response); + let verifytoken = token('#novelSearchForm > input').attr('value'); - const novel: Plugin.SourceNovel & { totalPages: number } = { - path: novelPath, - name: loadedCheerio("h1.novel-title").text().trim() || "Untitled", - cover: loadedCheerio("figure.cover > img").attr("data-src"), - author: loadedCheerio(".author > a > span").text(), - summary: loadedCheerio(".summary > .content").text().trim(), - status: loadedCheerio(".header-stats span:last strong").text(), - totalPages: Math.ceil(totalChapters / 100), - chapters: [], - }; + let formData = new FormData(); + formData.append('inputContent', searchTerm); - novel.genres = loadedCheerio('.categories ul li') - .map((a, ex) => loadedCheerio(ex).text().trim()) - .toArray() - .join(','); + const body = await fetchApi(url, { + method: 'POST', + headers: { LNRequestVerifyToken: verifytoken! }, + body: formData, + }).then(r => r.text()); - return novel; - } + let loadedCheerio = parseHTML(body); - async parsePage(novelPath: string, page: string): Promise { - const url = this.site + novelPath + '/chapters/page-' + page; - const body = await fetchApi(url).then((res) => res.text()); - const loadedCheerio = parseHTML(body); - const chapter: Plugin.ChapterItem[] = []; - loadedCheerio('.chapter-list li').each(function () { - const chapterName = - 'Chapter ' + - loadedCheerio(this).find('.chapter-no').text().trim() + - ' - ' + - loadedCheerio(this).find('.chapter-title').text().trim(); + let novels: Plugin.NovelItem[] = []; - const releaseDate = loadedCheerio(this) - .find('.chapter-update') - .attr('datetime'); + let results = JSON.parse(loadedCheerio('body').text()); - const chapterUrl = loadedCheerio(this).find('a').attr('href')?.substring(1); - if (!chapterUrl) return; + loadedCheerio = parseHTML(results.resultview); - chapter.push({ - name: chapterName, - path: chapterUrl, - releaseTime: dayjs(releaseDate).toISOString(), - }) - }); - const chapters = chapter; - return { chapters }; - } + loadedCheerio('.novel-item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h4.novel-title').text().trim(); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + const novelUrl = loadedCheerio(ele).find('a').attr('href')?.substring(1); + if (!novelUrl) return; + novels.push({ + name: novelName, + path: novelUrl, + cover: novelCover, + }); + }); - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); + return novels; + } - const loadedCheerio = parseHTML(body); + async fetchImage(url: string): Promise { + return fetchFile(url, { headers: this.headers }); + } - let chapterText = loadedCheerio("#chapter-container").html() || ''; - - return chapterText; - } - - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.site}lnsearchlive`; - const link = `${this.site}search`; - const response = await fetchApi(link).then((r) => r.text()); - const token = parseHTML(response); - let verifytoken = token("#novelSearchForm > input").attr("value"); - - let formData = new FormData(); - formData.append("inputContent", searchTerm); - - const body = await fetchApi( - url, - { - method: "POST", - headers: { LNRequestVerifyToken: verifytoken! }, - body: formData, - }, - ).then((r) => r.text()); - - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - let results = JSON.parse(loadedCheerio("body").text()); - - loadedCheerio = parseHTML(results.resultview); - - loadedCheerio(".novel-item").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find("h4.novel-title") - .text() - .trim(); - const novelCover = loadedCheerio(ele).find("img").attr("src"); - const novelUrl = loadedCheerio(ele).find("a").attr("href")?.substring(1); - if (!novelUrl) return; - novels.push({ - name: novelName, - path: novelUrl, - cover: novelCover, - }); - }); - - return novels; - } - - async fetchImage(url: string): Promise { - return fetchFile(url, { headers: this.headers }); - } - - filters = { - order: { - value: "popular", - label: "Order by", - options: [ - { label: "New", value: "new" }, - { label: "Popular", value: "popular" }, - { label: "Updates", value: "updated" }, - ], - type: FilterTypes.Picker, - }, - status: { - value: "all", - label: "Status", - options: [ - { label: "All", value: "all" }, - { label: "Completed", value: "completed" }, - { label: "Ongoing", value: "ongoing" } - ], - type: FilterTypes.Picker, - }, - genres: { - value: "all", - label: "Genre", - options: [ - { label: "All", value: "all" }, - { label: "Action", value: "action" }, - { label: "Adventure", value: "adventure" }, - { label: "Drama", value: "drama" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Harem", value: "harem" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Romance", value: "romance" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Xuanhuan", value: "xuanhuan" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Comedy", value: "comedy" }, - { label: "Slice of Life", value: "slice-of-life" }, - { label: "Mystery", value: "mystery" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Psychological", value: "psychological" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Xianxia", value: "xianxia" }, - { label: "School Life", value: "school-life" }, - { label: "Josei", value: "josei" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Shounen", value: "shounen" }, - { label: "Horror", value: "horror" }, - { label: "Mecha", value: "mecha" }, - { label: "Historical", value: "historical" }, - { label: "Shoujo", value: "shoujo" }, - { label: "Adult", value: "adult" }, - { label: "Seinen", value: "seinen" }, - { label: "Sports", value: "sports" }, - { label: "Lolicon", value: "lolicon" }, - { label: "Gender Bender", value: "gender-bender" }, - { label: "Shounen Ai", value: "shounen-ai" }, - { label: "Yaoi", value: "yaoi" }, - { label: "Video Games", value: "video-games" }, - { label: "Smut", value: "smut" }, - { label: "Magical Realism", value: "magical-realism" }, - { label: "Eastern Fantasy", value: "eastern-fantasy" }, - { label: "Contemporary Romance", value: "contemporary-romance" }, - { label: "Fantasy Romance", value: "fantasy-romance" }, - { label: "Shoujo Ai", value: "shoujo-ai" }, - { label: "Yuri", value: "yuri" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + order: { + value: 'popular', + label: 'Order by', + options: [ + { label: 'New', value: 'new' }, + { label: 'Popular', value: 'popular' }, + { label: 'Updates', value: 'updated' }, + ], + type: FilterTypes.Picker, + }, + status: { + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Completed', value: 'completed' }, + { label: 'Ongoing', value: 'ongoing' }, + ], + type: FilterTypes.Picker, + }, + genres: { + value: 'all', + label: 'Genre', + options: [ + { label: 'All', value: 'all' }, + { label: 'Action', value: 'action' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Drama', value: 'drama' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Harem', value: 'harem' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Romance', value: 'romance' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Josei', value: 'josei' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Horror', value: 'horror' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Historical', value: 'historical' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Adult', value: 'adult' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Sports', value: 'sports' }, + { label: 'Lolicon', value: 'lolicon' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Shounen Ai', value: 'shounen-ai' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Video Games', value: 'video-games' }, + { label: 'Smut', value: 'smut' }, + { label: 'Magical Realism', value: 'magical-realism' }, + { label: 'Eastern Fantasy', value: 'eastern-fantasy' }, + { label: 'Contemporary Romance', value: 'contemporary-romance' }, + { label: 'Fantasy Romance', value: 'fantasy-romance' }, + { label: 'Shoujo Ai', value: 'shoujo-ai' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new LightNovelWorld(); diff --git a/plugins/english/lnmtl.ts b/plugins/english/lnmtl.ts index b257446..41a6571 100644 --- a/plugins/english/lnmtl.ts +++ b/plugins/english/lnmtl.ts @@ -1,247 +1,245 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; class LnMTLPlugin implements Plugin.PagePlugin { - id = "lnmtl"; - name = "LnMTL"; - icon = "src/en/lnmtl/icon.png"; - site = "https://lnmtl.com/"; - version = "1.0.1"; + id = 'lnmtl'; + name = 'LnMTL'; + icon = 'src/en/lnmtl/icon.png'; + site = 'https://lnmtl.com/'; + version = '1.0.1'; - async sleep (ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)) + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site + 'novel?'; + link += `orderBy=${filters.order.value}`; + link += `&order=${filters.sort.value}`; + link += `&filter=${filters.storyStatus.value}`; + link += `&page=${page}`; + + const body = await fetchApi(link).then(result => result.text()); + + const loadedCheerio = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.media').each((i, el) => { + const novelName = loadedCheerio(el).find('h4').text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + const novelUrl = loadedCheerio(el).find('h4 > a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel( + novelPath: string, + ): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const volumes = JSON.parse( + loadedCheerio('main') + .next() + .html() + ?.match(/lnmtl.volumes = \[(.*?)\]/)![0] + ?.replace('lnmtl.volumes = ', '') || '', + ); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('.novel-name').text() || 'Untitled', + cover: loadedCheerio('div.novel').find('img').attr('src'), + summary: loadedCheerio('div.description').text().trim(), + totalPages: volumes.length, + chapters: [], + }; + + loadedCheerio('.panel-body > dl').each(function () { + const detailName = loadedCheerio(this).find('dt').text().trim(); + const detail = loadedCheerio(this).find('dd').text().trim(); + + switch (detailName) { + case 'Authors': + novel.author = detail; + break; + case 'Current status': + novel.status = detail; + break; + } + }); + + novel.genres = loadedCheerio('.panel-heading:contains(" Genres ")') + .next() + .find('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + return novel; + } + + async parsePage(novelPath: string, page: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const volumes = JSON.parse( + loadedCheerio('main') + .next() + .html() + ?.match(/lnmtl.volumes = \[(.*?)\]/)![0] + ?.replace('lnmtl.volumes = ', '') || '', + )[+page - 1]; + + const chapter: Plugin.ChapterItem[] = []; + + await this.sleep(1000); + const volumeData = await fetchApi( + `${this.site}chapter?volumeId=${volumes.id}`, + ); + const volumePage = await volumeData.json(); + const firstPage = volumePage.data.map((chapter: ChapterEntry) => ({ + name: `#${chapter.number} - ${chapter.title}`, + path: `chapter/${chapter.slug}`, + releaseTime: new Date(chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert + })); + chapter.push(...firstPage); + + for (let i = 2; i <= volumePage.last_page; i++) { + await this.sleep(1000); + const chapterData = await fetchApi( + `${this.site}chapter?page=${i}&volumeId=${volumes.id}`, + ); + const chapterInfo = await chapterData.json(); + + const chapterDetails = chapterInfo.data.map((chapter: ChapterEntry) => ({ + name: `#${chapter.number} ${chapter.title}`, + path: `chapter/${chapter.slug}`, + releaseTime: new Date(chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert + })); + + chapter.push(...chapterDetails); + } + const chapters = chapter; + return { chapters }; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + loadedCheerio('.original, script').remove(); + loadedCheerio('sentence.translated').wrap('

'); + + let chapterText = loadedCheerio('.chapter-body').html()?.replace(/„/g, '“'); + + if (!chapterText) { + chapterText = loadedCheerio('.alert.alert-warning').text(); } - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = this.site + "novel?"; - link += `orderBy=${filters.order.value}`; - link += `&order=${filters.sort.value}`; - link += `&filter=${filters.storyStatus.value}`; - link += `&page=${page}`; + return chapterText; + } - const body = await fetchApi(link).then((result) => - result.text() - ); + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const body = await fetchApi(this.site).then(r => r.text()); + const loadedCheerio = parseHTML(body); - const loadedCheerio = parseHTML(body); - const novels: Plugin.NovelItem[] = []; + const list = loadedCheerio('footer') + .next() + .next() + .html() + ?.match(/prefetch: '\/(.*json)/)![1]; - loadedCheerio(".media").each((i,el) => { - const novelName = loadedCheerio(el).find("h4").text(); - const novelCover = loadedCheerio(el).find("img").attr("src"); - const novelUrl = loadedCheerio(el).find("h4 > a").attr("href"); + const search = await fetch(`${this.site}${list}`); + const data = await search.json(); - if (!novelUrl) return; + const nov = data.filter((res: { name: string }) => + res.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; + const novels: Plugin.NovelItem[] = []; + nov.map((res: { name: string; slug: string; image: string }) => { + const novelName = res.name; + const novelUrl = `novel/${res.slug}`; + const novelCover = res.image; - novels.push(novel); - }); + const novel = { + path: novelUrl, + name: novelName, + cover: novelCover, + }; + novels.push(novel); + }); + return novels; + } - return novels; - }; + async fetchImage(url: string): Promise { + return await fetchFile(url); + } - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const volumes = JSON.parse( - loadedCheerio("main") - .next() - .html() - ?.match(/lnmtl.volumes = \[(.*?)\]/)![0] - ?.replace("lnmtl.volumes = ", "") || "" - ); - - const novel: Plugin.SourceNovel & {totalPages: number} = { - path: novelPath, - name: loadedCheerio(".novel-name").text() || 'Untitled', - cover: loadedCheerio("div.novel").find("img").attr("src"), - summary: loadedCheerio("div.description").text().trim(), - totalPages: volumes.length, - chapters: [], - }; - - loadedCheerio(".panel-body > dl").each(function () { - const detailName = loadedCheerio(this).find("dt").text().trim(); - const detail = loadedCheerio(this).find("dd").text().trim(); - - switch (detailName) { - case "Authors": - novel.author = detail; - break; - case "Current status": - novel.status = detail; - break; - } - }); - - novel.genres = loadedCheerio('.panel-heading:contains(" Genres ")') - .next() - .find('a') - .map((i,el) => loadedCheerio(el).text()) - .toArray() - .join(","); - - return novel; - }; - - async parsePage(novelPath: string, page: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const volumes = JSON.parse( - loadedCheerio("main") - .next() - .html() - ?.match(/lnmtl.volumes = \[(.*?)\]/)![0] - ?.replace("lnmtl.volumes = ", "") || "" - )[+page-1]; - - const chapter: Plugin.ChapterItem[] = []; - - await this.sleep(1000); - const volumeData = await fetchApi( - `${this.site}chapter?volumeId=${volumes.id}` - ); - const volumePage = await volumeData.json(); - const firstPage = volumePage.data.map( - (chapter: ChapterEntry) => ({ - name: `#${chapter.number} - ${chapter.title}`, - path: `chapter/${chapter.slug}`, - releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert - })) - chapter.push(...firstPage); - - for (let i = 2; i <= volumePage.last_page; i++) { - await this.sleep(1000); - const chapterData = await fetchApi( - `${this.site}chapter?page=${i}&volumeId=${volumes.id}` - ); - const chapterInfo = await chapterData.json(); - - const chapterDetails = chapterInfo.data.map( - (chapter: ChapterEntry) => ({ - name: `#${chapter.number} ${chapter.title}`, - path: `chapter/${chapter.slug}`, - releaseTime: new Date (chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert - })); - - chapter.push(...chapterDetails); - } - const chapters = chapter; - return {chapters}; - } - - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - loadedCheerio('.original, script').remove(); - loadedCheerio('sentence.translated').wrap('

'); - - let chapterText = loadedCheerio('.chapter-body').html()?.replace(/„/g, '“'); - - if (!chapterText) { - chapterText = loadedCheerio(".alert.alert-warning").text(); - } - - return chapterText; - }; - - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - const body = await fetchApi(this.site).then((r) => r.text()); - const loadedCheerio = parseHTML(body); - - const list = loadedCheerio('footer') - .next() - .next() - .html() - ?.match(/prefetch: '\/(.*json)/)![1]; - - const search = await fetch(`${this.site}${list}`); - const data = await search.json(); - - const nov = data.filter((res: { name: string; }) => - res.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - - const novels: Plugin.NovelItem[] = []; - nov.map((res: { name: string; slug: string; image: string; }) => { - const novelName = res.name; - const novelUrl = `novel/${res.slug}`; - const novelCover = res.image; - - const novel = { - path: novelUrl, - name: novelName, - cover: novelCover, - }; - novels.push(novel); - }); - return novels; - }; - - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - order: { - value: "favourites", - label: "Order by", - options: [ - { label: "Favourites", value: "favourites" }, - { label: "Name", value: "name" }, - { label: "Addition Date", value: "date" }, - ], - type: FilterTypes.Picker, - }, - sort: { - value: "desc", - label: "Sort by", - options: [ - { label: "Descending", value: "desc" }, - { label: "Ascending", value: "asc" }, - ], - type: FilterTypes.Picker, - }, - storyStatus: { - value: "all", - label: "Status", - options: [ - { label: "All", value: "all" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Finished", value: "finished" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + order: { + value: 'favourites', + label: 'Order by', + options: [ + { label: 'Favourites', value: 'favourites' }, + { label: 'Name', value: 'name' }, + { label: 'Addition Date', value: 'date' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'desc', + label: 'Sort by', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Finished', value: 'finished' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new LnMTLPlugin(); interface ChapterEntry { - number: number; - title: string; - slug: string; - created_at: string; -} \ No newline at end of file + number: number; + title: string; + slug: string; + created_at: string; +} diff --git a/plugins/english/mtlnovel.ts b/plugins/english/mtlnovel.ts index ef04f4e..2d52bd5 100644 --- a/plugins/english/mtlnovel.ts +++ b/plugins/english/mtlnovel.ts @@ -1,191 +1,187 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; class MTLNovel implements Plugin.PluginBase { - id = "mtlnovel"; - name = "MTL Novel"; - version = "1.0.0"; - icon = "src/en/mtlnovel/icon.png"; - site = "https://www.mtlnovel.com/"; - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}novel-list/?`; - link += `orderby=${filters.order.value}`; - link += `&order=${filters.sort.value}`; - link += `&status=${filters.storyStatus.value}`; - link += `&pg=${page}` - - const headers = new Headers(); - headers.append('Alt-Used', 'www.mtlnovel.com'); - const body = await fetch(link, {headers}).then((result) => result.text()); + id = 'mtlnovel'; + name = 'MTL Novel'; + version = '1.0.0'; + icon = 'src/en/mtlnovel/icon.png'; + site = 'https://www.mtlnovel.com/'; + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}novel-list/?`; + link += `orderby=${filters.order.value}`; + link += `&order=${filters.sort.value}`; + link += `&status=${filters.storyStatus.value}`; + link += `&pg=${page}`; - const loadedCheerio = parseHTML(body); + const headers = new Headers(); + headers.append('Alt-Used', 'www.mtlnovel.com'); + const body = await fetch(link, { headers }).then(result => result.text()); - const novels: Plugin.NovelItem[] = []; + const loadedCheerio = parseHTML(body); - loadedCheerio("div.box.wide").each((i,el) => { - const novelName = loadedCheerio(el) - .find("a.list-title") - .text() - .slice(4); - const novelCover = loadedCheerio(el).find("amp-img").attr("src"); - const novelUrl = loadedCheerio(el).find("a.list-title").attr("href"); + const novels: Plugin.NovelItem[] = []; - if (!novelUrl) return; + loadedCheerio('div.box.wide').each((i, el) => { + const novelName = loadedCheerio(el).find('a.list-title').text().slice(4); + const novelCover = loadedCheerio(el).find('amp-img').attr('src'); + const novelUrl = loadedCheerio(el).find('a.list-title').attr('href'); - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; + if (!novelUrl) return; - novels.push(novel); + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + async parseNovel(novelPath: string): Promise { + const headers = new Headers(); + headers.append('Referer', `${this.site}novel-list/`); + headers.append('Alt-Used', 'www.mtlnovel.com'); + + const body = await fetch(this.site + novelPath, { + headers, + }).then(result => result.text()); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.entry-title').text() || 'Untitled', + cover: loadedCheerio('.nov-head > amp-img').attr('src'), + summary: loadedCheerio('div.desc > h2').next().text().trim(), + author: loadedCheerio('#author').text(), + status: loadedCheerio('#status').text(), + genres: loadedCheerio('#genre').text().replace(/\s+/g, ''), + chapters: [], + }; + + const chapterListUrl = this.site + novelPath + 'chapter-list/'; + + const getChapters = async () => { + const listResult = await fetch(chapterListUrl, { headers }); + const listBody = await listResult.text(); + + loadedCheerio = parseHTML(listBody); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('div.ch-list') + .find('a.ch-link') + .each((i, el) => { + const chapterName = loadedCheerio(el).text().replace('~ ', ''); + const releaseDate = null; + const chapterUrl = loadedCheerio(el).attr('href'); + if (!chapterUrl) return; + chapter.push({ + path: chapterUrl.replace(this.site, ''), + name: chapterName, + releaseTime: releaseDate, + }); }); + return chapter.reverse(); + }; - return novels; + novel.chapters = await getChapters(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const headers = new Headers(); + headers.append('Alt-Used', 'www.mtlnovel.com'); + const body = await fetch(this.site + chapterPath, { headers }).then(r => + r.text(), + ); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('div.par').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const headers = new Headers(); + headers.append('Alt-Used', 'www.mtlnovel.com'); + const searchUrl = + this.site + + 'wp-admin/admin-ajax.php?action=autosuggest&q=' + + searchTerm + + '&__amp_source_origin=https%3A%2F%2Fwww.mtlnovel.com'; + + const res = await fetch(searchUrl, { headers }); + const result = await res.json(); + + const novels: Plugin.NovelItem[] = []; + interface SearchEntry { + title: string; + thumbnail: string; + permalink: string; } - async parseNovel(novelPath: string): Promise { - const headers = new Headers(); - headers.append('Referer', `${this.site}novel-list/`) - headers.append('Alt-Used', 'www.mtlnovel.com'); + result.items[0].results.map((item: SearchEntry) => { + const novelName = item.title.replace(/<\/?strong>/g, ''); + const novelCover = item.thumbnail; + const novelUrl = item.permalink.replace(this.site, ''); - const body = await fetch( - this.site + novelPath, - { - headers - }, - ).then((result) => result.text()); + const novel = { name: novelName, cover: novelCover, path: novelUrl }; - let loadedCheerio = parseHTML(body); + novels.push(novel); + }); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio("h1.entry-title").text() || 'Untitled', - cover: loadedCheerio(".nov-head > amp-img").attr("src"), - summary: loadedCheerio("div.desc > h2").next().text().trim(), - author: loadedCheerio("#author").text(), - status: loadedCheerio("#status").text(), - genres: loadedCheerio("#genre").text().replace(/\s+/g, ""), - chapters: [], - }; + return novels; + } + async fetchImage(url: string): Promise { + const headers = new Headers(); + headers.append('Alt-Used', 'www.mtlnovel.com'); + return fetchFile(url, { headers }); + } - const chapterListUrl = this.site + novelPath + "chapter-list/"; - - const getChapters = async () => { - const listResult = await fetch(chapterListUrl, {headers}); - const listBody = await listResult.text(); - - loadedCheerio = parseHTML(listBody); - - const chapter: Plugin.ChapterItem[] = []; - - loadedCheerio("div.ch-list") - .find("a.ch-link") - .each((i,el) => { - const chapterName = loadedCheerio(el) - .text() - .replace("~ ", ""); - const releaseDate = null; - const chapterUrl = loadedCheerio(el).attr("href"); - if (!chapterUrl) return; - chapter.push({ - path: chapterUrl.replace(this.site, ''), - name: chapterName, - releaseTime: releaseDate, - }); - - }); - return chapter.reverse(); - } - - novel.chapters = await getChapters(); - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const headers = new Headers(); - headers.append('Alt-Used', 'www.mtlnovel.com'); - const body = await fetch(this.site + chapterPath, {headers}).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio("div.par").html() || ''; - - return chapterText; - } - - async searchNovels(searchTerm: string, pageNo: number): Promise { - const headers = new Headers(); - headers.append('Alt-Used', 'www.mtlnovel.com'); - const searchUrl = - this.site + - "wp-admin/admin-ajax.php?action=autosuggest&q=" + - searchTerm + - "&__amp_source_origin=https%3A%2F%2Fwww.mtlnovel.com"; - - const res = await fetch(searchUrl, {headers}); - const result = await res.json(); - - const novels: Plugin.NovelItem[] = []; - interface SearchEntry { - title: string; - thumbnail: string; - permalink: string; - } - result.items[0].results.map((item: SearchEntry) => { - const novelName = item.title.replace(/<\/?strong>/g, ""); - const novelCover = item.thumbnail; - const novelUrl = item.permalink.replace(this.site, ''); - - const novel = { name: novelName, cover: novelCover, path: novelUrl }; - - novels.push(novel); - }); - - return novels; - } - async fetchImage(url: string): Promise { - const headers = new Headers(); - headers.append('Alt-Used', 'www.mtlnovel.com'); - return fetchFile(url, {headers}); - } - - filters = { - order: { - value: "view", - label: "Order by", - options: [ - { label: "Date", value: "date" }, - { label: "Name", value: "name" }, - { label: "Rating", value: "rating" }, - { label: "View", value: "view" }, - ], - type: FilterTypes.Picker, - }, - sort: { - value: "desc", - label: "Sort by", - options: [ - { label: "Descending", value: "desc" }, - { label: "Ascending", value: "asc" }, - ], - type: FilterTypes.Picker, - }, - storyStatus: { - value: "all", - label: "Status", - options: [ - { label: "All", value: "all" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Complete", value: "completed" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + order: { + value: 'view', + label: 'Order by', + options: [ + { label: 'Date', value: 'date' }, + { label: 'Name', value: 'name' }, + { label: 'Rating', value: 'rating' }, + { label: 'View', value: 'view' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'desc', + label: 'Sort by', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Complete', value: 'completed' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new MTLNovel(); diff --git a/plugins/english/mtlreader.ts b/plugins/english/mtlreader.ts index 163ce85..6dc3932 100644 --- a/plugins/english/mtlreader.ts +++ b/plugins/english/mtlreader.ts @@ -1,106 +1,111 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; class MTLReader implements Plugin.PluginBase { - id = "mtlreader"; - name = "MTL Reader"; - version = "1.0.0"; - icon = "src/en/mtlreader/icon.png"; - site = "https://mtlreader.com/"; - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + id = 'mtlreader'; + name = 'MTL Reader'; + version = '1.0.0'; + icon = 'src/en/mtlreader/icon.png'; + site = 'https://mtlreader.com/'; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".col-md-4").each((i,el) => { - const novelName = loadedCheerio(el).find("h5").text(); - const novelCover = loadedCheerio(el).find("img").attr("src"); - const novelUrl = loadedCheerio(el).find("h5 > a").attr("href"); + loadedCheerio('.col-md-4').each((i, el) => { + const novelName = loadedCheerio(el).find('h5').text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + const novelUrl = loadedCheerio(el).find('h5 > a').attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; - novels.push(novel); - }); + novels.push(novel); + }); - return novels; - } + return novels; + } - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.site}novels?page=${pageNo}`; + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const url = `${this.site}novels?page=${pageNo}`; - const body = await fetchApi(url).then((r) => r.text()); + const body = await fetchApi(url).then(r => r.text()); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((r) => r.text()); + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".agent-title").text().trim() || "Untitled", - cover: loadedCheerio(".agent-p-img > img").attr("src"), - summary: loadedCheerio("#editdescription").text().trim(), - chapters: [], - }; + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.agent-title').text().trim() || 'Untitled', + cover: loadedCheerio('.agent-p-img > img').attr('src'), + summary: loadedCheerio('#editdescription').text().trim(), + chapters: [], + }; - novel.author = loadedCheerio("i.fa-user") - .parent() - .text() - .replace("Author: ", "") - .trim(); + novel.author = loadedCheerio('i.fa-user') + .parent() + .text() + .replace('Author: ', '') + .trim(); - const chapter: Plugin.ChapterItem[] = []; + const chapter: Plugin.ChapterItem[] = []; - loadedCheerio("tr.spaceUnder").each((i,el) => { - const chapterName = loadedCheerio(el).find("a").text().trim(); - const chapterUrl = loadedCheerio(el).find("a").attr("href"); + loadedCheerio('tr.spaceUnder').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text().trim(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); - if (!chapterUrl) return; + if (!chapterUrl) return; - chapter.push({ - name: chapterName, - path: chapterUrl.replace(this.site, ''), - }); - }); + chapter.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); - novel.chapters = chapter; + novel.chapters = chapter; - return novel; - } - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); + return novel; + } + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - loadedCheerio(".container ins,script,p.mtlreader").remove(); - const chapterText = loadedCheerio(".container").html() || ''; + loadedCheerio('.container ins,script,p.mtlreader').remove(); + const chapterText = loadedCheerio('.container').html() || ''; - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const body = await fetchApi(this.site).then((r) => r.text()); - const tokenCheerio = parseHTML(body); - const token = tokenCheerio('input[name="_token"]').attr('value'); + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const body = await fetchApi(this.site).then(r => r.text()); + const tokenCheerio = parseHTML(body); + const token = tokenCheerio('input[name="_token"]').attr('value'); - const searchUrl = `${this.site}search?_token=${token}&input=${searchTerm}`; - const seacrhBody = await fetchApi(searchUrl).then((r) => r.text()); - const loadedCheerio = parseHTML(seacrhBody); - return this.parseNovels(loadedCheerio); - } - - async fetchImage(url: string): Promise { - return fetchFile(url); - } + const searchUrl = `${this.site}search?_token=${token}&input=${searchTerm}`; + const seacrhBody = await fetchApi(searchUrl).then(r => r.text()); + const loadedCheerio = parseHTML(seacrhBody); + return this.parseNovels(loadedCheerio); + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new MTLReader(); diff --git a/plugins/english/novelbin.ts b/plugins/english/novelbin.ts index 4c98249..676025d 100644 --- a/plugins/english/novelbin.ts +++ b/plugins/english/novelbin.ts @@ -1,231 +1,233 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; class NovelBin implements Plugin.PluginBase { - id = "novelbin"; - name = "Novel Bin"; - icon = "src/en/novelbin/icon.png"; - site = "https://binnovel.com/"; - version = "1.0.0"; - parseNovels(loadedCheerio:CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + id = 'novelbin'; + name = 'Novel Bin'; + icon = 'src/en/novelbin/icon.png'; + site = 'https://binnovel.com/'; + version = '1.0.0'; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".col-novel-main .list-novel .row").each((i,el) => { - const novelName = loadedCheerio(el).find("h3.novel-title > a").text(); - const novelCover = loadedCheerio(el).find("img.cover").attr("src"); - const novelUrl = loadedCheerio(el) - .find("h3.novel-title > a") - .attr("href"); + loadedCheerio('.col-novel-main .list-novel .row').each((i, el) => { + const novelName = loadedCheerio(el).find('h3.novel-title > a').text(); + const novelCover = loadedCheerio(el).find('img.cover').attr('src'); + const novelUrl = loadedCheerio(el) + .find('h3.novel-title > a') + .attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; - novels.push(novel); + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site; + if (filters.genre.value !== '') link += `gen/${filters.genre.value}`; + else link += `sort/${filters.sort.value}`; + + if (filters.complete.value) link += `/completed`; + + link += `?page=${page}`; + + const body = await fetchApi(link).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('div.book > img').attr('alt') || 'Untitled', + cover: loadedCheerio('div.book > img').attr('src'), + summary: loadedCheerio('div.desc-text').text().trim(), + chapters: [], + }; + + loadedCheerio('ul.info > li > h3').each(function () { + let detailName = loadedCheerio(this).text(); + let detail = loadedCheerio(this) + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + switch (detailName) { + case 'Author:': + novel.author = detail; + break; + case 'Status:': + novel.status = detail; + break; + case 'Genre:': + novel.genres = detail; + break; + } + }); + + const novelId = loadedCheerio('#rating').attr('data-novel-id'); + + const getChapters = async (id: string) => { + const chapterListUrl = this.site + 'ajax/chapter-archive?novelId=' + id; + + const data = await fetchApi(chapterListUrl); + const chapterdata = await data.text(); + + loadedCheerio = parseHTML(chapterdata); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('ul.list-chapter > li').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').attr('title'); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + + if (!chapterName || !chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), }); + }); + return chapter; + }; - return novels; + if (novelId) { + novel.chapters = await getChapters(novelId); } - async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions): Promise { - let link = this.site; - if (filters.genre.value !== '') - link += `gen/${filters.genre.value}`; - else - link += `sort/${filters.sort.value}`; + return novel; + } - if (filters.complete.value) - link += `/completed`; + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); - link += `?page=${page}`; + const loadedCheerio = parseHTML(body); - const body = await fetchApi(link).then((r) => r.text()); + loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove(); + const chapterText = loadedCheerio('#chr-content').html() || ''; - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + return chapterText; + } - async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((r) => r.text()); + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}search/?keyword=${searchTerm}`; + const body = await fetchApi(url).then(r => r.text()); - let loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio("div.book > img").attr("alt") || "Untitled", - cover: loadedCheerio("div.book > img").attr("src"), - summary: loadedCheerio("div.desc-text").text().trim(), - chapters: [], - }; + async fetchImage(url: string): Promise { + return fetchFile(url); + } - loadedCheerio("ul.info > li > h3").each(function () { - let detailName = loadedCheerio(this).text(); - let detail = loadedCheerio(this) - .siblings() - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - - switch (detailName) { - case "Author:": - novel.author = detail; - break; - case "Status:": - novel.status = detail; - break; - case "Genre:": - novel.genres = detail; - break; - } - }); - - const novelId = loadedCheerio("#rating").attr("data-novel-id"); - - const getChapters = async (id: string) => { - const chapterListUrl = - this.site + "ajax/chapter-archive?novelId=" + id; - - const data = await fetchApi(chapterListUrl); - const chapterdata = await data.text(); - - loadedCheerio = parseHTML(chapterdata); - - const chapter: Plugin.ChapterItem[] = []; - - loadedCheerio("ul.list-chapter > li").each((i,el) => { - const chapterName = loadedCheerio(el).find("a").attr("title"); - const chapterUrl = loadedCheerio(el).find("a").attr("href"); - - if (!chapterName || !chapterUrl) return; - - chapter.push({ - name: chapterName, - path: chapterUrl.replace(this.site, ''), - }); - }); - return chapter; - } - - if (novelId) { - novel.chapters = await getChapters(novelId); - } - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove(); - const chapterText = loadedCheerio("#chr-content").html() || ''; - - return chapterText; - } - - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.site}search/?keyword=${searchTerm}`; - const body = await fetchApi(url).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async fetchImage(url: string): Promise { - return fetchFile(url); - } - - filters = { - sort: { - value: "popular-novels", - label: "Sort by", - options: [ - { label: "Latest Release", value: "update" }, - { label: "Hot Novel", value: "hot-novels" }, - { label: "Completed Novel", value: "c" }, - { label: "Most Popular", value: "popular-novels" }, - ], - type: FilterTypes.Picker, - }, - genre: { - value: "", - label: "Genres", - options: [ - { label: "None", value: "" }, - { label: "Action", value: "action" }, - { label: "Adult", value: "adult" }, - { label: "Adventure", value: "adventure" }, - { label: "Anime", value: "anime" }, - { label: "Arts", value: "arts" }, - { label: "Comedy", value: "comedy" }, - { label: "Drama", value: "drama" }, - { label: "Eastern", value: "eastern" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Fan-fiction", value: "fan-fiction" }, - { label: "Fanfiction", value: "fanfiction" }, - { label: "Fantas", value: "fantas" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Game", value: "game" }, - { label: "Gender bender", value: "gender-bender" }, - { label: "Harem", value: "harem" }, - { label: "Hentai", value: "hentai" }, - { label: "Historical", value: "historical" }, - { label: "Horror", value: "horror" }, - { label: "Isekai", value: "isekai" }, - { label: "Josei", value: "josei" }, - { label: "Lgbt+", value: "lgbt+" }, - { label: "Litrpg", value: "litrpg" }, - { label: "Magic", value: "magic" }, - { label: "Magical realism", value: "magical-realism" }, - { label: "Manhua", value: "manhua" }, - { label: "Martial arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Mecha", value: "mecha" }, - { label: "Military", value: "military" }, - { label: "Modern life", value: "modern-life" }, - { label: "Movies", value: "movies" }, - { label: "Mystery", value: "mystery" }, - { label: "Portal fantasy", value: "portal-fantasy" }, - { label: "Psychological", value: "psychological" }, - { label: "Realistic fiction", value: "realistic-fiction" }, - { label: "Reincarnation", value: "reincarnation" }, - { label: "Romance", value: "romance" }, - { label: "School life", value: "school-life" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Seinen", value: "seinen" }, - { label: "Shoujo", value: "shoujo" }, - { label: "Shoujo ai", value: "shoujo-ai" }, - { label: "Shounen", value: "shounen" }, - { label: "Shounen ai", value: "shounen-ai" }, - { label: "Slice of life", value: "slice-of-life" }, - { label: "Smut", value: "smut" }, - { label: "Sports", value: "sports" }, - { label: "Supernatural", value: "supernatural" }, - { label: "System", value: "system" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Urban life", value: "urban-life" }, - { label: "Video games", value: "video-games" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Xianxia", value: "xianxia" }, - { label: "Xuanhuan", value: "xuanhuan" }, - { label: "Yaoi", value: "yaoi" }, - { label: "Yuri", value: "yuri" } - ], - type: FilterTypes.Picker, - }, - complete: { - label: "Show Completed Novels Only", - value: false, - type: FilterTypes.Switch, - }, - } satisfies Filters; + filters = { + sort: { + value: 'popular-novels', + label: 'Sort by', + options: [ + { label: 'Latest Release', value: 'update' }, + { label: 'Hot Novel', value: 'hot-novels' }, + { label: 'Completed Novel', value: 'c' }, + { label: 'Most Popular', value: 'popular-novels' }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: '', + label: 'Genres', + options: [ + { label: 'None', value: '' }, + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Anime', value: 'anime' }, + { label: 'Arts', value: 'arts' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Eastern', value: 'eastern' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fan-fiction', value: 'fan-fiction' }, + { label: 'Fanfiction', value: 'fanfiction' }, + { label: 'Fantas', value: 'fantas' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Game', value: 'game' }, + { label: 'Gender bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Hentai', value: 'hentai' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Isekai', value: 'isekai' }, + { label: 'Josei', value: 'josei' }, + { label: 'Lgbt+', value: 'lgbt+' }, + { label: 'Litrpg', value: 'litrpg' }, + { label: 'Magic', value: 'magic' }, + { label: 'Magical realism', value: 'magical-realism' }, + { label: 'Manhua', value: 'manhua' }, + { label: 'Martial arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Military', value: 'military' }, + { label: 'Modern life', value: 'modern-life' }, + { label: 'Movies', value: 'movies' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Portal fantasy', value: 'portal-fantasy' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Realistic fiction', value: 'realistic-fiction' }, + { label: 'Reincarnation', value: 'reincarnation' }, + { label: 'Romance', value: 'romance' }, + { label: 'School life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shoujo ai', value: 'shoujo-ai' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Shounen ai', value: 'shounen-ai' }, + { label: 'Slice of life', value: 'slice-of-life' }, + { label: 'Smut', value: 'smut' }, + { label: 'Sports', value: 'sports' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'System', value: 'system' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Urban life', value: 'urban-life' }, + { label: 'Video games', value: 'video-games' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.Picker, + }, + complete: { + label: 'Show Completed Novels Only', + value: false, + type: FilterTypes.Switch, + }, + } satisfies Filters; } export default new NovelBin(); diff --git a/plugins/english/novelbuddy.ts b/plugins/english/novelbuddy.ts index 7bce39a..f4352df 100644 --- a/plugins/english/novelbuddy.ts +++ b/plugins/english/novelbuddy.ts @@ -1,171 +1,178 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; class NovelBuddy implements Plugin.PluginBase { - id = "novelbuddy"; - name = "NovelBuddy.io"; - site = "https://novelbuddy.io/"; - version = "1.0.0"; - icon = "src/en/novelbuddy/icon.png"; - filters?: undefined; //TODO: Filters, Filters in Filters required + id = 'novelbuddy'; + name = 'NovelBuddy.io'; + site = 'https://novelbuddy.io/'; + version = '1.0.0'; + icon = 'src/en/novelbuddy/icon.png'; + filters?: undefined; //TODO: Filters, Filters in Filters required - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".book-item").each((idx, ele) => { - const novelName = loadedCheerio(ele).find(".title").text(); - const novelCover = - "https:" + loadedCheerio(ele).find("img").attr("data-src"); - const novelUrl = loadedCheerio(ele).find(".title a").attr("href")?.substring(1); + loadedCheerio('.book-item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.title').text(); + const novelCover = + 'https:' + loadedCheerio(ele).find('img').attr('data-src'); + const novelUrl = loadedCheerio(ele) + .find('.title a') + .attr('href') + ?.substring(1); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { name: novelName, cover: novelCover, path: novelUrl }; + const novel = { name: novelName, cover: novelCover, path: novelUrl }; - novels.push(novel); + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const url = `${this.site}popular?page=${pageNo}`; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.name h1').text().trim() || 'Untitled', + cover: 'https:' + loadedCheerio('.img-cover img').attr('data-src'), + summary: loadedCheerio('.section-body.summary').text().trim(), + chapters: [], + }; + + loadedCheerio('.meta.box p').each((i, el) => { + const detailName = loadedCheerio(el).find('strong').text(); + const detail = loadedCheerio(el).find('a'); + + switch (detailName) { + case 'Authors :': + novel.author = detail + .find('span') + .map((a, ex) => loadedCheerio(ex).text()) + .toArray() + .join(', '); + break; + case 'Status :': + novel.status = detail.text(); + break; + case 'Genres :': + novel.genres = detail.text().trim(); + break; + } + }); + const novelId = loadedCheerio('script') + .text() + .match(/bookId = (\d+);/)![1]; + const chapter: Plugin.ChapterItem[] = []; + + const getChapters = async (id: string) => { + const chapterListUrl = `${this.site}api/manga/${id}/chapters?source=detail`; + const data = await fetchApi(chapterListUrl); + const chapterlist = await data.text(); + + loadedCheerio = parseHTML(chapterlist); + + loadedCheerio('li').each((i, el) => { + const chapterName = loadedCheerio(el) + .find('.chapter-title') + .text() + .trim(); + + const releaseDate = loadedCheerio(el) + .find('.chapter-update') + .text() + .trim(); + + const months = [ + 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'jun', + 'jul', + 'aug', + 'sep', + 'oct', + 'nov', + 'dec', + ].join('|'); + const rx = new RegExp(`(${months}) (\\d{1,2}), (\\d{4})`, 'i').exec( + releaseDate, + ); + if (!rx) return; + const year = +rx[3]; + const month = months.indexOf(rx[1].toLowerCase()); + const day = +rx[2]; + + const chapterUrl = loadedCheerio(el).find('a').attr('href')?.slice(1); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + releaseTime: new Date(year, month, day).toISOString(), + path: chapterUrl, }); + }); + return chapter; + }; - return novels; - } + novel.chapters = (await getChapters(novelId)).reverse(); - async popularNovels( - pageNo: number, - options: Plugin.PopularNovelsOptions - ): Promise { - const url = `${this.site}popular?page=${pageNo}`; + return novel; + } - const result = await fetchApi(url); - const body = await result.text(); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const loadedCheerio = parseHTML(body); - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + loadedCheerio('#listen-chapter').remove(); + loadedCheerio('#google_translate_element').remove(); - let loadedCheerio = parseHTML(body); + const chapterText = loadedCheerio('.chapter__content').html() || ''; - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".name h1").text().trim() || "Untitled", - cover: "https:" + loadedCheerio(".img-cover img").attr("data-src"), - summary: loadedCheerio('.section-body.summary').text().trim(), - chapters: [], - }; + return chapterText; + } - loadedCheerio('.meta.box p').each((i,el) => { - const detailName = loadedCheerio(el).find('strong').text(); - const detail = loadedCheerio(el).find('a'); - - switch(detailName){ - case "Authors :": - novel.author = detail.find('span') - .map((a,ex) => loadedCheerio(ex).text()) - .toArray() - .join(', '); - break; - case "Status :": - novel.status = detail.text(); - break; - case "Genres :": - novel.genres = detail.text().trim(); - break; - } - }) - const novelId = loadedCheerio('script') - .text() - .match(/bookId = (\d+);/)![1]; - const chapter: Plugin.ChapterItem[] = []; + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const url = `${this.site}search?q=${searchTerm}&page=${page}`; - const getChapters = async (id: string) => { - const chapterListUrl = `${this.site}api/manga/${id}/chapters?source=detail`; - const data = await fetchApi(chapterListUrl); - const chapterlist = await data.text(); + const result = await fetchApi(url); + const body = await result.text(); - loadedCheerio = parseHTML(chapterlist); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - loadedCheerio("li").each((i,el) => { - const chapterName = loadedCheerio(el) - .find(".chapter-title") - .text() - .trim(); - - const releaseDate = loadedCheerio(el) - .find(".chapter-update") - .text() - .trim(); - - const months = [ - "jan", - "feb", - "mar", - "apr", - "may", - "jun", - "jul", - "aug", - "sep", - "oct", - "nov", - "dec", - ].join("|"); - const rx = new RegExp(`(${months}) (\\d{1,2}), (\\d{4})`,'i') - .exec(releaseDate); - if (!rx) return; - const year = +rx[3]; - const month = months.indexOf(rx[1].toLowerCase()); - const day = +rx[2]; - - const chapterUrl = loadedCheerio(el).find("a").attr("href")?.slice(1); - - if (!chapterUrl) return; - - chapter.push({ - name: chapterName, - releaseTime: new Date(year, month, day).toISOString(), - path: chapterUrl, - }); - }); - return chapter; - } - - novel.chapters = (await getChapters(novelId)).reverse(); - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - loadedCheerio("#listen-chapter").remove(); - loadedCheerio("#google_translate_element").remove(); - - const chapterText = loadedCheerio(".chapter__content").html() || ''; - - return chapterText; - } - - async searchNovels(searchTerm: string, page: number): Promise { - const url = `${this.site}search?q=${searchTerm}&page=${page}`; - - const result = await fetchApi(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async fetchImage(url: string): Promise { - return fetchFile(url); - } - + async fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new NovelBuddy(); diff --git a/plugins/english/novelfull.ts b/plugins/english/novelfull.ts index e0acfe6..e0b3c80 100644 --- a/plugins/english/novelfull.ts +++ b/plugins/english/novelfull.ts @@ -1,200 +1,201 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; - +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; class NovelFull implements Plugin.PluginBase { - id = "novelfull"; - name = "NovelFull"; - version = "1.0.0"; - icon = "src/en/novelfull/icon.png"; - site = "https://novelfull.com/"; + id = 'novelfull'; + name = 'NovelFull'; + version = '1.0.0'; + icon = 'src/en/novelfull/icon.png'; + site = 'https://novelfull.com/'; - parseNovels(loadedCheerio:CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".col-truyen-main .list-truyen .row").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find("h3.truyen-title > a") - .text(); + loadedCheerio('.col-truyen-main .list-truyen .row').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text(); - const novelCover = - this.site + loadedCheerio(ele).find("img").attr("src")?.slice(1); + const novelCover = + this.site + loadedCheerio(ele).find('img').attr('src')?.slice(1); - const novelUrl = loadedCheerio(ele) - .find("h3.truyen-title > a") - .attr("href") - ?.slice(1); + const novelUrl = loadedCheerio(ele) + .find('h3.truyen-title > a') + .attr('href') + ?.slice(1); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl, - }; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl, + }; - novels.push(novel); + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site; + if (filters.genre.value !== '') link += `genre/${filters.genre.value}`; + else link += filters.sort.value; + + link += `?page=${page}`; + + const body = await fetchApi(link).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('div.book > img').attr('alt') || 'Untitled', + cover: this.site + loadedCheerio('div.book > img').attr('src'), + summary: loadedCheerio('div.desc-text').text().trim(), + status: loadedCheerio('h3:contains("Status")').next().text(), + chapters: [], + }; + + novel.author = loadedCheerio('h3:contains("Author")') + .parent() + .contents() + .text() + .replace('Author:', ''); + + novel.genres = loadedCheerio('h3:contains("Genre")') + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + const novelId = loadedCheerio('#rating').attr('data-novel-id')!; + const chapter: Plugin.ChapterItem[] = []; + + const getChapters = async (id: string) => { + const chapterListUrl = this.site + 'ajax/chapter-option?novelId=' + id; + + const data = await fetchApi(chapterListUrl); + const chapterlist = await data.text(); + + loadedCheerio = parseHTML(chapterlist); + + loadedCheerio('select > option').each(function () { + const chapterName = loadedCheerio(this).text(); + const chapterUrl = loadedCheerio(this).attr('value')?.slice(1); + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl, }); + }); + return chapter; + }; - return novels; - } + novel.chapters = await getChapters(novelId); - async popularNovels(page: number, {filters}: Plugin.PopularNovelsOptions): Promise { - let link = this.site; - if (filters.genre.value !== '') - link += `genre/${filters.genre.value}`; - else - link += filters.sort.value + return novel; + } - link += `?page=${page}`; + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - const body = await fetchApi(link).then((r) => r.text()); + const loadedCheerio = parseHTML(body); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + loadedCheerio('#chapter-content div.ads').remove(); + const chapterText = loadedCheerio('#chapter-content').html() || ''; - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + return chapterText; + } - let loadedCheerio = parseHTML(body); + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`; - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio("div.book > img").attr("alt") || "Untitled", - cover: this.site + loadedCheerio("div.book > img").attr("src"), - summary: loadedCheerio("div.desc-text").text().trim(), - status: loadedCheerio('h3:contains("Status")').next().text(), - chapters: [], - }; + const result = await fetchApi(searchUrl); + const body = await result.text(); - novel.author = loadedCheerio('h3:contains("Author")') - .parent() - .contents() - .text() - .replace("Author:", ""); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - novel.genres = loadedCheerio('h3:contains("Genre")') - .siblings() - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); + async fetchImage(url: string): Promise { + return fetchFile(url); + } - const novelId = loadedCheerio("#rating").attr("data-novel-id")!; - const chapter: Plugin.ChapterItem[] = []; - - const getChapters = async (id: string) => { - const chapterListUrl = this.site + "ajax/chapter-option?novelId=" + id; - - const data = await fetchApi(chapterListUrl); - const chapterlist = await data.text(); - - loadedCheerio = parseHTML(chapterlist); - - loadedCheerio("select > option").each(function () { - const chapterName = loadedCheerio(this).text(); - const chapterUrl = loadedCheerio(this).attr("value")?.slice(1); - if (!chapterUrl) return; - - chapter.push({ - name: chapterName, - path: chapterUrl, - }); - }); - return chapter; - } - - novel.chapters = await getChapters(novelId); - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - loadedCheerio("#chapter-content div.ads").remove(); - const chapterText = loadedCheerio("#chapter-content").html() || ''; - - return chapterText; - } - - async searchNovels(searchTerm: string, page: number): Promise { - const searchUrl = `${this.site}search?keyword=${searchTerm}&page=${page}`; - - const result = await fetchApi(searchUrl); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - async fetchImage(url: string): Promise { - return fetchFile(url); - } - - filters = { - sort: { - value: "most-popular", - label: "Sort by", - options: [ - { label: "Latest Release", value: "latest-release-novel" }, - { label: "Hot Novel", value: "hot-novel" }, - { label: "Completed Novel", value: "completed-novel" }, - { label: "Most Popular", value: "most-popular" }, - ], - type: FilterTypes.Picker, - }, - genre: { - value: "", - label: "Genres", - options: [ - { label: "None", value: "" }, - { label: "Shounen", value: "Shounen" }, - { label: "Harem", value: "Harem" }, - { label: "Comedy", value: "Comedy" }, - { label: "Martial Arts", value: "Martial+Arts" }, - { label: "School Life", value: "School+Life" }, - { label: "Mystery", value: "Mystery" }, - { label: "Shoujo", value: "Shoujo" }, - { label: "Romance", value: "Romance" }, - { label: "Sci-fi", value: "Sci-fi" }, - { label: "Gender Bender", value: "Gender+Bender" }, - { label: "Mature", value: "Mature" }, - { label: "Fantasy", value: "Fantasy" }, - { label: "Horror", value: "Horror" }, - { label: "Drama", value: "Drama" }, - { label: "Tragedy", value: "Tragedy" }, - { label: "Supernatural", value: "Supernatural" }, - { label: "Ecchi", value: "Ecchi" }, - { label: "Xuanhuan", value: "Xuanhuan" }, - { label: "Adventure", value: "Adventure" }, - { label: "Action", value: "Action" }, - { label: "Psychological", value: "Psychological" }, - { label: "Xianxia", value: "Xianxia" }, - { label: "Wuxia", value: "Wuxia" }, - { label: "Historical", value: "Historical" }, - { label: "Slice of Life", value: "Slice+of+Life" }, - { label: "Seinen", value: "Seinen" }, - { label: "Lolicon", value: "Lolicon" }, - { label: "Adult", value: "Adult" }, - { label: "Josei", value: "Josei" }, - { label: "Sports", value: "Sports" }, - { label: "Smut", value: "Smut" }, - { label: "Mecha", value: "Mecha" }, - { label: "Yaoi", value: "Yaoi" }, - { label: "Shounen Ai", value: "Shounen+Ai" }, - { label: "History", value: "History" }, - { label: "Martial", value: "Martial" } - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + sort: { + value: 'most-popular', + label: 'Sort by', + options: [ + { label: 'Latest Release', value: 'latest-release-novel' }, + { label: 'Hot Novel', value: 'hot-novel' }, + { label: 'Completed Novel', value: 'completed-novel' }, + { label: 'Most Popular', value: 'most-popular' }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: '', + label: 'Genres', + options: [ + { label: 'None', value: '' }, + { label: 'Shounen', value: 'Shounen' }, + { label: 'Harem', value: 'Harem' }, + { label: 'Comedy', value: 'Comedy' }, + { label: 'Martial Arts', value: 'Martial+Arts' }, + { label: 'School Life', value: 'School+Life' }, + { label: 'Mystery', value: 'Mystery' }, + { label: 'Shoujo', value: 'Shoujo' }, + { label: 'Romance', value: 'Romance' }, + { label: 'Sci-fi', value: 'Sci-fi' }, + { label: 'Gender Bender', value: 'Gender+Bender' }, + { label: 'Mature', value: 'Mature' }, + { label: 'Fantasy', value: 'Fantasy' }, + { label: 'Horror', value: 'Horror' }, + { label: 'Drama', value: 'Drama' }, + { label: 'Tragedy', value: 'Tragedy' }, + { label: 'Supernatural', value: 'Supernatural' }, + { label: 'Ecchi', value: 'Ecchi' }, + { label: 'Xuanhuan', value: 'Xuanhuan' }, + { label: 'Adventure', value: 'Adventure' }, + { label: 'Action', value: 'Action' }, + { label: 'Psychological', value: 'Psychological' }, + { label: 'Xianxia', value: 'Xianxia' }, + { label: 'Wuxia', value: 'Wuxia' }, + { label: 'Historical', value: 'Historical' }, + { label: 'Slice of Life', value: 'Slice+of+Life' }, + { label: 'Seinen', value: 'Seinen' }, + { label: 'Lolicon', value: 'Lolicon' }, + { label: 'Adult', value: 'Adult' }, + { label: 'Josei', value: 'Josei' }, + { label: 'Sports', value: 'Sports' }, + { label: 'Smut', value: 'Smut' }, + { label: 'Mecha', value: 'Mecha' }, + { label: 'Yaoi', value: 'Yaoi' }, + { label: 'Shounen Ai', value: 'Shounen+Ai' }, + { label: 'History', value: 'History' }, + { label: 'Martial', value: 'Martial' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new NovelFull(); diff --git a/plugins/english/novelhall.ts b/plugins/english/novelhall.ts index 3d75180..9a0d3c4 100644 --- a/plugins/english/novelhall.ts +++ b/plugins/english/novelhall.ts @@ -1,128 +1,133 @@ -import { load, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; -import { defaultCover } from "@libs/defaultCover"; +import { load, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; class NovelHall implements Plugin.PluginBase { - id = "novelhall"; - name = "Novel Hall"; - version = "1.0.0"; - icon = "src/en/novelhall/icon.png"; - filters?: Filters | undefined; //TODO: Filters Requires hideOnSelect - site = "https://novelhall.com/"; + id = 'novelhall'; + name = 'Novel Hall'; + version = '1.0.0'; + icon = 'src/en/novelhall/icon.png'; + filters?: Filters | undefined; //TODO: Filters Requires hideOnSelect + site = 'https://novelhall.com/'; - async popularNovels(page: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.site}all2022-${page}.html`; + async popularNovels( + page: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const url = `${this.site}all2022-${page}.html`; - const body = await fetchApi(url).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); + const body = await fetchApi(url).then(r => r.text()); - const novels: Plugin.NovelItem[] = []; + const loadedCheerio = parseHTML(body); - loadedCheerio("li.btm").each((idx, ele) => { - const novelName = loadedCheerio(ele).text().trim() - const novelUrl = loadedCheerio(ele).find("a").attr("href"); - if(!novelUrl) return; + const novels: Plugin.NovelItem[] = []; - const novel = { - name: novelName, - cover: defaultCover, - path: novelUrl, - }; + loadedCheerio('li.btm').each((idx, ele) => { + const novelName = loadedCheerio(ele).text().trim(); + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + if (!novelUrl) return; - novels.push(novel); - }); + const novel = { + name: novelName, + cover: defaultCover, + path: novelUrl, + }; - return novels; - } - async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((r) => r.text()); + novels.push(novel); + }); - const loadedCheerio = parseHTML(body); + return novels; + } + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".book-info > h1").text() || 'Untitled', - cover: loadedCheerio('meta[property="og:image"]').attr('content'), - summary: loadedCheerio(".intro").text().trim(), - chapters: [], - }; + const loadedCheerio = parseHTML(body); - loadedCheerio('.total').find('p').remove(); - novel.author = loadedCheerio('.total span:contains("Author")') - .text() - .replace("Author:", "") - .trim(); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.book-info > h1').text() || 'Untitled', + cover: loadedCheerio('meta[property="og:image"]').attr('content'), + summary: loadedCheerio('.intro').text().trim(), + chapters: [], + }; - novel.status = loadedCheerio('.total span:contains("Status")') - .text() - .replace("Status:", "") - .replace("Active", "Ongoing") - .trim(); + loadedCheerio('.total').find('p').remove(); + novel.author = loadedCheerio('.total span:contains("Author")') + .text() + .replace('Author:', '') + .trim(); - novel.genres = loadedCheerio('.total a') - .map((a,ex) => loadedCheerio(ex).text()) - .toArray() - .join(','); + novel.status = loadedCheerio('.total span:contains("Status")') + .text() + .replace('Status:', '') + .replace('Active', 'Ongoing') + .trim(); - const chapter: Plugin.ChapterItem[] = []; + novel.genres = loadedCheerio('.total a') + .map((a, ex) => loadedCheerio(ex).text()) + .toArray() + .join(','); - loadedCheerio("#morelist ul > li").each((idx, ele) => { - const chapterName = loadedCheerio(ele).find("a").text().trim(); - const chapterUrl = loadedCheerio(ele).find("a").attr("href"); - if (!chapterUrl) return; + const chapter: Plugin.ChapterItem[] = []; - chapter.push({ - name: chapterName, - path: chapterUrl, - }); - }); + loadedCheerio('#morelist ul > li').each((idx, ele) => { + const chapterName = loadedCheerio(ele).find('a').text().trim(); + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + if (!chapterUrl) return; - novel.chapters = chapter; - return novel; - } + chapter.push({ + name: chapterName, + path: chapterUrl, + }); + }); - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); - const loadedCheerio = parseHTML(body); - const chapterText = loadedCheerio(".content").html() || ''; - return chapterText; - } + novel.chapters = chapter; + return novel; + } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.site}index.php?s=so&module=book&keyword=${searchTerm}`; - const body = await fetchApi(url).then((r) => r.text()); - const loadedCheerio = parseHTML(body); + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + const chapterText = loadedCheerio('.content').html() || ''; + return chapterText; + } - const novels: Plugin.NovelItem[] = []; + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}index.php?s=so&module=book&keyword=${searchTerm}`; + const body = await fetchApi(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); - loadedCheerio("#article_list_content > li").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find("h3") - .text() - .replace(/\t+/g, "") - .replace(/\n/g, " "); - const novelCover = loadedCheerio(ele).find("img").attr("data-src"); - const novelUrl = loadedCheerio(ele).find("a").attr("href")?.slice(1); - if (!novelUrl) return; + const novels: Plugin.NovelItem[] = []; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl, - }; + loadedCheerio('#article_list_content > li').each((idx, ele) => { + const novelName = loadedCheerio(ele) + .find('h3') + .text() + .replace(/\t+/g, '') + .replace(/\n/g, ' '); + const novelCover = loadedCheerio(ele).find('img').attr('data-src'); + const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1); + if (!novelUrl) return; - novels.push(novel); - }); + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl, + }; - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + novels.push(novel); + }); + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new NovelHall(); diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts index 5be0dfc..8fd57ff 100644 --- a/plugins/english/novelupdates.ts +++ b/plugins/english/novelupdates.ts @@ -1,463 +1,470 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; class NovelUpdates implements Plugin.PluginBase { - id = "novelupdates"; - name = "Novel Updates"; - version = "0.5.1"; - icon = "src/en/novelupdates/icon.png"; - site = "https://www.novelupdates.com/"; + id = 'novelupdates'; + name = 'Novel Updates'; + version = '0.5.1'; + icon = 'src/en/novelupdates/icon.png'; + site = 'https://www.novelupdates.com/'; - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio("div.search_main_box_nu").each((idx, ele) => { - const novelCover = loadedCheerio(ele).find("img").attr("src"); - const novelName = loadedCheerio(ele).find(".search_title > a").text(); - const novelUrl = loadedCheerio(ele) - .find(".search_title > a") - .attr("href"); + loadedCheerio('div.search_main_box_nu').each((idx, ele) => { + const novelCover = loadedCheerio(ele).find('img').attr('src'); + const novelName = loadedCheerio(ele).find('.search_title > a').text(); + const novelUrl = loadedCheerio(ele) + .find('.search_title > a') + .attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; - novels.push(novel); - }); + novels.push(novel); + }); - return novels; + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}`; + if ( + filters.language.value.length || + filters.novelType.value.length || + filters.genres.value.include?.length || + filters.genres.value.exclude?.length || + filters.storyStatus.value !== '' + ) { + link += 'series-finder/?sf=1'; + } else if (showLatestNovels) { + link += 'latest-series/?st=1'; + } else { + link += 'series-ranking/?rank=week'; } - async popularNovels(page: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions): Promise { - let link = `${this.site}`; - if (filters.language.value.length || - filters.novelType.value.length || - filters.genres.value.include?.length || - filters.genres.value.exclude?.length || - filters.storyStatus.value !== '' ){ - link += "series-finder/?sf=1" - } else if (showLatestNovels){ - link += "latest-series/?st=1" - } else { - link += "series-ranking/?rank=week" - }; - - if (filters.language.value.length) - link += '&org=' + filters.language.value.join(','); - - if (filters.novelType.value.length) - link += '&nt=' + filters.novelType.value.join(','); + if (filters.language.value.length) + link += '&org=' + filters.language.value.join(','); - if (filters.genres.value.include?.length) - link += '&gi=' + filters.genres.value.include.join(','); + if (filters.novelType.value.length) + link += '&nt=' + filters.novelType.value.join(','); - if (filters.genres.value.exclude?.length) - link += '&ge=' + filters.genres.value.exclude.join(','); + if (filters.genres.value.include?.length) + link += '&gi=' + filters.genres.value.include.join(','); - if (filters.genres.value.include?.length || filters.genres.value.exclude?.length) - link += '&mgi=' + filters.genre_operator.value; + if (filters.genres.value.exclude?.length) + link += '&ge=' + filters.genres.value.exclude.join(','); - if (filters.storyStatus.value.length) - link += '&ss=' + filters.storyStatus.value; + if ( + filters.genres.value.include?.length || + filters.genres.value.exclude?.length + ) + link += '&mgi=' + filters.genre_operator.value; - link += '&sort=' + filters.sort.value; + if (filters.storyStatus.value.length) + link += '&ss=' + filters.storyStatus.value; - link += '&order=' + filters.order.value; + link += '&sort=' + filters.sort.value; - link += '&pg=' + page; + link += '&order=' + filters.order.value; - const body = await fetchApi(link).then((result) => - result.text() - ); + link += '&pg=' + page; - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); + const body = await fetchApi(link).then(result => result.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + const result = await fetchApi(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.seriestitlenu').text() || 'Untitled', + cover: loadedCheerio('.wpb_wrapper img').attr('src'), + chapters: [], + }; + + novel.author = loadedCheerio('#authtag') + .map((i, el) => loadedCheerio(el).text().trim()) + .toArray() + .join(', '); + + novel.genres = loadedCheerio('#seriesgenre') + .children('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + novel.status = loadedCheerio('#editstatus').text().includes('Ongoing') + ? 'Ongoing' + : 'Completed'; + + const type = loadedCheerio('#showtype').text().trim(); + + const summary = loadedCheerio('#editdescription').text().trim(); + + novel.summary = summary + `\n\nType: ${type}`; + + const chapter: Plugin.ChapterItem[] = []; + + const novelId = loadedCheerio('input#mypostid').attr('value')!; + + const formData = new FormData(); + formData.append('action', 'nd_getchapters'); + formData.append('mygrr', '0'); + formData.append('mypostid', novelId); + + const link = `${this.site}wp-admin/admin-ajax.php`; + + const text = await fetchApi(link, { + method: 'POST', + body: formData, + }).then(data => data.text()); + + loadedCheerio = parseHTML(text); + + loadedCheerio('li.sp_li_chp').each((i, el) => { + const chapterName = loadedCheerio(el).text().trim(); + const chapterUrl = + 'https:' + loadedCheerio(el).find('a').first().next().attr('href'); + + chapter.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + + getLocation(href: string) { + var match = href.match( + /^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/, + ); + return match && `${match[1]}//${match[3]}`; + } + + async parseChapter(chapterPath: string): Promise { + let chapterText = ''; + + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + // console.log(result.chapterUrl); + + // console.log('Redirected URL: ', result.chapterUrl); + + const loadedCheerio = parseHTML(body); + + let isWuxiaWorld = result.url.toLowerCase().includes('wuxiaworld'); + + let isBlogspot = result.url.toLowerCase().includes('blogspot'); + + let isTumblr = result.url.toLowerCase().includes('tumblr'); + + let isWattpad = result.url.toLowerCase().includes('wattpad'); + + let isAnomalously = result.url.toLowerCase().includes('anotivereads'); + + let kofi = result.url.toLowerCase().includes('ko-fi'); + + let isBlossomTranslation = result.url + .toLowerCase() + .includes('blossomtranslation'); + + let isLightNovelsTls = result.url + .toLowerCase() + .includes('lightnovelstranslations'); + + let isiNovelTranslation = result.url + .toLowerCase() + .includes('inoveltranslation'); + + let isTravisTranslation = result.url + .toLowerCase() + .includes('travistranslations'); + + /** + * Checks if its a wordpress site + */ + let isWordPressStr = + loadedCheerio('meta[name="generator"]').attr('content') || + loadedCheerio('footer').text(); + + let isWordPress = false; + + if (isWordPressStr) { + isWordPress = + isWordPressStr.toLowerCase().includes('wordpress') || + isWordPressStr.includes('Site Kit by Google') || + loadedCheerio('.powered-by').text().toLowerCase().includes('wordpress'); } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - const result = await fetchApi(url); - const body = await result.text(); + let isRainOfSnow = result.url.toLowerCase().includes('rainofsnow'); - let loadedCheerio = parseHTML(body); + let isWebNovel = result.url.toLowerCase().includes('webnovel'); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".seriestitlenu").text() || "Untitled", - cover: loadedCheerio('.wpb_wrapper img').attr('src'), - chapters: [], - }; + let isHostedNovel = result.url.toLowerCase().includes('hostednovel'); - novel.author = loadedCheerio('#authtag') - .map((i, el) => loadedCheerio(el).text().trim()) - .toArray() - .join(', '); + let isScribbleHub = result.url.toLowerCase().includes('scribblehub'); - novel.genres = loadedCheerio("#seriesgenre") - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); + if (isWuxiaWorld) { + chapterText = loadedCheerio('#chapter-content').html()!; + } else if (isRainOfSnow) { + chapterText = loadedCheerio('div.content').html()!; + } else if (isTumblr) { + chapterText = loadedCheerio('.post').html()!; + } else if (isBlogspot) { + loadedCheerio('.post-share-buttons').remove(); + chapterText = + loadedCheerio('.entry-content').html() || + loadedCheerio('#post-body').html()!; + } else if (isHostedNovel) { + chapterText = loadedCheerio('.chapter').html()!; + } else if (isScribbleHub) { + chapterText = loadedCheerio('div.chp_raw').html()!; + } else if (isWattpad) { + chapterText = loadedCheerio('.container pre').html()!; + } else if (isTravisTranslation) { + chapterText = loadedCheerio('.reader-content').html()!; + } else if (isLightNovelsTls) { + chapterText = loadedCheerio('.text_story').html()!; + } else if (isBlossomTranslation) { + chapterText = loadedCheerio('.manga-child-content').html()!; + } else if (isAnomalously) { + chapterText = loadedCheerio('#comic').html()!; + } else if (kofi) { + chapterText = loadedCheerio('script:contains("shadowDom.innerHTML")') + .html() + ?.match(/shadowDom\.innerHTML \+= '( r.json()); + chapterText = json.content.replace(/\n/g, '
'); + if (json.notes) { + chapterText += + '


TL Notes:
' + json.notes.replace(/\n/g, '
'); + } + } else if (isWordPress) { + /** + * Remove wordpress bloat tags + */ - novel.status = loadedCheerio("#editstatus").text().includes("Ongoing") - ? "Ongoing" - : "Completed"; + const bloatClasses = [ + '.c-ads', + '#madara-comments', + '#comments', + '.content-comments', + '.sharedaddy', + '.wp-dark-mode-switcher', + '.wp-next-post-navi', + '.wp-block-buttons', + '.wp-block-columns', + '.post-cats', + '.sidebar', + '.author-avatar', + '.ezoic-ad', + ]; - const type = loadedCheerio("#showtype").text().trim(); + bloatClasses.map(tag => loadedCheerio(tag).remove()); - const summary = loadedCheerio("#editdescription").text().trim(); + chapterText = + loadedCheerio('.entry-content').html() || + loadedCheerio('.single_post').html() || + loadedCheerio('.post-entry').html() || + loadedCheerio('.main-content').html() || + loadedCheerio('article.post').html() || + loadedCheerio('.content').html() || + loadedCheerio('#content').html() || + loadedCheerio('.page-body').html() || + loadedCheerio('.td-page-content').html()!; + } else if (isWebNovel) { + chapterText = loadedCheerio('.cha-words').html()!; - novel.summary = summary + `\n\nType: ${type}`; + if (!chapterText) { + chapterText = loadedCheerio('._content').html()!; + } + } else { + /** + * Remove unnecessary tags + */ + const tags = ['nav', 'header', 'footer', '.hidden']; - const chapter: Plugin.ChapterItem[] = []; + tags.map(tag => loadedCheerio(tag).remove()); - const novelId = loadedCheerio("input#mypostid").attr("value")!; - - const formData = new FormData(); - formData.append("action", "nd_getchapters"); - formData.append("mygrr", "0"); - formData.append("mypostid", novelId); - - const link = `${this.site}wp-admin/admin-ajax.php`; - - const text = await fetchApi( - link, - { - method: "POST", - body: formData, - }, - ).then((data) => data.text()); - - loadedCheerio = parseHTML(text); - - loadedCheerio("li.sp_li_chp").each((i,el) => { - const chapterName = loadedCheerio(el).text().trim(); - const chapterUrl = - "https:" + - loadedCheerio(el).find("a").first().next().attr("href"); - - chapter.push({ - name: chapterName, - path: chapterUrl.replace(this.site, ''), - }); - }); - - novel.chapters = chapter.reverse(); - - return novel; + chapterText = loadedCheerio('body').html()!; } - getLocation(href: string) { - var match = href.match( - /^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/ - ); - return match && `${match[1]}//${match[3]}`; + if (chapterText) { + /** + * Convert relative urls to absolute + */ + chapterText = chapterText.replace( + /href="\//g, + `href="${this.getLocation(result.url)}/`, + ); } - async parseChapter(chapterPath: string): Promise { - let chapterText = ""; + return chapterText; + } - const result = await fetchApi( - this.site + chapterPath, - ); - const body = await result.text(); + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const url = + this.site + + '?s=' + + searchTerm.replace(/\s+/g, '+') + + '&post_type=seriesplans'; + const result = await fetchApi(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - // console.log(result.chapterUrl); + fetchImage(url: string): Promise { + return fetchFile(url); + } - // console.log('Redirected URL: ', result.chapterUrl); - - const loadedCheerio = parseHTML(body); - - let isWuxiaWorld = result.url.toLowerCase().includes("wuxiaworld"); - - let isBlogspot = result.url.toLowerCase().includes("blogspot"); - - let isTumblr = result.url.toLowerCase().includes("tumblr"); - - let isWattpad = result.url.toLowerCase().includes("wattpad"); - - let isAnomalously = result.url.toLowerCase().includes('anotivereads'); - - let kofi = result.url.toLowerCase().includes('ko-fi'); - - let isBlossomTranslation = result.url - .toLowerCase() - .includes('blossomtranslation'); - - let isLightNovelsTls = result.url - .toLowerCase() - .includes("lightnovelstranslations"); - - let isiNovelTranslation = result.url - .toLowerCase() - .includes("inoveltranslation"); - - let isTravisTranslation = result.url - .toLowerCase() - .includes("travistranslations"); - - /** - * Checks if its a wordpress site - */ - let isWordPressStr = - loadedCheerio('meta[name="generator"]').attr("content") || - loadedCheerio("footer").text(); - - let isWordPress = false; - - if (isWordPressStr) { - isWordPress = - isWordPressStr.toLowerCase().includes("wordpress") || - isWordPressStr.includes("Site Kit by Google") || - loadedCheerio(".powered-by") - .text() - .toLowerCase() - .includes("wordpress"); - } - - let isRainOfSnow = result.url.toLowerCase().includes("rainofsnow"); - - let isWebNovel = result.url.toLowerCase().includes("webnovel"); - - let isHostedNovel = result.url.toLowerCase().includes("hostednovel"); - - let isScribbleHub = result.url.toLowerCase().includes("scribblehub"); - - if (isWuxiaWorld) { - chapterText = loadedCheerio("#chapter-content").html()!; - } else if (isRainOfSnow) { - chapterText = loadedCheerio("div.content").html()!; - } else if (isTumblr) { - chapterText = loadedCheerio(".post").html()!; - } else if (isBlogspot) { - loadedCheerio('.post-share-buttons').remove(); - chapterText = - loadedCheerio('.entry-content').html() || - loadedCheerio('#post-body').html()!; - } else if (isHostedNovel) { - chapterText = loadedCheerio(".chapter").html()!; - } else if (isScribbleHub) { - chapterText = loadedCheerio("div.chp_raw").html()!; - } else if (isWattpad) { - chapterText = loadedCheerio(".container pre").html()!; - } else if (isTravisTranslation) { - chapterText = loadedCheerio(".reader-content").html()!; - } else if (isLightNovelsTls) { - chapterText = loadedCheerio('.text_story').html()!; - } else if (isBlossomTranslation) { - chapterText = loadedCheerio('.manga-child-content').html()!; - } else if (isAnomalously) { - chapterText = loadedCheerio('#comic').html()!; - } else if (kofi) { - chapterText = loadedCheerio('script:contains("shadowDom.innerHTML")') - .html() - ?.match(/shadowDom\.innerHTML \+= '( r.json()); - chapterText = json.content.replace(/\n/g, '
'); - if (json.notes) { - chapterText += - '


TL Notes:
' + json.notes.replace(/\n/g, '
'); - } - } else if (isWordPress) { - /** - * Remove wordpress bloat tags - */ - - const bloatClasses = [ - ".c-ads", - "#madara-comments", - "#comments", - ".content-comments", - ".sharedaddy", - ".wp-dark-mode-switcher", - ".wp-next-post-navi", - ".wp-block-buttons", - ".wp-block-columns", - ".post-cats", - ".sidebar", - ".author-avatar", - ".ezoic-ad", - ]; - - bloatClasses.map((tag) => loadedCheerio(tag).remove()); - - chapterText = - loadedCheerio(".entry-content").html() || - loadedCheerio(".single_post").html() || - loadedCheerio(".post-entry").html() || - loadedCheerio(".main-content").html() || - loadedCheerio("article.post").html() || - loadedCheerio(".content").html() || - loadedCheerio("#content").html() || - loadedCheerio(".page-body").html() || - loadedCheerio(".td-page-content").html()!; - } else if (isWebNovel) { - chapterText = loadedCheerio(".cha-words").html()!; - - if (!chapterText) { - chapterText = loadedCheerio("._content").html()!; - } - } else { - /** - * Remove unnecessary tags - */ - const tags = ["nav", "header", "footer", ".hidden"]; - - tags.map((tag) => loadedCheerio(tag).remove()); - - chapterText = loadedCheerio("body").html()!; - } - - if (chapterText) { - /** - * Convert relative urls to absolute - */ - chapterText = chapterText.replace( - /href="\//g, - `href="${this.getLocation(result.url)}/` - ); - } - - return chapterText; - } - - async searchNovels(searchTerm: string, page: number): Promise { - const url = this.site + '?s=' + searchTerm.replace(/\s+/g, '+') + '&post_type=seriesplans'; - const result = await fetchApi(url); - const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - - fetchImage(url: string): Promise { - return fetchFile(url); - } - - filters = { - sort: { - label: "Sort Results By", - value: "sdate", - options: [ - { label: "Last Updated", value: "sdate" }, - { label: "Rating", value: "srate" }, - { label: "Rank", value: "srank" }, - { label: "Reviews", value: "sreview" }, - { label: "Chapters", value: "srel" }, - { label: "Title", value: "abc" }, - { label: "Readers", value: "sread" }, - { label: "Frequency", value: "sfrel" }, - ], - type: FilterTypes.Picker, - }, - order: { - label: "Order", - value: "desc", - options: [ - { label: "Descending", value: "desc" }, - { label: "Ascending", value: "asc" }, - ], - type: FilterTypes.Picker, - }, - storyStatus: { - label: "Story Status (Translation)", - value: "", - options: [ - { label: "All", value: "" }, - { label: "Completed", value: "2" }, - { label: "Ongoing", value: "3" }, - { label: "Hiatus", value: "4" }, - ], - type: FilterTypes.Picker, - }, - language: { - label: "Language", - value: [], - options: [ - { label: "Chinese", value: "495" }, - { label: "Filipino", value: "9181" }, - { label: "Indonesian", value: "9179" }, - { label: "Japanese", value: "496" }, - { label: "Khmer", value: "18657" }, - { label: "Korean", value: "497" }, - { label: "Malaysian", value: "9183" }, - { label: "Thai", value: "9954" }, - { label: "Vietnamese", value: "9177" }, - ], - type: FilterTypes.CheckboxGroup, - }, - novelType: { - label: "Novel Type", - value: [], - options: [ - { label: "Light Novel", value: "2443" }, - { label: "Published Novel", value: "26874" }, - { label: "Web Novel", value: "2444" }, - ], - type: FilterTypes.CheckboxGroup, - }, - genres: { - label: "Genres", - type: FilterTypes.ExcludableCheckboxGroup, - value: { - include: [], - exclude: [], - }, - options: [ - { label: "Action", value: "8" }, - { label: "Adult", value: "280" }, - { label: "Adventure", value: "13" }, - { label: "Comedy", value: "17" }, - { label: "Drama", value: "9" }, - { label: "Ecchi", value: "292" }, - { label: "Fantasy", value: "5" }, - { label: "Gender Bender", value: "168" }, - { label: "Harem", value: "3" }, - { label: "Historical", value: "330" }, - { label: "Horror", value: "343" }, - { label: "Josei", value: "324" }, - { label: "Martial Arts", value: "14" }, - { label: "Mature", value: "4" }, - { label: "Mecha", value: "10" }, - { label: "Mystery", value: "245" }, - { label: "Psychoical", value: "486" }, - { label: "Romance", value: "15" }, - { label: "School Life", value: "6" }, - { label: "Sci-fi", value: "11" }, - { label: "Seinen", value: "18" }, - { label: "Shoujo", value: "157" }, - { label: "Shoujo Ai", value: "851" }, - { label: "Shounen", value: "12" }, - { label: "Shounen Ai", value: "1692" }, - { label: "Slice of Life", value: "7" }, - { label: "Smut", value: "281" }, - { label: "Sports", value: "1357" }, - { label: "Supernatural", value: "16" }, - { label: "Tragedy", value: "132" }, - { label: "Wuxia", value: "479" }, - { label: "Xianxia", value: "480" }, - { label: "Xuanhuan", value: "3954" }, - { label: "Yaoi", value: "560" }, - { label: "Yuri", value: "922" }, - ], - }, - genre_operator: { - label: "Genre (AND/OR)", - value: "and", - options: [ - { label: "AND", value: "and" }, - { label: "OR", value: "or" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + filters = { + sort: { + label: 'Sort Results By', + value: 'sdate', + options: [ + { label: 'Last Updated', value: 'sdate' }, + { label: 'Rating', value: 'srate' }, + { label: 'Rank', value: 'srank' }, + { label: 'Reviews', value: 'sreview' }, + { label: 'Chapters', value: 'srel' }, + { label: 'Title', value: 'abc' }, + { label: 'Readers', value: 'sread' }, + { label: 'Frequency', value: 'sfrel' }, + ], + type: FilterTypes.Picker, + }, + order: { + label: 'Order', + value: 'desc', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + label: 'Story Status (Translation)', + value: '', + options: [ + { label: 'All', value: '' }, + { label: 'Completed', value: '2' }, + { label: 'Ongoing', value: '3' }, + { label: 'Hiatus', value: '4' }, + ], + type: FilterTypes.Picker, + }, + language: { + label: 'Language', + value: [], + options: [ + { label: 'Chinese', value: '495' }, + { label: 'Filipino', value: '9181' }, + { label: 'Indonesian', value: '9179' }, + { label: 'Japanese', value: '496' }, + { label: 'Khmer', value: '18657' }, + { label: 'Korean', value: '497' }, + { label: 'Malaysian', value: '9183' }, + { label: 'Thai', value: '9954' }, + { label: 'Vietnamese', value: '9177' }, + ], + type: FilterTypes.CheckboxGroup, + }, + novelType: { + label: 'Novel Type', + value: [], + options: [ + { label: 'Light Novel', value: '2443' }, + { label: 'Published Novel', value: '26874' }, + { label: 'Web Novel', value: '2444' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genres: { + label: 'Genres', + type: FilterTypes.ExcludableCheckboxGroup, + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Action', value: '8' }, + { label: 'Adult', value: '280' }, + { label: 'Adventure', value: '13' }, + { label: 'Comedy', value: '17' }, + { label: 'Drama', value: '9' }, + { label: 'Ecchi', value: '292' }, + { label: 'Fantasy', value: '5' }, + { label: 'Gender Bender', value: '168' }, + { label: 'Harem', value: '3' }, + { label: 'Historical', value: '330' }, + { label: 'Horror', value: '343' }, + { label: 'Josei', value: '324' }, + { label: 'Martial Arts', value: '14' }, + { label: 'Mature', value: '4' }, + { label: 'Mecha', value: '10' }, + { label: 'Mystery', value: '245' }, + { label: 'Psychoical', value: '486' }, + { label: 'Romance', value: '15' }, + { label: 'School Life', value: '6' }, + { label: 'Sci-fi', value: '11' }, + { label: 'Seinen', value: '18' }, + { label: 'Shoujo', value: '157' }, + { label: 'Shoujo Ai', value: '851' }, + { label: 'Shounen', value: '12' }, + { label: 'Shounen Ai', value: '1692' }, + { label: 'Slice of Life', value: '7' }, + { label: 'Smut', value: '281' }, + { label: 'Sports', value: '1357' }, + { label: 'Supernatural', value: '16' }, + { label: 'Tragedy', value: '132' }, + { label: 'Wuxia', value: '479' }, + { label: 'Xianxia', value: '480' }, + { label: 'Xuanhuan', value: '3954' }, + { label: 'Yaoi', value: '560' }, + { label: 'Yuri', value: '922' }, + ], + }, + genre_operator: { + label: 'Genre (AND/OR)', + value: 'and', + options: [ + { label: 'AND', value: 'and' }, + { label: 'OR', value: 'or' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new NovelUpdates(); diff --git a/plugins/english/pawread.ts b/plugins/english/pawread.ts index 6f6c75f..1aa0629 100644 --- a/plugins/english/pawread.ts +++ b/plugins/english/pawread.ts @@ -1,226 +1,230 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; class PawRead implements Plugin.PluginBase { - id = "pawread"; - name = "PawRead"; - version = "2.0.0"; - icon = "src/en/pawread/icon.png"; - site = "https://m.pawread.com/"; + id = 'pawread'; + name = 'PawRead'; + version = '2.0.0'; + icon = 'src/en/pawread/icon.png'; + site = 'https://m.pawread.com/'; - parseNovels(loadedCheerio:CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".list-comic, .itemBox").each((idx, ele) => { - loadedCheerio(ele).find('.serialise').remove(); - const novelName = loadedCheerio(ele).find('a').text().trim(); - const novelCover = loadedCheerio(ele).find('img').attr('src'); - const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1); + loadedCheerio('.list-comic, .itemBox').each((idx, ele) => { + loadedCheerio(ele).find('.serialise').remove(); + const novelName = loadedCheerio(ele).find('a').text().trim(); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + const novelUrl = loadedCheerio(ele).find('a').attr('href')?.slice(1); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl, - }; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl, + }; - novels.push(novel); - }); + novels.push(novel); + }); - return novels; - } + return novels; + } - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}list/`; + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}list/`; - const filterValues = [ - filters.status.value, - filters.lang.value, - filters.genre.value - ]; + const filterValues = [ + filters.status.value, + filters.lang.value, + filters.genre.value, + ]; - link += filterValues.filter(value => value !== '').join('-'); - if (filterValues.some(value => value !== '')) - link += '/'; + link += filterValues.filter(value => value !== '').join('-'); + if (filterValues.some(value => value !== '')) link += '/'; - if (filters.order.value) - link += '-'; - - link += filters.sort.value; - link += `/?page=${page}`; + if (filters.order.value) link += '-'; - const body = await fetchApi(link).then((r) => r.text()); + link += filters.sort.value; + link += `/?page=${page}`; - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const body = await fetchApi(link).then(r => r.text()); - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - const loadedCheerio = parseHTML(body); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio("#Cover img").attr('title') || "Untitled", - cover: loadedCheerio("#Cover img").attr('src'), - author: loadedCheerio('.icon01 <').text().trim(), - status: loadedCheerio('.txtItme:first').text().trim(), - chapters: [], - }; + const loadedCheerio = parseHTML(body); - novel.summary = loadedCheerio("#full-des") - .find('br') - .replaceWith('\n') - .end() - .text() - .trim(); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('#Cover img').attr('title') || 'Untitled', + cover: loadedCheerio('#Cover img').attr('src'), + author: loadedCheerio('.icon01 <').text().trim(), + status: loadedCheerio('.txtItme:first').text().trim(), + chapters: [], + }; - novel.genres = loadedCheerio('a.btn-default') - .map((i, el) => loadedCheerio(el).text().trim()) - .toArray() - .join(","); + novel.summary = loadedCheerio('#full-des') + .find('br') + .replaceWith('\n') + .end() + .text() + .trim(); - const chapter: Plugin.ChapterItem[] = []; + novel.genres = loadedCheerio('a.btn-default') + .map((i, el) => loadedCheerio(el).text().trim()) + .toArray() + .join(','); - loadedCheerio(".item-box").each((idx, ele) => { - const smallText = loadedCheerio(ele).find('span:last').text().trim(); - if (smallText === "Advanced Chapter") return; - const releaseDate = smallText.split('.').map(x => Number(x)); - const chapterName = loadedCheerio(ele).find("span:first").text().trim(); - const chapterUrl = loadedCheerio(ele).attr("onclick")?.match(/\d+/)![0]; - if (!chapterUrl) return; + const chapter: Plugin.ChapterItem[] = []; - chapter.push({ - name: chapterName, - path: `${novelPath}${chapterUrl}.html`, - releaseTime: new Date(releaseDate[0], releaseDate[1], releaseDate[2]).toISOString(), - }); - }); + loadedCheerio('.item-box').each((idx, ele) => { + const smallText = loadedCheerio(ele).find('span:last').text().trim(); + if (smallText === 'Advanced Chapter') return; + const releaseDate = smallText.split('.').map(x => Number(x)); + const chapterName = loadedCheerio(ele).find('span:first').text().trim(); + const chapterUrl = loadedCheerio(ele).attr('onclick')?.match(/\d+/)![0]; + if (!chapterUrl) return; - novel.chapters = chapter; - return novel; - } + chapter.push({ + name: chapterName, + path: `${novelPath}${chapterUrl}.html`, + releaseTime: new Date( + releaseDate[0], + releaseDate[1], + releaseDate[2], + ).toISOString(), + }); + }); + novel.chapters = chapter; + return novel; + } - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - const steal = ['bit.ly', 'tinyurl', 'pawread']; - steal.map(tag => loadedCheerio(`p:icontains(${tag})`).remove()); - const chapterText = loadedCheerio('.main').html() || ""; + const steal = ['bit.ly', 'tinyurl', 'pawread']; + steal.map(tag => loadedCheerio(`p:icontains(${tag})`).remove()); + const chapterText = loadedCheerio('.main').html() || ''; - return chapterText; - } + return chapterText; + } - async searchNovels(searchTerm: string, page: number): Promise { - const searchUrl = `${this.site}search/?keywords=${searchTerm}&page=${page}`; + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const searchUrl = `${this.site}search/?keywords=${searchTerm}&page=${page}`; - const result = await fetchApi(searchUrl); - const body = await result.text(); + const result = await fetchApi(searchUrl); + const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + async fetchImage(url: string): Promise { + return fetchFile(url); + } - filters = { - status: { - value: "", - label: "Status", - options: [ - { label: "All", value: "" }, - { label: "Completed", value: "wanjie" }, - { label: "Ongoing", value: "lianzai" }, - { label: "Hiatus", value: "hiatus" }, - ], - type: FilterTypes.Picker, - }, - lang: { - value: "", - label: "Languages", - options: [ - { label: "All", value: "" }, - { label: "Chinese", value: "chinese" }, - { label: "Korean", value: "korean" }, - { label: "Japanese", value: "japanese" }, - ], - type: FilterTypes.Picker, - }, - genre: { - value: "", - label: "Genres", - options: [ - { label: "All", value: "" }, - { label: "Fantasy", value: "Fantasy" }, - { label: "Action", value: "Action" }, - { label: "Xuanhuan", value: "Xuanhuan" }, - { label: "Romance", value: "Romance" }, - { label: "Comedy", value: "Comedy" }, - { label: "Mystery", value: "Mystery" }, - { label: "Mature", value: "Mature" }, - { label: "Harem", value: "Harem" }, - { label: "Wuxia", value: "Wuxia" }, - { label: "Xianxia", value: "Xianxia" }, - { label: "Tragedy", value: "Tragedy" }, - { label: "Sci-fi", value: "Scifi" }, - { label: "Historical", value: "Historical" }, - { label: "Ecchi", value: "Ecchi" }, - { label: "Adventure", value: "Adventure" }, - { label: "Adult", value: "Adult" }, - { label: "Supernatural", value: "Supernatural" }, - { label: "Psychological", value: "Psychological" }, - { label: "Drama", value: "Drama" }, - { label: "Horror", value: "Horror" }, - { label: "Josei", value: "Josei" }, - { label: "Mecha", value: "Mecha" }, - { label: "Seinen", value: "Seinen" }, - { label: "Shoujo", value: "Shoujo" }, - { label: "Shounen", value: "Shounen" }, - { label: "Smut", value: "Smut" }, - { label: "Yaoi", value: "Yaoi" }, - { label: "Yuri", value: "Yuri" }, - { label: "Martial Arts", value: "MartialArts" }, - { label: "School Life", value: "SchoolLife" }, - { label: "Shoujo Ai", value: "ShoujoAi" }, - { label: "Shounen Ai", value: "ShounenAi" }, - { label: "Slice of Life", value: "SliceofLife" }, - { label: "Gender Bender", value: "GenderBender" }, - { label: "Sports", value: "Sports" }, - { label: "Urban", value: "Urban" }, - { label: "Adventurer", value: "Adventurer" } - ], - type: FilterTypes.Picker, - }, - sort: { - value: "click", - label: "Sort By", - options: [ - { label: "Time Updated", value: "update" }, - { label: "Time Posted", value: "post" }, - { label: "Clicks", value: "click" }, - ], - type: FilterTypes.Picker, - }, - order: { - value: false, - label: "Order ↑ ↓", - type: FilterTypes.Switch, - }, - } satisfies Filters; + filters = { + status: { + value: '', + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Completed', value: 'wanjie' }, + { label: 'Ongoing', value: 'lianzai' }, + { label: 'Hiatus', value: 'hiatus' }, + ], + type: FilterTypes.Picker, + }, + lang: { + value: '', + label: 'Languages', + options: [ + { label: 'All', value: '' }, + { label: 'Chinese', value: 'chinese' }, + { label: 'Korean', value: 'korean' }, + { label: 'Japanese', value: 'japanese' }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: '', + label: 'Genres', + options: [ + { label: 'All', value: '' }, + { label: 'Fantasy', value: 'Fantasy' }, + { label: 'Action', value: 'Action' }, + { label: 'Xuanhuan', value: 'Xuanhuan' }, + { label: 'Romance', value: 'Romance' }, + { label: 'Comedy', value: 'Comedy' }, + { label: 'Mystery', value: 'Mystery' }, + { label: 'Mature', value: 'Mature' }, + { label: 'Harem', value: 'Harem' }, + { label: 'Wuxia', value: 'Wuxia' }, + { label: 'Xianxia', value: 'Xianxia' }, + { label: 'Tragedy', value: 'Tragedy' }, + { label: 'Sci-fi', value: 'Scifi' }, + { label: 'Historical', value: 'Historical' }, + { label: 'Ecchi', value: 'Ecchi' }, + { label: 'Adventure', value: 'Adventure' }, + { label: 'Adult', value: 'Adult' }, + { label: 'Supernatural', value: 'Supernatural' }, + { label: 'Psychological', value: 'Psychological' }, + { label: 'Drama', value: 'Drama' }, + { label: 'Horror', value: 'Horror' }, + { label: 'Josei', value: 'Josei' }, + { label: 'Mecha', value: 'Mecha' }, + { label: 'Seinen', value: 'Seinen' }, + { label: 'Shoujo', value: 'Shoujo' }, + { label: 'Shounen', value: 'Shounen' }, + { label: 'Smut', value: 'Smut' }, + { label: 'Yaoi', value: 'Yaoi' }, + { label: 'Yuri', value: 'Yuri' }, + { label: 'Martial Arts', value: 'MartialArts' }, + { label: 'School Life', value: 'SchoolLife' }, + { label: 'Shoujo Ai', value: 'ShoujoAi' }, + { label: 'Shounen Ai', value: 'ShounenAi' }, + { label: 'Slice of Life', value: 'SliceofLife' }, + { label: 'Gender Bender', value: 'GenderBender' }, + { label: 'Sports', value: 'Sports' }, + { label: 'Urban', value: 'Urban' }, + { label: 'Adventurer', value: 'Adventurer' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'click', + label: 'Sort By', + options: [ + { label: 'Time Updated', value: 'update' }, + { label: 'Time Posted', value: 'post' }, + { label: 'Clicks', value: 'click' }, + ], + type: FilterTypes.Picker, + }, + order: { + value: false, + label: 'Order ↑ ↓', + type: FilterTypes.Switch, + }, + } satisfies Filters; } -export default new PawRead(); \ No newline at end of file +export default new PawRead(); diff --git a/plugins/english/ranobes.ts b/plugins/english/ranobes.ts index 13e82e9..32985ff 100644 --- a/plugins/english/ranobes.ts +++ b/plugins/english/ranobes.ts @@ -1,184 +1,188 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; interface ChapterEntry { - id: number; - title: string; - date: string; - link: string; + id: number; + title: string; + date: string; + link: string; } class RanobesPlugin implements Plugin.PagePlugin { - id = 'ranobes'; - name = 'Ranobes'; - icon = 'src/en/ranobes/icon.png'; - site = 'https://ranobes.top'; - filters?: Filters | undefined; - version = '1.0.0'; + id = 'ranobes'; + name = 'Ranobes'; + icon = 'src/en/ranobes/icon.png'; + site = 'https://ranobes.top'; + filters?: Filters | undefined; + version = '1.0.0'; - async sleep (ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)) + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.short-cont').each((i, el) => { + const novelName = loadedCheerio(el).find('h2.title').text(); + const novelUrl = loadedCheerio(el).find('h2.title a').attr('href'); + const novelCover = loadedCheerio(el) + .find('figure') + .attr('style') + ?.replace(/.*url\((.*?)\)./g, '$1'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl?.replace(this.site, ''), + }; + novels.push(novel); + }); + return novels; + } + + parseChapters(data: { chapters: ChapterEntry[] }) { + const chapter: Plugin.ChapterItem[] = []; + data.chapters.map((item: ChapterEntry) => { + chapter.push({ + name: item.title, + releaseTime: new Date(item.date).toISOString(), + path: item.link.replace(this.site, ''), + }); + }); + return chapter.reverse(); + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}/novels/page/${page}/`; + const body = await fetchApi(link).then(result => result.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel( + novelPath: string, + ): Promise { + const url = this.site + novelPath; + const result = await fetchApi(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('.poster img').attr('alt') || 'Untitled', + cover: this.site + loadedCheerio('.poster img').attr('src'), + author: loadedCheerio('[itemprop="creator"]').text(), + chapters: [], + totalPages: 1, + }; + + novel.summary = loadedCheerio('.moreless') + .find('br') + .replaceWith('\n') + .end() + .text() + .trim(); + + novel.status = + loadedCheerio('li:contains("Status") span').text() === 'Ongoing' + ? 'Ongoing' + : 'Completed'; + + novel.genres = loadedCheerio('div#mc-fs-genre').text().trim(); + + let chapterListUrl = loadedCheerio('.read-continue:last').attr('href'); + + if (!chapterListUrl?.startsWith('http')) { + chapterListUrl = this.site + chapterListUrl; } - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + const chaptersHtmlToString = await fetchApi(chapterListUrl).then(r => + r.text(), + ); + const pageCheerio = parseHTML(chaptersHtmlToString); - loadedCheerio('.short-cont').each((i,el) => { - const novelName = loadedCheerio(el).find('h2.title').text(); - const novelUrl = loadedCheerio(el).find('h2.title a').attr('href'); - const novelCover = loadedCheerio(el) - .find('figure') - .attr('style') - ?.replace(/.*url\((.*?)\)./g, '$1'); - - if (!novelUrl) return; + const json = pageCheerio('#dle-content main').next().html()!; + const data = JSON.parse(json.replace('window.__DATA__ =', '')); + novel.totalPages = Number(data.pages_count); + novel.chapters = this.parseChapters(data); - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl?.replace(this.site, ''), - } - novels.push(novel); - }); - return novels; - } + return novel; + } - parseChapters(data: {chapters: ChapterEntry[]}){ - const chapter: Plugin.ChapterItem[] = []; - data.chapters.map((item: ChapterEntry) => { - chapter.push({ - name: item.title, - releaseTime: new Date(item.date).toISOString(), - path: item.link.replace(this.site, ''), - }); - }); - return chapter.reverse(); - } + async parsePage(novelPath: string, page: string): Promise { + const pagePath = novelPath.split('-')[0]; + const firstUrl = this.site + '/chapters' + pagePath.replace('novels/', ''); + const pageUrl = firstUrl + '/page/' + page; - async popularNovels( - page: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}/novels/page/${page}/`; - const body = await fetchApi(link).then((result) => - result.text() - ); + const body = await fetchApi(firstUrl).then(r => r.text()); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + let loadedCheerio = parseHTML(body); + let json = loadedCheerio('#dle-content main').next().html()!; + let data = JSON.parse(json.replace('window.__DATA__ =', '')); - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - const result = await fetchApi(url); - const body = await result.text(); + const latestChapterUrl = data.chapters[0].link; + const latestChapterName = data.chapters[0].title; + const latestChapterDate = data.chapters[0].date; - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel & { totalPages: number; } = { - path: novelPath, - name: loadedCheerio('.poster img').attr('alt') || "Untitled", - cover: this.site + loadedCheerio('.poster img').attr('src'), - author: loadedCheerio('[itemprop="creator"]').text(), - chapters: [], - totalPages: 1, - }; - - novel.summary = loadedCheerio('.moreless') - .find('br') - .replaceWith('\n') - .end() - .text() - .trim(); - - novel.status = loadedCheerio('li:contains("Status") span') - .text() === 'Ongoing' - ? 'Ongoing' - : 'Completed'; - - novel.genres = loadedCheerio('div#mc-fs-genre').text().trim(); - - let chapterListUrl = loadedCheerio('.read-continue:last').attr('href'); - - if (!chapterListUrl?.startsWith('http')) { - chapterListUrl = this.site + chapterListUrl; + const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl + ? { + path: latestChapterUrl.replace(this.site, ''), + name: latestChapterName, + releaseTime: new Date(latestChapterDate).toISOString(), } + : undefined; - const chaptersHtmlToString = await fetchApi(chapterListUrl).then(r => r.text()); - const pageCheerio = parseHTML(chaptersHtmlToString); + await this.sleep(1000); - const json = pageCheerio('#dle-content main').next().html()!; - const data = JSON.parse(json.replace('window.__DATA__ =', '')); - novel.totalPages = Number(data.pages_count); - novel.chapters = this.parseChapters(data); - - return novel; + const pageBody = await fetchApi(pageUrl).then(r => r.text()); + loadedCheerio = parseHTML(pageBody); + json = loadedCheerio('#dle-content main').next().html()!; + data = JSON.parse(json.replace('window.__DATA__ =', '')); + const chapters = this.parseChapters(data); + return { + chapters, + latestChapter, }; + } - async parsePage(novelPath: string, page: string): Promise { - const pagePath = novelPath.split('-')[0]; - const firstUrl = this.site + - '/chapters' + - pagePath.replace('novels/', ''); - const pageUrl = firstUrl + '/page/' + page; + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - const body = await fetchApi(firstUrl).then(r => r.text()); - - let loadedCheerio = parseHTML(body); - let json = loadedCheerio('#dle-content main').next().html()!; - let data = JSON.parse(json.replace('window.__DATA__ =', '')); + const loadedCheerio = parseHTML(body); - const latestChapterUrl = data.chapters[0].link; - const latestChapterName = data.chapters[0].title; - const latestChapterDate = data.chapters[0].date; + const chapterText = loadedCheerio('#arrticle').html() || ''; - const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl ? { - path: latestChapterUrl.replace(this.site, ''), - name: latestChapterName, - releaseTime: new Date(latestChapterDate).toISOString(), - } : undefined + return chapterText; + } - await this.sleep(1000); + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + let link = `${this.site}/search/${searchTerm}/page/${page}`; - const pageBody = await fetchApi(pageUrl).then(r => r.text()); - loadedCheerio = parseHTML(pageBody); - json = loadedCheerio('#dle-content main').next().html()!; - data = JSON.parse(json.replace('window.__DATA__ =', '')); - const chapters = this.parseChapters(data); - return { - chapters, - latestChapter - } - } + const body = await fetchApi(link).then(r => r.text()); - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio('#arrticle').html() || ""; - - return chapterText; - }; - - async searchNovels( - searchTerm: string, - page: number - ): Promise { - let link = `${this.site}/search/${searchTerm}/page/${page}`; - - const body = await fetchApi(link).then(r => r.text()); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio) - }; - - async fetchImage(url: string): Promise { - return await fetchFile(url); - } + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + async fetchImage(url: string): Promise { + return await fetchFile(url); + } } export default new RanobesPlugin(); diff --git a/plugins/english/readlitenovel.ts b/plugins/english/readlitenovel.ts index 197b611..017658a 100644 --- a/plugins/english/readlitenovel.ts +++ b/plugins/english/readlitenovel.ts @@ -1,189 +1,196 @@ -import { load as parseHTML } from "cheerio"; -import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import dayjs from "dayjs"; +import { load as parseHTML } from 'cheerio'; +import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import dayjs from 'dayjs'; class ReadLiteNovel implements Plugin.PluginBase { - id = "rln.app"; - name = "ReadLiteNovel"; - version = "1.0.0"; - icon = "src/en/readlitenovel/icon.png"; - site = "https://rln.app"; + id = 'rln.app'; + name = 'ReadLiteNovel'; + version = '1.0.0'; + icon = 'src/en/readlitenovel/icon.png'; + site = 'https://rln.app'; - parseAgoDate(date: string | undefined){ //parseMadaraDate - if (date?.includes("ago")) { - const dayJSDate = dayjs(new Date()); // today - const timeAgo = date.match(/\d+/)?.[0] || ""; - const timeAgoInt = parseInt(timeAgo, 10); - - if (!timeAgo) return null; // there is no number! - - if (date.includes("hours ago") || date.includes("hour ago")) { - dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours - } - - if (date.includes("days ago") || date.includes("day ago")) { - dayJSDate.subtract(timeAgoInt, "days"); // go back N days - } - - if (date.includes("months ago") || date.includes("month ago")) { - dayJSDate.subtract(timeAgoInt, "months"); // go back N months - } + parseAgoDate(date: string | undefined) { + //parseMadaraDate + if (date?.includes('ago')) { + const dayJSDate = dayjs(new Date()); // today + const timeAgo = date.match(/\d+/)?.[0] || ''; + const timeAgoInt = parseInt(timeAgo, 10); - if (date.includes("years ago") || date.includes("year ago")) { - dayJSDate.subtract(timeAgoInt, "years"); // go back N years - } + if (!timeAgo) return null; // there is no number! - return dayJSDate.toISOString(); - } - return null; // there is no "ago" so give up - }; + if (date.includes('hours ago') || date.includes('hour ago')) { + dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours + } - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - const link = `${this.site}/ranking/${filters.order.value}/${page}`; - const result = await fetchApi(link); - const body = await result.text(); + if (date.includes('days ago') || date.includes('day ago')) { + dayJSDate.subtract(timeAgoInt, 'days'); // go back N days + } - const loadedCheerio = parseHTML(body); + if (date.includes('months ago') || date.includes('month ago')) { + dayJSDate.subtract(timeAgoInt, 'months'); // go back N months + } - const novels: Plugin.NovelItem[] = []; + if (date.includes('years ago') || date.includes('year ago')) { + dayJSDate.subtract(timeAgoInt, 'years'); // go back N years + } - loadedCheerio(".category-items li").each((i,el) => { - const novelUrl = loadedCheerio(el) - .find(".category-name a") - .attr("href"); - - if (!novelUrl) return; - const novelName = loadedCheerio(el) - .find(".category-name a") - .text() - .trim(); - let novelCover = loadedCheerio(el) - .find(".category-img img") - .attr("src"); - - if (novelCover && !isUrlAbsolute(novelCover)) { - novelCover = this.site + novelCover; - } - - const novel = { - path: novelUrl?.replace(this.site, ''), - name: novelName, - cover: novelCover, - }; - novels.push(novel); - }) - return novels; - }; - - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".novel-title").text() || "Untitled", - cover: loadedCheerio(".novels-detail img").attr("src"), - summary: loadedCheerio('.empty-box').text().trim(), - chapters: [], - }; - - loadedCheerio('.novels-detail-right li').each((i,el) => { - const detailName = loadedCheerio(el).find('div:first').text(); - const detail = loadedCheerio(el).find('div:last'); - - switch(detailName){ - case "Status:": - novel.status = detail.text(); - break; - case "Genres:": - novel.genres = detail.find('a') - .map((i,el) => loadedCheerio(el).text()) - .toArray() - .join(', ') || detail.text(); - break; - case "Author(s):": - novel.author = detail.find('a') - .map((i,el) => loadedCheerio(el).text()) - .toArray() - .join(', ') || detail.text(); - break; - case "Translator:": - novel.artist = detail.text(); - break; - } - }) - const chapter: Plugin.ChapterItem[] = []; - loadedCheerio(".cm-tabs-content li").each((i,el) => { - const chapterUrl = loadedCheerio(el).find("a").attr("href"); - if (!chapterUrl) return; - const chapterName = loadedCheerio(el).find("a").text().trim(); - const releaseDate = this.parseAgoDate( - loadedCheerio(el).find('svg').attr('data-bs-original-title')! - ); - - chapter.push({ - name: chapterName, - path: chapterUrl?.replace(this.site, ''), - releaseTime: releaseDate, - })}) - - novel.chapters = chapter; - return novel; - }; - - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const chapterText = loadedCheerio("#chapterText").html() || ""; - - return chapterText; - }; - - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`; - const result = await fetchApi(url); - const body = await result.json(); - const novels: Plugin.NovelItem[] = []; - - body.results.forEach((item: {link: string; original_title:string; image:string;}) => - novels.push({ - path: item.link, - name: item.original_title, - cover: item.image, - }) - ); - - return novels; - }; - - async fetchImage(url: string): Promise { - return fetchFile(url); + return dayJSDate.toISOString(); } + return null; // there is no "ago" so give up + } - filters = { - order: { - value: "top-rated", - label: "Order by", - options: [ - { label: "MOST VIEWED", value: "most-viewed" }, - { label: "TOP RATED", value: "top-rated" }, - { label: "BOOKMARKS", value: "subscribers" }, - { label: "NEW", value: "new" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + const link = `${this.site}/ranking/${filters.order.value}/${page}`; + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.category-items li').each((i, el) => { + const novelUrl = loadedCheerio(el).find('.category-name a').attr('href'); + + if (!novelUrl) return; + const novelName = loadedCheerio(el) + .find('.category-name a') + .text() + .trim(); + let novelCover = loadedCheerio(el).find('.category-img img').attr('src'); + + if (novelCover && !isUrlAbsolute(novelCover)) { + novelCover = this.site + novelCover; + } + + const novel = { + path: novelUrl?.replace(this.site, ''), + name: novelName, + cover: novelCover, + }; + novels.push(novel); + }); + return novels; + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.novel-title').text() || 'Untitled', + cover: loadedCheerio('.novels-detail img').attr('src'), + summary: loadedCheerio('.empty-box').text().trim(), + chapters: [], + }; + + loadedCheerio('.novels-detail-right li').each((i, el) => { + const detailName = loadedCheerio(el).find('div:first').text(); + const detail = loadedCheerio(el).find('div:last'); + + switch (detailName) { + case 'Status:': + novel.status = detail.text(); + break; + case 'Genres:': + novel.genres = + detail + .find('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(', ') || detail.text(); + break; + case 'Author(s):': + novel.author = + detail + .find('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(', ') || detail.text(); + break; + case 'Translator:': + novel.artist = detail.text(); + break; + } + }); + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('.cm-tabs-content li').each((i, el) => { + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + if (!chapterUrl) return; + const chapterName = loadedCheerio(el).find('a').text().trim(); + const releaseDate = this.parseAgoDate( + loadedCheerio(el).find('svg').attr('data-bs-original-title')!, + ); + + chapter.push({ + name: chapterName, + path: chapterUrl?.replace(this.site, ''), + releaseTime: releaseDate, + }); + }); + + novel.chapters = chapter; + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#chapterText').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = + this.site + `/search/autocomplete?dataType=json&query=${searchTerm}`; + const result = await fetchApi(url); + const body = await result.json(); + const novels: Plugin.NovelItem[] = []; + + body.results.forEach( + (item: { link: string; original_title: string; image: string }) => + novels.push({ + path: item.link, + name: item.original_title, + cover: item.image, + }), + ); + + return novels; + } + + async fetchImage(url: string): Promise { + return fetchFile(url); + } + + filters = { + order: { + value: 'top-rated', + label: 'Order by', + options: [ + { label: 'MOST VIEWED', value: 'most-viewed' }, + { label: 'TOP RATED', value: 'top-rated' }, + { label: 'BOOKMARKS', value: 'subscribers' }, + { label: 'NEW', value: 'new' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new ReadLiteNovel(); diff --git a/plugins/english/royalroad.ts b/plugins/english/royalroad.ts index 82d8caf..00216d8 100644 --- a/plugins/english/royalroad.ts +++ b/plugins/english/royalroad.ts @@ -1,185 +1,191 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; class RoyalRoad implements Plugin.PluginBase { - id = "royalroad"; - name = "Royal Road"; - version = "1.0.1"; - icon = "src/en/royalroad/icon.png"; - site = "https://www.royalroad.com/"; + id = 'royalroad'; + name = 'Royal Road'; + version = '1.0.1'; + icon = 'src/en/royalroad/icon.png'; + site = 'https://www.royalroad.com/'; - parseNovels(loadedCheerio:CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".fiction-list-item").each((idx, ele) => { - const novelName = loadedCheerio(ele).find('a.bold').text(); - const novelCover = loadedCheerio(ele).find('img').attr('src'); - const novelUrl = loadedCheerio(ele).find('a.bold').attr('href')?.slice(1); + loadedCheerio('.fiction-list-item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('a.bold').text(); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + const novelUrl = loadedCheerio(ele).find('a.bold').attr('href')?.slice(1); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl, - }; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl, + }; - novels.push(novel); - }); + novels.push(novel); + }); - return novels; - } + return novels; + } - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}fictions/`; + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}fictions/`; - link += filters.order.value; - link += `?page=${page}`; + link += filters.order.value; + link += `?page=${page}`; - if (filters.genre.value !== '') - link += `&genre=${filters.genre.value}`; + if (filters.genre.value !== '') link += `&genre=${filters.genre.value}`; - const body = await fetchApi(link).then((r) => r.text()); + const body = await fetchApi(link).then(r => r.text()); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio("h1").text() || "Untitled", - cover: loadedCheerio('.cover-art-container img').attr('src'), - author: loadedCheerio('h4 a').text(), - summary: loadedCheerio(".description").text().trim(), - status: loadedCheerio('span.label-sm:last').text().trim(), - chapters: [], - }; + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1').text() || 'Untitled', + cover: loadedCheerio('.cover-art-container img').attr('src'), + author: loadedCheerio('h4 a').text(), + summary: loadedCheerio('.description').text().trim(), + status: loadedCheerio('span.label-sm:last').text().trim(), + chapters: [], + }; - novel.genres = loadedCheerio('span.tags a') - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); + novel.genres = loadedCheerio('span.tags a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); - const chapterJson = JSON.parse(loadedCheerio('script:contains("window.chapters")') - .html() - ?.match(/window.chapters = (.+])(?=;)/)![1] || ""); + const chapterJson = JSON.parse( + loadedCheerio('script:contains("window.chapters")') + .html() + ?.match(/window.chapters = (.+])(?=;)/)![1] || '', + ); - const volumeJson = JSON.parse(loadedCheerio('script:contains("window.chapters")') - .html() - ?.match(/window.volumes = (.+])(?=;)/)![1] || ""); + const volumeJson = JSON.parse( + loadedCheerio('script:contains("window.chapters")') + .html() + ?.match(/window.volumes = (.+])(?=;)/)![1] || '', + ); - const chapter : Plugin.ChapterItem[] = chapterJson.map( - (chapter: ChapterEntry) => { - const matchingVolume = volumeJson.find( - (volume: VolumeEntry) => volume.id === chapter.volumeId); - return { - name: chapter.title, - path: chapter.url.slice(1), - releaseTime: chapter.date, - chapterNumber: chapter?.order, - page: matchingVolume ? matchingVolume.title : null, - }; - } + const chapter: Plugin.ChapterItem[] = chapterJson.map( + (chapter: ChapterEntry) => { + const matchingVolume = volumeJson.find( + (volume: VolumeEntry) => volume.id === chapter.volumeId, ); + return { + name: chapter.title, + path: chapter.url.slice(1), + releaseTime: chapter.date, + chapterNumber: chapter?.order, + page: matchingVolume ? matchingVolume.title : null, + }; + }, + ); - novel.chapters = chapter; - return novel; - } + novel.chapters = chapter; + return novel; + } + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); + const loadedCheerio = parseHTML(body); - const loadedCheerio = parseHTML(body); + const chapterText = loadedCheerio('.chapter-content').html() || ''; - const chapterText = loadedCheerio(".chapter-content").html() || ''; + return chapterText; + } - return chapterText; - } + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const searchUrl = `${this.site}fictions/search?page=${page}&title=${searchTerm}`; - async searchNovels(searchTerm: string, page: number): Promise { - const searchUrl = `${this.site}fictions/search?page=${page}&title=${searchTerm}`; + const result = await fetchApi(searchUrl); + const body = await result.text(); - const result = await fetchApi(searchUrl); - const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + async fetchImage(url: string): Promise { + return fetchFile(url); + } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - - filters = { - order: { - value: "weekly-popular", - label: "Order By", - options: [ - { label: "Best Rated", value: "best-rated" }, - { label: "Trending", value: "trending" }, - { label: "Ongoing Fictions", value: "active-popular" }, - { label: "Complete", value: "complete" }, - { label: "Popular this week", value: "weekly-popular" }, - { label: "Latest Updates", value: "latest-updates" }, - { label: "Newest Fictions", value: "new" }, - { label: "Rising Stars", value: "rising-stars" }, - { label: "Writathon", value: "writathon" }, - ], - type: FilterTypes.Picker, - }, - genre: { - value: "", - label: "Genres", - options: [ - { label: "ALL", value: "" }, - { label: "Action", value: "action" }, - { label: "Adventure", value: "adventure" }, - { label: "Comedy", value: "comedy" }, - { label: "Contemporary", value: "contemporary" }, - { label: "Drama", value: "drama" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Historical", value: "historical" }, - { label: "Horror", value: "horror" }, - { label: "Mystery", value: "mystery" }, - { label: "Psychological", value: "psychological" }, - { label: "Romance", value: "romance" }, - { label: "Satire", value: "satire" }, - { label: "Sci-fi", value: "sci_fi" }, - { label: "Short Story", value: "one_shot" }, - { label: "Tragedy", value: "tragedy" } - ], - type: FilterTypes.Picker, - } - } satisfies Filters; + filters = { + order: { + value: 'weekly-popular', + label: 'Order By', + options: [ + { label: 'Best Rated', value: 'best-rated' }, + { label: 'Trending', value: 'trending' }, + { label: 'Ongoing Fictions', value: 'active-popular' }, + { label: 'Complete', value: 'complete' }, + { label: 'Popular this week', value: 'weekly-popular' }, + { label: 'Latest Updates', value: 'latest-updates' }, + { label: 'Newest Fictions', value: 'new' }, + { label: 'Rising Stars', value: 'rising-stars' }, + { label: 'Writathon', value: 'writathon' }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: '', + label: 'Genres', + options: [ + { label: 'ALL', value: '' }, + { label: 'Action', value: 'action' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Contemporary', value: 'contemporary' }, + { label: 'Drama', value: 'drama' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Romance', value: 'romance' }, + { label: 'Satire', value: 'satire' }, + { label: 'Sci-fi', value: 'sci_fi' }, + { label: 'Short Story', value: 'one_shot' }, + { label: 'Tragedy', value: 'tragedy' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new RoyalRoad(); interface ChapterEntry { - id: number; - volumeId: number; - title: string; - date: string; - order: number; - url: string; + id: number; + volumeId: number; + title: string; + date: string; + order: number; + url: string; } - + interface VolumeEntry { - id: number; - title: string; - cover: string; - order: number; -} \ No newline at end of file + id: number; + title: string; + cover: string; + order: number; +} diff --git a/plugins/english/scribblehub.ts b/plugins/english/scribblehub.ts index 997318c..d7b4ea4 100644 --- a/plugins/english/scribblehub.ts +++ b/plugins/english/scribblehub.ts @@ -1,306 +1,307 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import dayjs from "dayjs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import dayjs from 'dayjs'; class ScribbleHubPlugin implements Plugin.PluginBase { - id = "scribblehub"; - name = "ScribbleHub"; - icon = "src/en/scribblehub/icon.png"; - site = "https://www.scribblehub.com/"; - version = "1.0.0"; - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + id = 'scribblehub'; + name = 'ScribbleHub'; + icon = 'src/en/scribblehub/icon.png'; + site = 'https://www.scribblehub.com/'; + version = '1.0.0'; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio('.search_main_box').each((i,el) => { - const novelName = loadedCheerio(el).find('.search_title > a').text(); - const novelCover = loadedCheerio(el).find('.search_img > img').attr('src'); - const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href'); - - if (!novelUrl) return; + loadedCheerio('.search_main_box').each((i, el) => { + const novelName = loadedCheerio(el).find('.search_title > a').text(); + const novelCover = loadedCheerio(el) + .find('.search_img > img') + .attr('src'); + const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href'); - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - } - novels.push(novel); - }); - return novels; - } + if (!novelUrl) return; - async popularNovels( - page: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}`; - if (showLatestNovels) - link += "latest-series/" - else // if (filters.genres.value.include?.length || - // filters.genres.value.exclude?.length) - link += "series-finder/?sf=1" - // else - // link += "series-ranking/"; - // TODO, series-ranking filters when hideOnSelect come out + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + }); + return novels; + } - if (filters.genres.value.include?.length) - link += '&gi=' + filters.genres.value.include.join(','); + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}`; + if (showLatestNovels) link += 'latest-series/'; + // if (filters.genres.value.include?.length || + // filters.genres.value.exclude?.length) + else link += 'series-finder/?sf=1'; + // else + // link += "series-ranking/"; + // TODO, series-ranking filters when hideOnSelect come out - if (filters.genres.value.exclude?.length) - link += '&ge=' + filters.genres.value.exclude.join(','); + if (filters.genres.value.include?.length) + link += '&gi=' + filters.genres.value.include.join(','); - if (filters.genres.value.include?.length || filters.genres.value.exclude?.length) - link += '&mgi=' + filters.genre_operator.value; + if (filters.genres.value.exclude?.length) + link += '&ge=' + filters.genres.value.exclude.join(','); - if (filters.content_warning.value.include?.length) - link += '&cti=' + filters.content_warning.value.include.join(','); + if ( + filters.genres.value.include?.length || + filters.genres.value.exclude?.length + ) + link += '&mgi=' + filters.genre_operator.value; - if (filters.content_warning.value.exclude?.length) - link += '&cte=' + filters.content_warning.value.exclude.join(','); + if (filters.content_warning.value.include?.length) + link += '&cti=' + filters.content_warning.value.include.join(','); - if (filters.content_warning.value.include?.length || - filters.content_warning.value.exclude?.length) - link += '&mct' + filters.content_warning_operator.value; + if (filters.content_warning.value.exclude?.length) + link += '&cte=' + filters.content_warning.value.exclude.join(','); - link += '&sort=' + filters.sort.value; - link += '&order=' + filters.order.value; + if ( + filters.content_warning.value.include?.length || + filters.content_warning.value.exclude?.length + ) + link += '&mct' + filters.content_warning_operator.value; - const body = await fetchApi(link).then((result) => - result.text() - ); + link += '&sort=' + filters.sort.value; + link += '&order=' + filters.order.value; - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const body = await fetchApi(link).then(result => result.text()); - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - let loadedCheerio = parseHTML(body); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio('.fic_title').text() || 'Untitled', - cover: loadedCheerio('.fic_image > img').attr('src'), - summary: loadedCheerio('.wi_fic_desc').text(), - author: loadedCheerio('.auth_name_fic').text(), - chapters: [], - }; - - novel.genres = loadedCheerio('.fic_genre') - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - - novel.status = loadedCheerio('.rnd_stats') - .next() - .text() - .includes('Ongoing') - ? 'Ongoing' - : 'Completed'; + let loadedCheerio = parseHTML(body); - const formData = new FormData(); - formData.append('action', 'wi_getreleases_pagination'); - formData.append('pagenum', '-1'); - formData.append('mypostid', novelPath.split('/')[1]); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.fic_title').text() || 'Untitled', + cover: loadedCheerio('.fic_image > img').attr('src'), + summary: loadedCheerio('.wi_fic_desc').text(), + author: loadedCheerio('.auth_name_fic').text(), + chapters: [], + }; - const data = await fetchApi( - `${this.site}wp-admin/admin-ajax.php`, - { - method: 'POST', - body: formData, - }, - ); - const text = await data.text(); + novel.genres = loadedCheerio('.fic_genre') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); - loadedCheerio = parseHTML(text); + novel.status = loadedCheerio('.rnd_stats').next().text().includes('Ongoing') + ? 'Ongoing' + : 'Completed'; - const chapter: Plugin.ChapterItem[] = []; + const formData = new FormData(); + formData.append('action', 'wi_getreleases_pagination'); + formData.append('pagenum', '-1'); + formData.append('mypostid', novelPath.split('/')[1]); - const parseISODate = (date: string) => { - if (date.includes("ago")) { - const dayJSDate = dayjs(new Date()); // today - const timeAgo = date.match(/\d+/)?.[0] || ""; - const timeAgoInt = parseInt(timeAgo, 10); - - if (!timeAgo) return null; // there is no number! - - if (date.includes("hours ago") || date.includes("hour ago")) { - dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours - } - - if (date.includes("days ago") || date.includes("day ago")) { - dayJSDate.subtract(timeAgoInt, "days"); // go back N days - } - - if (date.includes("months ago") || date.includes("month ago")) { - dayJSDate.subtract(timeAgoInt, "months"); // go back N months - } - - return dayJSDate.toISOString(); - } - return null; - }; + const data = await fetchApi(`${this.site}wp-admin/admin-ajax.php`, { + method: 'POST', + body: formData, + }); + const text = await data.text(); - loadedCheerio('.toc_w').each((i,el) => { - const chapterName = loadedCheerio(el).find('.toc_a').text(); - const releaseDate = loadedCheerio(el).find('.fic_date_pub').text(); - const chapterUrl = loadedCheerio(el).find('a').attr('href'); + loadedCheerio = parseHTML(text); - if (!chapterUrl) return; - chapter.push({ - name: chapterName, - releaseTime: parseISODate(releaseDate), - path: chapterUrl.replace(this.site, ''), - }); - }); + const chapter: Plugin.ChapterItem[] = []; - novel.chapters = chapter.reverse(); + const parseISODate = (date: string) => { + if (date.includes('ago')) { + const dayJSDate = dayjs(new Date()); // today + const timeAgo = date.match(/\d+/)?.[0] || ''; + const timeAgoInt = parseInt(timeAgo, 10); - return novel; - } + if (!timeAgo) return null; // there is no number! - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); + if (date.includes('hours ago') || date.includes('hour ago')) { + dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours + } - const loadedCheerio = parseHTML(body); + if (date.includes('days ago') || date.includes('day ago')) { + dayJSDate.subtract(timeAgoInt, 'days'); // go back N days + } - const chapterText = loadedCheerio('div.chp_raw').html() || ''; - return chapterText; - } + if (date.includes('months ago') || date.includes('month ago')) { + dayJSDate.subtract(timeAgoInt, 'months'); // go back N months + } - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - const url =`${this.site}?s=${searchTerm}&post_type=fictionposts`; - const result = await fetchApi(url); - const body = await result.text(); + return dayJSDate.toISOString(); + } + return null; + }; - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + loadedCheerio('.toc_w').each((i, el) => { + const chapterName = loadedCheerio(el).find('.toc_a').text(); + const releaseDate = loadedCheerio(el).find('.fic_date_pub').text(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); - async fetchImage(url: string): Promise { - return await fetchFile(url); - } + if (!chapterUrl) return; + chapter.push({ + name: chapterName, + releaseTime: parseISODate(releaseDate), + path: chapterUrl.replace(this.site, ''), + }); + }); - filters = { - sort: { - label: "Sort Results By", - value: "ratings", - options: [ - { label: "Chapters", value: "chapters" }, - { label: "Chapters per week", value: "frequency" }, - { label: "Date Added", value: "dateadded" }, - { label: "Favorites", value: "favorites" }, - { label: "Last Updated", value: "lastchdate" }, - { label: "Number of Ratings", value: "numofrate" }, - { label: "Pages", value: "pages" }, - { label: "Pageviews", value: "pageviews" }, - { label: "Ratings", value: "ratings" }, - { label: "Readers", value: "readers" }, - { label: "Reviews", value: "reviews" }, - { label: "Total Words", value: "totalwords" }, - ], - type: FilterTypes.Picker, - }, - order: { - label: "Order by", - value: "desc", - options: [ - { label: "Descending", value: "desc" }, - { label: "Ascending", value: "asc" }, - ], - type: FilterTypes.Picker, - }, - storyStatus: { - label: "Story Status (Translation)", - value: "", - options: [ - { label: "All", value: "all" }, - { label: "Completed", value: "completed" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Hiatus", value: "hiatus" }, - ], - type: FilterTypes.Picker, - }, - genres: { - label: 'Genres', - value: { - include: [], - exclude: [], - }, - options: [ - { label: 'Action', value: '9' }, - { label: 'Adult', value: '902' }, - { label: 'Adventure', value: '8' }, - { label: 'Boys Love', value: '891' }, - { label: 'Comedy', value: '7' }, - { label: 'Drama', value: '903' }, - { label: 'Ecchi', value: '904' }, - { label: 'Fanfiction', value: '38' }, - { label: 'Fantasy', value: '19' }, - { label: 'Gender Bender', value: '905' }, - { label: 'Girls Love', value: '892' }, - { label: 'Harem', value: '1015' }, - { label: 'Historical', value: '21' }, - { label: 'Horror', value: '22' }, - { label: 'Isekai', value: '37' }, - { label: 'Josei', value: '906' }, - { label: 'LitRPG', value: '1180' }, - { label: 'Martial Arts', value: '907' }, - { label: 'Mature', value: '20' }, - { label: 'Mecha', value: '908' }, - { label: 'Mystery', value: '909' }, - { label: 'Psychological', value: '910' }, - { label: 'Romance', value: '6' }, - { label: 'School Life', value: '911' }, - { label: 'Sci-fi', value: '912' }, - { label: 'Seinen', value: '913' }, - { label: 'Slice of Life', value: '914' }, - { label: 'Smut', value: '915' }, - { label: 'Sports', value: '916' }, - { label: 'Supernatural', value: '5' }, - { label: 'Tragedy', value: '901' }, - ], - type: FilterTypes.ExcludableCheckboxGroup, - }, - genre_operator : { - value: 'and', - label: 'Genre And/Or', - options: [ - { label: 'OR', value: 'or' }, - { label: 'AND', value: 'and' }, - ], - type: FilterTypes.Picker, - }, - content_warning: { - value: { - include: [], - exclude: [], - }, - label: "Mature Content", - options: [ - { label: 'Gore', value: '48' }, - { label: 'Sexual Content', value: '50' }, - { label: 'Strong Language', value: '49' }, - ], - type: FilterTypes.ExcludableCheckboxGroup - }, - content_warning_operator: { - value: 'and', - label: 'Mature Content And/Or', - options: [ - { label: 'OR', value: 'or' }, - { label: 'AND', value: 'and' }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + novel.chapters = chapter.reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('div.chp_raw').html() || ''; + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}?s=${searchTerm}&post_type=fictionposts`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async fetchImage(url: string): Promise { + return await fetchFile(url); + } + + filters = { + sort: { + label: 'Sort Results By', + value: 'ratings', + options: [ + { label: 'Chapters', value: 'chapters' }, + { label: 'Chapters per week', value: 'frequency' }, + { label: 'Date Added', value: 'dateadded' }, + { label: 'Favorites', value: 'favorites' }, + { label: 'Last Updated', value: 'lastchdate' }, + { label: 'Number of Ratings', value: 'numofrate' }, + { label: 'Pages', value: 'pages' }, + { label: 'Pageviews', value: 'pageviews' }, + { label: 'Ratings', value: 'ratings' }, + { label: 'Readers', value: 'readers' }, + { label: 'Reviews', value: 'reviews' }, + { label: 'Total Words', value: 'totalwords' }, + ], + type: FilterTypes.Picker, + }, + order: { + label: 'Order by', + value: 'desc', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + label: 'Story Status (Translation)', + value: '', + options: [ + { label: 'All', value: 'all' }, + { label: 'Completed', value: 'completed' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Hiatus', value: 'hiatus' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'Genres', + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Action', value: '9' }, + { label: 'Adult', value: '902' }, + { label: 'Adventure', value: '8' }, + { label: 'Boys Love', value: '891' }, + { label: 'Comedy', value: '7' }, + { label: 'Drama', value: '903' }, + { label: 'Ecchi', value: '904' }, + { label: 'Fanfiction', value: '38' }, + { label: 'Fantasy', value: '19' }, + { label: 'Gender Bender', value: '905' }, + { label: 'Girls Love', value: '892' }, + { label: 'Harem', value: '1015' }, + { label: 'Historical', value: '21' }, + { label: 'Horror', value: '22' }, + { label: 'Isekai', value: '37' }, + { label: 'Josei', value: '906' }, + { label: 'LitRPG', value: '1180' }, + { label: 'Martial Arts', value: '907' }, + { label: 'Mature', value: '20' }, + { label: 'Mecha', value: '908' }, + { label: 'Mystery', value: '909' }, + { label: 'Psychological', value: '910' }, + { label: 'Romance', value: '6' }, + { label: 'School Life', value: '911' }, + { label: 'Sci-fi', value: '912' }, + { label: 'Seinen', value: '913' }, + { label: 'Slice of Life', value: '914' }, + { label: 'Smut', value: '915' }, + { label: 'Sports', value: '916' }, + { label: 'Supernatural', value: '5' }, + { label: 'Tragedy', value: '901' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + genre_operator: { + value: 'and', + label: 'Genre And/Or', + options: [ + { label: 'OR', value: 'or' }, + { label: 'AND', value: 'and' }, + ], + type: FilterTypes.Picker, + }, + content_warning: { + value: { + include: [], + exclude: [], + }, + label: 'Mature Content', + options: [ + { label: 'Gore', value: '48' }, + { label: 'Sexual Content', value: '50' }, + { label: 'Strong Language', value: '49' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + content_warning_operator: { + value: 'and', + label: 'Mature Content And/Or', + options: [ + { label: 'OR', value: 'or' }, + { label: 'AND', value: 'and' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new ScribbleHubPlugin(); diff --git a/plugins/english/wtrlab.ts b/plugins/english/wtrlab.ts index 4fe9bb3..07903d8 100644 --- a/plugins/english/wtrlab.ts +++ b/plugins/english/wtrlab.ts @@ -1,230 +1,247 @@ -import { Plugin } from "@typings/plugin"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { load as parseHTML } from "cheerio"; +import { Plugin } from '@typings/plugin'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { load as parseHTML } from 'cheerio'; class WTRLAB implements Plugin.PluginBase { - id = "WTRLAB"; - name = "WTR-LAB"; - site = "https://wtr-lab.com/"; - version = "1.0.0"; - icon = "src/en/wtrlab/icon.png"; - sourceLang = "en/" + id = 'WTRLAB'; + name = 'WTR-LAB'; + site = 'https://wtr-lab.com/'; + version = '1.0.0'; + icon = 'src/en/wtrlab/icon.png'; + sourceLang = 'en/'; async popularNovels( page: number, - { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, ): Promise { - let link = this.site + this.sourceLang + "novel-list?"; - link += `orderBy=${filters.order.value}`; - link += `&order=${filters.sort.value}`; - link += `&filter=${filters.storyStatus.value}`; - link += `&page=${page}`; //TODO Genre & Advance Searching Filter. Ez to implement, too much manual work, too lazy. - - if(showLatestNovels){ + let link = this.site + this.sourceLang + 'novel-list?'; + link += `orderBy=${filters.order.value}`; + link += `&order=${filters.sort.value}`; + link += `&filter=${filters.storyStatus.value}`; + link += `&page=${page}`; //TODO Genre & Advance Searching Filter. Ez to implement, too much manual work, too lazy. + + if (showLatestNovels) { const response = await fetch('https://wtr-lab.com/api/home/recent', { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, - body: JSON.stringify({ page: page }) + body: JSON.stringify({ page: page }), }); const recentNovel: JsonNovel = await response.json(); // Parse novels from JSON - const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({ - name: datum.serie.data.title || "", - cover: datum.serie.data.image, - path: this.sourceLang + "serie-" + datum.serie.raw_id + "/" + datum.serie.slug || "", - })); + const novels: Plugin.NovelItem[] = recentNovel.data.map( + (datum: Datum) => ({ + name: datum.serie.data.title || '', + cover: datum.serie.data.image, + path: + this.sourceLang + + 'serie-' + + datum.serie.raw_id + + '/' + + datum.serie.slug || '', + }), + ); return novels; - } - - else { - const body = await fetchApi(link).then((res) => res.text()); + } else { + const body = await fetchApi(link).then(res => res.text()); const loadedCheerio = parseHTML(body); //console.log(link); - const novels: Plugin.NovelItem[] = loadedCheerio(".serie-item") + const novels: Plugin.NovelItem[] = loadedCheerio('.serie-item') .map((index, element) => ({ - name: loadedCheerio(element).find(".title-wrap > a").text().replace(loadedCheerio(element).find(".rawtitle").text(), "") || "", - cover: loadedCheerio(element).find("img").attr("src"), - path: loadedCheerio(element).find("a").attr("href") || "", + name: + loadedCheerio(element) + .find('.title-wrap > a') + .text() + .replace(loadedCheerio(element).find('.rawtitle').text(), '') || + '', + cover: loadedCheerio(element).find('img').attr('src'), + path: loadedCheerio(element).find('a').attr('href') || '', })) .get() - .filter((novel) => novel.name && novel.path); - return novels; + .filter(novel => novel.name && novel.path); + return novels; } - } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const body = await fetchApi(this.site + novelPath).then(res => res.text()); const loadedCheerio = parseHTML(body); //console.log(this.site + novelPath); const novel: Plugin.SourceNovel = { path: novelPath, - name: loadedCheerio("h1.text-uppercase").text(), - cover: loadedCheerio(".img-wrap > img").attr("src"), - summary: loadedCheerio(".lead").text().trim(), + name: loadedCheerio('h1.text-uppercase').text(), + cover: loadedCheerio('.img-wrap > img').attr('src'), + summary: loadedCheerio('.lead').text().trim(), }; - novel.genres = loadedCheerio('td:contains("Genre")') - .next() - .find('a') - .map((i,el) => loadedCheerio(el).text()) - .toArray() - .join(","); + novel.genres = loadedCheerio('td:contains("Genre")') + .next() + .find('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); novel.author = loadedCheerio('td:contains("Author")') .next() .text() - .replace(/[\t\n]/g, ""); + .replace(/[\t\n]/g, ''); novel.status = loadedCheerio('td:contains("Status")') .next() .text() - .replace(/[\t\n]/g, ""); + .replace(/[\t\n]/g, ''); - - const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ""; + const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ''; const jsonData: NovelJson = JSON.parse(chapterJson); - const chapters: Plugin.ChapterItem[] = jsonData.props.pageProps.serie.chapters.map((jsonChapter, chapterIndex) => ({ - name: "Chapter " + jsonChapter.slug + " " + jsonChapter.title, - path: this.sourceLang + "serie-" +jsonData.props.pageProps.serie.serie_data.raw_id + "/" + jsonData.props.pageProps.serie.serie_data.slug + "/chapter-" + jsonChapter.slug, // Assuming 'slug' is the intended path - releaseTime: jsonChapter.created_at.substring(0, 10), - chapterNumber: chapterIndex + 1 - })); + const chapters: Plugin.ChapterItem[] = + jsonData.props.pageProps.serie.chapters.map( + (jsonChapter, chapterIndex) => ({ + name: 'Chapter ' + jsonChapter.slug + ' ' + jsonChapter.title, + path: + this.sourceLang + + 'serie-' + + jsonData.props.pageProps.serie.serie_data.raw_id + + '/' + + jsonData.props.pageProps.serie.serie_data.slug + + '/chapter-' + + jsonChapter.slug, // Assuming 'slug' is the intended path + releaseTime: jsonChapter.created_at.substring(0, 10), + chapterNumber: chapterIndex + 1, + }), + ); novel.chapters = chapters; return novel; } - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); - const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ""; + const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ''; const jsonData: NovelJson = JSON.parse(chapterJson); //console.log(body); - const chapterContent = JSON.stringify(jsonData.props.pageProps.serie.chapter_data.data.body); + const chapterContent = JSON.stringify( + jsonData.props.pageProps.serie.chapter_data.data.body, + ); //console.log(chapterContent); const parsedArray = JSON.parse(chapterContent); let htmlString = ''; for (const text of parsedArray) { htmlString += `

${text}

`; - } - - - + } return htmlString; -} + } async searchNovels(searchTerm: string): Promise { - const response = await fetch(this.site + "api/search", { + const response = await fetch(this.site + 'api/search', { headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', Referer: this.site + this.sourceLang, Origin: this.site, }, - method: "POST", - body: JSON.stringify({ text: searchTerm}), + method: 'POST', + body: JSON.stringify({ text: searchTerm }), }); const recentNovel: JsonNovel = await response.json(); - - // Parse novels from JSON - const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({ - name: datum.data.title || "", - cover: datum.data.image, - path: this.sourceLang + "serie-" + datum.raw_id + "/" + datum.slug || "", - })); + // Parse novels from JSON + const novels: Plugin.NovelItem[] = recentNovel.data.map((datum: Datum) => ({ + name: datum.data.title || '', + cover: datum.data.image, + path: this.sourceLang + 'serie-' + datum.raw_id + '/' + datum.slug || '', + })); return novels; } - fetchImage = fetchFile; filters = { order: { - value: "chapter", - label: "Order by", - options: [ - { label: "View", value: "view" }, - { label: "Name", value: "name" }, - { label: "Addition Date", value: "date" }, - { label: "Reader", value: "reader" }, - { label: "Chapter", value: "chapter" }, - ], - type: FilterTypes.Picker, + value: 'chapter', + label: 'Order by', + options: [ + { label: 'View', value: 'view' }, + { label: 'Name', value: 'name' }, + { label: 'Addition Date', value: 'date' }, + { label: 'Reader', value: 'reader' }, + { label: 'Chapter', value: 'chapter' }, + ], + type: FilterTypes.Picker, }, sort: { - value: "desc", - label: "Sort by", - options: [ - { label: "Descending", value: "desc" }, - { label: "Ascending", value: "asc" }, - ], - type: FilterTypes.Picker, + value: 'desc', + label: 'Sort by', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, }, storyStatus: { - value: "all", - label: "Status", - options: [ - { label: "All", value: "all" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + ], + type: FilterTypes.Picker, }, } satisfies Filters; } - - interface NovelJson { - props: Props; - page: string; + props: Props; + page: string; } interface Props { pageProps: PageProps; - __N_SSP: boolean; + __N_SSP: boolean; } interface PageProps { - serie: Serie; + serie: Serie; server_time: Date; } interface Serie { - serie_data: SerieData; - chapters: Chapter[]; + serie_data: SerieData; + chapters: Chapter[]; recommendation: SerieData[]; - chapter_data: ChapterData; - id: number; - raw_id: number; - slug: string; - data: Data; + chapter_data: ChapterData; + id: number; + raw_id: number; + slug: string; + data: Data; is_default: boolean; - raw_type: string; + raw_type: string; } interface Chapter { - serie_id: number; - id: number; - slug: string; - title: string; - name: string; + serie_id: number; + id: number; + slug: string; + title: string; + name: string; created_at: string; } interface ChapterData { @@ -236,51 +253,47 @@ interface ChapterContent { } interface SerieData { - serie_id?: number; + serie_id?: number; recommendation_id?: number; - score?: string; - id: number; - slug: string; - search_text: string; - status: number; - data: Data; - created_at: string; - updated_at: string; - view: number; - in_library: number; - rating: number | null; - chapter_count: number; - power: number; - total_rate: number; - user_status: number; - verified: boolean; - from: null; - raw_id: number; - genres?: number[]; + score?: string; + id: number; + slug: string; + search_text: string; + status: number; + data: Data; + created_at: string; + updated_at: string; + view: number; + in_library: number; + rating: number | null; + chapter_count: number; + power: number; + total_rate: number; + user_status: number; + verified: boolean; + from: null; + raw_id: number; + genres?: number[]; } interface Data { - title: string; - author: string; + title: string; + author: string; description: string; - image: string; + image: string; } interface JsonNovel { success: boolean; - data: Datum[]; + data: Datum[]; } interface Datum { - serie: Serie; - chapters: Chapter[]; + serie: Serie; + chapters: Chapter[]; updated_at: Date; - raw_id: number; - slug: string; - data: Data; - + raw_id: number; + slug: string; + data: Data; } - - - export default new WTRLAB(); diff --git a/plugins/english/wuxiaworld.ts b/plugins/english/wuxiaworld.ts index 0a0aaad..f824c91 100644 --- a/plugins/english/wuxiaworld.ts +++ b/plugins/english/wuxiaworld.ts @@ -1,283 +1,295 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile, fetchProto } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; -import { NovelStatus } from "@libs/novelStatus"; +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile, fetchProto } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; interface StringValue { - value: string; + value: string; } interface BoolValue { - value: boolean; + value: boolean; } interface DecimalValue { - units: bigint; - nanos: number; + units: bigint; + nanos: number; } interface Timestamp { - seconds: number; - nanos: number; + seconds: number; + nanos: number; } interface NovelEntry { - name: string; - coverUrl: string; - slug: string; + name: string; + coverUrl: string; + slug: string; } interface RelatedChapterUserInfo { - isChapterUnlocked?: BoolValue; + isChapterUnlocked?: BoolValue; } interface ChapterItem { - entityId: number; - name: string; - slug: string; - number?: DecimalValue; - content?: StringValue; - relatedUserInfo?: RelatedChapterUserInfo; - offset: number; - publishedAt?: Timestamp; + entityId: number; + name: string; + slug: string; + number?: DecimalValue; + content?: StringValue; + relatedUserInfo?: RelatedChapterUserInfo; + offset: number; + publishedAt?: Timestamp; } interface GetChapterResponse { - item?: ChapterItem; + item?: ChapterItem; } interface ChapterGroupCounts { - total: number; - advance: number; - normal: number; + total: number; + advance: number; + normal: number; } interface ChapterGroupItem { - id: number; - title: string; - order: number; - fromChapterNumber?: DecimalValue; - toChapterNumber?: DecimalValue; - chapterList: ChapterItem[]; - counts?: ChapterGroupCounts; + id: number; + title: string; + order: number; + fromChapterNumber?: DecimalValue; + toChapterNumber?: DecimalValue; + chapterList: ChapterItem[]; + counts?: ChapterGroupCounts; } interface GetChapterListResponse { - items: ChapterGroupItem[]; + items: ChapterGroupItem[]; } enum NovelItem_Status { - Finished = 0, - Active = 1, - Hiatus = 2, - All = -1 + Finished = 0, + Active = 1, + Hiatus = 2, + All = -1, } interface NovelKarmaInfo { - isActive: boolean; - isFree: boolean; - maxFreeChapter?: DecimalValue; - canUnlockWithVip: boolean; + isActive: boolean; + isFree: boolean; + maxFreeChapter?: DecimalValue; + canUnlockWithVip: boolean; } interface NovelItem { - id: number; - name: string; - slug: string; - status: NovelItem_Status; - visible: boolean; - description?: StringValue; - synopsis?: StringValue; - coverUrl?: StringValue; - translatorName?: StringValue; - authorName?: StringValue; - karmaInfo?: NovelKarmaInfo; - genres: string[]; + id: number; + name: string; + slug: string; + status: NovelItem_Status; + visible: boolean; + description?: StringValue; + synopsis?: StringValue; + coverUrl?: StringValue; + translatorName?: StringValue; + authorName?: StringValue; + karmaInfo?: NovelKarmaInfo; + genres: string[]; } interface GetNovelResponse { - item?: NovelItem; + item?: NovelItem; } class WuxiaWorld implements Plugin.PluginBase { - id = "wuxiaworld"; - name = "Wuxia World"; - icon = "src/en/wuxiaworld/icon.png"; - site = "https://www.wuxiaworld.com/"; - filters?: Filters | undefined; - version = "0.5.0"; + id = 'wuxiaworld'; + name = 'Wuxia World'; + icon = 'src/en/wuxiaworld/icon.png'; + site = 'https://www.wuxiaworld.com/'; + filters?: Filters | undefined; + version = '0.5.0'; - parseNovels(data: {items: NovelEntry[]}){ - const novels: Plugin.NovelItem[] = []; + parseNovels(data: { items: NovelEntry[] }) { + const novels: Plugin.NovelItem[] = []; - data.items.map((novel: NovelEntry) => { - const name = novel.name; - const cover = novel.coverUrl; - const path = `novel/${novel.slug}/`; + data.items.map((novel: NovelEntry) => { + const name = novel.name; + const cover = novel.coverUrl; + const path = `novel/${novel.slug}/`; - novels.push({ - name, - cover, - path, - }); - }); + novels.push({ + name, + cover, + path, + }); + }); - return novels; + return novels; + } + + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const link = `${this.site}api/novels`; + + const result = await fetchApi(link); + const data = await result.json(); + + return this.parseNovels(data); + } + + async parseNovel(novelPath: string): Promise { + const data = await fetchProto( + { + proto: this.proto, + requestType: 'GetNovelRequest', + responseType: 'GetNovelResponse', + requestData: { slug: novelPath.split('/')[1] }, + }, + 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Novels/GetNovel', + { + headers: { + 'Content-Type': 'application/grpc-web+proto', + }, + }, + ); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: data.item?.name || 'Untitled', + cover: data.item?.coverUrl?.value, + summary: parseHTML( + data.item?.description?.value + '\n\n' + data.item?.synopsis?.value, + ).text(), + author: data.item?.authorName?.value, + genres: data.item?.genres.join(','), + chapters: [], + }; + + const status = data.item?.status; + switch (status) { + case NovelItem_Status.Active: + novel.status = NovelStatus.Ongoing; + break; + case NovelItem_Status.Hiatus: + novel.status = NovelStatus.OnHiatus; + break; + case NovelItem_Status.All: + novel.status = NovelStatus.Unknown; + break; + default: + novel.status = NovelStatus.Completed; } - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const link = `${this.site}api/novels`; + // novel.summary = loadedCheerio('.relative > .absolute:first') + // .children('span') + // .map((i,el) => loadedCheerio(el).text().trim()) + // .toArray() + // .join('\n'); - const result = await fetchApi(link); - const data = await result.json(); + // novel.genres = loadedCheerio("a.MuiLink-underlineNone") + // .map((i,el) => loadedCheerio(el).text()) + // .toArray() + // .join(','); - return this.parseNovels(data) - } + // novel.status = loadedCheerio("div.font-set-b10").text(); - async parseNovel(novelPath: string): Promise { - const data = await fetchProto( - { - proto: this.proto, - requestType: 'GetNovelRequest', - responseType: 'GetNovelResponse', - requestData: {slug: novelPath.split('/')[1]}, + const list = await fetchProto( + { + proto: this.proto, + requestType: 'GetChapterListRequest', + responseType: 'GetChapterListResponse', + requestData: { novelId: data.item?.id }, + }, + 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapterList', + { + headers: { + 'Content-Type': 'application/grpc-web+proto', + }, + }, + ); + + const freeChapter = + Number(data.item?.karmaInfo?.maxFreeChapter?.units) + + (data.item?.karmaInfo?.maxFreeChapter?.nanos || 0) / 1000000000 || 50; + + const chapter: Plugin.ChapterItem[] = list.items.flatMap( + (ChapterGroupItem: ChapterGroupItem) => + ChapterGroupItem.chapterList.map((chapterItem: ChapterItem) => ({ + page: ChapterGroupItem.title, + name: + chapterItem.name + + (chapterItem.relatedUserInfo?.isChapterUnlocked?.value === false || + (!chapterItem.relatedUserInfo && + Number(chapterItem.number?.units) + + (chapterItem.number?.nanos || 0) / 1000000000 > + freeChapter) + ? ' 🔒' + : ''), + path: novelPath + chapterItem.slug, + chapterNumber: chapterItem.offset, + releaseTime: new Date( + (chapterItem.publishedAt?.seconds || 0) * 1000 + + (chapterItem.publishedAt?.nanos || 0) / 1000000, + ).toISOString(), + })), + ); + + novel.chapters = chapter; + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const paths = chapterPath.split('/'); + const data = await fetchProto( + { + proto: this.proto, + requestType: 'GetChapterRequest', + responseType: 'GetChapterResponse', + requestData: { + chapterProperty: { + slugs: { + novelSlug: paths[1], + chapterSlug: paths[2], }, - 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Novels/GetNovel', - { - headers: { - "Content-Type": "application/grpc-web+proto", - } - } - ) + }, + }, + }, + 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapter', + { + headers: { + 'Content-Type': 'application/grpc-web+proto', + }, + }, + ); + // loadedCheerio(".chapter-nav").remove(); + // loadedCheerio("#chapter-content > script").remove(); + // const chapterText = loadedCheerio("#chapter-content").html() || ''; - const novel: Plugin.SourceNovel = { - path: novelPath, - name: data.item?.name || "Untitled", - cover: data.item?.coverUrl?.value, - summary: parseHTML(data.item?.description?.value + '\n\n' + data.item?.synopsis?.value).text(), - author: data.item?.authorName?.value, - genres: data.item?.genres.join(','), - chapters: [], - }; + const chapterText = data.item?.content?.value || ''; + return chapterText; + } - const status = data.item?.status; - switch (status){ - case NovelItem_Status.Active: - novel.status = NovelStatus.Ongoing; - break; - case NovelItem_Status.Hiatus: - novel.status = NovelStatus.OnHiatus; - break; - case NovelItem_Status.All: - novel.status = NovelStatus.Unknown; - break; - default: novel.status = NovelStatus.Completed; - } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const searchUrl = 'https://www.wuxiaworld.com/api/novels/search?query='; - // novel.summary = loadedCheerio('.relative > .absolute:first') - // .children('span') - // .map((i,el) => loadedCheerio(el).text().trim()) - // .toArray() - // .join('\n'); + const url = searchUrl + searchTerm; - // novel.genres = loadedCheerio("a.MuiLink-underlineNone") - // .map((i,el) => loadedCheerio(el).text()) - // .toArray() - // .join(','); + const result = await fetchApi(url); + const data = await result.json(); - // novel.status = loadedCheerio("div.font-set-b10").text(); + return this.parseNovels(data); + } - const list = await fetchProto( - { - proto: this.proto, - requestType: 'GetChapterListRequest', - responseType: 'GetChapterListResponse', - requestData: {novelId: data.item?.id}, - }, - 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapterList', - { - headers: { - "Content-Type": "application/grpc-web+proto", - } - } - ) + fetchImage(url: string): Promise { + return fetchFile(url); + } - const freeChapter = Number(data.item?.karmaInfo?.maxFreeChapter?.units) + - (data.item?.karmaInfo?.maxFreeChapter?.nanos || 0) / 1000000000 || 50; - - const chapter: Plugin.ChapterItem[] = list.items.flatMap((ChapterGroupItem: ChapterGroupItem) => - ChapterGroupItem.chapterList.map((chapterItem: ChapterItem) => ({ - page: ChapterGroupItem.title, - name: chapterItem.name + ( - (chapterItem.relatedUserInfo?.isChapterUnlocked?.value === false) || - (!chapterItem.relatedUserInfo && - (Number(chapterItem.number?.units) + - (chapterItem.number?.nanos || 0) / 1000000000) > freeChapter) - ? ' 🔒' - : '' - ), - path: novelPath + chapterItem.slug, - chapterNumber: chapterItem.offset, - releaseTime: new Date( - (chapterItem.publishedAt?.seconds || 0) * 1000 + - (chapterItem.publishedAt?.nanos || 0) / 1000000 - ).toISOString(), - }) - )); - - novel.chapters = chapter; - - return novel; - } - - async parseChapter(chapterPath: string): Promise { - const paths = chapterPath.split('/'); - const data = await fetchProto( - { - proto: this.proto, - requestType: 'GetChapterRequest', - responseType: 'GetChapterResponse', - requestData: { - chapterProperty: { - slugs: { - novelSlug: paths[1], - chapterSlug: paths[2], - } - } - }, - }, - 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.Chapters/GetChapter', - { - headers: { - "Content-Type": "application/grpc-web+proto", - } - } - ) - // loadedCheerio(".chapter-nav").remove(); - // loadedCheerio("#chapter-content > script").remove(); - // const chapterText = loadedCheerio("#chapter-content").html() || ''; - - const chapterText = data.item?.content?.value || ""; - return chapterText - } - - async searchNovels(searchTerm: string, pageNo: number): Promise { - const searchUrl = "https://www.wuxiaworld.com/api/novels/search?query="; - - const url = searchUrl + searchTerm; - - const result = await fetchApi(url); - const data = await result.json(); - - return this.parseNovels(data); - } - - fetchImage(url: string): Promise { - return fetchFile(url); - } - - proto = ` + proto = ` syntax = "proto3"; option optimize_for = CODE_SIZE; package wuxiaworld.api.v2; @@ -460,7 +472,7 @@ class WuxiaWorld implements Plugin.PluginBase { service Novels { rpc GetNovel(GetNovelRequest) returns (GetNovelResponse); } - ` + `; } -export default new WuxiaWorld(); \ No newline at end of file +export default new WuxiaWorld(); diff --git a/plugins/french/chireads.ts b/plugins/french/chireads.ts index 7d4b6ed..5118bd3 100644 --- a/plugins/french/chireads.ts +++ b/plugins/french/chireads.ts @@ -1,265 +1,341 @@ -import { CheerioAPI, load } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import dayjs from "dayjs"; -import { defaultCover } from "@libs/defaultCover"; -import { NovelStatus } from "@libs/novelStatus"; +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import dayjs from 'dayjs'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; class ChireadsPlugin implements Plugin.PluginBase { - id = "chireads"; - name = "Chireads"; - icon = "src/fr/chireads/icon.png"; - site = "https://chireads.com"; - version = "1.0.2"; + id = 'chireads'; + name = 'Chireads'; + icon = 'src/fr/chireads/icon.png'; + site = 'https://chireads.com'; + version = '1.0.2'; - async getCheerio(url: string): Promise { - const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }}); - const body = await r.text(); - const $ = load(body); - return ($); - } + async getCheerio(url: string): Promise { + const r = await fetchApi(url, { + headers: { 'Accept-Encoding': 'deflate' }, + }); + const body = await r.text(); + const $ = load(body); + return $; + } - async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions): Promise { - let url = this.site; - let tag: string = "all"; - if (showLatestNovels) - url += "/category/translatedtales/page/" + pageNo; - else { - if (filters && typeof filters.tag.value === "string" && filters.tag.value !== "all") - tag = filters.tag.value; - if (tag !== "all") - url += "/tag/" + tag + "/page/" + pageNo; - else if (pageNo > 1) - return []; - } - let $ = await this.getCheerio(url); + async popularNovels( + pageNo: number, + { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.site; + let tag: string = 'all'; + if (showLatestNovels) url += '/category/translatedtales/page/' + pageNo; + else { + if ( + filters && + typeof filters.tag.value === 'string' && + filters.tag.value !== 'all' + ) + tag = filters.tag.value; + if (tag !== 'all') url += '/tag/' + tag + '/page/' + pageNo; + else if (pageNo > 1) return []; + } + let $ = await this.getCheerio(url); - let novels: Plugin.NovelItem[] = []; - let novel: Plugin.NovelItem; + let novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; - if (showLatestNovels || tag !== "all") { - let loop = 1; - if (showLatestNovels) - loop = 2; - for (let i = 0; i < loop; i++) { - if (i === 1) - $ = await this.getCheerio(this.site + "/category/original/page/" + pageNo); - let romans = $(".romans-content li"); - if (!romans.length) - romans = $("#content li"); - romans.each((i, elem) => { - const novelName = $(elem).contents().find("div").first().text().trim(); - const novelCover = $(elem).find("div").first().find("img").attr("src"); - const novelUrl = $(elem).find("div").first().find("a").attr("href") - - if (novelUrl){ - novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, "") }; - novels.push(novel); - } - }); - } - } - else { - const populaire = $(":contains(\"Populaire\")").last().parent().next().find("li > div") - if (populaire.length === 12) // pc - { - let novelCover : string | undefined; - let novelName : string | undefined; - let novelUrl : string | undefined; - populaire.each((i, elem) => { - if (i % 2 === 0) - novelCover = $(elem).find("img").attr("src"); - else { - novelName = $(elem).text().trim(); - novelUrl = $(elem).find("a").attr("href"); - - if (!novelUrl) return; - - novel = { name: novelName, cover: novelCover || defaultCover, path: novelUrl.replace(this.site, "") }; - - novels.push(novel); - } - }); - } - else // mobile - { - const imgs = populaire.find("div.popular-list-img img"); - const txts = populaire.find("div.popular-list-name"); + if (showLatestNovels || tag !== 'all') { + let loop = 1; + if (showLatestNovels) loop = 2; + for (let i = 0; i < loop; i++) { + if (i === 1) + $ = await this.getCheerio( + this.site + '/category/original/page/' + pageNo, + ); + let romans = $('.romans-content li'); + if (!romans.length) romans = $('#content li'); + romans.each((i, elem) => { + const novelName = $(elem) + .contents() + .find('div') + .first() + .text() + .trim(); + const novelCover = $(elem) + .find('div') + .first() + .find('img') + .attr('src'); + const novelUrl = $(elem).find('div').first().find('a').attr('href'); - txts.each((i, elem) => { - const novelName = $(elem).text().trim(); - const novelCover = $(imgs[i]).attr("src"); - const novelUrl = $(elem).find("a").attr("href"); - - if (novelUrl){ - novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, "") }; - novels.push(novel); - } - }); - } - } - return novels; - }; - - async parseNovel(novelPath: string): Promise { - const novel: Plugin.SourceNovel = { path: novelPath, name: "Sans titre" }; - - let $ = await this.getCheerio(this.site + novelPath); - - novel.name = $(".inform-product-txt").first().text().trim() || $(".inform-title").text().trim(); - novel.cover = $(".inform-product img").attr("src") || $(".inform-product-img img").attr("src") || defaultCover; - novel.summary = $(".inform-inform-txt").text().trim() || $(".inform-intr-txt").text().trim(); - - let infos = $("div.inform-product-txt > div.inform-intr-col").text().trim() || $("div.inform-inform-data > h6").text().trim(); - if (infos.includes("Auteur : ")) - novel.author = infos.substring(infos.indexOf("Auteur : ") + 9, infos.indexOf("Statut de Parution : ")).trim(); - else if (infos.includes("Fantrad : ")) - novel.author = infos.substring(infos.indexOf("Fantrad : ") + 10, infos.indexOf("Statut de Parution : ")).trim(); - else - novel.author = "Inconnu"; - switch (infos.substring(infos.indexOf("Statut de Parution : ") + 21).toLowerCase()) { - case "en pause": - novel.status = NovelStatus.OnHiatus; - break; - case "complet": - novel.status = NovelStatus.Completed; - break; - default: - novel.status = NovelStatus.Ongoing; - break; - } - - let chapters: Plugin.ChapterItem[] = []; - - let chapterList = $(".chapitre-table a"); - if (!chapterList.length) - { - $("div.inform-annexe-list").first().remove(); - chapterList = $(".inform-annexe-list").find("a"); - } - chapterList.each((i, elem) => { - const chapterName = $(elem).text().trim(); - const chapterUrl = $(elem).attr("href"); - const releaseDate = dayjs(chapterUrl?.substring(chapterUrl.length - 11, chapterUrl.length - 1)).format("DD MMMM YYYY"); - - if (chapterUrl){ - chapters.push({ - name: chapterName, - releaseTime: releaseDate, - path: chapterUrl.replace(this.site, ""), - }); - } + if (novelUrl) { + novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } }); + } + } else { + const populaire = $(':contains("Populaire")') + .last() + .parent() + .next() + .find('li > div'); + if (populaire.length === 12) { + // pc + let novelCover: string | undefined; + let novelName: string | undefined; + let novelUrl: string | undefined; + populaire.each((i, elem) => { + if (i % 2 === 0) novelCover = $(elem).find('img').attr('src'); + else { + novelName = $(elem).text().trim(); + novelUrl = $(elem).find('a').attr('href'); - novel.chapters = chapters; + if (!novelUrl) return; - return novel; - }; + novel = { + name: novelName, + cover: novelCover || defaultCover, + path: novelUrl.replace(this.site, ''), + }; - async parseChapter(chapterUrl: string): Promise { - const $ = await this.getCheerio(this.site + chapterUrl); + novels.push(novel); + } + }); + } // mobile + else { + const imgs = populaire.find('div.popular-list-img img'); + const txts = populaire.find('div.popular-list-name'); - const chapterText = $("#content").html() || ""; + txts.each((i, elem) => { + const novelName = $(elem).text().trim(); + const novelCover = $(imgs[i]).attr('src'); + const novelUrl = $(elem).find('a').attr('href'); - return chapterText; - }; + if (novelUrl) { + novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + } + } + return novels; + } - async searchNovels(searchTerm: string, pageNo: number): Promise { - if (pageNo !== 1) - return []; - let novels: Plugin.NovelItem[] = []; + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { path: novelPath, name: 'Sans titre' }; - let i = 1; - let finised = false; - while ( !finised ) + let $ = await this.getCheerio(this.site + novelPath); + + novel.name = + $('.inform-product-txt').first().text().trim() || + $('.inform-title').text().trim(); + novel.cover = + $('.inform-product img').attr('src') || + $('.inform-product-img img').attr('src') || + defaultCover; + novel.summary = + $('.inform-inform-txt').text().trim() || + $('.inform-intr-txt').text().trim(); + + let infos = + $('div.inform-product-txt > div.inform-intr-col').text().trim() || + $('div.inform-inform-data > h6').text().trim(); + if (infos.includes('Auteur : ')) + novel.author = infos + .substring( + infos.indexOf('Auteur : ') + 9, + infos.indexOf('Statut de Parution : '), + ) + .trim(); + else if (infos.includes('Fantrad : ')) + novel.author = infos + .substring( + infos.indexOf('Fantrad : ') + 10, + infos.indexOf('Statut de Parution : '), + ) + .trim(); + else novel.author = 'Inconnu'; + switch ( + infos.substring(infos.indexOf('Statut de Parution : ') + 21).toLowerCase() + ) { + case 'en pause': + novel.status = NovelStatus.OnHiatus; + break; + case 'complet': + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Ongoing; + break; + } + + let chapters: Plugin.ChapterItem[] = []; + + let chapterList = $('.chapitre-table a'); + if (!chapterList.length) { + $('div.inform-annexe-list').first().remove(); + chapterList = $('.inform-annexe-list').find('a'); + } + chapterList.each((i, elem) => { + const chapterName = $(elem).text().trim(); + const chapterUrl = $(elem).attr('href'); + const releaseDate = dayjs( + chapterUrl?.substring(chapterUrl.length - 11, chapterUrl.length - 1), + ).format('DD MMMM YYYY'); + + if (chapterUrl) { + chapters.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.site, ''), + }); + } + }); + + novel.chapters = chapters; + + return novel; + } + + async parseChapter(chapterUrl: string): Promise { + const $ = await this.getCheerio(this.site + chapterUrl); + + const chapterText = $('#content').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo !== 1) return []; + let novels: Plugin.NovelItem[] = []; + + let i = 1; + let finised = false; + while (!finised) { + await this.popularNovels(i, { + showLatestNovels: true, + filters: undefined, + }).then(res => { + if (res.length === 0) finised = true; + novels.push(...res); + }); + i++; + } + + novels = novels.filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''), + ), + ); + + return novels; + } + + fetchImage = fetchFile; + + filters = { + tag: { + type: FilterTypes.Picker, + label: 'Tag', + value: 'all', + options: [ + { label: 'Tous', value: 'all' }, + { label: 'Arts martiaux', value: 'arts-martiaux' }, + { label: 'De faible à fort', value: 'de-faible-a-fort' }, + { label: 'Adapté en manhua', value: 'adapte-en-manhua' }, + { label: 'Cultivation', value: 'cultivation' }, + { label: 'Action', value: 'action' }, + { label: 'Aventure', value: 'aventure' }, + { label: 'Monstres', value: 'monstres' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Fantastique', value: 'fantastique' }, + { label: 'Adapté en Animé', value: 'adapte-en-anime' }, + { label: 'Alchimie', value: 'alchimie' }, + { label: 'Éléments de jeux', value: 'elements-de-jeux' }, + { label: 'Calme Protagoniste', value: 'calme-protagoniste' }, { - await this.popularNovels(i, { showLatestNovels: true, filters: undefined } ).then((res) => { - if (res.length === 0) - finised = true; - novels.push(...res); - }); - i++; - } + label: 'Protagoniste intelligent', + value: 'protagoniste-intelligent', + }, + { label: 'Polygamie', value: 'polygamie' }, + { label: 'Belle femelle Lea', value: 'belle-femelle-lea' }, + { label: 'Personnages arrogants', value: 'personnages-arrogants' }, + { label: 'Système de niveau', value: 'systeme-de-niveau' }, + { label: 'Cheat', value: 'cheat' }, + { label: 'Protagoniste génie', value: 'protagoniste-genie' }, + { label: 'Comédie', value: 'comedie' }, + { label: 'Gamer', value: 'gamer' }, + { label: 'Mariage', value: 'mariage' }, + { label: 'seeking Protag', value: 'seeking-protag' }, + { label: 'Romance précoce', value: 'romance-precoce' }, + { label: 'Croissance accélérée', value: 'croissance-acceleree' }, + { label: 'Artefacts', value: 'artefacts' }, + { + label: 'Intelligence artificielle', + value: 'intelligence-artificielle', + }, + { label: 'Mariage arrangé', value: 'mariage-arrange' }, + { label: 'Mature', value: 'mature' }, + { label: 'Adulte', value: 'adulte' }, + { + label: 'Administrateur de système', + value: 'administrateur-de-systeme', + }, + { label: 'Beau protagoniste', value: 'beau-protagoniste' }, + { + label: 'Protagoniste charismatique', + value: 'protagoniste-charismatique', + }, + { label: 'Protagoniste masculin', value: 'protagoniste-masculin' }, + { label: 'Démons', value: 'demons' }, + { label: 'Reincarnation', value: 'reincarnation' }, + { label: 'Académie', value: 'academie' }, + { + label: 'Cacher les vraies capacités', + value: 'cacher-les-vraies-capacites', + }, + { + label: 'Protagoniste surpuissant', + value: 'protagoniste-surpuissant', + }, + { label: 'Joueur', value: 'joueur' }, + { + label: 'Protagoniste fort dès le départ', + value: 'protagoniste-fort-des-le-depart', + }, + { label: 'Immortels', value: 'immortels' }, + { label: 'Cultivation rapide', value: 'cultivation-rapide' }, + { label: 'Harem', value: 'harem' }, + { label: 'Assasins', value: 'assasins' }, + { label: 'De pauvre à riche', value: 'de-pauvre-a-riche' }, + { + label: 'Système de classement de jeux', + value: 'systeme-de-classement-de-jeux', + }, + { label: 'Capacités spéciales', value: 'capacites-speciales' }, + { label: 'Vengeance', value: 'vengeance' }, + ], + }, + } satisfies Filters; +} - novels = novels.filter((novel) => - novel.name - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .includes( - searchTerm - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - ) - ); - - return novels; - }; - - fetchImage = fetchFile; - - filters = { - tag: { - type: FilterTypes.Picker, - label: "Tag", - value: "all", - options: [ - { label: "Tous", value: "all" }, - { label: "Arts martiaux", value: "arts-martiaux" }, - { label: "De faible à fort", value: "de-faible-a-fort" }, - { label: "Adapté en manhua", value: "adapte-en-manhua" }, - { label: "Cultivation", value: "cultivation" }, - { label: "Action", value: "action" }, - { label: "Aventure", value: "aventure" }, - { label: "Monstres", value: "monstres" }, - { label: "Xuanhuan", value: "xuanhuan" }, - { label: "Fantastique", value: "fantastique" }, - { label: "Adapté en Animé", value: "adapte-en-anime" }, - { label: "Alchimie", value: "alchimie" }, - { label: "Éléments de jeux", value: "elements-de-jeux" }, - { label: "Calme Protagoniste", value: "calme-protagoniste" }, - { label: "Protagoniste intelligent", value: "protagoniste-intelligent" }, - { label: "Polygamie", value: "polygamie" }, - { label: "Belle femelle Lea", value: "belle-femelle-lea" }, - { label: "Personnages arrogants", value: "personnages-arrogants" }, - { label: "Système de niveau", value: "systeme-de-niveau" }, - { label: "Cheat", value: "cheat" }, - { label: "Protagoniste génie", value: "protagoniste-genie" }, - { label: "Comédie", value: "comedie" }, - { label: "Gamer", value: "gamer" }, - { label: "Mariage", value: "mariage" }, - { label: "seeking Protag", value: "seeking-protag" }, - { label: "Romance précoce", value: "romance-precoce" }, - { label: "Croissance accélérée", value: "croissance-acceleree" }, - { label: "Artefacts", value: "artefacts" }, - { label: "Intelligence artificielle", value: "intelligence-artificielle" }, - { label: "Mariage arrangé", value: "mariage-arrange" }, - { label: "Mature", value: "mature" }, - { label: "Adulte", value: "adulte" }, - { label: "Administrateur de système", value: "administrateur-de-systeme" }, - { label: "Beau protagoniste", value: "beau-protagoniste" }, - { label: "Protagoniste charismatique", value: "protagoniste-charismatique" }, - { label: "Protagoniste masculin", value: "protagoniste-masculin" }, - { label: "Démons", value: "demons" }, - { label: "Reincarnation", value: "reincarnation" }, - { label: "Académie", value: "academie" }, - { label: "Cacher les vraies capacités", value: "cacher-les-vraies-capacites" }, - { label: "Protagoniste surpuissant", value: "protagoniste-surpuissant" }, - { label: "Joueur", value: "joueur" }, - { label: "Protagoniste fort dès le départ", value: "protagoniste-fort-des-le-depart" }, - { label: "Immortels", value: "immortels" }, - { label: "Cultivation rapide", value: "cultivation-rapide" }, - { label: "Harem", value: "harem" }, - { label: "Assasins", value: "assasins" }, - { label: "De pauvre à riche", value: "de-pauvre-a-riche" }, - { label: "Système de classement de jeux", value: "systeme-de-classement-de-jeux" }, - { label: "Capacités spéciales", value: "capacites-speciales" }, - { label: "Vengeance", value: "vengeance" }, - ], - } - } satisfies Filters; -}; - -export default new ChireadsPlugin(); \ No newline at end of file +export default new ChireadsPlugin(); diff --git a/plugins/french/noveldeglace.ts b/plugins/french/noveldeglace.ts index c04bd5f..9fbe947 100644 --- a/plugins/french/noveldeglace.ts +++ b/plugins/french/noveldeglace.ts @@ -1,335 +1,349 @@ -import { load, CheerioAPI } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import { defaultCover } from "@libs/defaultCover"; -import dayjs from "dayjs"; +import { load, CheerioAPI } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import dayjs from 'dayjs'; -class NovelDeGlacePlugin implements Plugin.PluginBase{ - id = "noveldeglace"; - name = "NovelDeGlace"; - icon = "src/fr/noveldeglace/icon.png"; - site = "https://noveldeglace.com/"; - version = "1.0.2"; +class NovelDeGlacePlugin implements Plugin.PluginBase { + id = 'noveldeglace'; + name = 'NovelDeGlace'; + icon = 'src/fr/noveldeglace/icon.png'; + site = 'https://noveldeglace.com/'; + version = '1.0.2'; - async getCheerio(url: string): Promise { - const r = await fetchApi(url, {headers: { 'Accept-Encoding': 'deflate' }}); - if (!r.ok) return (undefined); - const body = await r.text(); - const loadedCheerio = load(body); - return (loadedCheerio); + async getCheerio(url: string): Promise { + const r = await fetchApi(url, { + headers: { 'Accept-Encoding': 'deflate' }, + }); + if (!r.ok) return undefined; + const body = await r.text(); + const loadedCheerio = load(body); + return loadedCheerio; + } + + parseDate(date: string): string { + const monthMapping: { [key: string]: string } = { + janvier: 'January', + fevrier: 'February', + mars: 'March', + avril: 'April', + mai: 'May', + juin: 'June', + juillet: 'July', + aout: 'August', + septembre: 'September', + octobre: 'October', + novembre: 'November', + decembre: 'December', + }; + const [day, month, year] = date.split(' '); + return dayjs( + `${day} ${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]} ${year}`, + 'D MMMM YYYY', + ).format('YYYY-MM-DD'); + } + + parseNovels( + $: CheerioAPI, + showLatestNovels: boolean | undefined, + ): Plugin.NovelItem[] { + let novels: Plugin.NovelItem[] = []; + + $('article').each((i, el) => { + const cheerio = $(el); + const novelName = cheerio.find('h2').text().trim(); + const novelCover = cheerio.find('img').attr('src'); + let novelUrl: string | undefined; + if (showLatestNovels) + novelUrl = cheerio.find('span.Roman > a').attr('href'); + else novelUrl = cheerio.find('h2 > a').attr('href'); + + if (novelUrl) { + const novel: Plugin.NovelItem = { + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }; + novels.push(novel); } + }); - parseDate(date: string): string { - const monthMapping: { [key: string]: string } = { janvier: "January", fevrier: "February", mars: "March", avril: "April", mai: "May", juin: "June", juillet: "July", aout: "August", septembre: "September", octobre: "October", novembre: "November", decembre: "December"}; - const [day, month, year] = date.split(" "); - return dayjs(`${day} ${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]} ${year}`, "D MMMM YYYY").format("YYYY-MM-DD"); + return novels; + } + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.site; + if (showLatestNovels) url += 'chapitre'; + else { + let cat_gen: string = 'all'; + if (filters && typeof filters.categorie_genre.value == 'string') + cat_gen = filters.categorie_genre.value; + if ( + cat_gen != 'all' && + cat_gen != 'categorie_roman' && + cat_gen != 'genre' + ) { + if (cat_gen[0] == 'c') url += 'categorie_roman/' + cat_gen.substring(2); + else if (cat_gen[0] == 'g') url += 'genre/' + cat_gen.substring(2); + } else if (pageNo > 1) + return []; // when asking for all novels, there is only 1 page + else url += 'roman'; + } + url += '/page/' + pageNo; + let $ = await this.getCheerio(url); + if (!$) return []; + return this.parseNovels($, showLatestNovels); + } + + async parseNovel(novelPath: string): Promise { + const $ = await this.getCheerio(this.site + novelPath); + if (!$) throw new Error('Failed to load page (open in web view)'); + + let novel: Plugin.SourceNovel = { path: novelPath, name: 'Untitled' }; + + novel.name = + ( + $('div.entry-content > div > strong')[0].nextSibling as Text | null + )?.nodeValue?.trim() || 'Untitled'; + + novel.cover = $('.su-row > div > div > img').attr('src') || defaultCover; + + let novelInfos = $('div[data-title=Tomes] >').toArray(); + novelInfos.pop(); + novelInfos.shift(); + novel.summary = + $('div[data-title=Synopsis]').text().trim() + + '\n\n' + + novelInfos + .map(el => $(el).text()) + .join('\n') + .trim(); + + novel.author = $("strong:contains('Auteur :')") + .parent() + .text() + .replace('Auteur : ', '') + .trim(); + + novel.artist = $("strong:contains('Illustrateur :')") + .parent() + .text() + .replace('Illustrateur :', '') + .trim(); + + const categorie = $('.categorie').text().replace('Catégorie :', '').trim(); + const genres = $('.genre') + .text() + .replace('Genre :', '') + .replace(/, /g, ',') + .trim(); + if (categorie && categorie != 'Autre') novel.genres = categorie; + if (genres) + novel.genres = novel.genres ? novel.genres + ',' + genres : genres; + + let status = $("strong:contains('Statut :')").parent().attr('class'); + switch (status) { + case 'type etat0': + case 'type etat1': + novel.status = NovelStatus.Ongoing; + break; + case 'type etat4': + novel.status = NovelStatus.OnHiatus; + break; + case 'type etat5': + novel.status = NovelStatus.Completed; + break; + case 'type etat6': + novel.status = NovelStatus.Cancelled; + break; + default: + novel.status = NovelStatus.Unknown; + break; } - parseNovels($: CheerioAPI, showLatestNovels: boolean | undefined): Plugin.NovelItem[] { - let novels: Plugin.NovelItem[] = []; - - $("article").each((i, el) => { - const cheerio = $(el); - const novelName = cheerio.find("h2").text().trim(); - const novelCover = cheerio.find("img").attr("src"); - let novelUrl: string | undefined; - if (showLatestNovels) - novelUrl = cheerio.find("span.Roman > a").attr("href"); - else - novelUrl = cheerio.find("h2 > a").attr("href"); - - if (novelUrl){ - const novel:Plugin.NovelItem = { - name: novelName, - path: novelUrl.replace(this.site, ""), - cover: novelCover, + let novelChapters: Plugin.ChapterItem[] = []; + + const volumes = $('div[data-title=Tomes] > div').last().contents(); + const hasMultipleVolumes = volumes.length > 1; + + let chapterName = ''; + let site = this.site; + volumes.each((volumeIndex: number, el) => { + if (hasMultipleVolumes) chapterName = 'T.' + (volumeIndex + 1) + ' '; + $(el) + .find('.chpt') + .each((chapterIndex: number, el) => { + const cheerio = $(el); + const newChapterName = + chapterName + cheerio.find('a').first().text().trim() || ''; + if (!cheerio.find('i.fa').length) { + // no parts + const dateHtml = + cheerio.html()?.substring(cheerio.html()?.indexOf('') || 0) || + ''; + let releaseDate = + dateHtml?.substring( + dateHtml.indexOf('(') + 1, + dateHtml.indexOf(')'), + ) || undefined; + if (releaseDate) + try { + releaseDate = this.parseDate(releaseDate); + } catch (e) {} + const chapterUrl = cheerio.find('a').attr('href'); + if (chapterUrl) { + const chapter: Plugin.ChapterItem = { + name: newChapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(site, ''), + chapterNumber: chapterIndex, + }; + + novelChapters.push(chapter); + } + } // has parts that needs to be added individually + else { + const items = + cheerio.find('i').parent().next().html()?.split('') || []; + items?.shift(); + const dates: string[] = []; + items?.forEach(item => { + // there is a date on every publish parts + dates.push( + item.substring(item.indexOf('(') + 1, item.indexOf(')')), + ); + }); + const hrefs: string[] = []; + cheerio + .find('i') + .parent() + .next() + .find('a') + .each(function () { + hrefs.push(this['attribs']['href']); + }); + if (dates.length == hrefs.length) + dates.forEach((date, index) => { + let releaseDate = date; + try { + releaseDate = this.parseDate(date); + } catch (e) {} + const chapter: Plugin.ChapterItem = { + name: newChapterName + ' (' + (index + 1) + ')', + releaseTime: releaseDate, + path: hrefs[index].replace(site, ''), + chapterNumber: chapterIndex + (index + 1) / 1000, }; - novels.push(novel); - } + novelChapters.push(chapter); + }); + } }); - - return novels; - } + }); - async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions): Promise { - let url = this.site; - if (showLatestNovels) - url += "chapitre" - else - { - let cat_gen: string = "all"; - if (filters && typeof filters.categorie_genre.value == "string") - cat_gen = filters.categorie_genre.value; - if (cat_gen != "all" && cat_gen != "categorie_roman" && cat_gen != "genre") - { - if (cat_gen[0] == "c") - url += "categorie_roman/" + cat_gen.substring(2); - else if (cat_gen[0] == "g") - url += "genre/" + cat_gen.substring(2); - } - else if (pageNo > 1) - return ([]); // when asking for all novels, there is only 1 page - else - url += "roman"; - } - url += "/page/" + pageNo; - let $ = await this.getCheerio(url); - if (!$) - return ([]); - return this.parseNovels($, showLatestNovels); - }; - - async parseNovel(novelPath: string): Promise { - const $ = await this.getCheerio(this.site + novelPath); - if (!$) - throw new Error("Failed to load page (open in web view)"); + novel.chapters = novelChapters; - let novel: Plugin.SourceNovel = { path: novelPath, name: "Untitled" }; + return novel; + } - novel.name = ($("div.entry-content > div > strong")[0] - .nextSibling as Text | null) - ?.nodeValue - ?.trim() || "Untitled"; + async parseChapter(chapterPath: string): Promise { + const $ = await this.getCheerio(this.site + chapterPath); + if (!$) throw new Error('Failed to load page (open in web view)'); - novel.cover = $(".su-row > div > div > img") - .attr("src") || defaultCover; - - let novelInfos = $("div[data-title=Tomes] >") - .toArray(); - novelInfos.pop(); - novelInfos.shift(); - novel.summary = $("div[data-title=Synopsis]") - .text().trim() + - "\n\n" + novelInfos.map((el) => $(el).text()).join("\n").trim(); + $('.mistape_caption').remove(); + let chapterText = + $('.chapter-content').html() || $('.entry-content').html() || ''; + return chapterText; + } - novel.author = $("strong:contains('Auteur :')") - .parent() - .text() - .replace("Auteur : ", "") - .trim(); + async searchNovels( + searchTerm: string, + num: number, + ): Promise { + if (num !== 1) return []; // only 1 page of results + let url = this.site + 'roman'; + const $ = await this.getCheerio(url); + if (!$) throw new Error('Failed to load page (open in web view)'); - novel.artist = $("strong:contains('Illustrateur :')") - .parent() - .text() - .replace("Illustrateur :", "") - .trim(); + let novels = this.parseNovels($, false); - const categorie = $(".categorie") - .text() - .replace("Catégorie :", "") - .trim(); - const genres = $(".genre") - .text() - .replace("Genre :", "") - .replace(/, /g, ",") - .trim(); - if (categorie && categorie != "Autre") - novel.genres = categorie; - if (genres) - novel.genres = novel.genres ? novel.genres + "," + genres : genres; + novels = novels.filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); - let status = $("strong:contains('Statut :')") - .parent() - .attr("class"); - switch (status) { - case "type etat0": - case "type etat1": - novel.status = NovelStatus.Ongoing; - break; - case "type etat4": - novel.status = NovelStatus.OnHiatus; - break; - case "type etat5": - novel.status = NovelStatus.Completed; - break; - case "type etat6": - novel.status = NovelStatus.Cancelled; - break; - default: - novel.status = NovelStatus.Unknown; - break; - } + return novels; + } - let novelChapters: Plugin.ChapterItem[] = []; + fetchImage = fetchFile; - const volumes = $("div[data-title=Tomes] > div") - .last() - .contents(); - const hasMultipleVolumes = volumes.length > 1; - - let chapterName = "" - let site = this.site; - volumes.each((volumeIndex: number, el) => { - if (hasMultipleVolumes) - chapterName = "T." + (volumeIndex + 1) + " "; - $(el) - .find(".chpt") - .each((chapterIndex: number, el) => { - const cheerio = $(el); - const newChapterName = chapterName + cheerio - .find("a") - .first() - .text().trim() || ""; - if (!cheerio.find("i.fa").length) // no parts - { - const dateHtml = cheerio.html()?.substring(cheerio.html()?.indexOf("") || 0) || ""; - let releaseDate = dateHtml?.substring(dateHtml.indexOf("(") + 1, dateHtml.indexOf(")")) || undefined; - if (releaseDate) - try { - releaseDate = this.parseDate(releaseDate); - } catch (e) {} - const chapterUrl = cheerio.find("a").attr("href"); - if (chapterUrl) - { - const chapter: Plugin.ChapterItem = { - name: newChapterName, - releaseTime: releaseDate, - path: chapterUrl.replace(site, ""), - chapterNumber: chapterIndex, - }; - - novelChapters.push(chapter); - } - } - else // has parts that needs to be added individually - { - const items = cheerio - .find("i") - .parent() - .next() - .html() - ?.split("") || []; - items?.shift(); - const dates: string[] = []; - items?.forEach((item) => { // there is a date on every publish parts - dates.push(item.substring(item.indexOf("(") + 1, item.indexOf(")"))); - }); - const hrefs: string[] = []; - cheerio - .find("i") - .parent() - .next() - .find("a") - .each(function () { - hrefs.push(this["attribs"]["href"]); - }); - if (dates.length == hrefs.length) - dates.forEach((date, index) => { - let releaseDate = date; - try { - releaseDate = this.parseDate(date); - } catch (e) {} - const chapter: Plugin.ChapterItem = { - name: newChapterName + " (" + (index + 1) + ")", - releaseTime: releaseDate, - path: hrefs[index].replace(site, ""), - chapterNumber: chapterIndex + (index + 1) / 1000, - }; - novelChapters.push(chapter); - } - ); - } - }); - }); - - novel.chapters = novelChapters; - - return novel; - }; - - async parseChapter(chapterPath: string): Promise { - const $ = await this.getCheerio(this.site + chapterPath); - if (!$) - throw new Error("Failed to load page (open in web view)"); - - $(".mistape_caption").remove(); - let chapterText = $(".chapter-content").html() - || $(".entry-content").html() - || ""; - return chapterText; - }; - - async searchNovels(searchTerm: string, num: number): Promise { - if (num !== 1) return ([]); // only 1 page of results - let url = this.site + "roman"; - const $ = await this.getCheerio(url); - if (!$) - throw new Error("Failed to load page (open in web view)"); - - let novels = this.parseNovels($, false); - - novels = novels.filter((novel) => - novel.name - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .trim() - .includes( - searchTerm - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .trim() - ) - ); - - return novels; - }; - - fetchImage = fetchFile; - - filters = { - categorie_genre: { - type: FilterTypes.Picker, - label: "Catégorie/Genre", - value: "all", - options: [ - { label: "Tous", value: "all" }, - { label: "═══CATÉGORIES═══", value: "categorie_roman" }, - { label: "Seinen", value: "c_seinen" }, - { label: "Shonen", value: "c_shonen" }, - { label: "Original", value: "c_original" }, - { label: "Yuri", value: "c_yuri" }, - { label: "Autre", value: "c_autre" }, - { label: "Fille", value: "c_fille" }, - { label: "Roman pour Adulte", value: "c_roman-pour-adulte" }, - { label: "Xuanhuan", value: "c_xuanhuan" }, - { label: "Yaoi", value: "c_yaoi" }, - { label: "═══GENRES═══", value: "genre" }, - { label: "Action", value: "g_action" }, - { label: "Aventure", value: "g_aventure" }, - { label: "Comédie", value: "g_comedie" }, - { label: "Drame", value: "g_drame" }, - { label: "Fantastique", value: "g_fantastique" }, - { label: "Harem", value: "g_harem" }, - { label: "Psychologique", value: "g_psychologique" }, - { label: "Romance", value: "g_romance" }, - { label: "Ecchi", value: "g_ecchi" }, - { label: "Mature", value: "g_mature" }, - { label: "Surnaturel", value: "g_surnaturel" }, - { label: "Vie scolaire", value: "g_vie-scolaire" }, - { label: "Adulte", value: "g_adulte" }, - { label: "Tragédie", value: "g_tragedie" }, - { label: "Arts Martiaux", value: "g_arts-martiaux" }, - { label: "Pas de harem", value: "g_pas-de-harem" }, - { label: "Tranche de vie", value: "g_tranche-de-vie" }, - { label: "Mecha", value: "g_mecha" }, - { label: "Sci-fi", value: "g_sci-fi" }, - { label: "Science-Fiction", value: "g_science-fiction" }, - { label: "Anti-Héros", value: "g_anti-heros" }, - { label: "Horreur", value: "g_horreur" }, - { label: "Insectes", value: "g_insectes" }, - { label: "Mystère", value: "g_mystere" }, - { label: "Lolicon", value: "g_lolicon" }, - { label: "Shoujo Ai", value: "g_shoujo-ai" }, - { label: "Smut", value: "g_smut" }, - { label: "Xuanhuan", value: "g_xuanhuan" }, - { label: "Shotacon", value: "g_shotacon" }, - ], - }, - } satisfies Filters; + filters = { + categorie_genre: { + type: FilterTypes.Picker, + label: 'Catégorie/Genre', + value: 'all', + options: [ + { label: 'Tous', value: 'all' }, + { label: '═══CATÉGORIES═══', value: 'categorie_roman' }, + { label: 'Seinen', value: 'c_seinen' }, + { label: 'Shonen', value: 'c_shonen' }, + { label: 'Original', value: 'c_original' }, + { label: 'Yuri', value: 'c_yuri' }, + { label: 'Autre', value: 'c_autre' }, + { label: 'Fille', value: 'c_fille' }, + { label: 'Roman pour Adulte', value: 'c_roman-pour-adulte' }, + { label: 'Xuanhuan', value: 'c_xuanhuan' }, + { label: 'Yaoi', value: 'c_yaoi' }, + { label: '═══GENRES═══', value: 'genre' }, + { label: 'Action', value: 'g_action' }, + { label: 'Aventure', value: 'g_aventure' }, + { label: 'Comédie', value: 'g_comedie' }, + { label: 'Drame', value: 'g_drame' }, + { label: 'Fantastique', value: 'g_fantastique' }, + { label: 'Harem', value: 'g_harem' }, + { label: 'Psychologique', value: 'g_psychologique' }, + { label: 'Romance', value: 'g_romance' }, + { label: 'Ecchi', value: 'g_ecchi' }, + { label: 'Mature', value: 'g_mature' }, + { label: 'Surnaturel', value: 'g_surnaturel' }, + { label: 'Vie scolaire', value: 'g_vie-scolaire' }, + { label: 'Adulte', value: 'g_adulte' }, + { label: 'Tragédie', value: 'g_tragedie' }, + { label: 'Arts Martiaux', value: 'g_arts-martiaux' }, + { label: 'Pas de harem', value: 'g_pas-de-harem' }, + { label: 'Tranche de vie', value: 'g_tranche-de-vie' }, + { label: 'Mecha', value: 'g_mecha' }, + { label: 'Sci-fi', value: 'g_sci-fi' }, + { label: 'Science-Fiction', value: 'g_science-fiction' }, + { label: 'Anti-Héros', value: 'g_anti-heros' }, + { label: 'Horreur', value: 'g_horreur' }, + { label: 'Insectes', value: 'g_insectes' }, + { label: 'Mystère', value: 'g_mystere' }, + { label: 'Lolicon', value: 'g_lolicon' }, + { label: 'Shoujo Ai', value: 'g_shoujo-ai' }, + { label: 'Smut', value: 'g_smut' }, + { label: 'Xuanhuan', value: 'g_xuanhuan' }, + { label: 'Shotacon', value: 'g_shotacon' }, + ], + }, + } satisfies Filters; } -export default new NovelDeGlacePlugin(); \ No newline at end of file +export default new NovelDeGlacePlugin(); diff --git a/plugins/indonesian/indowebnovel.ts b/plugins/indonesian/indowebnovel.ts index cdd87aa..3ac435a 100644 --- a/plugins/indonesian/indowebnovel.ts +++ b/plugins/indonesian/indowebnovel.ts @@ -1,214 +1,219 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; class IndoWebNovel implements Plugin.PluginBase { - id = "IDWN.id"; - name = "IndoWebNovel"; - icon = "src/id/indowebnovel/icon.png"; - site = "https://indowebnovel.id/"; - version = "1.2.0"; + id = 'IDWN.id'; + name = 'IndoWebNovel'; + icon = 'src/id/indowebnovel/icon.png'; + site = 'https://indowebnovel.id/'; + version = '1.2.0'; - parseNovels(loadedCheerio:CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".flexbox2-item").each((i,el) => { - const novelName = loadedCheerio(el) - .find(".flexbox2-title span") - .first() - .text(); - const novelCover = loadedCheerio(el).find("img").attr("src"); - const novelUrl = loadedCheerio(el) - .find(".flexbox2-content > a") - .attr("href"); + loadedCheerio('.flexbox2-item').each((i, el) => { + const novelName = loadedCheerio(el) + .find('.flexbox2-title span') + .first() + .text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + const novelUrl = loadedCheerio(el) + .find('.flexbox2-content > a') + .attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - novels.push({ - name: novelName, - cover: novelCover, - path: novelUrl.slice(this.site.length), - }); - }); + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.slice(this.site.length), + }); + }); - return novels; - } + return novels; + } - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}advanced-search/page/${page}/?title=&author=&yearx=`; - link += `&status=${filters.status.value}` - link += `&type=${filters.type.value}`; - link += `&order=${filters.sort.value}`; + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}advanced-search/page/${page}/?title=&author=&yearx=`; + link += `&status=${filters.status.value}`; + link += `&type=${filters.type.value}`; + link += `&order=${filters.sort.value}`; - if (filters.lang.value.length) - link += filters.lang.value.map((i) => `&country[]=${i}`).join(""); - if (filters.genre.value.length) - link += filters.genre.value.map((i) => `&genre[]=${i}`).join(""); + if (filters.lang.value.length) + link += filters.lang.value.map(i => `&country[]=${i}`).join(''); + if (filters.genre.value.length) + link += filters.genre.value.map(i => `&genre[]=${i}`).join(''); - const result = await fetchApi(link); - const body = await result.text(); + const result = await fetchApi(link); + const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio) - } + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); - const loadedCheerio = parseHTML(body); - loadedCheerio(".series-synops div").remove(); + const loadedCheerio = parseHTML(body); + loadedCheerio('.series-synops div').remove(); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".series-title").text().trim() || "Untitled", - cover: loadedCheerio(".series-thumb img").attr("src"), - author: loadedCheerio("ul.series-infolist b:contains('Author') +").text().trim(), - status: loadedCheerio(".status").text().trim(), - summary: loadedCheerio(".series-synops").text().trim(), - chapters: [], - }; + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.series-title').text().trim() || 'Untitled', + cover: loadedCheerio('.series-thumb img').attr('src'), + author: loadedCheerio("ul.series-infolist b:contains('Author') +") + .text() + .trim(), + status: loadedCheerio('.status').text().trim(), + summary: loadedCheerio('.series-synops').text().trim(), + chapters: [], + }; - novel.genres = loadedCheerio(".series-genres a") - .map((i,el) => loadedCheerio(el).text().trim()) - .toArray() - .join(','); + novel.genres = loadedCheerio('.series-genres a') + .map((i, el) => loadedCheerio(el).text().trim()) + .toArray() + .join(','); - const chapters: Plugin.ChapterItem[] = []; + const chapters: Plugin.ChapterItem[] = []; - loadedCheerio(".series-chapterlist li").each((i,el) => { - const chapterName = loadedCheerio(el).find("a").text().trim(); - const chapterUrl = loadedCheerio(el).find("a").attr("href"); + loadedCheerio('.series-chapterlist li').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text().trim(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); - if (!chapterUrl) return; + if (!chapterUrl) return; - chapters.push({ - name: chapterName, - path: chapterUrl.slice(this.site.length), - }); - }); + chapters.push({ + name: chapterName, + path: chapterUrl.slice(this.site.length), + }); + }); - novel.chapters = chapters.reverse(); + novel.chapters = chapters.reverse(); - return novel; - } + return novel; + } - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - const chapterText = loadedCheerio(".readerss").html() || ''; + const chapterText = loadedCheerio('.readerss').html() || ''; - return chapterText; - } + return chapterText; + } - async searchNovels(searchTerm: string, page: number): Promise { - let link = `${this.site}advanced-search/page/${page}/?title=${searchTerm}&author=&yearx=`; - link += `&status=${this.filters.status.value}` - link += `&type=${this.filters.type.value}`; - link += `&order=${this.filters.sort.value}`; - link += this.filters.lang.value.map((i) => `&country[]=${i}`).join(""); + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + let link = `${this.site}advanced-search/page/${page}/?title=${searchTerm}&author=&yearx=`; + link += `&status=${this.filters.status.value}`; + link += `&type=${this.filters.type.value}`; + link += `&order=${this.filters.sort.value}`; + link += this.filters.lang.value.map(i => `&country[]=${i}`).join(''); - const result = await fetchApi(link); - const body = await result.text(); + const result = await fetchApi(link); + const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio) - } + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + async fetchImage(url: string): Promise { + return fetchFile(url); + } - filters = { - status: { - value: "", - label: "Status", - options: [ - { label: "All", value: "" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, - }, - type: { - value: "", - label: "Type", - options: [ - { label: "All", value: "" }, - { label: "Web Novel", value: "Web+Novel" }, - { label: "Light Novel", value: "Light+Novel" }, - ], - type: FilterTypes.Picker, - }, - sort: { - value: "rating", - label: "Order By", - options: [ - { label: "A-Z", value: "title" }, - { label: "Z-A", value: "titlereverse" }, - { label: "Latest Update", value: "update" }, - { label: "Latest Added", value: "latest" }, - { label: "Popular", value: "popular" }, - { label: "Rating", value: "rating" }, - ], - type: FilterTypes.Picker, - }, - lang: { - value: ['china','jepang','korea','unknown'], - label: "Country", - options: [ - { label: "China", value: "china" }, - { label: "Jepang", value: "jepang" }, - { label: "Korea", value: "korea" }, - { label: "Unknown", value: "unknown" }, - ], - type: FilterTypes.CheckboxGroup, - }, - genre: { - value: [], - label: "Genres", - options: [ - { label: "Action", value: "action" }, - { label: "Adult", value: "adult" }, - { label: "Adventure", value: "adventure" }, - { label: "Comedy", value: "comedy" }, - { label: "Drama", value: "drama" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Gender Bender", value: "gender-bender" }, - { label: "Harem", value: "harem" }, - { label: "Horror", value: "horror" }, - { label: "Josei", value: "josei" }, - { label: "Josei", value: "josei" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Mecha", value: "mecha" }, - { label: "Mystery", value: "mystery" }, - { label: "Psychological", value: "psychological" }, - { label: "Romance", value: "romance" }, - { label: "School Life", value: "school-life" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Seinen", value: "seinen" }, - { label: "Shoujo", value: "shoujo" }, - { label: "Shounen", value: "shounen" }, - { label: "Slice of Life", value: "slice-of-life" }, - { label: "Smut", value: "smut" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Xianxia", value: "xianxia" }, - { label: "Xuanhuan", value: "xuanhuan" } - ], - type: FilterTypes.CheckboxGroup, - }, - } satisfies Filters; + filters = { + status: { + value: '', + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + ], + type: FilterTypes.Picker, + }, + type: { + value: '', + label: 'Type', + options: [ + { label: 'All', value: '' }, + { label: 'Web Novel', value: 'Web+Novel' }, + { label: 'Light Novel', value: 'Light+Novel' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'rating', + label: 'Order By', + options: [ + { label: 'A-Z', value: 'title' }, + { label: 'Z-A', value: 'titlereverse' }, + { label: 'Latest Update', value: 'update' }, + { label: 'Latest Added', value: 'latest' }, + { label: 'Popular', value: 'popular' }, + { label: 'Rating', value: 'rating' }, + ], + type: FilterTypes.Picker, + }, + lang: { + value: ['china', 'jepang', 'korea', 'unknown'], + label: 'Country', + options: [ + { label: 'China', value: 'china' }, + { label: 'Jepang', value: 'jepang' }, + { label: 'Korea', value: 'korea' }, + { label: 'Unknown', value: 'unknown' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genre: { + value: [], + label: 'Genres', + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Horror', value: 'horror' }, + { label: 'Josei', value: 'josei' }, + { label: 'Josei', value: 'josei' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Romance', value: 'romance' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Smut', value: 'smut' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; } export default new IndoWebNovel(); diff --git a/plugins/indonesian/novelringan.ts b/plugins/indonesian/novelringan.ts index 027aa25..1dfd788 100644 --- a/plugins/indonesian/novelringan.ts +++ b/plugins/indonesian/novelringan.ts @@ -1,261 +1,260 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; class NovelRingan implements Plugin.PluginBase { - id = "novelringan.com"; - name = "NovelRingan"; - icon = "src/id/novelringan/icon.png"; - site = "https://novelringan.com/"; - version = "1.0.0"; - coverUriPrefix = "https://i0.wp.com/novelringan.com/wp-content/uploads/"; + id = 'novelringan.com'; + name = 'NovelRingan'; + icon = 'src/id/novelringan/icon.png'; + site = 'https://novelringan.com/'; + version = '1.0.0'; + coverUriPrefix = 'https://i0.wp.com/novelringan.com/wp-content/uploads/'; - parseNovels(loadedCheerio:CheerioAPI) { - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio("article.post").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find(".entry-title") - .text() - ?.trim(); - const novelCover = - this.coverUriPrefix + loadedCheerio(ele).find("img").attr("data-sxrx"); - const novelUrl = loadedCheerio(ele).find("h2 > a").attr("href"); + loadedCheerio('article.post').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.entry-title').text()?.trim(); + const novelCover = + this.coverUriPrefix + loadedCheerio(ele).find('img').attr('data-sxrx'); + const novelUrl = loadedCheerio(ele).find('h2 > a').attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; - novels.push(novel); - }); + novels.push(novel); + }); - return novels; - } + return novels; + } - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}advanced-search/page/${page}/?title`; - link += `&status=${filters.status.value}` - link += `&order=${filters.sort.value}`; + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}advanced-search/page/${page}/?title`; + link += `&status=${filters.status.value}`; + link += `&order=${filters.sort.value}`; - if (filters.type.value.length) - link += filters.type.value.map((i) => `&type[]=${i}`).join(""); - if (filters.genre.value.length) - link += filters.genre.value.map((i) => `&genre[]=${i}`).join(""); + if (filters.type.value.length) + link += filters.type.value.map(i => `&type[]=${i}`).join(''); + if (filters.genre.value.length) + link += filters.genre.value.map(i => `&genre[]=${i}`).join(''); + const result = await fetchApi(link); + const body = await result.text(); - const result = await fetchApi(link); - const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + const loadedCheerio = parseHTML(body); - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + const styletag = Array.from( + loadedCheerio('meta[name=msapplication-TileImage] + style') + .html() + ?.matchAll(/"(.*?)"/g) || [], + m => m[1], + ); - const loadedCheerio = parseHTML(body); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: styletag[0] || loadedCheerio('.entry-title').text() || 'Untitled', + author: styletag[1], + summary: loadedCheerio('.maininfo span p').text(), + chapters: [], + }; - const styletag = Array.from( - ( - loadedCheerio('meta[name=msapplication-TileImage] + style') - .html() - ?.matchAll(/"(.*?)"/g) - ) || [], - m => m[1] - ); + novel.cover = + this.coverUriPrefix + + loadedCheerio('img.ts-post-image').attr('data-sxrx'); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: styletag[0] || loadedCheerio('.entry-title').text() || "Untitled", - author: styletag[1], - summary: loadedCheerio('.maininfo span p').text(), - chapters: [], - }; + loadedCheerio('.maininfo li').each(function () { + let detailName = loadedCheerio(this).find('b').text().trim(); + let detail = loadedCheerio(this).find('b').remove().end().text().trim(); - novel.cover = - this.coverUriPrefix + - loadedCheerio("img.ts-post-image").attr("data-sxrx"); + switch (detailName) { + case 'Status:': + novel.status = detail; + break; + case 'Genre:': + novel.genres = detail; + break; + } + }); - loadedCheerio('.maininfo li').each(function() { - let detailName = loadedCheerio(this).find('b').text().trim(); - let detail = loadedCheerio(this).find('b').remove().end().text().trim() - - switch(detailName) { - case "Status:": - novel.status = detail; - break; - case "Genre:": - novel.genres = detail; - break; - } - }) + const chapter: Plugin.ChapterItem[] = []; - const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('.bxcl > ul > li').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); - loadedCheerio(".bxcl > ul > li").each((i,el) => { - const chapterName = loadedCheerio(el).find("a").text(); - const chapterUrl = loadedCheerio(el).find("a").attr("href"); + if (!chapterUrl) return; - if (!chapterUrl) return; + chapter.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); - chapter.push({ - name: chapterName, - path: chapterUrl.replace(this.site, ''), - }); - }); + novel.chapters = chapter.reverse(); - novel.chapters = chapter.reverse(); + return novel; + } - return novel; - } + async parseChapter(chapterPath: string): Promise { + const url = this.site + chapterPath; - async parseChapter(chapterPath: string): Promise { - const url = this.site + chapterPath; + const result = await fetchApi(url); + const body = await result.text(); - const result = await fetchApi(url); - const body = await result.text(); + const loadedCheerio = parseHTML(body); - const loadedCheerio = parseHTML(body); + loadedCheerio('.entry-content div[style="display:none"]').remove(); - loadedCheerio('.entry-content div[style="display:none"]').remove(); + const chapterText = loadedCheerio('.entry-content').html() || ''; - const chapterText = loadedCheerio(".entry-content").html() || ''; + return chapterText; + } - return chapterText; - } + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const url = this.site + 'page/' + page + '/?s=' + searchTerm; - async searchNovels(searchTerm: string, page: number): Promise { - const url = this.site + "page/" + page + "/?s=" + searchTerm; + const result = await fetchApi(url); + const body = await result.text(); - const result = await fetchApi(url); - const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio) - } + async fetchImage(url: string): Promise { + return fetchFile(url); + } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - - filters = { - status: { - value: "", - label: "Status", - options: [ - { label: "All", value: "" }, - { label: "Ongoing", value: "Ongoing" }, - { label: "Completed", value: "Completed" }, - ], - type: FilterTypes.Picker, + filters = { + status: { + value: '', + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Ongoing', value: 'Ongoing' }, + { label: 'Completed', value: 'Completed' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'popular', + label: 'Urutkan', + options: [ + { label: 'A-Z', value: 'title' }, + { label: 'Z-A', value: 'titlereverse' }, + { label: 'Terbarui', value: 'update' }, + { label: 'Ditambahkan', value: 'latest' }, + { label: 'Terpopuler', value: 'popular' }, + ], + type: FilterTypes.Picker, + }, + type: { + value: [], + label: 'Tipe', + options: [ + { label: 'Chinese Novel', value: 'chinese-novel' }, + { label: 'Chinese Web Novel', value: 'chinese-web-novel' }, + { label: 'Filipino Novel', value: 'filipino-novel' }, + { label: 'Indonesia Novel', value: 'indonesia-novel' }, + { label: 'Korean Novel', value: 'korean-novel' }, + { label: 'Light Novel', value: 'light-novel' }, + { label: 'Light Novel (CN)', value: 'light-novel-cn' }, + { label: 'Light Novel (JP)', value: 'light-novel-jp' }, + { label: 'Light Novel (KR)', value: 'light-novel-kr' }, + { label: 'Malaysian Novel', value: 'malaysian-novel' }, + { label: 'Published Novel (CN)', value: 'published-novel-cn' }, + { label: 'Published Novel (JP)', value: 'published-novel-jp' }, + { label: 'Published Novel (KR)', value: 'published-novel-kr' }, + { label: 'Published Novel (TH)', value: 'published-novel-th' }, + { label: 'Thai Novel', value: 'thai-novel' }, + { label: 'Vietnamese Novel', value: 'vietnamese-novel' }, + { label: 'Web Novel', value: 'web-novel' }, + { label: 'Webnovel', value: 'webnovel' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genre: { + value: [], + label: 'Genres', + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Celebrity', value: 'celebrity' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'ction', value: 'ction' }, + { label: 'Drama', value: 'drama' }, + { label: 'Eastern', value: 'eastern' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Game', value: 'game' }, + { label: 'Games', value: 'games' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Isekai', value: 'isekai' }, + { label: 'Josei', value: 'josei' }, + { label: 'Life', value: 'life' }, + { label: 'LitRPG', value: 'litrpg' }, + { label: 'Magical Realism', value: 'magical-realism' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychologic', value: 'psychologic' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Recarnation', value: 'recarnation' }, + { label: 'Reincarnation', value: 'reincarnation' }, + { label: 'Romance', value: 'romance' }, + { label: 'School', value: 'school' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shotacon', value: 'shotacon' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shoujo Ai', value: 'shoujo-ai' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Shounen Ai', value: 'shounen-ai' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Smut', value: 'smut' }, + { label: 'Sports', value: 'sports' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Urban', value: 'urban' }, + { label: 'Urban Life', value: 'urban-life' }, + { + label: 've names:N/A Genre:Romance', + value: 've-namesn-a-genreromance', }, - sort: { - value: "popular", - label: "Urutkan", - options: [ - { label: "A-Z", value: "title" }, - { label: "Z-A", value: "titlereverse" }, - { label: "Terbarui", value: "update" }, - { label: "Ditambahkan", value: "latest" }, - { label: "Terpopuler", value: "popular" }, - ], - type: FilterTypes.Picker, - }, - type: { - value: [], - label: "Tipe", - options: [ - { label: "Chinese Novel", value: "chinese-novel" }, - { label: "Chinese Web Novel", value: "chinese-web-novel" }, - { label: "Filipino Novel", value: "filipino-novel" }, - { label: "Indonesia Novel", value: "indonesia-novel" }, - { label: "Korean Novel", value: "korean-novel" }, - { label: "Light Novel", value: "light-novel" }, - { label: "Light Novel (CN)", value: "light-novel-cn" }, - { label: "Light Novel (JP)", value: "light-novel-jp" }, - { label: "Light Novel (KR)", value: "light-novel-kr" }, - { label: "Malaysian Novel", value: "malaysian-novel" }, - { label: "Published Novel (CN)", value: "published-novel-cn" }, - { label: "Published Novel (JP)", value: "published-novel-jp" }, - { label: "Published Novel (KR)", value: "published-novel-kr" }, - { label: "Published Novel (TH)", value: "published-novel-th" }, - { label: "Thai Novel", value: "thai-novel" }, - { label: "Vietnamese Novel", value: "vietnamese-novel" }, - { label: "Web Novel", value: "web-novel" }, - { label: "Webnovel", value: "webnovel" }, - ], - type: FilterTypes.CheckboxGroup, - }, - genre: { - value: [], - label: "Genres", - options: [ - { label: "Action", value: "action" }, - { label: "Adult", value: "adult" }, - { label: "Adventure", value: "adventure" }, - { label: "Celebrity", value: "celebrity" }, - { label: "Comedy", value: "comedy" }, - { label: "ction", value: "ction" }, - { label: "Drama", value: "drama" }, - { label: "Eastern", value: "eastern" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Game", value: "game" }, - { label: "Games", value: "games" }, - { label: "Gender Bender", value: "gender-bender" }, - { label: "Harem", value: "harem" }, - { label: "Historical", value: "historical" }, - { label: "Horror", value: "horror" }, - { label: "Isekai", value: "isekai" }, - { label: "Josei", value: "josei" }, - { label: "Life", value: "life" }, - { label: "LitRPG", value: "litrpg" }, - { label: "Magical Realism", value: "magical-realism" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Mecha", value: "mecha" }, - { label: "Mystery", value: "mystery" }, - { label: "Psychologic", value: "psychologic" }, - { label: "Psychological", value: "psychological" }, - { label: "Recarnation", value: "recarnation" }, - { label: "Reincarnation", value: "reincarnation" }, - { label: "Romance", value: "romance" }, - { label: "School", value: "school" }, - { label: "School Life", value: "school-life" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Seinen", value: "seinen" }, - { label: "Shotacon", value: "shotacon" }, - { label: "Shoujo", value: "shoujo" }, - { label: "Shoujo Ai", value: "shoujo-ai" }, - { label: "Shounen", value: "shounen" }, - { label: "Shounen Ai", value: "shounen-ai" }, - { label: "Slice of Life", value: "slice-of-life" }, - { label: "Smut", value: "smut" }, - { label: "Sports", value: "sports" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Urban", value: "urban" }, - { label: "Urban Life", value: "urban-life" }, - { label: "ve names:N/A Genre:Romance", value: "ve-namesn-a-genreromance" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Xianxia", value: "xianxia" }, - { label: "Xuanhuan", value: "xuanhuan" }, - { label: "Yaoi", value: "yaoi" }, - { label: "Yuri", value: "yuri" }, - ], - type: FilterTypes.CheckboxGroup, - }, - } satisfies Filters; + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; } export default new NovelRingan(); diff --git a/plugins/indonesian/sakuranovel.ts b/plugins/indonesian/sakuranovel.ts index f6579f4..8abb94e 100644 --- a/plugins/indonesian/sakuranovel.ts +++ b/plugins/indonesian/sakuranovel.ts @@ -1,233 +1,251 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters, FilterTypes } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; class SakuraNovel implements Plugin.PluginBase { - id = "sakura.id"; - name = "SakuraNovel"; - icon = "src/id/sakuranovel/icon.png"; - site = "https://sakuranovel.id/"; - version = "1.0.1"; + id = 'sakura.id'; + name = 'SakuraNovel'; + icon = 'src/id/sakuranovel/icon.png'; + site = 'https://sakuranovel.id/'; + version = '1.0.1'; - parseNovels(loadedCheerio:CheerioAPI){ - const novels: Plugin.NovelItem[] = []; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; - loadedCheerio(".flexbox2-item").each((i,el) => { - const novelName = loadedCheerio(el) - .find(".flexbox2-title span") - .first() - .text(); - const novelCover = loadedCheerio(el).find("img").attr("src"); - const novelUrl = loadedCheerio(el) - .find(".flexbox2-content > a") - .attr("href"); + loadedCheerio('.flexbox2-item').each((i, el) => { + const novelName = loadedCheerio(el) + .find('.flexbox2-title span') + .first() + .text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + const novelUrl = loadedCheerio(el) + .find('.flexbox2-content > a') + .attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - novels.push({ - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }); - }); + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + }); - return novels; - } + return novels; + } - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}advanced-search/page/${page}/?title&author&yearx`; - link += `&status=${filters.status.value}` - link += `&type=${filters.type.value}`; - link += `&order=${filters.sort.value}`; + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}advanced-search/page/${page}/?title&author&yearx`; + link += `&status=${filters.status.value}`; + link += `&type=${filters.type.value}`; + link += `&order=${filters.sort.value}`; - if (filters.lang.value.length) - link += filters.lang.value.map((i) => `&country[]=${i}`).join(""); - if (filters.genre.value.length) - link += filters.genre.value.map((i) => `&genre[]=${i}`).join(""); + if (filters.lang.value.length) + link += filters.lang.value.map(i => `&country[]=${i}`).join(''); + if (filters.genre.value.length) + link += filters.genre.value.map(i => `&genre[]=${i}`).join(''); - const result = await fetchApi(link); - const body = await result.text(); + const result = await fetchApi(link); + const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath); - const body = await result.text(); + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); - const loadedCheerio = parseHTML(body); - loadedCheerio(".series-synops div").remove(); + const loadedCheerio = parseHTML(body); + loadedCheerio('.series-synops div').remove(); - const novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".series-title h2").text().trim() || "Untitled", - cover: loadedCheerio(".series-thumb img").attr("src"), - author: loadedCheerio(".series-infolist > li b:contains('Author') +").text().trim(), - status: loadedCheerio(".status").text().trim(), - summary: loadedCheerio(".series-synops").text().trim(), - chapters: [] - }; + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.series-title h2').text().trim() || 'Untitled', + cover: loadedCheerio('.series-thumb img').attr('src'), + author: loadedCheerio(".series-infolist > li b:contains('Author') +") + .text() + .trim(), + status: loadedCheerio('.status').text().trim(), + summary: loadedCheerio('.series-synops').text().trim(), + chapters: [], + }; - novel.genres = loadedCheerio(".series-genres") - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); + novel.genres = loadedCheerio('.series-genres') + .children('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); - const imageTitle = novel.cover?.split('/').pop()?.split('-').join(' ').split('.')[0]; - const chapterTitle = novel.name.replace(/\(LN\)|\(WN\)/, "").split(',')[0].trim(); - const chapter: Plugin.ChapterItem[] = []; + const imageTitle = novel.cover + ?.split('/') + .pop() + ?.split('-') + .join(' ') + .split('.')[0]; + const chapterTitle = novel.name + .replace(/\(LN\)|\(WN\)/, '') + .split(',')[0] + .trim(); + const chapter: Plugin.ChapterItem[] = []; - loadedCheerio(".series-flexright li").each((i,el) => { - const chapterName = loadedCheerio(el) - .find("a span") - .first() - .text() - .replace(chapterTitle, '') - .replace(imageTitle!, '') - .replace(/Bahasa Indonesia/, '') - .replace(/\s+/g, " ") - .trim(); + loadedCheerio('.series-flexright li').each((i, el) => { + const chapterName = loadedCheerio(el) + .find('a span') + .first() + .text() + .replace(chapterTitle, '') + .replace(imageTitle!, '') + .replace(/Bahasa Indonesia/, '') + .replace(/\s+/g, ' ') + .trim(); - const releaseDate = loadedCheerio(el) - .find(".date") - .text() - .trim() - .split('/') - .map(x => Number(x)); + const releaseDate = loadedCheerio(el) + .find('.date') + .text() + .trim() + .split('/') + .map(x => Number(x)); - const chapterUrl = loadedCheerio(el).find("a").attr("href"); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); - if (!chapterUrl) return; + if (!chapterUrl) return; - chapter.push({ - name: chapterName, - releaseTime: new Date(releaseDate[2], releaseDate[1], releaseDate[0]).toISOString(), - path: chapterUrl.replace(this.site, ''), - }); - }); + chapter.push({ + name: chapterName, + releaseTime: new Date( + releaseDate[2], + releaseDate[1], + releaseDate[0], + ).toISOString(), + path: chapterUrl.replace(this.site, ''), + }); + }); - novel.chapters = chapter.reverse(); + novel.chapters = chapter.reverse(); - return novel; - } + return novel; + } - async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath); - const body = await result.text(); + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - const divi = loadedCheerio("div:contains('Daftar Isi') +") - .find("div:first") - .attr("class"); - loadedCheerio(`.${divi}`).remove() - const chapterText = loadedCheerio("div:contains('Daftar Isi') +").html() || ''; + const divi = loadedCheerio("div:contains('Daftar Isi') +") + .find('div:first') + .attr('class'); + loadedCheerio(`.${divi}`).remove(); + const chapterText = + loadedCheerio("div:contains('Daftar Isi') +").html() || ''; - return chapterText; - } + return chapterText; + } - async searchNovels(searchTerm: string, page: number): Promise { - const url = `${this.site}page/${page}/?s=${searchTerm}`; - const result = await fetchApi(url); - const body = await result.text(); + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const url = `${this.site}page/${page}/?s=${searchTerm}`; + const result = await fetchApi(url); + const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + async fetchImage(url: string): Promise { + return fetchFile(url); + } - filters = { - status: { - value: "", - label: "Status", - options: [ - { label: "All", value: "" }, - { label: "Ongoing", value: "ongoing" }, - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, - }, - type: { - value: "", - label: "Type", - options: [ - { label: "All", value: "" }, - { label: "Web Novel", value: "Web+Novel" }, - { label: "Light Novel", value: "Light+Novel" }, - ], - type: FilterTypes.Picker, - }, - sort: { - value: "rating", - label: "Order By", - options: [ - { label: "A-Z", value: "title" }, - { label: "Z-A", value: "titlereverse" }, - { label: "Latest Update", value: "update" }, - { label: "Latest Added", value: "latest" }, - { label: "Popular", value: "popular" }, - { label: "Rating", value: "rating" }, - ], - type: FilterTypes.Picker, - }, - lang: { - value: ['china','jepang','korea','unknown'], - label: "Country", - options: [ - { label: "China", value: "china" }, - { label: "Jepang", value: "jepang" }, - { label: "Korea", value: "korea" }, - { label: "Unknown", value: "unknown" }, - ], - type: FilterTypes.CheckboxGroup, - }, - genre: { - value: [], - label: "Genres", - options: [ - { label: "Action", value: "action" }, - { label: "Adult", value: "adult" }, - { label: "Adventure", value: "adventure" }, - { label: "Comedy", value: "comedy" }, - { label: "Drama", value: "drama" }, - { label: "Ecchi", value: "ecchi" }, - { label: "Fantasy", value: "fantasy" }, - { label: "Gender Bender", value: "gender-bender" }, - { label: "Harem", value: "harem" }, - { label: "Horror", value: "horror" }, - { label: "Josei", value: "josei" }, - { label: "Josei", value: "josei" }, - { label: "Martial Arts", value: "martial-arts" }, - { label: "Mature", value: "mature" }, - { label: "Mecha", value: "mecha" }, - { label: "Mystery", value: "mystery" }, - { label: "Psychological", value: "psychological" }, - { label: "Romance", value: "romance" }, - { label: "School Life", value: "school-life" }, - { label: "Sci-fi", value: "sci-fi" }, - { label: "Seinen", value: "seinen" }, - { label: "Shoujo", value: "shoujo" }, - { label: "Shounen", value: "shounen" }, - { label: "Slice of Life", value: "slice-of-life" }, - { label: "Smut", value: "smut" }, - { label: "Supernatural", value: "supernatural" }, - { label: "Tragedy", value: "tragedy" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Xianxia", value: "xianxia" }, - { label: "Xuanhuan", value: "xuanhuan" } - ], - type: FilterTypes.CheckboxGroup, - }, - } satisfies Filters; + filters = { + status: { + value: '', + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + ], + type: FilterTypes.Picker, + }, + type: { + value: '', + label: 'Type', + options: [ + { label: 'All', value: '' }, + { label: 'Web Novel', value: 'Web+Novel' }, + { label: 'Light Novel', value: 'Light+Novel' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'rating', + label: 'Order By', + options: [ + { label: 'A-Z', value: 'title' }, + { label: 'Z-A', value: 'titlereverse' }, + { label: 'Latest Update', value: 'update' }, + { label: 'Latest Added', value: 'latest' }, + { label: 'Popular', value: 'popular' }, + { label: 'Rating', value: 'rating' }, + ], + type: FilterTypes.Picker, + }, + lang: { + value: ['china', 'jepang', 'korea', 'unknown'], + label: 'Country', + options: [ + { label: 'China', value: 'china' }, + { label: 'Jepang', value: 'jepang' }, + { label: 'Korea', value: 'korea' }, + { label: 'Unknown', value: 'unknown' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genre: { + value: [], + label: 'Genres', + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Horror', value: 'horror' }, + { label: 'Josei', value: 'josei' }, + { label: 'Josei', value: 'josei' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Romance', value: 'romance' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Smut', value: 'smut' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; } export default new SakuraNovel(); diff --git a/plugins/japanese/Syosetu.ts b/plugins/japanese/Syosetu.ts index 87b85c7..2a4e5a4 100644 --- a/plugins/japanese/Syosetu.ts +++ b/plugins/japanese/Syosetu.ts @@ -1,292 +1,284 @@ -import { load, load as loadCheerio } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { defaultCover } from "@libs/defaultCover"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { NovelItem } from "../../test_web/static/js"; +import { load, load as loadCheerio } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { NovelItem } from '../../test_web/static/js'; // const novelStatus = require('@libs/novelStatus'); // const isUrlAbsolute = require('@libs/isAbsoluteUrl'); // const parseDate = require('@libs/parseDate'); class Syosetu implements Plugin.PluginBase { - id = "yomou.syosetu"; - name = "Syosetu"; - icon = "src/jp/syosetu/icon.png"; - site = "https://yomou.syosetu.com/"; - novelPrefix = "https://ncode.syosetu.com"; - version = "1.1.0"; - headers = { - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - }; - searchUrl = (pagenum?: number, order?: string) => { - return `https://yomou.syosetu.com/search.php?order=${order || "hyoka"}${ - pagenum !== undefined - ? `&p=${pagenum <= 1 || pagenum > 100 ? "1" : pagenum}` // check if pagenum is between 1 and 100 - : "" // if isn't don't set ?p - }`; - }; - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - const getNovelsFromPage = async (pagenumber: number) => { - // load page - let url = this.site; + id = 'yomou.syosetu'; + name = 'Syosetu'; + icon = 'src/jp/syosetu/icon.png'; + site = 'https://yomou.syosetu.com/'; + novelPrefix = 'https://ncode.syosetu.com'; + version = '1.1.0'; + headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }; + searchUrl = (pagenum?: number, order?: string) => { + return `https://yomou.syosetu.com/search.php?order=${order || 'hyoka'}${ + pagenum !== undefined + ? `&p=${pagenum <= 1 || pagenum > 100 ? '1' : pagenum}` // check if pagenum is between 1 and 100 + : '' // if isn't don't set ?p + }`; + }; + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + const getNovelsFromPage = async (pagenumber: number) => { + // load page + let url = this.site; - if (!filters.genre.value) { - url += `rank/list/type/${filters.ranking.value}_${filters.modifier.value}/?p=${pagenumber}`; - } else { - url += `rank/${ - filters.genre.value.length === 1 - ? "isekailist" - : "genrelist" - }/type/${filters.ranking.value}_${filters.genre.value}${ - filters.modifier.value === "total" - ? "" - : `_${filters.modifier.value}` - }/?p=${pagenumber}`; - } - const html = await (await fetchApi(url)).text(); + if (!filters.genre.value) { + url += `rank/list/type/${filters.ranking.value}_${filters.modifier.value}/?p=${pagenumber}`; + } else { + url += `rank/${ + filters.genre.value.length === 1 ? 'isekailist' : 'genrelist' + }/type/${filters.ranking.value}_${filters.genre.value}${ + filters.modifier.value === 'total' ? '' : `_${filters.modifier.value}` + }/?p=${pagenumber}`; + } + const html = await (await fetchApi(url)).text(); - const loadedCheerio = loadCheerio(html, { - decodeEntities: false, - }); + const loadedCheerio = loadCheerio(html, { + decodeEntities: false, + }); - if ( - parseInt(loadedCheerio(".is-current").html() || "1") !== - pagenumber - ) - return []; + if (parseInt(loadedCheerio('.is-current').html() || '1') !== pagenumber) + return []; - const novels: NovelItem[] = []; - loadedCheerio(".c-card").each((_, e) => { - const anchor = loadedCheerio(e).find( - ".p-ranklist-item__title a" - ); - const url = anchor.attr("href"); - if (!url) return; - const name = anchor.text(); - const novel: NovelItem = { - path: url.replace(this.novelPrefix, ''), - name, - cover: defaultCover, - }; - novels.push(novel); - }); - return novels; + const novels: NovelItem[] = []; + loadedCheerio('.c-card').each((_, e) => { + const anchor = loadedCheerio(e).find('.p-ranklist-item__title a'); + const url = anchor.attr('href'); + if (!url) return; + const name = anchor.text(); + const novel: NovelItem = { + path: url.replace(this.novelPrefix, ''), + name, + cover: defaultCover, }; - const novels = await getNovelsFromPage(pageNo); - return novels; - } - async parseNovel(novelPath: string): Promise { - let chapters: Plugin.ChapterItem[] = []; - const result = await fetch(this.novelPrefix + novelPath, { headers: this.headers }); - const body = await result.text(); - const loadedCheerio = loadCheerio(body, { decodeEntities: false }); + novels.push(novel); + }); + return novels; + }; + const novels = await getNovelsFromPage(pageNo); + return novels; + } + async parseNovel(novelPath: string): Promise { + let chapters: Plugin.ChapterItem[] = []; + const result = await fetch(this.novelPrefix + novelPath, { + headers: this.headers, + }); + const body = await result.text(); + const loadedCheerio = loadCheerio(body, { decodeEntities: false }); - // create novel object - let novel: Plugin.SourceNovel = { - path: novelPath, - name: loadedCheerio(".novel_title").text(), - author: loadedCheerio(".novel_writername") - .text() - .replace("作者:", ""), - cover: defaultCover, - }; + // create novel object + let novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.novel_title').text(), + author: loadedCheerio('.novel_writername').text().replace('作者:', ''), + cover: defaultCover, + }; - // Get all the chapters - const cqGetChapters = loadedCheerio(".novel_sublist2"); - if (cqGetChapters.length !== 0) { - // has more than 1 chapter - novel.summary = loadedCheerio("#novel_ex") - .text() - .replace(/<\s*br.*?>/g, "\n"); - cqGetChapters.each(function (i, e) { - const chapterA = loadedCheerio(this).find("a"); - const [chapterName, releaseDate, chapterUrl] = [ - // set the variables - chapterA.text(), - loadedCheerio(this) - .find("dt") // get title - .text() // get text - .replace(/(.)/g, "") // remove "(edited)" mark - .trim(), // trim spaces - chapterA.attr("href"), - ]; - if(chapterUrl) { - chapters.push({ - name: chapterName, - releaseTime: releaseDate, - path: chapterUrl, - }); - } - }); - } else { - /** - * Because there are oneshots on the site, they have to be treated with special care - * that's what pisses me off in Shosetsu app. They have this extension, - * but every oneshot is set as "there are no chapters" and all contents are thrown into the description!! - */ - // get summary for oneshot chapter - - const nameResult = await fetch( - this.searchUrl() + `&word=${novel.name}`, - { headers: this.headers } - ); - const nameBody = await nameResult.text(); - const summaryQuery = loadCheerio(nameBody, { - decodeEntities: false, - }); - const foundText = summaryQuery(".searchkekka_box") - .first() - .find(".ex") - .text() - .replace(/\s{2,}/g, "\n"); - novel.summary = foundText; - - // add single chapter - chapters.push({ - name: "Oneshot", - releaseTime: loadedCheerio("head") - .find("meta[name='WWWC']") - .attr("content"), // get date from metadata - path: novelPath, // set chapterUrl to oneshot so that chapterScraper knows it's a one-shot, - }); + // Get all the chapters + const cqGetChapters = loadedCheerio('.novel_sublist2'); + if (cqGetChapters.length !== 0) { + // has more than 1 chapter + novel.summary = loadedCheerio('#novel_ex') + .text() + .replace(/<\s*br.*?>/g, '\n'); + cqGetChapters.each(function (i, e) { + const chapterA = loadedCheerio(this).find('a'); + const [chapterName, releaseDate, chapterUrl] = [ + // set the variables + chapterA.text(), + loadedCheerio(this) + .find('dt') // get title + .text() // get text + .replace(/(.)/g, '') // remove "(edited)" mark + .trim(), // trim spaces + chapterA.attr('href'), + ]; + if (chapterUrl) { + chapters.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl, + }); } - novel.chapters = chapters; - return novel; + }); + } else { + /** + * Because there are oneshots on the site, they have to be treated with special care + * that's what pisses me off in Shosetsu app. They have this extension, + * but every oneshot is set as "there are no chapters" and all contents are thrown into the description!! + */ + // get summary for oneshot chapter + + const nameResult = await fetch(this.searchUrl() + `&word=${novel.name}`, { + headers: this.headers, + }); + const nameBody = await nameResult.text(); + const summaryQuery = loadCheerio(nameBody, { + decodeEntities: false, + }); + const foundText = summaryQuery('.searchkekka_box') + .first() + .find('.ex') + .text() + .replace(/\s{2,}/g, '\n'); + novel.summary = foundText; + + // add single chapter + chapters.push({ + name: 'Oneshot', + releaseTime: loadedCheerio('head') + .find("meta[name='WWWC']") + .attr('content'), // get date from metadata + path: novelPath, // set chapterUrl to oneshot so that chapterScraper knows it's a one-shot, + }); } - async parseChapter(chapterPath: string): Promise { - const result = await fetch(this.novelPrefix + chapterPath, { headers: this.headers }); - const body = await result.text(); + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.novelPrefix + chapterPath, { + headers: this.headers, + }); + const body = await result.text(); - // create cheerioQuery - const cheerioQuery = loadCheerio(body, { - decodeEntities: false, - }); + // create cheerioQuery + const cheerioQuery = loadCheerio(body, { + decodeEntities: false, + }); - let chapterText = - cheerioQuery("#novel_honbun") // get chapter text - .html() || ""; - return chapterText; - } - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - let novels = []; + let chapterText = + cheerioQuery('#novel_honbun') // get chapter text + .html() || ''; + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + let novels = []; - // returns list of novels from given page - const getNovelsFromPage = async (pagenumber: number) => { - // load page - const url = this.searchUrl(pagenumber) + `&word=${searchTerm}`; - const result = await fetch(url, { headers: this.headers }); - const body = await result.text(); - // Cheerio it! - const cheerioQuery = loadCheerio(body, { decodeEntities: false }); + // returns list of novels from given page + const getNovelsFromPage = async (pagenumber: number) => { + // load page + const url = this.searchUrl(pagenumber) + `&word=${searchTerm}`; + const result = await fetch(url, { headers: this.headers }); + const body = await result.text(); + // Cheerio it! + const cheerioQuery = loadCheerio(body, { decodeEntities: false }); - let pageNovels: Plugin.NovelItem[] = []; - // find class=searchkekka_box - cheerioQuery(".searchkekka_box").each((i, e) => { - // get div with link and name - const novelDIV = cheerioQuery(e).find(".novel_h"); - // get link element - const novelA = novelDIV.children()[0]; - // add new novel to array - const novelPath = novelA.attribs.href.replace(this.novelPrefix, ''); - if(novelPath){ - pageNovels.push({ - name: novelDIV.text(), // get the name - path: novelPath, // get last part of the link - cover: defaultCover, - }); - } - }); - // return all novels from this page - return pageNovels; - }; + let pageNovels: Plugin.NovelItem[] = []; + // find class=searchkekka_box + cheerioQuery('.searchkekka_box').each((i, e) => { + // get div with link and name + const novelDIV = cheerioQuery(e).find('.novel_h'); + // get link element + const novelA = novelDIV.children()[0]; + // add new novel to array + const novelPath = novelA.attribs.href.replace(this.novelPrefix, ''); + if (novelPath) { + pageNovels.push({ + name: novelDIV.text(), // get the name + path: novelPath, // get last part of the link + cover: defaultCover, + }); + } + }); + // return all novels from this page + return pageNovels; + }; - // counter of loaded pages - // let pagesLoaded = 0; - // do { - // // always load first one - // novels.push(...(await getNovelsFromPage(pagesLoaded + 1))); - // pagesLoaded++; - // } while (pagesLoaded < maxPageLoad && isNext); // check if we should load more + // counter of loaded pages + // let pagesLoaded = 0; + // do { + // // always load first one + // novels.push(...(await getNovelsFromPage(pagesLoaded + 1))); + // pagesLoaded++; + // } while (pagesLoaded < maxPageLoad && isNext); // check if we should load more - novels = await getNovelsFromPage(pageNo); + novels = await getNovelsFromPage(pageNo); - /** Use - * novels.push(...(await getNovelsFromPage(pageNumber))) - * if you want to load more - */ + /** Use + * novels.push(...(await getNovelsFromPage(pageNumber))) + * if you want to load more + */ - // respond with novels! - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - resolveUrl(path: string, isNovel?: boolean | undefined): string { - return this.novelPrefix + path; - } - filters = { - ranking: { - type: FilterTypes.Picker, - label: "Ranked by", - options: [ - { label: "日間", value: "daily" }, - { label: "週間", value: "weekly" }, - { label: "月間", value: "monthly" }, - { label: "四半期", value: "quarter" }, - { label: "年間", value: "yearly" }, - { label: "累計", value: "total" }, - ], - value: "total", - }, - genre: { - type: FilterTypes.Picker, - label: "Ranking Genre", - options: [ - { label: "総ジャンル", value: "" }, - { label: "異世界転生/転移〔恋愛〕〕", value: "1" }, - { label: "異世界転生/転移〔ファンタジー〕", value: "2" }, - { label: "異世界転生/転移〔文芸・SF・その他〕", value: "o" }, - { label: "異世界〔恋愛〕", value: "101" }, - { label: "現実世界〔恋愛〕", value: "102" }, - { label: "ハイファンタジー〔ファンタジー〕", value: "201" }, - { label: "ローファンタジー〔ファンタジー〕", value: "202" }, - { label: "純文学〔文芸〕", value: "301" }, - { label: "ヒューマンドラマ〔文芸〕", value: "302" }, - { label: "歴史〔文芸〕", value: "303" }, - { label: "推理〔文芸〕", value: "304" }, - { label: "ホラー〔文芸〕", value: "305" }, - { label: "アクション〔文芸〕", value: "306" }, - { label: "コメディー〔文芸〕", value: "307" }, - { label: "VRゲーム〔SF〕", value: "401" }, - { label: "宇宙〔SF〕", value: "402" }, - { label: "空想科学〔SF〕", value: "403" }, - { label: "パニック〔SF〕", value: "404" }, - { label: "童話〔その他〕", value: "9901" }, - { label: "詩〔その他〕", value: "9902" }, - { label: "エッセイ〔その他〕", value: "9903" }, - { label: "その他〔その他〕", value: "9999" }, - ], - value: "", - }, - modifier: { - type: FilterTypes.Picker, - label: "Modifier", - options: [ - { label: "すべて", value: "total" }, - { label: "連載中", value: "r" }, - { label: "完結済", value: "er" }, - { label: "短編", value: "t" }, - ], - value: "total", - }, - } satisfies Filters; + // respond with novels! + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } + resolveUrl(path: string, isNovel?: boolean | undefined): string { + return this.novelPrefix + path; + } + filters = { + ranking: { + type: FilterTypes.Picker, + label: 'Ranked by', + options: [ + { label: '日間', value: 'daily' }, + { label: '週間', value: 'weekly' }, + { label: '月間', value: 'monthly' }, + { label: '四半期', value: 'quarter' }, + { label: '年間', value: 'yearly' }, + { label: '累計', value: 'total' }, + ], + value: 'total', + }, + genre: { + type: FilterTypes.Picker, + label: 'Ranking Genre', + options: [ + { label: '総ジャンル', value: '' }, + { label: '異世界転生/転移〔恋愛〕〕', value: '1' }, + { label: '異世界転生/転移〔ファンタジー〕', value: '2' }, + { label: '異世界転生/転移〔文芸・SF・その他〕', value: 'o' }, + { label: '異世界〔恋愛〕', value: '101' }, + { label: '現実世界〔恋愛〕', value: '102' }, + { label: 'ハイファンタジー〔ファンタジー〕', value: '201' }, + { label: 'ローファンタジー〔ファンタジー〕', value: '202' }, + { label: '純文学〔文芸〕', value: '301' }, + { label: 'ヒューマンドラマ〔文芸〕', value: '302' }, + { label: '歴史〔文芸〕', value: '303' }, + { label: '推理〔文芸〕', value: '304' }, + { label: 'ホラー〔文芸〕', value: '305' }, + { label: 'アクション〔文芸〕', value: '306' }, + { label: 'コメディー〔文芸〕', value: '307' }, + { label: 'VRゲーム〔SF〕', value: '401' }, + { label: '宇宙〔SF〕', value: '402' }, + { label: '空想科学〔SF〕', value: '403' }, + { label: 'パニック〔SF〕', value: '404' }, + { label: '童話〔その他〕', value: '9901' }, + { label: '詩〔その他〕', value: '9902' }, + { label: 'エッセイ〔その他〕', value: '9903' }, + { label: 'その他〔その他〕', value: '9999' }, + ], + value: '', + }, + modifier: { + type: FilterTypes.Picker, + label: 'Modifier', + options: [ + { label: 'すべて', value: 'total' }, + { label: '連載中', value: 'r' }, + { label: '完結済', value: 'er' }, + { label: '短編', value: 't' }, + ], + value: 'total', + }, + } satisfies Filters; } export default new Syosetu(); diff --git a/plugins/korean/Agitoon.ts b/plugins/korean/Agitoon.ts index 6ec908f..10300ed 100644 --- a/plugins/korean/Agitoon.ts +++ b/plugins/korean/Agitoon.ts @@ -1,35 +1,35 @@ -import { fetchFile, fetchApi } from "@libs/fetch"; -import { Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { load as parseHTML } from "cheerio"; -import qs from "qs"; +import { fetchFile, fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { load as parseHTML } from 'cheerio'; +import qs from 'qs'; class Agitoon implements Plugin.PluginBase { - id = "agit.xyz"; - name = "Agitoon"; - icon = "src/kr/agitoon/agit.png"; - site = "https://agit501.xyz"; - version = "1.0.0"; + id = 'agit.xyz'; + name = 'Agitoon'; + icon = 'src/kr/agitoon/agit.png'; + site = 'https://agit501.xyz'; + version = '1.0.0'; async popularNovels( pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { - const res = await fetchApi(this.site + "/novel/index.update.php", { + const res = await fetchApi(this.site + '/novel/index.update.php', { headers: { - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', }, - method: "POST", + method: 'POST', body: qs.stringify({ - mode: "get_data_novel_list_p", - novel_menu: showLatestNovels ? "1" : "3", + mode: 'get_data_novel_list_p', + novel_menu: showLatestNovels ? '1' : '3', np_day: new Date().getDay(), - np_rank: "1", - np_distributor: "0", - np_genre: "00", - np_order: "1", - np_genre_ex_1: "00", - np_genre_ex_2: "00", + np_rank: '1', + np_distributor: '0', + np_genre: '00', + np_order: '1', + np_genre_ex_1: '00', + np_genre_ex_2: '00', list_limit: 20 * (pageNo - 1), is_query_first: pageNo == 1, }), @@ -37,10 +37,10 @@ class Agitoon implements Plugin.PluginBase { const resJson = (await res.json()) as response; const novels: Plugin.NovelItem[] = []; - resJson?.list?.forEach((novel) => + resJson?.list?.forEach(novel => novels.push({ name: novel.wr_subject, - cover: this.site + novel.np_dir + "/thumbnail/" + novel.np_thumbnail, + cover: this.site + novel.np_dir + '/thumbnail/' + novel.np_thumbnail, path: novel.wr_id, }), ); @@ -49,50 +49,51 @@ class Agitoon implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.resolveUrl(novelPath, true)) - .then((res) => res.text()); + const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); const loadedCheerio = parseHTML(result, { decodeEntities: false }); const novel: Plugin.SourceNovel = { path: novelPath, - name: loadedCheerio("h5.pt-2").text(), - cover: this.site + loadedCheerio("div.col-5.pr-0.pl-0 img").attr("src"), - summary: loadedCheerio(".pt-1.mt-1.pb-1.mb-1").text(), + name: loadedCheerio('h5.pt-2').text(), + cover: this.site + loadedCheerio('div.col-5.pr-0.pl-0 img').attr('src'), + summary: loadedCheerio('.pt-1.mt-1.pb-1.mb-1').text(), }; - novel.author = loadedCheerio(".post-item-list-cate-v") + novel.author = loadedCheerio('.post-item-list-cate-v') .first() .text() - .split(" : ")[1]; + .split(' : ')[1]; - const genres = loadedCheerio(".col-7 > .post-item-list-cate > span") + const genres = loadedCheerio('.col-7 > .post-item-list-cate > span') .map((index, element) => loadedCheerio(element).text().trim()) .get(); if (genres.length) { - novel.genres = genres.join(", "); + novel.genres = genres.join(', '); } const chapters: Plugin.ChapterItem[] = []; - const res = await fetchApi(this.site + "/novel/list.update.php", { + const res = await fetchApi(this.site + '/novel/list.update.php', { headers: { - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', }, - method: "POST", + method: 'POST', body: qs.stringify({ - mode: "get_data_novel_list_c", + mode: 'get_data_novel_list_c', wr_id_p: novelPath, - page_no: "1", - cnt_list: "10000", - order_type: "Asc", + page_no: '1', + cnt_list: '10000', + order_type: 'Asc', }), }); const resJson = (await res.json()) as responseBook; - resJson?.list?.forEach((chapter) => + resJson?.list?.forEach(chapter => chapters.push({ name: chapter.wr_subject, - path: chapter.wr_id + "/2", + path: chapter.wr_id + '/2', releaseTime: chapter.wr_datetime, }), ); @@ -102,28 +103,29 @@ class Agitoon implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.resolveUrl(chapterPath)) - .then((res) => res.text()); + const result = await fetchApi(this.resolveUrl(chapterPath)).then(res => + res.text(), + ); const loadedCheerio = parseHTML(result); - let content = loadedCheerio("#id_wr_content").html() || ""; + let content = loadedCheerio('#id_wr_content').html() || ''; // gets rid of the popup thingy content = content - .replace("팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다", "") + .replace('팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다', '') .trim(); return content; } async searchNovels(searchTerm: string): Promise { - const rawResults = await fetchApi("https://agit501.xyz/novel/search.php", { + const rawResults = await fetchApi('https://agit501.xyz/novel/search.php', { headers: { - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', }, - method: "POST", + method: 'POST', body: qs.stringify({ - mode: "get_data_novel_list_p_sch", + mode: 'get_data_novel_list_p_sch', search_novel: searchTerm, list_limit: 0, }), @@ -131,10 +133,11 @@ class Agitoon implements Plugin.PluginBase { const resJson = (await rawResults.json()) as response; const novels: Plugin.NovelItem[] = []; - resJson?.list?.forEach((novel) => + resJson?.list?.forEach(novel => novels.push({ name: novel.wr_subject, - cover: this.site + "/" + novel.np_dir + "/thumbnail/" + novel.np_thumbnail, + cover: + this.site + '/' + novel.np_dir + '/thumbnail/' + novel.np_thumbnail, path: novel.wr_id, }), ); @@ -144,7 +147,7 @@ class Agitoon implements Plugin.PluginBase { fetchImage = fetchFile; resolveUrl = (path: string, isNovel?: boolean) => - this.site + (isNovel ? "/novel/list/" : "/novel/view/") + path; + this.site + (isNovel ? '/novel/list/' : '/novel/view/') + path; } export default new Agitoon(); diff --git a/plugins/polish/novelki.ts b/plugins/polish/novelki.ts index 7d335e5..c8375b8 100644 --- a/plugins/polish/novelki.ts +++ b/plugins/polish/novelki.ts @@ -1,229 +1,253 @@ -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { load as parseHTML } from "cheerio"; -import { NovelStatus } from "@libs/novelStatus"; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { load as parseHTML } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; class NovelkiPL implements Plugin.PluginBase { - id = "novelki.pl"; - name = "Novelki"; - icon = "src/pl/novelki/icon.png"; - site = "https://novelki.pl"; - version = "1.0.3"; + id = 'novelki.pl'; + name = 'Novelki'; + icon = 'src/pl/novelki/icon.png'; + site = 'https://novelki.pl'; + version = '1.0.3'; - async popularNovels( - page: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - //So far is working, If the author of the site changes it, it will have to be changed. - let link = this.site + "/projekty?filter=t" - link += '&genre=' + filters.genres.value; - link += '&status=' + filters.status.value; - link += '&type=' + filters.type.value; - link += '&page=' + page; + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + //So far is working, If the author of the site changes it, it will have to be changed. + let link = this.site + '/projekty?filter=t'; + link += '&genre=' + filters.genres.value; + link += '&status=' + filters.status.value; + link += '&type=' + filters.type.value; + link += '&page=' + page; + const body = await fetchApi(link).then(res => { + if (res.url == `${this.site}/guest`) + throw new Error('Failed to load novels (open in web view and login)'); + return res.text(); + }); + const loadedCheerio = parseHTML(body); + const load = loadedCheerio('#projects > div'); - const body = await fetchApi(link).then((res) => { - if(res.url == `${this.site}/guest`) throw new Error("Failed to load novels (open in web view and login)") - return res.text() - }); - const loadedCheerio = parseHTML(body); - const load = loadedCheerio("#projects > div") - - //if(load.length == 0) throw new Error("Failed to load page (open in web view and login)"); + //if(load.length == 0) throw new Error("Failed to load page (open in web view and login)"); - const novels: Plugin.NovelItem[] = load - .map((index, element) => ({ - name: loadedCheerio(element).find(".card-title").attr('title') as string, - cover: this.site+loadedCheerio(element).find(".card-img-top").attr("src"), - path: loadedCheerio(element).find("a").attr("href") || "", - })) - .get() - .filter((novel) => novel.name && novel.path); - return novels; - } - - async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((r) => r.text()); - const loadedCheerio = parseHTML(body); + const novels: Plugin.NovelItem[] = load + .map((index, element) => ({ + name: loadedCheerio(element) + .find('.card-title') + .attr('title') as string, + cover: + this.site + loadedCheerio(element).find('.card-img-top').attr('src'), + path: loadedCheerio(element).find('a').attr('href') || '', + })) + .get() + .filter(novel => novel.name && novel.path); + return novels; + } - const novel: Plugin.SourceNovel = { - path: novelPath, - name: '' - }; - - loadedCheerio("p.h5").each((i, e) => { - const text = loadedCheerio(e).text().trim() - if(text.includes('Autor:')) novel.author = text.split(':')[1].trim() - if(text.includes("Status projektu:")) { - switch (`${text.split(':')[1].trim()}`.toLowerCase()) { - case "wstrzymany": - novel.status = NovelStatus.OnHiatus; - break; - case "zakończony": - novel.status = NovelStatus.Completed; - break; - case "aktywny": - novel.status = NovelStatus.Ongoing; - break; - case "porzucony": - novel.status = NovelStatus.Cancelled; - break; - case "zlicencjonowany": - novel.status = NovelStatus.Licensed; - break; - default: - novel.status = NovelStatus.Unknown; - break; - } - } - - }) - novel.name = loadedCheerio("div.col-sm-12.col-md-6.col-lg-8.mb-5").find("h3").text().trim(); - novel.cover = this.site+loadedCheerio(".img-fluid").attr("src"); // TODO: return not only undefined + async parseNovel(novelPath: string): Promise { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); - novel.genres = loadedCheerio("span.badge").map((i, e) => loadedCheerio(e).text()).get().join(", ") - - novel.summary = loadedCheerio('.h5:contains("Opis:")').next("p").next("p").text().trim() + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + }; - //TODO: Dodać tłumacza danej serii :D + loadedCheerio('p.h5').each((i, e) => { + const text = loadedCheerio(e).text().trim(); + if (text.includes('Autor:')) novel.author = text.split(':')[1].trim(); + if (text.includes('Status projektu:')) { + switch (`${text.split(':')[1].trim()}`.toLowerCase()) { + case 'wstrzymany': + novel.status = NovelStatus.OnHiatus; + break; + case 'zakończony': + novel.status = NovelStatus.Completed; + break; + case 'aktywny': + novel.status = NovelStatus.Ongoing; + break; + case 'porzucony': + novel.status = NovelStatus.Cancelled; + break; + case 'zlicencjonowany': + novel.status = NovelStatus.Licensed; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + } + }); + novel.name = loadedCheerio('div.col-sm-12.col-md-6.col-lg-8.mb-5') + .find('h3') + .text() + .trim(); + novel.cover = this.site + loadedCheerio('.img-fluid').attr('src'); // TODO: return not only undefined - let chapters: Plugin.ChapterItem[] = []; + novel.genres = loadedCheerio('span.badge') + .map((i, e) => loadedCheerio(e).text()) + .get() + .join(', '); - let chaptersList = loadedCheerio(".chapters > .col-md-3 > div").get() - chaptersList.forEach((e, i) => { - let urlChapters = loadedCheerio(e).find("a").attr("href") || ""; + novel.summary = loadedCheerio('.h5:contains("Opis:")') + .next('p') + .next('p') + .text() + .trim(); - const chapter: Plugin.ChapterItem = { - name: loadedCheerio(e).find("a")?.text().trim(), - path: urlChapters, - releaseTime: loadedCheerio(e).find(".card-footer > span").text().trim().split('-').reverse().join('-'), - chapterNumber: chaptersList.length-i, - }; - chapters.push(chapter); - }) + //TODO: Dodać tłumacza danej serii :D - novel.chapters = chapters.reverse(); - return novel; - } - async parseChapter(chapterPath: string): Promise { - var pattern = /\/projekty\/([^\/]+)\/([^\/]+)/; - let codeChapter = pattern.exec(chapterPath) || ""; - const body = await fetchApi(`${this.site}/api/reader/chapters/${codeChapter[2]}`).then((res) => { - if(res.url == `${this.site}/guest`) throw new Error("Failed to load chapter (open in web view and login)") - return res.json() - }); + let chapters: Plugin.ChapterItem[] = []; - const chapterText = body.data.content - return chapterText; - } - async searchNovels( - searchTerm: string, - page: number - ): Promise { - const body = await fetchApi(`${this.site}/projekty?filter=t&title=${searchTerm}+&page=${page}`).then((res) => { - if(res.url == `${this.site}/guest`) throw new Error("Failed to search novels (open in web view and login)") - return res.text() - }); - const loadedCheerio = parseHTML(body); + let chaptersList = loadedCheerio('.chapters > .col-md-3 > div').get(); + chaptersList.forEach((e, i) => { + let urlChapters = loadedCheerio(e).find('a').attr('href') || ''; + const chapter: Plugin.ChapterItem = { + name: loadedCheerio(e).find('a')?.text().trim(), + path: urlChapters, + releaseTime: loadedCheerio(e) + .find('.card-footer > span') + .text() + .trim() + .split('-') + .reverse() + .join('-'), + chapterNumber: chaptersList.length - i, + }; + chapters.push(chapter); + }); - let novels: Plugin.NovelItem[] = loadedCheerio("#projects > div") - .map((index, element) => ({ - name: loadedCheerio(element).find(".card-title").attr('title') as string, - cover: this.site+loadedCheerio(element).find(".card-img-top").attr("src"), - path: loadedCheerio(element).find("a").attr("href") || "", - })) - .get() - .filter((novel) => novel.name && novel.path) + novel.chapters = chapters.reverse(); + return novel; + } + async parseChapter(chapterPath: string): Promise { + var pattern = /\/projekty\/([^\/]+)\/([^\/]+)/; + let codeChapter = pattern.exec(chapterPath) || ''; + const body = await fetchApi( + `${this.site}/api/reader/chapters/${codeChapter[2]}`, + ).then(res => { + if (res.url == `${this.site}/guest`) + throw new Error('Failed to load chapter (open in web view and login)'); + return res.json(); + }); - return novels; - } - async fetchImage(url: string): Promise { - // if your plugin has images and they won't load - // this is the function to fiddle with - return fetchFile(url); - } - filters = { - genres: { - value: "", - label: "Genre", - options: [ - { label: "Wybierz gatunek", value: "" }, - { label: "Adaptacja do anime", value: "35" }, - { label: "Adaptacja do dramy", value: "40" }, - { label: "Adaptacja do mangi", value: "36" }, - { label: "Akcja", value: "1" }, - { label: "Boys Love", value: "22" }, - { label: "Bromans", value: "33" }, - { label: "Dojrzały", value: "11" }, - { label: "Dramat", value: "10" }, - { label: "Ecchi", value: "29" }, - { label: "Fantasy", value: "2" }, - { label: "Gender Bender", value: "28" }, - { label: "Girls Love", value: "23" }, - { label: "Harem", value: "8" }, - { label: "Hentai", value: "43" }, - { label: "Historyczny", value: "30" }, - { label: "Horror", value: "15" }, - { label: "Isekai", value: "44" }, - { label: "Josei", value: "27" }, - { label: "Komedia", value: "3" }, - { label: "Mecha", value: "16" }, - { label: "Nadprzyrodzone", value: "9" }, - { label: "Okruchy życia", value: "18" }, - { label: "Oneshot", value: "41" }, - { label: "Parodia", value: "19" }, - { label: "Przygodowy", value: "4" }, - { label: "Psychologiczny", value: "12" }, - { label: "Reinkarnacja", value: "42" }, - { label: "Romans", value: "7" }, - { label: "RPG", value: "17" }, - { label: "Sci-fi", value: "21" }, - { label: "Seinen", value: "26" }, - { label: "Shoujo", value: "25" }, - { label: "Shounen", value: "24" }, - { label: "Smut", value: "31" }, - { label: "Sport", value: "32" }, - { label: "Świat gry", value: "5" }, - { label: "Szkolne życie", value: "20" }, - { label: "Sztuki walki", value: "6" }, - { label: "Tajemnica", value: "14" }, - { label: "Tragedia", value: "13" }, + const chapterText = body.data.content; + return chapterText; + } + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const body = await fetchApi( + `${this.site}/projekty?filter=t&title=${searchTerm}+&page=${page}`, + ).then(res => { + if (res.url == `${this.site}/guest`) + throw new Error('Failed to search novels (open in web view and login)'); + return res.text(); + }); + const loadedCheerio = parseHTML(body); - ], - type: FilterTypes.Picker, - }, - status: { - label: "Status", - value: "", - options: [ - { label: "Wybierz status", value: "" }, - { label: "Zakończony", value: "0" }, - { label: "Aktywny", value: "1" }, - { label: "Porzucony", value: "2" }, - { label: "Wstrzymany", value: "3" }, - { label: "Zlicencjonowany", value: "4" }, + let novels: Plugin.NovelItem[] = loadedCheerio('#projects > div') + .map((index, element) => ({ + name: loadedCheerio(element) + .find('.card-title') + .attr('title') as string, + cover: + this.site + loadedCheerio(element).find('.card-img-top').attr('src'), + path: loadedCheerio(element).find('a').attr('href') || '', + })) + .get() + .filter(novel => novel.name && novel.path); - ], - type: FilterTypes.Picker, - }, - type: { - label: "Type", - value: "", - options: [ - { label: "Wybierz typ", value: "" }, - { label: "Nie zdefiniowano", value: "0" }, - { label: "Light Novel", value: "1" }, - { label: "Wuxia", value: "2" }, - { label: "Xianxia", value: "3" }, - { label: "Web Novel", value: "4" }, - { label: "Autorska", value: "5" }, - { label: "Inne", value: "6" }, - ], - type: FilterTypes.Picker, - }, - } satisfies Filters; + return novels; + } + async fetchImage(url: string): Promise { + // if your plugin has images and they won't load + // this is the function to fiddle with + return fetchFile(url); + } + filters = { + genres: { + value: '', + label: 'Genre', + options: [ + { label: 'Wybierz gatunek', value: '' }, + { label: 'Adaptacja do anime', value: '35' }, + { label: 'Adaptacja do dramy', value: '40' }, + { label: 'Adaptacja do mangi', value: '36' }, + { label: 'Akcja', value: '1' }, + { label: 'Boys Love', value: '22' }, + { label: 'Bromans', value: '33' }, + { label: 'Dojrzały', value: '11' }, + { label: 'Dramat', value: '10' }, + { label: 'Ecchi', value: '29' }, + { label: 'Fantasy', value: '2' }, + { label: 'Gender Bender', value: '28' }, + { label: 'Girls Love', value: '23' }, + { label: 'Harem', value: '8' }, + { label: 'Hentai', value: '43' }, + { label: 'Historyczny', value: '30' }, + { label: 'Horror', value: '15' }, + { label: 'Isekai', value: '44' }, + { label: 'Josei', value: '27' }, + { label: 'Komedia', value: '3' }, + { label: 'Mecha', value: '16' }, + { label: 'Nadprzyrodzone', value: '9' }, + { label: 'Okruchy życia', value: '18' }, + { label: 'Oneshot', value: '41' }, + { label: 'Parodia', value: '19' }, + { label: 'Przygodowy', value: '4' }, + { label: 'Psychologiczny', value: '12' }, + { label: 'Reinkarnacja', value: '42' }, + { label: 'Romans', value: '7' }, + { label: 'RPG', value: '17' }, + { label: 'Sci-fi', value: '21' }, + { label: 'Seinen', value: '26' }, + { label: 'Shoujo', value: '25' }, + { label: 'Shounen', value: '24' }, + { label: 'Smut', value: '31' }, + { label: 'Sport', value: '32' }, + { label: 'Świat gry', value: '5' }, + { label: 'Szkolne życie', value: '20' }, + { label: 'Sztuki walki', value: '6' }, + { label: 'Tajemnica', value: '14' }, + { label: 'Tragedia', value: '13' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Status', + value: '', + options: [ + { label: 'Wybierz status', value: '' }, + { label: 'Zakończony', value: '0' }, + { label: 'Aktywny', value: '1' }, + { label: 'Porzucony', value: '2' }, + { label: 'Wstrzymany', value: '3' }, + { label: 'Zlicencjonowany', value: '4' }, + ], + type: FilterTypes.Picker, + }, + type: { + label: 'Type', + value: '', + options: [ + { label: 'Wybierz typ', value: '' }, + { label: 'Nie zdefiniowano', value: '0' }, + { label: 'Light Novel', value: '1' }, + { label: 'Wuxia', value: '2' }, + { label: 'Xianxia', value: '3' }, + { label: 'Web Novel', value: '4' }, + { label: 'Autorska', value: '5' }, + { label: 'Inne', value: '6' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; } export default new NovelkiPL(); diff --git a/plugins/portuguese/lightnovelbrasil.broken.ts b/plugins/portuguese/lightnovelbrasil.broken.ts index 9ada1ba..b5760bc 100644 --- a/plugins/portuguese/lightnovelbrasil.broken.ts +++ b/plugins/portuguese/lightnovelbrasil.broken.ts @@ -1,413 +1,402 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; - -class LightNovelBrasil implements Plugin.PluginBase { - id = "lightnovelbrasil"; - name = "Light Novel Brasil"; - icon = "multisrc/wpmangastream/icons/lightnovelbrasil.png"; - site = "https://lightnovelbrasil.com/"; - version = "1.0.0"; - - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}series/?page=${pageNo}`; - - if (filters.genres.value.length) - link += filters.genres.value.map((i) => `&genre[]=${i}`).join(""); - - if (filters.type.value.length) - link += filters.type.value.map((i) => `&lang[]=${i}`).join(""); - - link += "&status=" + (filters?.status ? filters.status : ""); - - link += "&order=" + (filters?.order ? filters.order : "popular"); - - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => - result.text() - ); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.bs").each(function () { - const novelName = loadedCheerio(this).find(".ntitle").text().trim(); - let image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - - const novelUrl = loadedCheerio(this).find("a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; - } - - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url, - chapters: [], - }; - - novel.name = loadedCheerio(".entry-title").text(); - - novel.cover = - loadedCheerio("img.wp-post-image").attr("data-src") || - loadedCheerio("img.wp-post-image").attr("src"); - - loadedCheerio("div.spe > span").each(function () { - const detailName = loadedCheerio(this).find("b").text().trim(); - const detail = loadedCheerio(this) - .find("b") - .remove() - .end() - .text() - .trim(); - - switch (detailName) { - case "Autor:": - case "Author:": - novel.author = detail; - break; - case "Status:": - novel.status = detail; - break; - } - }); - - novel.genres = loadedCheerio(".genxed") - .text() - .trim() - .replace(/\s/g, ","); - - loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove(); - novel.summary = loadedCheerio('div[itemprop="description"]') - .find("br") - .replaceWith("\n") - .end() - .text(); - - let chapter: Plugin.ChapterItem[] = []; - - loadedCheerio(".eplister") - .find("li") - .each(function () { - const chapterName = - loadedCheerio(this).find(".epl-num").text() + - " - " + - loadedCheerio(this).find(".epl-title").text(); - - const releaseDate = loadedCheerio(this) - .find(".epl-date") - .text() - .trim(); - - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (!chapterUrl) return; - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - - novel.chapters = chapter.reverse(); - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const headers = new Headers(); - const result = await fetchApi(chapterUrl, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - let chapterText = loadedCheerio(".epcontent").html() || ""; - - return chapterText; - } - - async searchNovels( - searchTerm: string, - pageNo?: number | undefined - ): Promise { - const url = `${this.site}?s=${searchTerm}`; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.bs").each(function () { - const novelName = loadedCheerio(this).find(".ntitle").text().trim(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("a").attr("href"); - if (!novelUrl) return; - - novels.push({ - name: novelName, - url: novelUrl, - cover: novelCover, - }); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - order: { - label: "Sort By", - value: "", - options: [ - { label: "Default", value: "" }, - - { label: "A-Z", value: "title" }, - - { label: "Z-A", value: "titlereverse" }, - - { label: "Latest Update", value: "update" }, - - { label: "Latest Added", value: "latest" }, - - { label: "Popular", value: "popular" }, - ], - type: FilterTypes.Picker, - }, - status: { - label: "Status", - value: "", - options: [ - { label: "All", value: "" }, - - { label: "Ongoing", value: "ongoing" }, - - { label: "Hiatus", value: "hiatus" }, - - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, - }, - type: { - label: "Type", - value: [], - options: [ - { label: "Chinese novel", value: "chinese-novel" }, - - { label: "habyeol", value: "habyeol" }, - - { label: "korean novel", value: "korean-novel" }, - - { label: "Web Novel", value: "web-novel" }, - - { label: "삼심", value: "%ec%82%bc%ec%8b%ac" }, - - { label: "호곡", value: "%ed%98%b8%ea%b3%a1" }, - ], - type: FilterTypes.CheckboxGroup, - }, - genres: { - value: [], - label: "Genres", - options: [ - { label: "A.I", value: "a.i" }, - - { label: "Academy", value: "academy" }, - - { label: "Action", value: "action" }, - - { label: "Adult", value: "adult" }, - - { label: "Adventure", value: "adventure" }, - - { label: "Alternative History", value: "alternative-history" }, - - { label: "Another World", value: "another-world" }, - - { label: "Apocalypse", value: "apocalypse" }, - - { label: "Bromance", value: "bromance" }, - - { label: "Comedy", value: "comedy" }, - - { label: "Dark fantasy", value: "dark-fantasy" }, - - { label: "Demons", value: "demons" }, - - { label: "Drama", value: "drama" }, - - { label: "Dystopia", value: "dystopia" }, - - { label: "Ecchi", value: "ecchi" }, - - { label: "Entertainment", value: "entertainment" }, - - { label: "Exhaustion", value: "exhaustion" }, - - { label: "Fanfiction", value: "fanfiction" }, - - { label: "fantasy", value: "fantasy" }, - - { label: "finance", value: "finance" }, - - { label: "Full color", value: "full-color" }, - - { label: "Game", value: "game" }, - - { label: "Gender Bender", value: "gender-bender" }, - - { label: "Genius", value: "genius" }, - - { label: "Harem", value: "harem" }, - - { label: "Hero", value: "hero" }, - - { label: "Historical", value: "historical" }, - - { label: "Hunter", value: "hunter" }, - - { label: "korean novel", value: "korean-novel" }, - - { label: "Light Novel", value: "light-novel" }, - - { - label: "List Adventure Manga Genres", - value: "list-adventure-manga-genres", - }, - - { label: "Long Strip", value: "long-strip" }, - - { label: "Love comedy", value: "love-comedy" }, - - { label: "magic", value: "magic" }, - - { label: "Manhua", value: "manhua" }, - - { label: "Martial Arts", value: "martial-arts" }, - - { label: "Mature", value: "mature" }, - - { label: "Medieval", value: "medieval" }, - - { label: "Misunderstanding", value: "misunderstanding" }, - - { label: "Modern", value: "modern" }, - - { label: "modern fantasy", value: "modern-fantasy" }, - - { label: "music", value: "music" }, - - { label: "Mystery", value: "mystery" }, - - { label: "Necromancy", value: "necromancy" }, - - { label: "No Romance", value: "no-romance" }, - - { label: "NTL", value: "ntl" }, - - { label: "o", value: "o" }, - - { label: "Obsession", value: "obsession" }, - - { label: "Politics", value: "politics" }, - - { label: "Possession", value: "possession" }, - - { label: "Programming", value: "programming" }, - - { label: "Psychological", value: "psychological" }, - - { label: "Pure Love", value: "pure-love" }, - - { label: "Redemption", value: "redemption" }, - - { label: "Regression", value: "regression" }, - - { label: "Regret", value: "regret" }, - - { label: "Reincarnation", value: "reincarnation" }, - - { label: "Revenge", value: "revenge" }, - - { label: "Romance", value: "romance" }, - - { label: "Romance Fanrasy", value: "romance-fanrasy" }, - - { label: "Salvation", value: "salvation" }, - - { label: "School Life", value: "school-life" }, - - { label: "Sci-fi", value: "sci-fi" }, - - { label: "Science fiction", value: "science-fiction" }, - - { label: "Seinen", value: "seinen" }, - - { label: "Shounen", value: "shounen" }, - - { label: "Slice of Life", value: "slice-of-life" }, - - { label: "Soft yandere", value: "soft-yandere" }, - - { label: "Sports", value: "sports" }, - - { label: "Supernatural", value: "supernatural" }, - - { label: "Survival", value: "survival" }, - - { label: "system", value: "system" }, - - { label: "Time limit", value: "time-limit" }, - - { label: "Tragedy", value: "tragedy" }, - - { label: "Transmigration", value: "transmigration" }, - - { label: "TS", value: "ts" }, - - { label: "Tsundere", value: "tsundere" }, - - { label: "Unique", value: "unique" }, - - { label: "Wholesome", value: "wholesome" }, - - { label: "Wuxia", value: "wuxia" }, - - { label: "Xuanhuan", value: "xuanhuan" }, - - { label: "Yandere", value: "yandere" }, - - { label: "Yuri", value: "yuri" }, - ], - type: FilterTypes.CheckboxGroup, - }, - } satisfies Filters; -} - -export default new LightNovelBrasil(); +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; + +class LightNovelBrasil implements Plugin.PluginBase { + id = 'lightnovelbrasil'; + name = 'Light Novel Brasil'; + icon = 'multisrc/wpmangastream/icons/lightnovelbrasil.png'; + site = 'https://lightnovelbrasil.com/'; + version = '1.0.0'; + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}series/?page=${pageNo}`; + + if (filters.genres.value.length) + link += filters.genres.value.map(i => `&genre[]=${i}`).join(''); + + if (filters.type.value.length) + link += filters.type.value.map(i => `&lang[]=${i}`).join(''); + + link += '&status=' + (filters?.status ? filters.status : ''); + + link += '&order=' + (filters?.order ? filters.order : 'popular'); + + const headers = new Headers(); + const body = await fetchApi(link, { headers }).then(result => + result.text(), + ); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('article.bs').each(function () { + const novelName = loadedCheerio(this).find('.ntitle').text().trim(); + let image = loadedCheerio(this).find('img'); + const novelCover = image.attr('data-src') || image.attr('src'); + + const novelUrl = loadedCheerio(this).find('a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; + const headers = new Headers(); + const result = await fetchApi(url, { headers }); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + url, + chapters: [], + }; + + novel.name = loadedCheerio('.entry-title').text(); + + novel.cover = + loadedCheerio('img.wp-post-image').attr('data-src') || + loadedCheerio('img.wp-post-image').attr('src'); + + loadedCheerio('div.spe > span').each(function () { + const detailName = loadedCheerio(this).find('b').text().trim(); + const detail = loadedCheerio(this).find('b').remove().end().text().trim(); + + switch (detailName) { + case 'Autor:': + case 'Author:': + novel.author = detail; + break; + case 'Status:': + novel.status = detail; + break; + } + }); + + novel.genres = loadedCheerio('.genxed').text().trim().replace(/\s/g, ','); + + loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove(); + novel.summary = loadedCheerio('div[itemprop="description"]') + .find('br') + .replaceWith('\n') + .end() + .text(); + + let chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('.eplister') + .find('li') + .each(function () { + const chapterName = + loadedCheerio(this).find('.epl-num').text() + + ' - ' + + loadedCheerio(this).find('.epl-title').text(); + + const releaseDate = loadedCheerio(this).find('.epl-date').text().trim(); + + const chapterUrl = loadedCheerio(this).find('a').attr('href'); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const headers = new Headers(); + const result = await fetchApi(chapterUrl, { headers }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + let chapterText = loadedCheerio('.epcontent').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo?: number | undefined, + ): Promise { + const url = `${this.site}?s=${searchTerm}`; + const headers = new Headers(); + const result = await fetchApi(url, { headers }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('article.bs').each(function () { + const novelName = loadedCheerio(this).find('.ntitle').text().trim(); + const novelCover = loadedCheerio(this).find('img').attr('src'); + const novelUrl = loadedCheerio(this).find('a').attr('href'); + if (!novelUrl) return; + + novels.push({ + name: novelName, + url: novelUrl, + cover: novelCover, + }); + }); + + return novels; + } + async fetchImage(url: string): Promise { + return await fetchFile(url); + } + + filters = { + order: { + label: 'Sort By', + value: '', + options: [ + { label: 'Default', value: '' }, + + { label: 'A-Z', value: 'title' }, + + { label: 'Z-A', value: 'titlereverse' }, + + { label: 'Latest Update', value: 'update' }, + + { label: 'Latest Added', value: 'latest' }, + + { label: 'Popular', value: 'popular' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Status', + value: '', + options: [ + { label: 'All', value: '' }, + + { label: 'Ongoing', value: 'ongoing' }, + + { label: 'Hiatus', value: 'hiatus' }, + + { label: 'Completed', value: 'completed' }, + ], + type: FilterTypes.Picker, + }, + type: { + label: 'Type', + value: [], + options: [ + { label: 'Chinese novel', value: 'chinese-novel' }, + + { label: 'habyeol', value: 'habyeol' }, + + { label: 'korean novel', value: 'korean-novel' }, + + { label: 'Web Novel', value: 'web-novel' }, + + { label: '삼심', value: '%ec%82%bc%ec%8b%ac' }, + + { label: '호곡', value: '%ed%98%b8%ea%b3%a1' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genres: { + value: [], + label: 'Genres', + options: [ + { label: 'A.I', value: 'a.i' }, + + { label: 'Academy', value: 'academy' }, + + { label: 'Action', value: 'action' }, + + { label: 'Adult', value: 'adult' }, + + { label: 'Adventure', value: 'adventure' }, + + { label: 'Alternative History', value: 'alternative-history' }, + + { label: 'Another World', value: 'another-world' }, + + { label: 'Apocalypse', value: 'apocalypse' }, + + { label: 'Bromance', value: 'bromance' }, + + { label: 'Comedy', value: 'comedy' }, + + { label: 'Dark fantasy', value: 'dark-fantasy' }, + + { label: 'Demons', value: 'demons' }, + + { label: 'Drama', value: 'drama' }, + + { label: 'Dystopia', value: 'dystopia' }, + + { label: 'Ecchi', value: 'ecchi' }, + + { label: 'Entertainment', value: 'entertainment' }, + + { label: 'Exhaustion', value: 'exhaustion' }, + + { label: 'Fanfiction', value: 'fanfiction' }, + + { label: 'fantasy', value: 'fantasy' }, + + { label: 'finance', value: 'finance' }, + + { label: 'Full color', value: 'full-color' }, + + { label: 'Game', value: 'game' }, + + { label: 'Gender Bender', value: 'gender-bender' }, + + { label: 'Genius', value: 'genius' }, + + { label: 'Harem', value: 'harem' }, + + { label: 'Hero', value: 'hero' }, + + { label: 'Historical', value: 'historical' }, + + { label: 'Hunter', value: 'hunter' }, + + { label: 'korean novel', value: 'korean-novel' }, + + { label: 'Light Novel', value: 'light-novel' }, + + { + label: 'List Adventure Manga Genres', + value: 'list-adventure-manga-genres', + }, + + { label: 'Long Strip', value: 'long-strip' }, + + { label: 'Love comedy', value: 'love-comedy' }, + + { label: 'magic', value: 'magic' }, + + { label: 'Manhua', value: 'manhua' }, + + { label: 'Martial Arts', value: 'martial-arts' }, + + { label: 'Mature', value: 'mature' }, + + { label: 'Medieval', value: 'medieval' }, + + { label: 'Misunderstanding', value: 'misunderstanding' }, + + { label: 'Modern', value: 'modern' }, + + { label: 'modern fantasy', value: 'modern-fantasy' }, + + { label: 'music', value: 'music' }, + + { label: 'Mystery', value: 'mystery' }, + + { label: 'Necromancy', value: 'necromancy' }, + + { label: 'No Romance', value: 'no-romance' }, + + { label: 'NTL', value: 'ntl' }, + + { label: 'o', value: 'o' }, + + { label: 'Obsession', value: 'obsession' }, + + { label: 'Politics', value: 'politics' }, + + { label: 'Possession', value: 'possession' }, + + { label: 'Programming', value: 'programming' }, + + { label: 'Psychological', value: 'psychological' }, + + { label: 'Pure Love', value: 'pure-love' }, + + { label: 'Redemption', value: 'redemption' }, + + { label: 'Regression', value: 'regression' }, + + { label: 'Regret', value: 'regret' }, + + { label: 'Reincarnation', value: 'reincarnation' }, + + { label: 'Revenge', value: 'revenge' }, + + { label: 'Romance', value: 'romance' }, + + { label: 'Romance Fanrasy', value: 'romance-fanrasy' }, + + { label: 'Salvation', value: 'salvation' }, + + { label: 'School Life', value: 'school-life' }, + + { label: 'Sci-fi', value: 'sci-fi' }, + + { label: 'Science fiction', value: 'science-fiction' }, + + { label: 'Seinen', value: 'seinen' }, + + { label: 'Shounen', value: 'shounen' }, + + { label: 'Slice of Life', value: 'slice-of-life' }, + + { label: 'Soft yandere', value: 'soft-yandere' }, + + { label: 'Sports', value: 'sports' }, + + { label: 'Supernatural', value: 'supernatural' }, + + { label: 'Survival', value: 'survival' }, + + { label: 'system', value: 'system' }, + + { label: 'Time limit', value: 'time-limit' }, + + { label: 'Tragedy', value: 'tragedy' }, + + { label: 'Transmigration', value: 'transmigration' }, + + { label: 'TS', value: 'ts' }, + + { label: 'Tsundere', value: 'tsundere' }, + + { label: 'Unique', value: 'unique' }, + + { label: 'Wholesome', value: 'wholesome' }, + + { label: 'Wuxia', value: 'wuxia' }, + + { label: 'Xuanhuan', value: 'xuanhuan' }, + + { label: 'Yandere', value: 'yandere' }, + + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new LightNovelBrasil(); diff --git a/plugins/russian/LitSpace.ts b/plugins/russian/LitSpace.ts index 32209a3..0338c0b 100644 --- a/plugins/russian/LitSpace.ts +++ b/plugins/russian/LitSpace.ts @@ -1,16 +1,16 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; class freedlit implements Plugin.PluginBase { - id = "freedlit.space"; - name = "LitSpace"; - site = "https://freedlit.space"; - version = "1.0.0"; - icon = "src/ru/freedlit/icon.png"; + id = 'freedlit.space'; + name = 'LitSpace'; + site = 'https://freedlit.space'; + version = '1.0.0'; + icon = 'src/ru/freedlit/icon.png'; async popularNovels( page: number, @@ -19,73 +19,75 @@ class freedlit implements Plugin.PluginBase { filters, }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/get-books/all/list/" + page + "?sort="; - url += showLatestNovels ? "recent" : filters?.sort?.value || "popular"; - url += "&status=" + (filters?.status?.value || "all"); - url += "&access=" + (filters?.access?.value || "all"); - url += "&adult=" + (filters?.adult?.value || "hide"); + let url = this.site + '/get-books/all/list/' + page + '?sort='; + url += showLatestNovels ? 'recent' : filters?.sort?.value || 'popular'; + url += '&status=' + (filters?.status?.value || 'all'); + url += '&access=' + (filters?.access?.value || 'all'); + url += '&adult=' + (filters?.adult?.value || 'hide'); if (filters?.genre?.value?.include?.length) { url += filters.genre.value.include - .map((id) => "&genres_included[]=" + id) - .join(""); + .map(id => '&genres_included[]=' + id) + .join(''); } if (filters?.genre?.value?.exclude?.length) { url += filters.genre.value.exclude - .map((id) => "&genres_excluded[]=" + id) - .join(""); + .map(id => '&genres_excluded[]=' + id) + .join(''); } - const body = await fetchApi(url).then((res) => res.text()); + const body = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio("#bookListBlock > div > div").each((index, element) => { - const name = loadedCheerio(element).find("div > h4 > a").text()?.trim(); + loadedCheerio('#bookListBlock > div > div').each((index, element) => { + const name = loadedCheerio(element).find('div > h4 > a').text()?.trim(); const cover = loadedCheerio(element) - .find("div > a > img") - .attr("src") + .find('div > a > img') + .attr('src') ?.trim(); const url = loadedCheerio(element) - .find("div > h4 > a") - .attr("href") + .find('div > h4 > a') + .attr('href') ?.trim(); if (!name || !url) return; - novels.push({ name, cover, path: url.replace(this.site, "") }); + novels.push({ name, cover, path: url.replace(this.site, '') }); }); return novels; } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const body = await fetchApi(this.site + novelPath).then(res => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, - name: loadedCheerio(".book-info h4").text(), - cover: loadedCheerio(".book-cover > div > img").attr("src")?.trim(), - summary: loadedCheerio("#nav-home").text()?.trim(), - author: loadedCheerio(".book-info h5 > a").text(), - genres: loadedCheerio(".genre-list > a") + name: loadedCheerio('.book-info h4').text(), + cover: loadedCheerio('.book-cover > div > img').attr('src')?.trim(), + summary: loadedCheerio('#nav-home').text()?.trim(), + author: loadedCheerio('.book-info h5 > a').text(), + genres: loadedCheerio('.genre-list > a') .map((index, element) => loadedCheerio(element).text()) .get() - .join(","), + .join(','), }; const chapters: Plugin.ChapterItem[] = []; - loadedCheerio("a.chapter-line").each((chapterIndex, element) => { - const name = loadedCheerio(element).find("h6").text(); - const url = loadedCheerio(element).attr("href"); + loadedCheerio('a.chapter-line').each((chapterIndex, element) => { + const name = loadedCheerio(element).find('h6').text(); + const url = loadedCheerio(element).attr('href'); if (!name || !url) return; - const releaseDate = loadedCheerio(element).find('span[class="date"]').text(); + const releaseDate = loadedCheerio(element) + .find('span[class="date"]') + .text(); chapters.push({ name, - path: url.replace(this.site, ""), + path: url.replace(this.site, ''), releaseTime: this.parseDate(releaseDate), chapterNumber: chapterIndex + 1, }); @@ -96,35 +98,37 @@ class freedlit implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); - loadedCheerio("div.mobile-block").remove(); - loadedCheerio("div.standart-block").remove(); + loadedCheerio('div.mobile-block').remove(); + loadedCheerio('div.standart-block').remove(); - const chapterText = loadedCheerio('div[class="chapter"]').html() || ""; + const chapterText = loadedCheerio('div[class="chapter"]').html() || ''; return chapterText; } async searchNovels(searchTerm: string): Promise { - const url = this.site + "/search?query=" + searchTerm + "&type=all"; - const body = await fetchApi(url).then((res) => res.text()); + const url = this.site + '/search?query=' + searchTerm + '&type=all'; + const body = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio("#bookGridBlock > div").each((index, element) => { - const name = loadedCheerio(element).find("h4 > a").text()?.trim(); - const cover = loadedCheerio(element).find("a > img").attr("src")?.trim(); - const url = loadedCheerio(element).find("h4 > a").attr("href")?.trim(); + loadedCheerio('#bookGridBlock > div').each((index, element) => { + const name = loadedCheerio(element).find('h4 > a').text()?.trim(); + const cover = loadedCheerio(element).find('a > img').attr('src')?.trim(); + const url = loadedCheerio(element).find('h4 > a').attr('href')?.trim(); if (!name || !url) return; - novels.push({ name, cover, path: url.replace(this.site, "") }); + novels.push({ name, cover, path: url.replace(this.site, '') }); }); return novels; } - parseDate = (dateString: string | undefined = "") => { + parseDate = (dateString: string | undefined = '') => { const months: { [key: string]: number } = { января: 1, февраля: 2, @@ -140,9 +144,9 @@ class freedlit implements Plugin.PluginBase { декабря: 12, }; - const [day, month, year] = dateString.split(" "); + const [day, month, year] = dateString.split(' '); if (day && months[month] && year) { - return dayjs(year + "-" + months[month] + "-" + day).format("LL"); + return dayjs(year + '-' + months[month] + '-' + day).format('LL'); } return dateString || null; }; @@ -151,124 +155,127 @@ class freedlit implements Plugin.PluginBase { filters = { sort: { - label: "Сортировка:", - value: "popular", + label: 'Сортировка:', + value: 'popular', options: [ - { label: "По популярности", value: "popular" }, - { label: "По количеству комментариев", value: "comments" }, - { label: "По количеству лайков", value: "likes" }, - { label: "По новизне", value: "recent" }, - { label: "По просмотрам", value: "views" }, + { label: 'По популярности', value: 'popular' }, + { label: 'По количеству комментариев', value: 'comments' }, + { label: 'По количеству лайков', value: 'likes' }, + { label: 'По новизне', value: 'recent' }, + { label: 'По просмотрам', value: 'views' }, ], type: FilterTypes.Picker, }, genre: { - label: "Жанры:", + label: 'Жанры:', value: { include: [], exclude: [] }, options: [ - { label: "Любой жанр", value: "all" }, - { label: "Альтернативная история", value: "alternative-history" }, - { label: "Антиутопия", value: "dystopia" }, - { label: "Бизнес-литература", value: "business-literature" }, - { label: "Боевая фантастика", value: "combat-fiction" }, - { label: "Боевик", value: "action" }, - { label: "Боевое фэнтези", value: "combat-fantasy" }, - { label: "Бояръ-Аниме", value: "boyar-anime" }, - { label: "Героическая фантастика", value: "heroic-fiction" }, - { label: "Героическое фэнтези", value: "heroic-fantasy" }, - { label: "Городское фэнтези", value: "urban-fantasy" }, - { label: "Гримдарк", value: "grimdark" }, - { label: "Детектив", value: "mystery" }, - { label: "Детская литература", value: "kids-literature" }, - { label: "Документальная проза", value: "biography" }, - { label: "Историческая проза", value: "historical-fiction" }, - { label: "Исторический детектив", value: "historical-mystery" }, - { label: "Исторический любовный роман", value: "historical-romantic-novel" }, - { label: "Историческое фэнтези", value: "historical-fantasy" }, - { label: "Киберпанк", value: "cyberpunk" }, - { label: "Космическая фантастика", value: "cosmic-fiction" }, - { label: "ЛитРПГ", value: "litrpg" }, - { label: "Лоу / Низкое фэнтези", value: "low-fantasy" }, - { label: "Любовное фэнтези", value: "romantic-fantasy" }, - { label: "Любовный роман", value: "romantic-novel" }, - { label: "Мистика", value: "mystic" }, - { label: "Мистический детектив", value: "mystic-detective" }, - { label: "Научная фантастика", value: "science-fiction" }, - { label: "Подростковая проза", value: "young-adult" }, - { label: "Политический роман", value: "political-romance" }, - { label: "Попаданцы", value: "accidental-travel" }, - { label: "Попаданцы в магические миры", value: "magic-worlds-travel" }, - { label: "Попаданцы во времени", value: "time-travel" }, - { label: "Порнотика", value: "pornotica" }, - { label: "Постапокалипсис", value: "post-apocalypse" }, - { label: "Поэзия", value: "poetry" }, - { label: "Приключения", value: "adventure" }, - { label: "Публицистика", value: "journalism" }, - { label: "Пьеса", value: "play" }, - { label: "Развитие личности", value: "how-to-book" }, - { label: "Разное", value: "other" }, - { label: "Реализм", value: "Realism" }, - { label: "РеалРПГ", value: "realrpg" }, - { label: "Репликация", value: "replication" }, - { label: "Романтическая эротика", value: "romantic-erotic-fiction" }, - { label: "Сказка", value: "fairy-tale" }, - { label: "Слэш", value: "slash" }, - { label: "Современная проза", value: "modern-prose" }, - { label: "Современный любовный роман", value: "modern-romantic-novel" }, - { label: "Социальная фантастика", value: "social-fiction" }, - { label: "Стимпанк", value: "steampunk" }, - { label: "Сценарий", value: "scenario" }, - { label: "Сюаньхуань", value: "xuanhuan" }, - { label: "Сянься", value: "xianxia" }, - { label: "Тёмное фэнтези", value: "dark-fantasy" }, - { label: "Триллер", value: "thriller" }, - { label: "Уся", value: "wuxia" }, - { label: "Фантастика", value: "fiction" }, - { label: "Фантастический детектив", value: "mystery-fiction" }, - { label: "Фанфик", value: "fan-fiction" }, - { label: "Фемслэш", value: "femslash" }, - { label: "Фэнтези", value: "fantasy" }, - { label: "Хоррор", value: "horror" }, - { label: "Шпионский детектив", value: "spy-crime" }, - { label: "Эпическое фэнтези", value: "epic-fantasy" }, - { label: "Эротика", value: "erotic-fiction" }, - { label: "Эротическая фантастика", value: "erotic-fiction" }, - { label: "Эротический фанфик", value: "erotic-fan-fiction" }, - { label: "Эротическое фэнтези", value: "erotic-fantasy" }, - { label: "Этническое фэнтези", value: "ethnic-fantasy" }, - { label: "Юмор", value: "humor" }, - { label: "Юмористическая фантастика", value: "humor-fiction" }, - { label: "Юмористическое фэнтези", value: "humor-fantasy" }, - { label: "RPS", value: "rps" }, + { label: 'Любой жанр', value: 'all' }, + { label: 'Альтернативная история', value: 'alternative-history' }, + { label: 'Антиутопия', value: 'dystopia' }, + { label: 'Бизнес-литература', value: 'business-literature' }, + { label: 'Боевая фантастика', value: 'combat-fiction' }, + { label: 'Боевик', value: 'action' }, + { label: 'Боевое фэнтези', value: 'combat-fantasy' }, + { label: 'Бояръ-Аниме', value: 'boyar-anime' }, + { label: 'Героическая фантастика', value: 'heroic-fiction' }, + { label: 'Героическое фэнтези', value: 'heroic-fantasy' }, + { label: 'Городское фэнтези', value: 'urban-fantasy' }, + { label: 'Гримдарк', value: 'grimdark' }, + { label: 'Детектив', value: 'mystery' }, + { label: 'Детская литература', value: 'kids-literature' }, + { label: 'Документальная проза', value: 'biography' }, + { label: 'Историческая проза', value: 'historical-fiction' }, + { label: 'Исторический детектив', value: 'historical-mystery' }, + { + label: 'Исторический любовный роман', + value: 'historical-romantic-novel', + }, + { label: 'Историческое фэнтези', value: 'historical-fantasy' }, + { label: 'Киберпанк', value: 'cyberpunk' }, + { label: 'Космическая фантастика', value: 'cosmic-fiction' }, + { label: 'ЛитРПГ', value: 'litrpg' }, + { label: 'Лоу / Низкое фэнтези', value: 'low-fantasy' }, + { label: 'Любовное фэнтези', value: 'romantic-fantasy' }, + { label: 'Любовный роман', value: 'romantic-novel' }, + { label: 'Мистика', value: 'mystic' }, + { label: 'Мистический детектив', value: 'mystic-detective' }, + { label: 'Научная фантастика', value: 'science-fiction' }, + { label: 'Подростковая проза', value: 'young-adult' }, + { label: 'Политический роман', value: 'political-romance' }, + { label: 'Попаданцы', value: 'accidental-travel' }, + { label: 'Попаданцы в магические миры', value: 'magic-worlds-travel' }, + { label: 'Попаданцы во времени', value: 'time-travel' }, + { label: 'Порнотика', value: 'pornotica' }, + { label: 'Постапокалипсис', value: 'post-apocalypse' }, + { label: 'Поэзия', value: 'poetry' }, + { label: 'Приключения', value: 'adventure' }, + { label: 'Публицистика', value: 'journalism' }, + { label: 'Пьеса', value: 'play' }, + { label: 'Развитие личности', value: 'how-to-book' }, + { label: 'Разное', value: 'other' }, + { label: 'Реализм', value: 'Realism' }, + { label: 'РеалРПГ', value: 'realrpg' }, + { label: 'Репликация', value: 'replication' }, + { label: 'Романтическая эротика', value: 'romantic-erotic-fiction' }, + { label: 'Сказка', value: 'fairy-tale' }, + { label: 'Слэш', value: 'slash' }, + { label: 'Современная проза', value: 'modern-prose' }, + { label: 'Современный любовный роман', value: 'modern-romantic-novel' }, + { label: 'Социальная фантастика', value: 'social-fiction' }, + { label: 'Стимпанк', value: 'steampunk' }, + { label: 'Сценарий', value: 'scenario' }, + { label: 'Сюаньхуань', value: 'xuanhuan' }, + { label: 'Сянься', value: 'xianxia' }, + { label: 'Тёмное фэнтези', value: 'dark-fantasy' }, + { label: 'Триллер', value: 'thriller' }, + { label: 'Уся', value: 'wuxia' }, + { label: 'Фантастика', value: 'fiction' }, + { label: 'Фантастический детектив', value: 'mystery-fiction' }, + { label: 'Фанфик', value: 'fan-fiction' }, + { label: 'Фемслэш', value: 'femslash' }, + { label: 'Фэнтези', value: 'fantasy' }, + { label: 'Хоррор', value: 'horror' }, + { label: 'Шпионский детектив', value: 'spy-crime' }, + { label: 'Эпическое фэнтези', value: 'epic-fantasy' }, + { label: 'Эротика', value: 'erotic-fiction' }, + { label: 'Эротическая фантастика', value: 'erotic-fiction' }, + { label: 'Эротический фанфик', value: 'erotic-fan-fiction' }, + { label: 'Эротическое фэнтези', value: 'erotic-fantasy' }, + { label: 'Этническое фэнтези', value: 'ethnic-fantasy' }, + { label: 'Юмор', value: 'humor' }, + { label: 'Юмористическая фантастика', value: 'humor-fiction' }, + { label: 'Юмористическое фэнтези', value: 'humor-fantasy' }, + { label: 'RPS', value: 'rps' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, status: { - label: "Статус:", - value: "all", + label: 'Статус:', + value: 'all', options: [ - { label: "Любой статус", value: "all" }, - { label: "В процессе", value: "in-process" }, - { label: "Завершено", value: "finished" }, + { label: 'Любой статус', value: 'all' }, + { label: 'В процессе', value: 'in-process' }, + { label: 'Завершено', value: 'finished' }, ], type: FilterTypes.Picker, }, access: { - label: "Доступ:", - value: "all", + label: 'Доступ:', + value: 'all', options: [ - { label: "Любой доступ", value: "all" }, - { label: "Бесплатные", value: "free" }, - { label: "Платные", value: "paid" }, + { label: 'Любой доступ', value: 'all' }, + { label: 'Бесплатные', value: 'free' }, + { label: 'Платные', value: 'paid' }, ], type: FilterTypes.Picker, }, adult: { - label: "Возрастные ограничения:", - value: "hide", + label: 'Возрастные ограничения:', + value: 'hide', options: [ - { label: "Скрыть 18+", value: "hide" }, - { label: "Показать +18", value: "show" }, + { label: 'Скрыть 18+', value: 'hide' }, + { label: 'Показать +18', value: 'show' }, ], type: FilterTypes.Picker, }, diff --git a/plugins/russian/authortoday.ts b/plugins/russian/authortoday.ts index cf50a3a..c12eba0 100644 --- a/plugins/russian/authortoday.ts +++ b/plugins/russian/authortoday.ts @@ -1,39 +1,39 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { defaultCover } from "@libs/defaultCover"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; -const apiUrl = "https://api.author.today/"; -const token = "Bearer guest"; +const apiUrl = 'https://api.author.today/'; +const token = 'Bearer guest'; class AuthorToday implements Plugin.PluginBase { - id = "AT"; - name = "Автор Тудей"; - icon = "src/ru/authortoday/icon.png"; - site = "https://author.today"; - version = "1.0.0"; + id = 'AT'; + name = 'Автор Тудей'; + icon = 'src/ru/authortoday/icon.png'; + site = 'https://author.today'; + version = '1.0.0'; async popularNovels( pageNo: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions, ): Promise { - let url = apiUrl + "v1/catalog/search?page=" + pageNo; + let url = apiUrl + 'v1/catalog/search?page=' + pageNo; if (filters?.genre?.value) { - url += "&genre=" + filters.genre.value; + url += '&genre=' + filters.genre.value; } url += - "&sorting=" + - (showLatestNovels ? "recent" : filters?.sort?.value || "popular"); + '&sorting=' + + (showLatestNovels ? 'recent' : filters?.sort?.value || 'popular'); - url += "&form=" + (filters?.form?.value || "any"); - url += "&state=" + (filters?.state?.value || "any"); - url += "&series=" + (filters?.series?.value || "any"); - url += "&access=" + (filters?.access?.value || "any"); - url += "&promo=" + (filters?.promo?.value || "hide"); + url += '&form=' + (filters?.form?.value || 'any'); + url += '&state=' + (filters?.state?.value || 'any'); + url += '&series=' + (filters?.series?.value || 'any'); + url += '&access=' + (filters?.access?.value || 'any'); + url += '&promo=' + (filters?.promo?.value || 'hide'); const result = await fetchApi(url, { headers: { @@ -43,15 +43,15 @@ class AuthorToday implements Plugin.PluginBase { const json = (await result.json()) as response; const novels: Plugin.NovelItem[] = []; - if (json.code === "NotFound") { + if (json.code === 'NotFound') { return novels; } - json?.searchResults?.forEach((novel) => + json?.searchResults?.forEach(novel => novels.push({ name: novel.title, cover: novel.coverUrl - ? "https://cm.author.today/content/" + novel.coverUrl + ? 'https://cm.author.today/content/' + novel.coverUrl : defaultCover, path: novel.id.toString(), }), @@ -71,24 +71,24 @@ class AuthorToday implements Plugin.PluginBase { const novel: Plugin.SourceNovel = { path: workID, name: book.title, - cover: book.coverUrl ? book.coverUrl.split("?")[0] : defaultCover, - genres: book.tags?.join(", "), - summary: "", + cover: book.coverUrl ? book.coverUrl.split('?')[0] : defaultCover, + genres: book.tags?.join(', '), + summary: '', author: book.originalAuthor || book.authorFIO || book.coAuthorFIO || book.secondCoAuthorFIO || book.translator || - "", + '', status: book.isFinished ? NovelStatus.Completed : NovelStatus.Ongoing, }; if (book.annotation) { - novel.summary += book.annotation + "\n"; + novel.summary += book.annotation + '\n'; } if (book.authorNotes) { - novel.summary += "Примечания автора:\n" + book.authorNotes; + novel.summary += 'Примечания автора:\n' + book.authorNotes; } const chaptersRaw = await fetchApi(`${apiUrl}v1/work/${workID}/content`, { @@ -103,11 +103,11 @@ class AuthorToday implements Plugin.PluginBase { chaptersJSON.forEach((chapter, chapterIndex) => { if (chapter.isAvailable && !chapter.isDraft) { chapters.push({ - name: chapter.title || "Глава " + (chapterIndex + 1), - path: workID + "/" + chapter.id, + name: chapter.title || 'Глава ' + (chapterIndex + 1), + path: workID + '/' + chapter.id, releaseTime: dayjs( chapter.publishTime || chapter.lastModificationTime, - ).format("LLL"), + ).format('LLL'), chapterNumber: (chapter.sortOrder || chapterIndex) + 1, }); } @@ -118,7 +118,7 @@ class AuthorToday implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const [workID, chapterID] = chapterPath.split("/"); + const [workID, chapterID] = chapterPath.split('/'); const result = await fetchApi( apiUrl + `v1/work/${workID}/chapter/${chapterID}/text`, { @@ -130,11 +130,11 @@ class AuthorToday implements Plugin.PluginBase { const json = (await result.json()) as encryptedСhapter; if (json.code) { - return json.code + "\n" + json?.message; + return json.code + '\n' + json?.message; } - const key = json.key.split("").reverse().join("") + "@_@"; - let text = ""; + const key = json.key.split('').reverse().join('') + '@_@'; + let text = ''; for (let i = 0; i < json.text.length; i++) { text += String.fromCharCode( @@ -143,10 +143,10 @@ class AuthorToday implements Plugin.PluginBase { } const loadedCheerio = parseHTML(text); - loadedCheerio("img").each((index, element) => { - if (!loadedCheerio(element).attr("src")?.startsWith("http")) { - const src = loadedCheerio(element).attr("src"); - loadedCheerio(element).attr("src", this.site + src); + loadedCheerio('img').each((index, element) => { + if (!loadedCheerio(element).attr('src')?.startsWith('http')) { + const src = loadedCheerio(element).attr('src'); + loadedCheerio(element).attr('src', this.site + src); } }); const chapterText = loadedCheerio.html(); @@ -158,23 +158,23 @@ class AuthorToday implements Plugin.PluginBase { pageNo: number | undefined = 1, ): Promise { const url = - this.site + "/search?category=works&q=" + searchTerm + "&page=" + pageNo; - const result = await fetchApi(url).then((res) => res.text()); + this.site + '/search?category=works&q=' + searchTerm + '&page=' + pageNo; + const result = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(result); const novels: Plugin.NovelItem[] = []; - loadedCheerio("a.work-row").each((index, element) => { + loadedCheerio('a.work-row').each((index, element) => { const name = loadedCheerio(element) .find('h4[class="work-title"]') .text() .trim(); - let cover = loadedCheerio(element).find("img").attr("data-src"); - const path = loadedCheerio(element).attr("href"); + let cover = loadedCheerio(element).find('img').attr('data-src'); + const path = loadedCheerio(element).attr('href'); - cover = cover ? cover.split("?")[0] : defaultCover; + cover = cover ? cover.split('?')[0] : defaultCover; if (!path) return; - novels.push({ name, cover, path: path.replace("/work/", "") }); + novels.push({ name, cover, path: path.replace('/work/', '') }); }); return novels; @@ -182,143 +182,149 @@ class AuthorToday implements Plugin.PluginBase { fetchImage = fetchFile; resolveUrl = (path: string, isNovel?: boolean) => - isNovel ? this.site + "/work/" + path : this.site + "/reader/" + path; + isNovel ? this.site + '/work/' + path : this.site + '/reader/' + path; filters = { sort: { - label: "Сортировка", - value: "popular", + label: 'Сортировка', + value: 'popular', options: [ - { label: "По популярности", value: "popular" }, - { label: "По количеству лайков", value: "likes" }, - { label: "По комментариям", value: "comments" }, - { label: "По новизне", value: "recent" }, - { label: "По просмотрам", value: "views" }, - { label: "Набирающие популярность", value: "trending" }, + { label: 'По популярности', value: 'popular' }, + { label: 'По количеству лайков', value: 'likes' }, + { label: 'По комментариям', value: 'comments' }, + { label: 'По новизне', value: 'recent' }, + { label: 'По просмотрам', value: 'views' }, + { label: 'Набирающие популярность', value: 'trending' }, ], type: FilterTypes.Picker, }, genre: { - label: "Жанры", - value: "", + label: 'Жанры', + value: '', options: [ - { label: "Все", value: "" }, - { label: "Альтернативная история", value: "sf-history" }, - { label: "Антиутопия", value: "dystopia" }, - { label: "Бизнес-литература", value: "biznes-literatura" }, - { label: "Боевая фантастика", value: "sf-action" }, - { label: "Боевик", value: "action" }, - { label: "Боевое фэнтези", value: "fantasy-action" }, - { label: "Бояръ-Аниме", value: "boyar-anime" }, - { label: "Героическая фантастика", value: "sf-heroic" }, - { label: "Героическое фэнтези", value: "heroic-fantasy" }, - { label: "Городское фэнтези", value: "urban-fantasy" }, - { label: "Детектив", value: "detective" }, - { label: "Детская литература", value: "detskaya-literatura" }, - { label: "Документальная проза", value: "non-fiction" }, - { label: "Историческая проза", value: "historical-fiction" }, - { label: "Исторические приключения", value: "historical-adventure" }, - { label: "Исторический детектив", value: "historical-mystery" }, - { label: "Исторический любовный роман", value: "historical-romance" }, - { label: "Историческое фэнтези", value: "historical-fantasy" }, - { label: "Киберпанк", value: "cyberpunk" }, - { label: "Короткий любовный роман", value: "short-romance" }, - { label: "Космическая фантастика", value: "sf-space" }, - { label: "ЛитРПГ", value: "litrpg" }, - { label: "Любовное фэнтези", value: "love-fantasy" }, - { label: "Любовные романы", value: "romance" }, - { label: "Мистика", value: "paranormal" }, - { label: "Назад в СССР", value: "back-to-ussr" }, - { label: "Научная фантастика", value: "science-fiction" }, - { label: "Подростковая проза", value: "teen-prose" }, - { label: "Политический роман", value: "political-fiction" }, - { label: "Попаданцы", value: "popadantsy" }, - { label: "Попаданцы в космос", value: "popadantsy-v-kosmos" }, - { label: "Попаданцы в магические миры", value: "popadantsy-v-magicheskie-miry" }, - { label: "Попаданцы во времени", value: "popadantsy-vo-vremeni" }, - { label: "Постапокалипсис", value: "postapocalyptic" }, - { label: "Поэзия", value: "poetry" }, - { label: "Приключения", value: "adventure" }, - { label: "Публицистика", value: "publicism" }, - { label: "Развитие личности", value: "razvitie-lichnosti" }, - { label: "Разное", value: "other" }, - { label: "РеалРПГ", value: "realrpg" }, - { label: "Романтическая эротика", value: "romantic-erotika" }, - { label: "Сказка", value: "fairy-tale" }, - { label: "Современная проза", value: "modern-prose" }, - { label: "Современный любовный роман", value: "contemporary-romance" }, - { label: "Социальная фантастика", value: "sf-social" }, - { label: "Стимпанк", value: "steampunk" }, - { label: "Темное фэнтези", value: "dark-fantasy" }, - { label: "Триллер", value: "thriller" }, - { label: "Ужасы", value: "horror" }, - { label: "Фантастика", value: "sci-fi" }, - { label: "Фантастический детектив", value: "detective-science-fiction" }, - { label: "Фанфик", value: "fanfiction" }, - { label: "Фэнтези", value: "fantasy" }, - { label: "Шпионский детектив", value: "spy-mystery" }, - { label: "Эпическое фэнтези", value: "epic-fantasy" }, - { label: "Эротика", value: "erotica" }, - { label: "Эротическая фантастика", value: "sf-erotika" }, - { label: "Эротический фанфик", value: "fanfiction-erotika" }, - { label: "Эротическое фэнтези", value: "fantasy-erotika" }, - { label: "Юмор", value: "humor" }, - { label: "Юмористическая фантастика", value: "sf-humor" }, - { label: "Юмористическое фэнтези", value: "ironical-fantasy" }, + { label: 'Все', value: '' }, + { label: 'Альтернативная история', value: 'sf-history' }, + { label: 'Антиутопия', value: 'dystopia' }, + { label: 'Бизнес-литература', value: 'biznes-literatura' }, + { label: 'Боевая фантастика', value: 'sf-action' }, + { label: 'Боевик', value: 'action' }, + { label: 'Боевое фэнтези', value: 'fantasy-action' }, + { label: 'Бояръ-Аниме', value: 'boyar-anime' }, + { label: 'Героическая фантастика', value: 'sf-heroic' }, + { label: 'Героическое фэнтези', value: 'heroic-fantasy' }, + { label: 'Городское фэнтези', value: 'urban-fantasy' }, + { label: 'Детектив', value: 'detective' }, + { label: 'Детская литература', value: 'detskaya-literatura' }, + { label: 'Документальная проза', value: 'non-fiction' }, + { label: 'Историческая проза', value: 'historical-fiction' }, + { label: 'Исторические приключения', value: 'historical-adventure' }, + { label: 'Исторический детектив', value: 'historical-mystery' }, + { label: 'Исторический любовный роман', value: 'historical-romance' }, + { label: 'Историческое фэнтези', value: 'historical-fantasy' }, + { label: 'Киберпанк', value: 'cyberpunk' }, + { label: 'Короткий любовный роман', value: 'short-romance' }, + { label: 'Космическая фантастика', value: 'sf-space' }, + { label: 'ЛитРПГ', value: 'litrpg' }, + { label: 'Любовное фэнтези', value: 'love-fantasy' }, + { label: 'Любовные романы', value: 'romance' }, + { label: 'Мистика', value: 'paranormal' }, + { label: 'Назад в СССР', value: 'back-to-ussr' }, + { label: 'Научная фантастика', value: 'science-fiction' }, + { label: 'Подростковая проза', value: 'teen-prose' }, + { label: 'Политический роман', value: 'political-fiction' }, + { label: 'Попаданцы', value: 'popadantsy' }, + { label: 'Попаданцы в космос', value: 'popadantsy-v-kosmos' }, + { + label: 'Попаданцы в магические миры', + value: 'popadantsy-v-magicheskie-miry', + }, + { label: 'Попаданцы во времени', value: 'popadantsy-vo-vremeni' }, + { label: 'Постапокалипсис', value: 'postapocalyptic' }, + { label: 'Поэзия', value: 'poetry' }, + { label: 'Приключения', value: 'adventure' }, + { label: 'Публицистика', value: 'publicism' }, + { label: 'Развитие личности', value: 'razvitie-lichnosti' }, + { label: 'Разное', value: 'other' }, + { label: 'РеалРПГ', value: 'realrpg' }, + { label: 'Романтическая эротика', value: 'romantic-erotika' }, + { label: 'Сказка', value: 'fairy-tale' }, + { label: 'Современная проза', value: 'modern-prose' }, + { label: 'Современный любовный роман', value: 'contemporary-romance' }, + { label: 'Социальная фантастика', value: 'sf-social' }, + { label: 'Стимпанк', value: 'steampunk' }, + { label: 'Темное фэнтези', value: 'dark-fantasy' }, + { label: 'Триллер', value: 'thriller' }, + { label: 'Ужасы', value: 'horror' }, + { label: 'Фантастика', value: 'sci-fi' }, + { + label: 'Фантастический детектив', + value: 'detective-science-fiction', + }, + { label: 'Фанфик', value: 'fanfiction' }, + { label: 'Фэнтези', value: 'fantasy' }, + { label: 'Шпионский детектив', value: 'spy-mystery' }, + { label: 'Эпическое фэнтези', value: 'epic-fantasy' }, + { label: 'Эротика', value: 'erotica' }, + { label: 'Эротическая фантастика', value: 'sf-erotika' }, + { label: 'Эротический фанфик', value: 'fanfiction-erotika' }, + { label: 'Эротическое фэнтези', value: 'fantasy-erotika' }, + { label: 'Юмор', value: 'humor' }, + { label: 'Юмористическая фантастика', value: 'sf-humor' }, + { label: 'Юмористическое фэнтези', value: 'ironical-fantasy' }, ], type: FilterTypes.Picker, }, form: { - label: "Форма произведения", - value: "any", + label: 'Форма произведения', + value: 'any', options: [ - { label: "Любой", value: "any" }, - { label: "Перевод", value: "translation" }, - { label: "Повесть", value: "tale" }, - { label: "Рассказ", value: "story" }, - { label: "Роман", value: "novel" }, - { label: "Сборник поэзии", value: "poetry" }, - { label: "Сборник рассказов", value: "story-book" }, + { label: 'Любой', value: 'any' }, + { label: 'Перевод', value: 'translation' }, + { label: 'Повесть', value: 'tale' }, + { label: 'Рассказ', value: 'story' }, + { label: 'Роман', value: 'novel' }, + { label: 'Сборник поэзии', value: 'poetry' }, + { label: 'Сборник рассказов', value: 'story-book' }, ], type: FilterTypes.Picker, }, state: { - label: "Статус произведения", - value: "any", + label: 'Статус произведения', + value: 'any', options: [ - { label: "Любой статус", value: "any" }, - { label: "В процессе", value: "in-progress" }, - { label: "Завершено", value: "finished" }, + { label: 'Любой статус', value: 'any' }, + { label: 'В процессе', value: 'in-progress' }, + { label: 'Завершено', value: 'finished' }, ], type: FilterTypes.Picker, }, series: { - label: "Статус цикла", - value: "any", + label: 'Статус цикла', + value: 'any', options: [ - { label: "Не важно", value: "any" }, - { label: "Вне цикла", value: "out" }, - { label: "Цикл завершен", value: "finished" }, - { label: "Цикл не завершен", value: "unfinished" }, + { label: 'Не важно', value: 'any' }, + { label: 'Вне цикла', value: 'out' }, + { label: 'Цикл завершен', value: 'finished' }, + { label: 'Цикл не завершен', value: 'unfinished' }, ], type: FilterTypes.Picker, }, access: { - label: "Тип доступа", - value: "any", + label: 'Тип доступа', + value: 'any', options: [ - { label: "Любой", value: "any" }, - { label: "Платный", value: "paid" }, - { label: "Бесплатный", value: "free" }, + { label: 'Любой', value: 'any' }, + { label: 'Платный', value: 'paid' }, + { label: 'Бесплатный', value: 'free' }, ], type: FilterTypes.Picker, }, promo: { - label: "Промо-фрагмент", - value: "hide", + label: 'Промо-фрагмент', + value: 'hide', options: [ - { label: "Скрывать", value: "hide" }, - { label: "Показывать", value: "show" }, + { label: 'Скрывать', value: 'hide' }, + { label: 'Показывать', value: 'show' }, ], type: FilterTypes.Picker, }, diff --git a/plugins/russian/bookriver.ts b/plugins/russian/bookriver.ts index 7864a66..4cd5046 100644 --- a/plugins/russian/bookriver.ts +++ b/plugins/russian/bookriver.ts @@ -1,16 +1,16 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; class Bookriver implements Plugin.PluginBase { - id = "bookriver"; - name = "Bookriver"; - site = "https://bookriver.ru"; - version = "1.0.0"; - icon = "src/ru/bookriver/icon.png"; + id = 'bookriver'; + name = 'Bookriver'; + site = 'https://bookriver.ru'; + version = '1.0.0'; + icon = 'src/ru/bookriver/icon.png'; async popularNovels( pageNo: number, @@ -19,23 +19,23 @@ class Bookriver implements Plugin.PluginBase { filters, }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/genre?page=" + pageNo + "&perPage=24&sortingType="; + let url = this.site + '/genre?page=' + pageNo + '&perPage=24&sortingType='; url += showLatestNovels - ? "last-update" - : filters?.sort?.value || "bestseller"; + ? 'last-update' + : filters?.sort?.value || 'bestseller'; if (filters?.genres?.value?.length) { - url += "&g=" + filters.genres.value.join(","); + url += '&g=' + filters.genres.value.join(','); } - const result = await fetchApi(url).then((res) => res.text()); + const result = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(result); const novels: Plugin.NovelItem[] = []; - const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); + const jsonRaw = loadedCheerio('#__NEXT_DATA__').html(); if (jsonRaw) { const json: response = JSON.parse(jsonRaw); - json.props.pageProps.state.pagesFilter?.genre?.books?.forEach((novel) => + json.props.pageProps.state.pagesFilter?.genre?.books?.forEach(novel => novels.push({ name: novel.name, cover: novel.coverImages[0].url, @@ -47,23 +47,24 @@ class Bookriver implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.resolveUrl(novelPath, true)) - .then((res) => res.text()); + const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); const loadedCheerio = parseHTML(result); - const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); - const json: response = JSON.parse(jsonRaw || "{}"); + const jsonRaw = loadedCheerio('#__NEXT_DATA__').html(); + const json: response = JSON.parse(jsonRaw || '{}'); const book = json.props.pageProps.state.book?.bookPage; const novel: Plugin.SourceNovel = { path: novelPath, - name: book?.name || "", + name: book?.name || '', cover: book?.coverImages[0].url, summary: book?.annotation, author: book?.author?.name, - genres: book?.tags?.map((tag) => tag.name).join(", "), + genres: book?.tags?.map(tag => tag.name).join(', '), status: - book?.statusComplete === "writing" + book?.statusComplete === 'writing' ? NovelStatus.Ongoing : NovelStatus.Completed, }; @@ -73,10 +74,10 @@ class Bookriver implements Plugin.PluginBase { if (chapter.available) { chapters.push({ name: chapter.name, - path: book?.slug + "/" + chapter.chapterId, + path: book?.slug + '/' + chapter.chapterId, releaseTime: dayjs( chapter.firstPublishedAt || chapter.createdAt || undefined, - ).format("LLL"), + ).format('LLL'), chapterNumber: chapterIndex + 1, }); } @@ -86,13 +87,13 @@ class Bookriver implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const url = "https://api.bookriver.ru/api/v1/books/chapter/text/"; - const result = await fetchApi(url + chapterPath.split("/").pop()); + const url = 'https://api.bookriver.ru/api/v1/books/chapter/text/'; + const result = await fetchApi(url + chapterPath.split('/').pop()); const json = (await result.json()) as responseChapter; - let chapterText = json.data.content || "Конец произведения"; + let chapterText = json.data.content || 'Конец произведения'; if (json.data?.audio?.available) { - chapterText += "\n" + json.data.audio.url; + chapterText += '\n' + json.data.audio.url; } return chapterText; @@ -103,13 +104,16 @@ class Bookriver implements Plugin.PluginBase { pageNo: number | undefined = 1, ): Promise { const url = - "https://api.bookriver.ru/api/v1/search/autocomplete?keyword=" + searchTerm + - "&page=" + pageNo + "&perPage=10"; + 'https://api.bookriver.ru/api/v1/search/autocomplete?keyword=' + + searchTerm + + '&page=' + + pageNo + + '&perPage=10'; const result = await fetchApi(url); const json = (await result.json()) as responseSearch; const novels: Plugin.NovelItem[] = []; - json.data?.books?.forEach((novel) => + json.data?.books?.forEach(novel => novels.push({ name: novel.name, cover: novel.coverImages[0].url, @@ -122,69 +126,84 @@ class Bookriver implements Plugin.PluginBase { fetchImage = fetchFile; resolveUrl = (path: string, isNovel?: boolean) => - this.site + (isNovel ? "/book/" : "/reader/") + path; + this.site + (isNovel ? '/book/' : '/reader/') + path; filters = { sort: { - label: "Сортировка", - value: "bestseller", + label: 'Сортировка', + value: 'bestseller', options: [ - { label: "Бестселлеры", value: "bestseller" }, - { label: "Дате добавления", value: "newest" }, - { label: "Дате обновления", value: "last-update" }, + { label: 'Бестселлеры', value: 'bestseller' }, + { label: 'Дате добавления', value: 'newest' }, + { label: 'Дате обновления', value: 'last-update' }, ], type: FilterTypes.Picker, }, genres: { - label: "жанры", + label: 'жанры', value: [], options: [ - { label: "Альтернативная история", value: "alternativnaya-istoriya" }, - { label: "Боевая фантастика", value: "boevaya-fantastika" }, - { label: "Боевое фэнтези", value: "boevoe-fentezi" }, - { label: "Бытовое фэнтези", value: "bytovoe-fentezi" }, - { label: "Героическая фантастика", value: "geroicheskaya-fantastika" }, - { label: "Героическое фэнтези", value: "geroicheskoe-fentezi" }, - { label: "Городское фэнтези", value: "gorodskoe-fentezi" }, - { label: "Детектив", value: "detektiv" }, - { label: "Детективная фантастика", value: "detektivnaya-fantastika" }, - { label: "Жёсткая эротика", value: "zhyostkaya-erotika" }, - { label: "Исторический детектив", value: "istoricheskii-detektiv" }, - { label: "Исторический любовный роман", value: "istoricheskii-lyubovnyi-roman" }, - { label: "Историческое фэнтези", value: "istoricheskoe-fentezi" }, - { label: "Киберпанк", value: "kiberpank" }, - { label: "Классический детектив", value: "klassicheskii-detektiv" }, - { label: "Короткий любовный роман", value: "korotkii-lyubovnyi-roman" }, - { label: "Космическая фантастика", value: "kosmicheskaya-fantastika" }, - { label: "Криминальный детектив", value: "kriminalnyi-detektiv" }, - { label: "ЛитРПГ", value: "litrpg" }, - { label: "Любовная фантастика", value: "lyubovnaya-fantastika" }, - { label: "Любовное фэнтези", value: "lyubovnoe-fentezi" }, - { label: "Любовный роман", value: "lyubovnyi-roman" }, - { label: "Мистика", value: "mistika" }, - { label: "Молодежная проза", value: "molodezhnaya-proza" }, - { label: "Научная фантастика", value: "nauchnaya-fantastika" }, - { label: "Остросюжетный любовный роман", value: "ostrosyuzhetnyi-lyubovnyi-roman" }, - { label: "Политический детектив", value: "politicheskii-detektiv" }, - { label: "Попаданцы", value: "popadantsy" }, - { label: "Постапокалипсис", value: "postapokalipsis" }, - { label: "Приключенческое фэнтези", value: "priklyuchencheskoe-fentezi" }, - { label: "Романтическая эротика", value: "romanticheskaya-erotika" }, - { label: "С элементами эротики", value: "s-elementami-erotiki" }, - { label: "Славянское фэнтези", value: "slavyanskoe-fentezi" }, - { label: "Современный любовный роман", value: "sovremennyi-lyubovnyi-roman" }, - { label: "Социальная фантастика", value: "sotsialnaya-fantastika" }, - { label: "Тёмное фэнтези", value: "temnoe-fentezi" }, - { label: "Фантастика", value: "fantastika" }, - { label: "Фэнтези", value: "fentezi" }, - { label: "Шпионский детектив", value: "shpionskii-detektiv" }, - { label: "Эпическое фэнтези", value: "epicheskoe-fentezi" }, - { label: "Эротика", value: "erotika" }, - { label: "Эротическая фантастика", value: "eroticheskaya-fantastika" }, - { label: "Эротический фанфик", value: "eroticheskii-fanfik" }, - { label: "Эротическое фэнтези", value: "eroticheskoe-fentezi" }, - { label: "Юмористический детектив", value: "yumoristicheskii-detektiv" }, - { label: "Юмористическое фэнтези", value: "yumoristicheskoe-fentezi" }, + { label: 'Альтернативная история', value: 'alternativnaya-istoriya' }, + { label: 'Боевая фантастика', value: 'boevaya-fantastika' }, + { label: 'Боевое фэнтези', value: 'boevoe-fentezi' }, + { label: 'Бытовое фэнтези', value: 'bytovoe-fentezi' }, + { label: 'Героическая фантастика', value: 'geroicheskaya-fantastika' }, + { label: 'Героическое фэнтези', value: 'geroicheskoe-fentezi' }, + { label: 'Городское фэнтези', value: 'gorodskoe-fentezi' }, + { label: 'Детектив', value: 'detektiv' }, + { label: 'Детективная фантастика', value: 'detektivnaya-fantastika' }, + { label: 'Жёсткая эротика', value: 'zhyostkaya-erotika' }, + { label: 'Исторический детектив', value: 'istoricheskii-detektiv' }, + { + label: 'Исторический любовный роман', + value: 'istoricheskii-lyubovnyi-roman', + }, + { label: 'Историческое фэнтези', value: 'istoricheskoe-fentezi' }, + { label: 'Киберпанк', value: 'kiberpank' }, + { label: 'Классический детектив', value: 'klassicheskii-detektiv' }, + { label: 'Короткий любовный роман', value: 'korotkii-lyubovnyi-roman' }, + { label: 'Космическая фантастика', value: 'kosmicheskaya-fantastika' }, + { label: 'Криминальный детектив', value: 'kriminalnyi-detektiv' }, + { label: 'ЛитРПГ', value: 'litrpg' }, + { label: 'Любовная фантастика', value: 'lyubovnaya-fantastika' }, + { label: 'Любовное фэнтези', value: 'lyubovnoe-fentezi' }, + { label: 'Любовный роман', value: 'lyubovnyi-roman' }, + { label: 'Мистика', value: 'mistika' }, + { label: 'Молодежная проза', value: 'molodezhnaya-proza' }, + { label: 'Научная фантастика', value: 'nauchnaya-fantastika' }, + { + label: 'Остросюжетный любовный роман', + value: 'ostrosyuzhetnyi-lyubovnyi-roman', + }, + { label: 'Политический детектив', value: 'politicheskii-detektiv' }, + { label: 'Попаданцы', value: 'popadantsy' }, + { label: 'Постапокалипсис', value: 'postapokalipsis' }, + { + label: 'Приключенческое фэнтези', + value: 'priklyuchencheskoe-fentezi', + }, + { label: 'Романтическая эротика', value: 'romanticheskaya-erotika' }, + { label: 'С элементами эротики', value: 's-elementami-erotiki' }, + { label: 'Славянское фэнтези', value: 'slavyanskoe-fentezi' }, + { + label: 'Современный любовный роман', + value: 'sovremennyi-lyubovnyi-roman', + }, + { label: 'Социальная фантастика', value: 'sotsialnaya-fantastika' }, + { label: 'Тёмное фэнтези', value: 'temnoe-fentezi' }, + { label: 'Фантастика', value: 'fantastika' }, + { label: 'Фэнтези', value: 'fentezi' }, + { label: 'Шпионский детектив', value: 'shpionskii-detektiv' }, + { label: 'Эпическое фэнтези', value: 'epicheskoe-fentezi' }, + { label: 'Эротика', value: 'erotika' }, + { label: 'Эротическая фантастика', value: 'eroticheskaya-fantastika' }, + { label: 'Эротический фанфик', value: 'eroticheskii-fanfik' }, + { label: 'Эротическое фэнтези', value: 'eroticheskoe-fentezi' }, + { + label: 'Юмористический детектив', + value: 'yumoristicheskii-detektiv', + }, + { label: 'Юмористическое фэнтези', value: 'yumoristicheskoe-fentezi' }, ], type: FilterTypes.CheckboxGroup, }, diff --git a/plugins/russian/ficbook.ts b/plugins/russian/ficbook.ts index f5d937d..9866ba3 100644 --- a/plugins/russian/ficbook.ts +++ b/plugins/russian/ficbook.ts @@ -1,17 +1,17 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import { defaultCover } from "@libs/defaultCover"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import dayjs from 'dayjs'; class ficbook implements Plugin.PluginBase { - id = "ficbook"; - name = "ficbook"; - site = "https://ficbook.net"; - version = "1.0.0"; - icon = "src/ru/ficbook/icon.png"; + id = 'ficbook'; + name = 'ficbook'; + site = 'https://ficbook.net'; + version = '1.0.0'; + icon = 'src/ru/ficbook/icon.png'; async popularNovels( pageNo: number, @@ -20,35 +20,37 @@ class ficbook implements Plugin.PluginBase { let url = this.site; if (filters?.directions?.value) { - url += "/popular-fanfics/" + filters.directions.value; + url += '/popular-fanfics/' + filters.directions.value; } else if (filters?.tags?.value) { - url += "/tags/" + filters.tags.value + "?p=" + pageNo; + url += '/tags/' + filters.tags.value + '?p=' + pageNo; } else { - url += "/" + (filters?.sort?.value || "fanfiction") + "?p=" + pageNo; + url += '/' + (filters?.sort?.value || 'fanfiction') + '?p=' + pageNo; } - const result = await fetchApi(url).then((res) => res.text()); + const result = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(result); const novels: Plugin.NovelItem[] = []; - loadedCheerio("article.fanfic-inline").each((index, element) => { - const name = loadedCheerio(element).find("h3 > a").text().trim(); - let cover = loadedCheerio(element).find("picture > img").attr("src"); - const url = loadedCheerio(element).find("h3 > a").attr("href"); + loadedCheerio('article.fanfic-inline').each((index, element) => { + const name = loadedCheerio(element).find('h3 > a').text().trim(); + let cover = loadedCheerio(element).find('picture > img').attr('src'); + const url = loadedCheerio(element).find('h3 > a').attr('href'); cover = cover - ? cover.replace(/covers\/m_|covers\/d_/g, "covers/") + ? cover.replace(/covers\/m_|covers\/d_/g, 'covers/') : defaultCover; if (!name || !url) return; - novels.push({ name, cover, path: url.replace(/\?.*/g, "") }); + novels.push({ name, cover, path: url.replace(/\?.*/g, '') }); }); return novels; } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + novelPath).then((res) => res.text()); + const result = await fetchApi(this.site + novelPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(result); const novel: Plugin.SourceNovel = { @@ -56,17 +58,17 @@ class ficbook implements Plugin.PluginBase { name: ( loadedCheerio('h1[itemprop="headline"]').text() || loadedCheerio('h1[itemprop="name"]').text() || - "" + '' ).trim(), }; - novel.cover = loadedCheerio('meta[property="og:image"]').attr("content"); + novel.cover = loadedCheerio('meta[property="og:image"]').attr('content'); novel.summary = loadedCheerio('div[itemprop="description"]').text().trim(); novel.author = loadedCheerio('a[itemprop="author"]').text(); novel.status = loadedCheerio( - "div.fanfic-main-info > section:nth-child(3) > div:nth-child(3) > span:nth-child(2)", - ).text() === "В процессе" + 'div.fanfic-main-info > section:nth-child(3) > div:nth-child(3) > span:nth-child(2)', + ).text() === 'В процессе' ? NovelStatus.Ongoing : NovelStatus.Completed; @@ -75,32 +77,34 @@ class ficbook implements Plugin.PluginBase { .get(); if (tags.length) { - novel.genres = tags.join(","); + novel.genres = tags.join(','); } - if (!novel.cover || novel.cover?.includes("/design/")) { + if (!novel.cover || novel.cover?.includes('/design/')) { novel.cover = defaultCover; } else { - novel.cover = novel.cover.replace(/covers\/m_|covers\/d_/g, "covers/"); + novel.cover = novel.cover.replace(/covers\/m_|covers\/d_/g, 'covers/'); } const chapters: Plugin.ChapterItem[] = []; - if (loadedCheerio("#content").length == 1) { - const name = loadedCheerio(".title-area > h2").text(); - const releaseTime = loadedCheerio(".part-date > span").attr("title"); + if (loadedCheerio('#content').length == 1) { + const name = loadedCheerio('.title-area > h2').text(); + const releaseTime = loadedCheerio('.part-date > span').attr('title'); if (name) chapters.push({ name, path: novelPath, releaseTime }); } else { - loadedCheerio("li.part").each((chapterIndex, element) => { - const name = loadedCheerio(element).find("h3").text(); - const url = loadedCheerio(element).find("a:nth-child(1)").attr("href"); + loadedCheerio('li.part').each((chapterIndex, element) => { + const name = loadedCheerio(element).find('h3').text(); + const url = loadedCheerio(element).find('a:nth-child(1)').attr('href'); if (!name || !url) return; - const releaseTime = loadedCheerio(element).find("div > span").attr("title"); + const releaseTime = loadedCheerio(element) + .find('div > span') + .attr('title'); chapters.push({ name, - path: url.replace(this.site, ""), + path: url.replace(this.site, ''), releaseTime, chapterNumber: chapterIndex + 1, }); @@ -112,22 +116,24 @@ class ficbook implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const result = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(result); - let chapterText = ""; + let chapterText = ''; - loadedCheerio("#content") + loadedCheerio('#content') .html() - ?.split("\n") - ?.forEach((line) => { + ?.split('\n') + ?.forEach(line => { if (line.trim()) { //blank line skip - if (line.includes("

")) { + if (line.includes('

')) { //new paragraph chapterText += line; } else { //plaintext - chapterText += "

" + line + "

"; + chapterText += '

' + line + '

'; } } }); @@ -140,21 +146,21 @@ class ficbook implements Plugin.PluginBase { pageNo: number | undefined = 1, ): Promise { const formData = new FormData(); - formData.append("term", searchTerm); - formData.append("page", pageNo.toString()); + formData.append('term', searchTerm); + formData.append('page', pageNo.toString()); - const result = await fetchApi(this.site + "/search/fanfic", { - method: "POST", + const result = await fetchApi(this.site + '/search/fanfic', { + method: 'POST', body: formData, }); const json = (await result.json()) as { data: Data }; const novels: Plugin.NovelItem[] = []; - json.data?.data?.forEach((novel) => { + json.data?.data?.forEach(novel => { const name = novel.title.trim(); - const path = "/readfic/" + novel.slug; + const path = '/readfic/' + novel.slug; const cover = novel.cover - ? "https://images.ficbook.net/fanfic-covers/" + novel.cover + ? 'https://images.ficbook.net/fanfic-covers/' + novel.cover : defaultCover; novels.push({ name, cover, path }); @@ -163,7 +169,7 @@ class ficbook implements Plugin.PluginBase { return novels; } - parseDate = (dateString: string | undefined = "") => { + parseDate = (dateString: string | undefined = '') => { const months: { [key: string]: number } = { января: 1, февраля: 2, @@ -179,9 +185,11 @@ class ficbook implements Plugin.PluginBase { декабря: 12, }; - const [day, month, year, , time] = dateString.split(" "); + const [day, month, year, , time] = dateString.split(' '); if (day && month && year && months[month] && time) { - return dayjs(year + "-" + months[month] + "-" + day + " " + time).format("LLL"); + return dayjs(year + '-' + months[month] + '-' + day + ' ' + time).format( + 'LLL', + ); } return dateString || null; }; @@ -190,2444 +198,2450 @@ class ficbook implements Plugin.PluginBase { filters = { sort: { - label: "Сортировка:", - value: "fanfiction", + label: 'Сортировка:', + value: 'fanfiction', options: [ - { label: "Горячие работы", value: "fanfiction" }, - { label: "Популярные ", value: "popular-fanfics" }, + { label: 'Горячие работы', value: 'fanfiction' }, + { label: 'Популярные ', value: 'popular-fanfics' }, ], type: FilterTypes.Picker, }, directions: { - label: "Направление:", - value: "", + label: 'Направление:', + value: '', options: [ - { label: "Все", value: "" }, - { label: "Джен", value: "gen" }, - { label: "Гет", value: "het" }, - { label: "Слэш", value: "slash-fics-3712917" }, - { label: "Фемслэш", value: "femslash-fanfics-9374932" }, - { label: "Статьи", value: "article" }, - { label: "Смешанный", value: "mixed" }, - { label: "Другой", value: "other" }, + { label: 'Все', value: '' }, + { label: 'Джен', value: 'gen' }, + { label: 'Гет', value: 'het' }, + { label: 'Слэш', value: 'slash-fics-3712917' }, + { label: 'Фемслэш', value: 'femslash-fanfics-9374932' }, + { label: 'Статьи', value: 'article' }, + { label: 'Смешанный', value: 'mixed' }, + { label: 'Другой', value: 'other' }, ], type: FilterTypes.Picker, }, tags: { - label: "Тэги:", - value: "", + label: 'Тэги:', + value: '', options: [ - { label: "Все", value: "" }, - { label: "#1fic1week", value: "884" }, - { label: "#7daystowrite", value: "2961" }, - { label: "#Бинго_ТФ", value: "1458" }, - { label: "#миниатюры3days", value: "2118" }, - { label: "#Не_Тайный_Санта", value: "2760" }, - { label: "#Писательские фанты-блиц", value: "2613" }, - { label: "#cosmowrite", value: "2284" }, - { label: "#femKHRweek", value: "2658" }, - { label: "#FourShips", value: "2588" }, - { label: "#Goretober", value: "1861" }, - { label: "#InternationalVampireDay / #WorldDraculaDay", value: "2048" }, - { label: "#KattWeek", value: "2717" }, - { label: "#Kinktober", value: "1867" }, - { label: "#LotorWeek", value: "2657" }, - { label: "#MerMay", value: "1122" }, - { label: "#NaNoWriMo", value: "1852" }, - { label: "#nsfweruriweek", value: "2944" }, - { label: "#RCMiaChallenge", value: "2700" }, - { label: "#SheithMonth", value: "2432" }, - { label: "#SixDrabbles", value: "2293" }, - { label: "#Writober", value: "1831" }, - { label: "1900-е годы", value: "369" }, - { label: "1910-е годы", value: "213" }, - { label: "1920-е годы", value: "81" }, - { label: "1930-е годы", value: "82" }, - { label: "1940-е годы", value: "83" }, - { label: "1950-е годы", value: "84" }, - { label: "1960-е годы", value: "85" }, - { label: "1970-е годы", value: "86" }, - { label: "1980-е годы", value: "87" }, - { label: "1990-е годы", value: "88" }, - { label: "2000-е годы", value: "590" }, - { label: "2010-е годы", value: "2562" }, - { label: "2014 год", value: "2977" }, - { label: "2P", value: "1910" }, - { label: "9 мая", value: "1019" }, - { label: "Аборт / Выкидыш", value: "306" }, - { label: "Аватары", value: "2401" }, - { label: "Авиатранспорт", value: "962" }, - { label: "Авиация", value: "455" }, - { label: "Аврорат", value: "1906" }, - { label: "Австралия", value: "767" }, - { label: "Австрия", value: "1024" }, - { label: "Автомастерские", value: "788" }, - { label: "Авторская пунктуация", value: "2886" }, - { label: "Авторские неологизмы", value: "1123" }, - { label: "Автоспорт", value: "856" }, - { label: "Автостоп", value: "1429" }, - { label: "Аддикции", value: "1230" }, - { label: "Адреналиновая зависимость", value: "1480" }, - { label: "Азартные игры", value: "1009" }, - { label: "Азкабан", value: "1922" }, - { label: "Аквакинез", value: "1380" }, - { label: "Аквапарки", value: "789" }, - { label: "Акростих", value: "911" }, - { label: "Актеры", value: "408" }, - { label: "Акустикофилия", value: "903" }, - { label: "Алекситимия", value: "2020" }, - { label: "Алкоголь", value: "201" }, - { label: "Алкогольные игры", value: "1534" }, - { label: "Аллергии", value: "746" }, - { label: "Алхимия", value: "1174" }, - { label: "Альбинизм", value: "1213" }, - { label: "Альпинизм", value: "1448" }, - { label: "Альтернативная мировая история", value: "442" }, - { label: "Альтернативное размножение", value: "2751" }, - { label: "Альтернативные судьбы", value: "2839" }, - { label: "Амазонки", value: "1424" }, - { label: "Амегакуре", value: "2571" }, - { label: "Американский футбол", value: "808" }, - { label: "Ампутация", value: "704" }, - { label: "Амфибии", value: "2792" }, - { label: "Анальгезия", value: "1254" }, - { label: "Анальный оргазм", value: "999" }, - { label: "Анальный секс", value: "215" }, - { label: "Анархия", value: "1175" }, - { label: "Анахронизмы", value: "780" }, - { label: "Ангелы", value: "91" }, - { label: "Ангелы-хранители", value: "1044" }, - { label: "Ангст", value: "1665" }, - { label: "Андрогинная внешность", value: "2602" }, - { label: "Андроиды", value: "155" }, - { label: "Анимагия", value: "2519" }, - { label: "Анимализм", value: "1443" }, - { label: "Аномальные зоны", value: "822" }, - { label: "Анонимный секс", value: "112" }, - { label: "Анрождение", value: "1246" }, - { label: "Антарктида", value: "2205" }, - { label: "Анти-Сью (Анти-Стью)", value: "1150" }, - { label: "Антигерои", value: "418" }, - { label: "Антизлодеи", value: "1833" }, - { label: "Антикварные магазины", value: "1496" }, - { label: "Антисоциальное расстройство личности", value: "734" }, - { label: "Антиутопия", value: "1700" }, - { label: "Античность", value: "237" }, - { label: "Апатия", value: "1850" }, - { label: "Аптеки", value: "1240" }, - { label: "Аргентина", value: "1146" }, - { label: "Арены", value: "940" }, - { label: "Аристократия", value: "16" }, - { label: "Арктика", value: "2426" }, - { label: "Армия", value: "22" }, - { label: "Аромантичные персонажи", value: "339" }, - { label: "Артефакты", value: "924" }, - { label: "Археологи", value: "405" }, - { label: "Архивы", value: "2594" }, - { label: "Архитекторы", value: "1011" }, - { label: "Асексофобия", value: "1120" }, - { label: "Асексуальные персонажи", value: "168" }, - { label: "Аскетизм", value: "606" }, - { label: "Асоциальность", value: "2019" }, - { label: "Ассистенты", value: "2641" }, - { label: "Астма", value: "1121" }, - { label: "Астральное проецирование", value: "2530" }, - { label: "Астральные миры", value: "1379" }, - { label: "Астрология", value: "476" }, - { label: "Астронавты", value: "2220" }, - { label: "Астрономия", value: "1372" }, - { label: "Асфиксия", value: "104" }, - { label: "Атеисты", value: "669" }, - { label: "Атлантида", value: "1144" }, - { label: "Атмокинез", value: "1964" }, - { label: "Атмосферная зарисовка", value: "2802" }, - { label: "Атомные электростанции", value: "2192" }, - { label: "Аудиокинез", value: "2849" }, - { label: "Аукционы", value: "607" }, - { label: "Аутоассасинофилия", value: "2025" }, - { label: "Аутоканнибализм", value: "2593" }, - { label: "Аутофелляция / Аутокуннилингус", value: "610" }, - { label: "Африка", value: "791" }, - { label: "Афродизиаки", value: "103" }, - { label: "Аффект", value: "938" }, - { label: "Аэрокинез", value: "2850" }, - { label: "Байкеры", value: "522" }, - { label: "Байронические герои", value: "2900" }, - { label: "Балет", value: "404" }, - { label: "Баллада", value: "2318" }, - { label: "Банки / Биржи", value: "937" }, - { label: "Банши", value: "250" }, - { label: "Барти Крауч-младший — не Пожиратель Смерти", value: "2795" }, - { label: "Бары", value: "142" }, - { label: "Баскетбол", value: "829" }, - { label: "Баски", value: "1763" }, - { label: "Бастарды", value: "603" }, - { label: "Батохаки", value: "2830" }, - { label: "Бедность", value: "979" }, - { label: "Без Волдеморта", value: "2160" }, - { label: "Без диалогов (стилизация)", value: "2885" }, - { label: "Без золотого трио", value: "2648" }, - { label: "Без Избранного (Гарри Поттер)", value: "2762" }, - { label: "Без канонических персонажей", value: "2694" }, - { label: "Без Таноса", value: "2381" }, - { label: "БезГражданки", value: "2418" }, - { label: "Бездомные", value: "916" }, - { label: "Безответственность", value: "2152" }, - { label: "Безумные ученые", value: "2559" }, - { label: "Безэмоциональность", value: "1488" }, - { label: "Бейсбол", value: "1489" }, - { label: "Беларусь", value: "1030" }, - { label: "Белый день", value: "683" }, - { label: "Белый стих", value: "1998" }, - { label: "Бельгия", value: "1039" }, - { label: "Беременность", value: "1712" }, - { label: "Бермудский треугольник", value: "1158" }, - { label: "Берсерки", value: "566" }, - { label: "Бесконтактный секс", value: "2214" }, - { label: "Бесплодие", value: "719" }, - { label: "Беспокойство", value: "2888" }, - { label: "Беспризорники", value: "604" }, - { label: "Беспричудный Изуку", value: "2422" }, - { label: "Бессмертие", value: "219" }, - { label: "Бессмертный конкурс 2023", value: "3016" }, - { label: "Бестелесность", value: "2607" }, - { label: "Би-персонажи", value: "300" }, - { label: "Библейские темы и мотивы", value: "1468" }, - { label: "Библиотекари", value: "2631" }, - { label: "Библиотеки", value: "310" }, - { label: "Бизнесмены / Бизнесвумен", value: "573" }, - { label: "Биологическое оружие", value: "1188" }, - { label: "Биопанк", value: "281" }, - { label: "Биороботы", value: "2256" }, - { label: "Биполярное расстройство", value: "690" }, - { label: "Битва за Хогвартс", value: "2774" }, - { label: "Битва Историй", value: "2960" }, - { label: "Благородные разбойники", value: "952" }, - { label: "Благотворительность", value: "613" }, - { label: "Бладплей", value: "109" }, - { label: "Ближний Восток", value: "1041" }, - { label: "Близкие враги", value: "721" }, - { label: "Близнецы", value: "295" }, - { label: "Блиц-свидания", value: "2511" }, - { label: "Блогеры", value: "1471" }, - { label: "Блокадный Ленинград", value: "1111" }, - { label: "Блэкигуд", value: "2905" }, - { label: "Богачи", value: "1549" }, - { label: "Боги / Божественные сущности", value: "149" }, - { label: "Боди-арт", value: "2260" }, - { label: "Бодипозитив", value: "1153" }, - { label: "Боевая пара", value: "1919" }, - { label: "Боевые искусства", value: "918" }, - { label: "Бои без правил", value: "1390" }, - { label: "Бокс", value: "765" }, - { label: "Болгария", value: "1786" }, - { label: "Болезнь Альцгеймера", value: "2533" }, - { label: "Боль", value: "1803" }, - { label: "Больницы", value: "20" }, - { label: "Большая Игра профессора Дамблдора", value: "2890" }, - { label: "Бордели", value: "120" }, - { label: "Борьба за власть", value: "2300" }, - { label: "Борьба за отношения", value: "1251" }, - { label: "Борьба за справедливость", value: "2335" }, - { label: "Боязнь боли", value: "2441" }, - { label: "Боязнь воды", value: "1084" }, - { label: "Боязнь врачей", value: "2063" }, - { label: "Боязнь высоты", value: "1085" }, - { label: "Боязнь грозы", value: "1124" }, - { label: "Боязнь громких звуков", value: "1090" }, - { label: "Боязнь грязи", value: "1277" }, - { label: "Боязнь замкнутых пространств", value: "821" }, - { label: "Боязнь зеркал", value: "2739" }, - { label: "Боязнь клоунов", value: "2970" }, - { label: "Боязнь крови", value: "1312" }, - { label: "Боязнь людей", value: "1082" }, - { label: "Боязнь мужчин", value: "1264" }, - { label: "Боязнь насекомых / Боязнь пауков", value: "1119" }, - { label: "Боязнь неудачи", value: "2713" }, - { label: "Боязнь огня", value: "1258" }, - { label: "Боязнь одиночества", value: "1491" }, - { label: "Боязнь открытых пространств", value: "965" }, - { label: "Боязнь привязанности", value: "1343" }, - { label: "Боязнь призраков", value: "2770" }, - { label: "Боязнь прикосновений", value: "823" }, - { label: "Боязнь сексуальных домогательств", value: "2319" }, - { label: "Боязнь смерти", value: "1515" }, - { label: "Боязнь сна", value: "2262" }, - { label: "Боязнь темноты", value: "1494" }, - { label: "Боязнь тишины", value: "2233" }, - { label: "Бразилия", value: "833" }, - { label: "Брак по договоренности", value: "35" }, - { label: "Брак по расчету", value: "303" }, - { label: "БРД", value: "2907" }, - { label: "Бродячие артисты", value: "2661" }, - { label: "Бродячие певцы", value: "1497" }, - { label: "Броманс", value: "171" }, - { label: "Будущее", value: "227" }, - { label: "Буллёзный эпидермолиз", value: "2725" }, - { label: "Буллинг", value: "436" }, - { label: "Бухгалтеры", value: "2509" }, - { label: "Буч и фэм", value: "486" }, - { label: "Бывшие", value: "311" }, - { label: "Бывшие враги", value: "2812" }, - { label: "Бывшие серийные убийцы", value: "2498" }, - { label: "Бытовое фэнтези", value: "2702" }, - { label: "В одном теле", value: "1425" }, - { label: "В поисках отношений", value: "1576" }, - { label: "В. А. О. Н.", value: "2377" }, - { label: "Вагинальный секс", value: "395" }, - { label: "Ваканда", value: "2668" }, - { label: "Валькирии", value: "557" }, - { label: "Вампиры", value: "27" }, - { label: "Василиски", value: "863" }, - { label: "Вдовство", value: "2030" }, - { label: "Вебкам-модели", value: "2299" }, - { label: "Вегетарианцы / Веганы", value: "577" }, - { label: "Ведьмы / Колдуны", value: "33" }, - { label: "Великая депрессия", value: "2224" }, - { label: "Великая французская революция", value: "2000" }, - { label: "Великобритания", value: "673" }, - { label: "Великое Княжество Литовское", value: "2587" }, - { label: "Великолепный мерзавец", value: "1244" }, - { label: "Велоспорт", value: "2978" }, - { label: "Венгрия", value: "1432" }, - { label: "Вендиго", value: "251" }, - { label: "Верность", value: "459" }, - { label: "Вертикальный инцест", value: "337" }, - { label: "Вестерн", value: "2329" }, - { label: "Ветераны", value: "555" }, - { label: "Ветеринары", value: "629" }, - { label: "Вечеринки", value: "854" }, - { label: "Вечная молодость", value: "2405" }, - { label: "Вещие сны", value: "1993" }, - { label: "Взаимопонимание", value: "2887" }, - { label: "Взросление", value: "909" }, - { label: "Вигиланты", value: "1514" }, - { label: "Видеоигры", value: "2508" }, - { label: "Визажисты / Гримеры", value: "1801" }, - { label: "Викинги", value: "676" }, - { label: "Виктимблейминг", value: "331" }, - { label: "Викторианская эпоха", value: "469" }, - { label: "Виртуальная реальность", value: "803" }, - { label: "Виртуальный секс", value: "268" }, - { label: "Витилиго", value: "1167" }, - { label: "Вице-королевство Новая Испания", value: "2603" }, - { label: "ВИЧ / СПИД", value: "542" }, - { label: "Влюбленность", value: "1303" }, - { label: "Вне закона", value: "1089" }, - { label: "Внутренний сексизм", value: "2155" }, - { label: "Внутренняя гомофобия", value: "2154" }, - { label: "Внутренняя трансфобия", value: "2716" }, - { label: "Водители / Шоферы", value: "879" }, - { label: "Военные", value: "313" }, - { label: "Военные городки", value: "2095" }, - { label: "Военные преступления", value: "2356" }, - { label: "Возвращение", value: "1522" }, - { label: "Возвращение Артура", value: "2570" }, - { label: "Воздержание", value: "921" }, - { label: "Воздушная атлетика", value: "2811" }, - { label: "Война", value: "232" }, - { label: "Война в Афганистане", value: "1050" }, - { label: "Война во Вьетнаме", value: "1332" }, - { label: "Война за независимость США", value: "2065" }, - { label: "Война миров", value: "1222" }, - { label: "Волдигуд", value: "1874" }, - { label: "Волейбол", value: "1095" }, - { label: "Волк в овечьей шкуре", value: "2228" }, - { label: "Волонтеры", value: "900" }, - { label: "Волшебники / Волшебницы", value: "93" }, - { label: "Воображаемые друзья", value: "738" }, - { label: "Ворарефилия", value: "157" }, - { label: "Воры", value: "320" }, - { label: "Воскрешение", value: "256" }, - { label: "Воспоминания", value: "1118" }, - { label: "Воссоединение", value: "498" }, - { label: "Восстание машин", value: "817" }, - { label: "Восточное побережье", value: "770" }, - { label: "Восьмое марта", value: "493" }, - { label: "Вражда", value: "681" }, - { label: "Вражда семей", value: "2867" }, - { label: "Врачи", value: "374" }, - { label: "Времена Мародеров", value: "1872" }, - { label: "Времена Основателей", value: "1891" }, - { label: "Временная смерть персонажа", value: "439" }, - { label: "Временное бессмертие", value: "2755" }, - { label: "Временное воскрешение персонажей", value: "2649" }, - { label: "Временные петли", value: "147" }, - { label: "Временные превращения", value: "927" }, - { label: "Врожденные заболевания", value: "1781" }, - { label: "Всадники Апокалипсиса", value: "1326" }, - { label: "Все живы / Никто не умер", value: "535" }, - { label: "Все за Одного — отец Изуку", value: "2421" }, - { label: "Всезнающий рассказчик", value: "1368" }, - { label: "Вселение в тело", value: "1181" }, - { label: "Вторая мировая", value: "488" }, - { label: "Вторжение пришельцев", value: "755" }, - { label: "Вторичный стыд", value: "1393" }, - { label: "Второй шанс", value: "1799" }, - { label: "Второстепенные оригинальные персонажи", value: "1536" }, - { label: "Вуайеризм", value: "174" }, - { label: "Вуду", value: "678" }, - { label: "Выбор", value: "2808" }, - { label: "Выгорание", value: "2914" }, - { label: "Выживание", value: "216" }, - { label: "Вымирающий вид", value: "2373" }, - { label: "Вымышленная анатомия", value: "1317" }, - { label: "Вымышленная география", value: "1294" }, - { label: "Вымышленная религия", value: "2062" }, - { label: "Вымышленная физика", value: "2132" }, - { label: "Вымышленные виды спорта", value: "1777" }, - { label: "Вымышленные заболевания", value: "1295" }, - { label: "Вымышленные науки", value: "1333" }, - { label: "Вымышленные праздники", value: "1769" }, - { label: "Вымышленные профессии", value: "1391" }, - { label: "Вымышленные существа", value: "1685" }, - { label: "Вымышленные языки", value: "536" }, - { label: "Выпускные", value: "717" }, - { label: "Высшее общество", value: "1093" }, - { label: "Высшие учебные заведения", value: "97" }, - { label: "Выученная беспомощность", value: "2772" }, - { label: "Выхаживание", value: "511" }, - { label: "Выход из нездоровых отношений", value: "2891" }, - { label: "Гадалки / Ясновидящие", value: "2662" }, - { label: "Гадкий утенок", value: "1225" }, - { label: "Газлайтинг", value: "1418" }, - { label: "Галлюцинации / Иллюзии", value: "365" }, - { label: "Гаремник", value: "50" }, - { label: "Гаремы", value: "115" }, - { label: "Гарпии", value: "1482" }, - { label: "Гарри Поттер и Драко Малфой — друзья", value: "1923" }, - { label: "Гастроли", value: "2487" }, - { label: "Гедонизм", value: "736" }, - { label: "Гей-клубы", value: "2482" }, - { label: "Геймеры", value: "356" }, - { label: "Геймлит", value: "23" }, - { label: "Гейши / Кисэн", value: "332" }, - { label: "Гемофилия", value: "1341" }, - { label: "Гендерная дисфория", value: "431" }, - { label: "Гендерная интрига", value: "55" }, - { label: "Гендерное неравенство", value: "2568" }, - { label: "Гендерный нонконформизм", value: "1417" }, - { label: "Гендерсвап", value: "1691" }, - { label: "Генетика / Эволюция", value: "2131" }, - { label: "Гении", value: "445" }, - { label: "Генитальные пытки", value: "2104" }, - { label: "Геноцид", value: "720" }, - { label: "Геокинез", value: "2889" }, - { label: "Геологи / Географы", value: "934" }, - { label: "Германия", value: "688" }, - { label: "Германия — не Священная Римская Империя", value: "2470" }, - { label: "Герой поневоле", value: "2145" }, - { label: "Геронтофилия", value: "162" }, - { label: "Гетерохромия", value: "1210" }, - { label: "Гетто", value: "1783" }, - { label: "Гибристофилия", value: "1843" }, - { label: "Гиганты", value: "735" }, - { label: "Гидра трэш-пати", value: "2804" }, - { label: "Гики", value: "482" }, - { label: "Гильдии", value: "957" }, - { label: "Гимнастика", value: "1974" }, - { label: "Гиперакузия", value: "2866" }, - { label: "Гиперсексуальность", value: "945" }, - { label: "Гипноз", value: "700" }, - { label: "Гладиаторы", value: "677" }, - { label: "Глобальные катастрофы", value: "525" }, - { label: "Глухота", value: "595" }, - { label: "Гнев", value: "2806" }, - { label: "Гнездование", value: "1452" }, - { label: "Гномы", value: "138" }, - { label: "Гоблины", value: "188" }, - { label: "Големы", value: "1208" }, - { label: "Голод", value: "868" }, - { label: "Голодные Фэндомные Игры", value: "2895" }, - { label: "Гомофобия", value: "428" }, - { label: "Гомункулы", value: "1366" }, - { label: "Горе / Утрата", value: "1113" }, - { label: "Горизонтальный инцест", value: "338" }, - { label: "Городские легенды", value: "1832" }, - { label: "Горы", value: "702" }, - { label: "Горячие источники", value: "2813" }, - { label: "Гостевой брак", value: "1354" }, - { label: "Гостиницы", value: "384" }, - { label: "Готический роман", value: "963" }, - { label: "Готы", value: "575" }, - { label: "Гравиокинез", value: "1985" }, - { label: "Гражданская война", value: "370" }, - { label: "Гражданская война в России", value: "1282" }, - { label: "Гражданская война в США", value: "1018" }, - { label: "Графичные описания", value: "2854" }, - { label: "Грейнджергад", value: "1892" }, - { label: "Грейромантичные персонажи", value: "707" }, - { label: "Гремлины", value: "189" }, - { label: "Греция", value: "1040" }, - { label: "Грифоны", value: "335" }, - { label: "Громкий секс", value: "1184" }, - { label: "Грубый секс", value: "121" }, - { label: "Грудное кормление", value: "296" }, - { label: "Групи", value: "2925" }, - { label: "Групповое изнасилование", value: "2203" }, - { label: "Групповой секс", value: "1654" }, - { label: "Грязный реализм", value: "634" }, - { label: "Гувернантки", value: "1179" }, - { label: "Гули", value: "367" }, - { label: "Гэвин Рид — андроид", value: "2692" }, - { label: "Даб-кон", value: "57" }, - { label: "Даби — не Тойя Тодороки", value: "2821" }, - { label: "Даби — Тойя Тодороки", value: "2423" }, - { label: "Дадзава", value: "2915" }, - { label: "Дадмайт", value: "2917" }, - { label: "Дайверы", value: "939" }, - { label: "Дайвинг", value: "2823" }, - { label: "Дама в беде", value: "1259" }, - { label: "Дамбигад", value: "1863" }, - { label: "Дамбигуд", value: "1873" }, - { label: "Дампиры", value: "2860" }, - { label: "Дандэрэ", value: "1364" }, - { label: "Дарк", value: "1678" }, - { label: "Даркнет", value: "1287" }, - { label: "Дачи", value: "930" }, - { label: "Двойники", value: "305" }, - { label: "Двойное проникновение", value: "107" }, - { label: "Двойной кноттинг", value: "2619" }, - { label: "Двойные агенты", value: "441" }, - { label: "Дворцовые интриги", value: "2075" }, - { label: "Девиантное поведение", value: "1814" }, - { label: "Девичники / Мальчишники", value: "1191" }, - { label: "Девочки из Эквестрии", value: "2912" }, - { label: "Дежавю", value: "2049" }, - { label: "Декаданс", value: "2353" }, - { label: "Деконструкция", value: "1322" }, - { label: "Деми-персонажи", value: "723" }, - { label: "Демиурги", value: "819" }, - { label: "Демонология", value: "2485" }, - { label: "Демоны", value: "73" }, - { label: "Демоны-хранители", value: "2045" }, - { label: "Дендрофилия", value: "491" }, - { label: "День благодарения", value: "754" }, - { label: "День знаний", value: "1075" }, - { label: "День матери", value: "2979" }, - { label: "День мертвых", value: "1074" }, - { label: "День независимости США", value: "1008" }, - { label: "День рождения", value: "298" }, - { label: "День святого Валентина", value: "204" }, - { label: "День святого Патрика", value: "1129" }, - { label: "День смерти", value: "2251" }, - { label: "День учителя", value: "1076" }, - { label: "Деперсонализация", value: "1197" }, - { label: "Депрессия", value: "354" }, - { label: "Депривация сна", value: "551" }, - { label: "Дереализация", value: "1198" }, - { label: "Деревни", value: "364" }, - { label: "Дерматозойный бред", value: "2779" }, - { label: "Деспоты / Тираны", value: "1345" }, - { label: "Детектив", value: "1672" }, - { label: "Дети", value: "217" }, - { label: "Детоненавистничество", value: "1836" }, - { label: "Детская влюбленность", value: "186" }, - { label: "Детские дома", value: "355" }, - { label: "Детские лагеря", value: "222" }, - { label: "Детские сады", value: "567" }, - { label: "Дефекты зрения", value: "2639" }, - { label: "Дефекты речи", value: "737" }, - { label: "Джинны / Пери", value: "456" }, - { label: "Джон Ватсон — преступник", value: "1932" }, - { label: "Джуманджи — не игровой мир", value: "2799" }, - { label: "Диабет", value: "820" }, - { label: "Диалоги (стилизация)", value: "1139" }, - { label: "Дизайнеры", value: "1875" }, - { label: "Дизельпанк", value: "294" }, - { label: "Дикие животные", value: "622" }, - { label: "Дикий Запад", value: "293" }, - { label: "Дилетанты", value: "2443" }, - { label: "Динозавры", value: "670" }, - { label: "Дипломатия", value: "1790" }, - { label: "Дисбаланс власти", value: "732" }, - { label: "Дискриминация", value: "2230" }, - { label: "Дискриминация по внешности", value: "1414" }, - { label: "Дискриминация по возрасту", value: "2843" }, - { label: "Дислексия", value: "860" }, - { label: "Дисморфофобия", value: "1108" }, - { label: "Диссоциативное расстройство идентичности", value: "2127" }, - { label: "Дистанционное обучение", value: "2592" }, - { label: "Дисфункциональные семьи", value: "602" }, - { label: "Дневники (стилизация)", value: "214" }, - { label: "Добрый Сальери", value: "2522" }, - { label: "Доверие", value: "1117" }, - { label: "Договоры / Сделки", value: "2137" }, - { label: "Документы / Отчеты (стилизация)", value: "752" }, - { label: "Долголетие", value: "2130" }, - { label: "Домашнее насилие", value: "42" }, - { label: "Домашние животные", value: "517" }, - { label: "Домашний арест", value: "2526" }, - { label: "Домашний ремонт", value: "867" }, - { label: "Домовые", value: "1423" }, - { label: "Допросы", value: "2654" }, - { label: "Дорожное приключение", value: "47" }, - { label: "Драббл", value: "2935" }, - { label: "Драки", value: "1367" }, - { label: "Драконы", value: "43" }, - { label: "Драма", value: "1668" }, - { label: "Драуги", value: "2264" }, - { label: "Древний Восток", value: "1940" }, - { label: "Древний Египет", value: "474" }, - { label: "Древний Китай", value: "1438" }, - { label: "Древняя Русь", value: "466" }, - { label: "Дремлющее зло", value: "2719" }, - { label: "Дремлющие способности", value: "2084" }, - { label: "Другая магическая школа", value: "1864" }, - { label: "Другие планеты", value: "1415" }, - { label: "Другой избранный", value: "2054" }, - { label: "Другой факультет", value: "1866" }, - { label: "Дружба", value: "1701" }, - { label: "Дружба втайне", value: "2303" }, - { label: "Дружба по расчету", value: "2331" }, - { label: "Друзья детства", value: "343" }, - { label: "Друзья по переписке", value: "1466" }, - { label: "Друзья поневоле", value: "627" }, - { label: "Друзья с привилегиями", value: "113" }, - { label: "Друиды", value: "1199" }, - { label: "Дрэг (субкультура)", value: "1248" }, - { label: "Дурмстранг", value: "2389" }, - { label: "Дурслигуд", value: "2058" }, - { label: "Духи природы", value: "503" }, - { label: "Духовенство", value: "478" }, - { label: "Духовная связь", value: "2090" }, - { label: "Духокинез", value: "2500" }, - { label: "Дуэли", value: "446" }, - { label: "Дьяволы", value: "2369" }, - { label: "Дэдди-кинк", value: "165" }, - { label: "Еда / Кулинария", value: "1195" }, - { label: "Единороги", value: "373" }, - { label: "Ёкаи", value: "416" }, - { label: "Естественные враги", value: "728" }, - { label: "Жамевю", value: "2050" }, - { label: "Жаргон", value: "1016" }, - { label: "Ждун Фест", value: "2936" }, - { label: "Железнодорожники", value: "2680" }, - { label: "Железные дороги", value: "811" }, - { label: "Женская дружба", value: "2156" }, - { label: "Жертвы обстоятельств", value: "1475" }, - { label: "Жестокое обращение с животными", value: "722" }, - { label: "Жестокость", value: "1710" }, - { label: "Живой сосуд", value: "2072" }, - { label: "Животные-компаньоны", value: "2371" }, - { label: "Живые машины", value: "961" }, - { label: "ЖиП", value: "143" }, - { label: "Журналисты", value: "438" }, - { label: "Заболевания", value: "1268" }, - { label: "Заболевания крови", value: "2254" }, - { label: "Заболевания сердца", value: "1767" }, - { label: "Забота / Поддержка", value: "1302" }, - { label: "Заброшенные города", value: "1276" }, - { label: "Заброшенные здания", value: "1483" }, - { label: "Забытый герой", value: "2337" }, - { label: "Завещание", value: "1262" }, - { label: "Зависимое расстройство личности", value: "2349" }, - { label: "Зависть", value: "990" }, - { label: "Завоевание", value: "978" }, - { label: "Загробный мир", value: "275" }, - { label: "Закавказье", value: "2819" }, - { label: "Заклятые друзья", value: "1316" }, - { label: "Закрытые города", value: "1481" }, - { label: "Закрытые учебные заведения", value: "98" }, - { label: "Закрытый детектив", value: "2616" }, - { label: "Заложники", value: "845" }, - { label: "Замки", value: "654" }, - { label: "Замкнутое пространство", value: "2306" }, - { label: "Замкнутый мир", value: "874" }, - { label: "Занавесочная история", value: "1688" }, - { label: "Запахи", value: "1385" }, - { label: "Заповедники", value: "618" }, - { label: "Запредельно одаренный персонаж", value: "1241" }, - { label: "Запрет на магию", value: "2663" }, - { label: "Запретные отношения", value: "362" }, - { label: "Засосы / Укусы", value: "913" }, - { label: "Застенчивость", value: "2807" }, - { label: "Затворники", value: "1059" }, - { label: "Затерянные миры", value: "2448" }, - { label: "Защита любимого", value: "2492" }, - { label: "Защитники природы", value: "628" }, - { label: "Защищенный секс", value: "636" }, - { label: "Звездные слезы", value: "2971" }, - { label: "Здоровые механизмы преодоления", value: "2147" }, - { label: "Здоровые отношения", value: "1853" }, - { label: "Зеркала", value: "724" }, - { label: "Зимняя Фандомная Битва", value: "254" }, - { label: "Злобные бывшие", value: "518" }, - { label: "Зловещая находка", value: "2275" }, - { label: "Знаки зодиака", value: "643" }, - { label: "Знаменитости", value: "1360" }, - { label: "Золотая клетка", value: "480" }, - { label: "Золотая молодежь", value: "796" }, - { label: "Золотая Орда", value: "1143" }, - { label: "Золушка", value: "1226" }, - { label: "Зомби", value: "31" }, - { label: "Зоопарки", value: "792" }, - { label: "Зоофилия", value: "1662" }, - { label: "Зоофобии", value: "2715" }, - { label: "ЗППП", value: "630" }, - { label: "ЗПР", value: "2259" }, - { label: "Зрелые персонажи", value: "851" }, - { label: "Иван Купала", value: "1068" }, - { label: "Игровое расстройство", value: "949" }, - { label: "Игрушки", value: "1346" }, - { label: "Игры на выживание", value: "554" }, - { label: "Игры на раздевание", value: "1535" }, - { label: "Игры с сосками", value: "423" }, - { label: "Игры с температурой", value: "230" }, - { label: "Идеализация", value: "1540" }, - { label: "Иерархический строй", value: "652" }, - { label: "Избранные", value: "847" }, - { label: "Избыточный физиологизм", value: "2334" }, - { label: "Изгнанники", value: "871" }, - { label: "Измена", value: "62" }, - { label: "Измененное состояние сознания", value: "2242" }, - { label: "Изменились за лето", value: "1908" }, - { label: "Изнасилование", value: "1650" }, - { label: "Изоляция", value: "1296" }, - { label: "Израиль", value: "1094" }, - { label: "Изуку Мидория — злодей", value: "2472" }, - { label: "Иллюзионисты", value: "532" }, - { label: "Имейджин", value: "838" }, - { label: "Импотенция", value: "632" }, - { label: "Импринтинг", value: "635" }, - { label: "Импрореал", value: "2955" }, - { label: "Импрофест", value: "2947" }, - { label: "Имя вместо цвета", value: "2919" }, - { label: "Инвалидность", value: "197" }, - { label: "Индия", value: "1002" }, - { label: "Инквизиция", value: "1130" }, - { label: "Инопланетяне", value: "58" }, - { label: "Инсектоиды / Арахноиды", value: "953" }, - { label: "Инсектофилия / Арахнофилия", value: "664" }, - { label: "Инсценированная смерть персонажа", value: "440" }, - { label: "Интерактивная работа", value: "419" }, - { label: "Интервью (стилизация)", value: "2446" }, - { label: "Интернет", value: "1286" }, - { label: "Интерсекс-персонажи", value: "299" }, - { label: "Интрамаммарный секс", value: "840" }, - { label: "Инфантильность", value: "1300" }, - { label: "Инцест", value: "1651" }, - { label: "Ипохондрия", value: "2178" }, - { label: "Ирландия", value: "1035" }, - { label: "Иронический детектив", value: "2638" }, - { label: "Искра Стайлз Стилински", value: "1975" }, - { label: "Искупление", value: "877" }, - { label: "Искусственно вызванные чувства", value: "626" }, - { label: "Искусственные интеллекты", value: "150" }, - { label: "Искусственные существа", value: "2277" }, - { label: "Искусство", value: "2257" }, - { label: "Искусствоведы", value: "2873" }, - { label: "Искушение", value: "776" }, - { label: "Испания", value: "1020" }, - { label: "Исповеди", value: "1342" }, - { label: "Исправительные лагеря", value: "2439" }, - { label: "Истинные", value: "1855" }, - { label: "Историческая Хеталия", value: "1865" }, - { label: "Исторические эпохи", value: "1698" }, - { label: "Исторический Кантриболс", value: "2913" }, - { label: "Историческое допущение", value: "2681" }, - { label: "Исцеление", value: "802" }, - { label: "Италия", value: "1003" }, - { label: "Ихтиоморфы", value: "972" }, - { label: "Йога", value: "483" }, - { label: "Йоль", value: "2143" }, - { label: "Кабаре", value: "457" }, - { label: "Кабэ-дон", value: "2626" }, - { label: "Казахстан", value: "1260" }, - { label: "Казино", value: "620" }, - { label: "Казнь", value: "548" }, - { label: "Как ориджинал", value: "1032" }, - { label: "Каламбуры", value: "881" }, - { label: "Каминг-аут", value: "559" }, - { label: "Канада", value: "695" }, - { label: "Каникулы", value: "805" }, - { label: "Каннибализм", value: "1706" }, - { label: "Канонная смерть персонажа", value: "564" }, - { label: "Капитан Гидра", value: "1928" }, - { label: "Карантинные зоны", value: "1290" }, - { label: "Карантины", value: "2734" }, - { label: "Караоке", value: "886" }, - { label: "Карательная психиатрия", value: "730" }, - { label: "Карибский бассейн", value: "1172" }, - { label: "Карма", value: "647" }, - { label: "Карьеризм", value: "956" }, - { label: "Каскадеры", value: "2267" }, - { label: "Катарсис", value: "824" }, - { label: "Кафе / Кофейни / Чайные", value: "13" }, - { label: "Квай-Гад", value: "2894" }, - { label: "Квантовая физика", value: "2744" }, - { label: "Квисины", value: "2722" }, - { label: "Кемономими", value: "997" }, - { label: "Кемпинг", value: "182" }, - { label: "Кентавры", value: "64" }, - { label: "Кибербуллинг", value: "1858" }, - { label: "Киберпанк", value: "24" }, - { label: "Киберспорт", value: "745" }, - { label: "Кибертрон", value: "2775" }, - { label: "Киборги", value: "154" }, - { label: "Киевская Русь", value: "515" }, - { label: "Кинк на беременность", value: "2281" }, - { label: "Кинк на волосы", value: "2089" }, - { label: "Кинк на волосы на теле", value: "1337" }, - { label: "Кинк на девственность", value: "2282" }, - { label: "Кинк на инвалидность", value: "633" }, - { label: "Кинк на интеллект", value: "2798" }, - { label: "Кинк на клыки", value: "2325" }, - { label: "Кинк на колготки", value: "2644" }, - { label: "Кинк на крылья", value: "2035" }, - { label: "Кинк на латекс", value: "134" }, - { label: "Кинк на мольбы", value: "1543" }, - { label: "Кинк на наручники", value: "1065" }, - { label: "Кинк на нижнее белье", value: "145" }, - { label: "Кинк на очки", value: "1723" }, - { label: "Кинк на ошейники", value: "388" }, - { label: "Кинк на полноту", value: "552" }, - { label: "Кинк на похвалу", value: "1042" }, - { label: "Кинк на протезы", value: "2682" }, - { label: "Кинк на рога", value: "2320" }, - { label: "Кинк на руки", value: "915" }, - { label: "Кинк на сердцебиение", value: "941" }, - { label: "Кинк на силу", value: "1476" }, - { label: "Кинк на слезы", value: "1373" }, - { label: "Кинк на служение", value: "2980" }, - { label: "Кинк на страх", value: "2246" }, - { label: "Кинк на стыд", value: "747" }, - { label: "Кинк на унижение", value: "2291" }, - { label: "Кинк на униформу", value: "1350" }, - { label: "Кинк на уши", value: "2370" }, - { label: "Кинк на формальную одежду", value: "1064" }, - { label: "Кинк на чужую одежду", value: "2920" }, - { label: "Кинк на чулки", value: "810" }, - { label: "Кинк на шрамы", value: "2831" }, - { label: "Кинк на щекотку", value: "2396" }, - { label: "Кинк-бинго", value: "2290" }, - { label: "Кинки / Фетиши", value: "1659" }, - { label: "Кинотеатры", value: "981" }, - { label: "Киригакуре", value: "2475" }, - { label: "Китай", value: "757" }, - { label: "Кладбища", value: "501" }, - { label: "Кланы", value: "2879" }, - { label: "Классизм", value: "1927" }, - { label: "Клейма", value: "1548" }, - { label: "Клептомания", value: "1160" }, - { label: "Клетка Люцифера", value: "1884" }, - { label: "Клетки", value: "1069" }, - { label: "Клиническая смерть", value: "2068" }, - { label: "Клокпанк / Ветропанк", value: "1812" }, - { label: "Клоны", value: "358" }, - { label: "Клоуны", value: "1520" }, - { label: "Клубы по интересам", value: "1369" }, - { label: "Клюква", value: "1001" }, - { label: "Кляпы / Затыкание рта", value: "379" }, - { label: "Книги", value: "2746" }, - { label: "Книжные магазины", value: "444" }, - { label: "Кноттинг", value: "99" }, - { label: "Ковбои", value: "330" }, - { label: "Кода", value: "1943" }, - { label: "Кожаная одежда", value: "1517" }, - { label: "Колдовстворец", value: "2600" }, - { label: "Кома", value: "516" }, - { label: "Командная работа", value: "708" }, - { label: "Комики", value: "1315" }, - { label: "Комиксисты / Мангаки", value: "576" }, - { label: "Коммунальные квартиры", value: "1437" }, - { label: "Коммуны", value: "797" }, - { label: "Комнатные растения", value: "1792" }, - { label: "Комплекс Бога", value: "2087" }, - { label: "Комплекс превосходства", value: "2191" }, - { label: "Комплексы", value: "2529" }, - { label: "Конкурс от BUBBLE", value: "2937" }, - { label: "Конный спорт", value: "1378" }, - { label: "Конохагакуре", value: "2477" }, - { label: "Конспирология", value: "1911" }, - { label: "Контрабандисты", value: "2363" }, - { label: "Контроль / Подчинение", value: "932" }, - { label: "Контроль памяти", value: "2540" }, - { label: "Контроль сознания", value: "257" }, - { label: "Конфликт мировоззрений", value: "2678" }, - { label: "Концентрационные лагеря", value: "547" }, - { label: "Концерты / Выступления", value: "1236" }, - { label: "Конюшни", value: "514" }, - { label: "Копирование сознания", value: "1231" }, - { label: "Копирование способностей", value: "1825" }, - { label: "Копро-кинк", value: "763" }, - { label: "Корё", value: "1142" }, - { label: "Корейская война", value: "2187" }, - { label: "Коренные народы", value: "1505" }, - { label: "Королевства", value: "2794" }, - { label: "Корпорация BOBSOC", value: "2940" }, - { label: "Корректирующее изнасилование", value: "2856" }, - { label: "Коррупция", value: "2150" }, - { label: "Косвенные убийства", value: "2846" }, - { label: "Космоопера", value: "48" }, - { label: "Космос", value: "236" }, - { label: "Косплееры", value: "944" }, - { label: "Кочевники", value: "1793" }, - { label: "Кошмары", value: "1228" }, - { label: "Красная нить судьбы", value: "608" }, - { label: "Крепости / Башни", value: "2321" }, - { label: "Крестовые походы", value: "2070" }, - { label: "Крестражи", value: "2238" }, - { label: "Кризис гендера", value: "1406" }, - { label: "Кризис ориентации", value: "1405" }, - { label: "Криминалистическая экспертиза", value: "2361" }, - { label: "Криминальная пара", value: "1920" }, - { label: "Криокинез", value: "1804" }, - { label: "Криптиды", value: "60" }, - { label: "Криптоистория", value: "179" }, - { label: "Кровь / Травмы", value: "800" }, - { label: "Кроссдрессинг", value: "65" }, - { label: "Кроссовер", value: "10" }, - { label: "Кроули — Архангел Рафаэль", value: "2471" }, - { label: "Круизы", value: "2488" }, - { label: "Крушение «Титаника»", value: "2200" }, - { label: "Крылатые", value: "92" }, - { label: "Крэк", value: "1408" }, - { label: "Ксенофилия", value: "1711" }, - { label: "Ксенофобия", value: "589" }, - { label: "Куба", value: "773" }, - { label: "Кудэрэ", value: "1216" }, - { label: "Куклы", value: "1221" }, - { label: "Куколдинг", value: "524" }, - { label: "Культ Дракона", value: "2244" }, - { label: "Кумогакуре", value: "2474" }, - { label: "Куннилингус", value: "172" }, - { label: "Купальни", value: "964" }, - { label: "Курение", value: "202" }, - { label: "Курортный роман", value: "181" }, - { label: "Курьеры", value: "2735" }, - { label: "Лабиринты", value: "1981" }, - { label: "Лаборатории", value: "1115" }, - { label: "Лабораторные опыты", value: "146" }, - { label: "Лав-отели", value: "801" }, - { label: "Лавандовый брак", value: "1357" }, - { label: "Лакросс", value: "830" }, - { label: "Лапслок", value: "267" }, - { label: "Латентная гомосексуальность", value: "2339" }, - { label: "ЛГБТ-родители", value: "2723" }, - { label: "Лебединая верность", value: "537" }, - { label: "Левитация", value: "1281" }, - { label: "Легкая атлетика", value: "1797" }, - { label: "Лекарственная зависимость", value: "2632" }, - { label: "Леса", value: "703" }, - { label: "Летние ОтМетки", value: "2956" }, - { label: "Летние школы", value: "2803" }, - { label: "Лето девяносто девятого", value: "1990" }, - { label: "Лжебоги", value: "2869" }, - { label: "Лингвистический кинк", value: "2042" }, - { label: "Лирика", value: "2005" }, - { label: "Лихтенштейн", value: "1023" }, - { label: "Личность против системы", value: "1912" }, - { label: "Ложная беременность", value: "1428" }, - { label: "Ложные воспоминания", value: "376" }, - { label: "Ложные обвинения", value: "1204" }, - { label: "Локальный постапокалипсис", value: "2455" }, - { label: "Лолита (субкультура)", value: "1207" }, - { label: "Ломка", value: "1846" }, - { label: "Лошади", value: "1083" }, - { label: "Лугнасад", value: "2621" }, - { label: "Лудомания", value: "947" }, - { label: "Лучники", value: "1521" }, - { label: "Любить луну", value: "2901" }, - { label: "Любовная магия", value: "2317" }, - { label: "Любовный магнит", value: "1407" }, - { label: "Любовный многоугольник", value: "63" }, - { label: "Любовь с первого взгляда", value: "813" }, - { label: "Любовь/Ненависть", value: "1703" }, - { label: "Люди", value: "1538" }, - { label: "Людоеды", value: "1288" }, - { label: "Магазины", value: "1063" }, - { label: "Магическая связь", value: "2720" }, - { label: "Магические клятвы", value: "2245" }, - { label: "Магические лавки", value: "2629" }, - { label: "Магические учебные заведения", value: "1338" }, - { label: "Магический реализм", value: "9" }, - { label: "Магия", value: "1419" }, - { label: "Магия крови", value: "2857" }, - { label: "Маленькие города", value: "221" }, - { label: "Малфоигуд", value: "1890" }, - { label: "Мамми-кинк", value: "273" }, - { label: "Мандалорцы", value: "2870" }, - { label: "Манекены", value: "1509" }, - { label: "Манипуляции", value: "992" }, - { label: "Марафонный секс", value: "316" }, - { label: "Маргиналы", value: "394" }, - { label: "Марди Гра", value: "2407" }, - { label: "Маскарады / Балы", value: "443" }, - { label: "Массаж", value: "2218" }, - { label: "Мастурбация", value: "226" }, - { label: "Матриархат", value: "706" }, - { label: "Матчасть", value: "2328" }, - { label: "Махотокоро", value: "2943" }, - { label: "Маяки", value: "1356" }, - { label: "Мегаломания", value: "985" }, - { label: "Мегаполисы", value: "451" }, - { label: "Медицинский кинк", value: "748" }, - { label: "Медицинское использование наркотиков", value: "925" }, - { label: "Медовый месяц", value: "2235" }, - { label: "Медсестры / Медбратья", value: "2236" }, - { label: "Межбедренный секс", value: "931" }, - { label: "Междумирье", value: "2280" }, - { label: "Межэтнические отношения", value: "2121" }, - { label: "Мейлдом", value: "59" }, - { label: "Мексика", value: "768" }, - { label: "Меланхолия", value: "2108" }, - { label: "Мелодрама", value: "2881" }, - { label: "Менструация", value: "1570" }, - { label: "Ментальная тюрьма", value: "2294" }, - { label: "Месть", value: "907" }, - { label: "Металлисты", value: "1206" }, - { label: "Металлокинез", value: "1532" }, - { label: "Метафизические существа", value: "2097" }, - { label: "Метки", value: "1398" }, - { label: "Метросексуалы", value: "558" }, - { label: "Механофилия / Технофилия", value: "587" }, - { label: "Мечники", value: "2783" }, - { label: "Мечты", value: "993" }, - { label: "Миддлпанк", value: "2076" }, - { label: "Мизофилия", value: "1970" }, - { label: "Микро / Макро", value: "407" }, - { label: "Микрофикшен", value: "417" }, - { label: "Мимикрия", value: "2083" }, - { label: "Минет", value: "231" }, - { label: "Министерство Магии", value: "1917" }, - { label: "Минотавры", value: "2820" }, - { label: "Мир без гомофобии", value: "2660" }, - { label: "Мир Желаний (Однажды в сказке)", value: "2923" }, - { label: "Мир Института Войны", value: "2379" }, - { label: "Мироустройство", value: "989" }, - { label: "Мистерия", value: "2012" }, - { label: "Мистика", value: "1671" }, - { label: "Мистическая беременность", value: "1784" }, - { label: "Мистические защитники", value: "2162" }, - { label: "Мифы и мифология", value: "18" }, - { label: "Мнимая беременность", value: "1096" }, - { label: "Мнимая проза", value: "1098" }, - { label: "Мнимая свобода", value: "2708" }, - { label: "Многодетные семьи", value: "2785" }, - { label: "Множественное проникновение", value: "2059" }, - { label: "Множественные оргазмы", value: "317" }, - { label: "Множественные финалы", value: "312" }, - { label: "Могучие Рейнджеры: Дино Заряд", value: "2931" }, - { label: "Могучие Рейнджеры: Мегафорс", value: "2930" }, - { label: "Могучие Рейнджеры: Мистическая Сила", value: "2933" }, - { label: "Могучие Рейнджеры: Самураи", value: "2929" }, - { label: "Могучие Рейнджеры: Успеть на помощь", value: "2934" }, - { label: "Могучие Рейнджеры: Ярость Джунглей", value: "2932" }, - { label: "Модели", value: "399" }, - { label: "Модельеры", value: "598" }, - { label: "Модельный бизнес", value: "435" }, - { label: "Модификации тела", value: "890" }, - { label: "Молдова", value: "2967" }, - { label: "Молчание", value: "2465" }, - { label: "Монастыри", value: "352" }, - { label: "Монахи", value: "353" }, - { label: "Монолог", value: "2333" }, - { label: "Монстрофилия", value: "94" }, - { label: "Монстры", value: "529" }, - { label: "Моральные дилеммы", value: "959" }, - { label: "Морлед", value: "2255" }, - { label: "Моря / Океаны", value: "955" }, - { label: "Моряки", value: "406" }, - { label: "Мотоспорт", value: "1461" }, - { label: "Мужская беременность", value: "1660" }, - { label: "Мужская дружба", value: "2157" }, - { label: "Мужское грудное кормление", value: "297" }, - { label: "Музеи / Галереи", value: "795" }, - { label: "Музы", value: "2001" }, - { label: "Музыканты", value: "141" }, - { label: "Муковисцидоз", value: "2764" }, - { label: "Мультикроссовер", value: "2247" }, - { label: "Мумии", value: "579" }, - { label: "Мутанты", value: "740" }, - { label: "Мэри Сью (Марти Стью)", value: "1655" }, - { label: "Мятежи / Восстания", value: "584" }, - { label: "На грани жизни и смерти", value: "2545" }, - { label: "Наблюдатели", value: "1816" }, - { label: "Навязчивая опека", value: "954" }, - { label: "Навязчивые мысли", value: "2851" }, - { label: "Наги", value: "90" }, - { label: "Надежда", value: "2627" }, - { label: "Наемники", value: "512" }, - { label: "Наемные убийцы", value: "192" }, - { label: "Названные сиблинги", value: "2927" }, - { label: "Намеки на отношения", value: "1387" }, - { label: "Намеки на секс", value: "1382" }, - { label: "Напарники", value: "1291" }, - { label: "Наперегонки со временем", value: "2240" }, - { label: "НапиСанта", value: "2946" }, - { label: "Наполеоновские войны", value: "470" }, - { label: "Нарколепсия", value: "586" }, - { label: "Наркоторговля", value: "1116" }, - { label: "Нарушение этических норм", value: "968" }, - { label: "Нарциссизм", value: "546" }, - { label: "Насекомые", value: "2358" }, - { label: "Насилие", value: "1649" }, - { label: "Насилие над детьми", value: "726" }, - { label: "Наследие (Гарри Поттер)", value: "1935" }, - { label: "Наследство", value: "2172" }, - { label: "Наставничество", value: "935" }, - { label: "Настольные игры", value: "1513" }, - { label: "Настольные ролевые игры", value: "644" }, - { label: "Натуризм / Нудизм", value: "1810" }, - { label: "Наука", value: "2624" }, - { label: "Научная фантастика", value: "596" }, - { label: "Научно-исследовательские организации", value: "1503" }, - { label: "Научное фэнтези", value: "180" }, - { label: "Начало отношений", value: "1328" }, - { label: "Небинарные персонажи", value: "314" }, - { label: "Невезение", value: "2689" }, - { label: "Невзаимные предначертанные", value: "1484" }, - { label: "Невзаимные чувства", value: "105" }, - { label: "Невидимость", value: "359" }, - { label: "Невидимый мир", value: "2286" }, - { label: "Неграфичные описания", value: "2855" }, - { label: "Недоверие", value: "1842" }, - { label: "Недопонимания", value: "614" }, - { label: "Недостатки внешности", value: "923" }, - { label: "Нежелательная беременность", value: "715" }, - { label: "Нежелательные сверхспособности", value: "1335" }, - { label: "Нежелательные чувства", value: "2078" }, - { label: "Нежный секс", value: "280" }, - { label: "Незащищенный секс", value: "497" }, - { label: "Нездоровые механизмы преодоления", value: "1528" }, - { label: "Нездоровые отношения", value: "71" }, - { label: "Нездоровый образ жизни", value: "2148" }, - { label: "Нездоровый BDSM", value: "1062" }, - { label: "Неидеальный омегаверс", value: "2712" }, - { label: "Неизвестность", value: "2826" }, - { label: "Неизвестные родственники", value: "2752" }, - { label: "Неизлечимые заболевания", value: "1269" }, - { label: "Нейтрализация сверхспособностей", value: "2343" }, - { label: "Некромаги", value: "679" }, - { label: "Некрофилия", value: "1663" }, - { label: "Нелинейное повествование", value: "1699" }, - { label: "Неловкий секс", value: "2883" }, - { label: "Неловкость", value: "2459" }, - { label: "Нелюбящие родители", value: "2817" }, - { label: "Немертвые", value: "495" }, - { label: "Немота", value: "414" }, - { label: "Ненависть", value: "685" }, - { label: "Ненависть к себе", value: "1441" }, - { label: "Ненависть с первого взгляда", value: "814" }, - { label: "Ненадежный рассказчик", value: "322" }, - { label: "Ненасильственная смерть", value: "2531" }, - { label: "Необитаемые острова", value: "1196" }, - { label: "Необратимые превращения", value: "926" }, - { label: "Неозвученные чувства", value: "258" }, - { label: "Неприятие отношений", value: "2056" }, - { label: "Неравные отношения", value: "545" }, - { label: "Неравный брак", value: "409" }, - { label: "Неразрывная связь", value: "2569" }, - { label: "Нервный срыв", value: "1190" }, - { label: "Нерды", value: "481" }, - { label: "Нереалистичность", value: "2350" }, - { label: "Несексуальная близость", value: "2874" }, - { label: "Нестандартная поэзия", value: "1704" }, - { label: "Несчастливые отношения", value: "2080" }, - { label: "Несчастливый финал", value: "61" }, - { label: "Несчастные случаи", value: "499" }, - { label: "Нетаймори", value: "2323" }, - { label: "Неторопливое повествование", value: "1815" }, - { label: "Нетрадиционное использование Силы", value: "2520" }, - { label: "Неумышленное употребление наркотических веществ", value: "1192" }, - { label: "Неуставные отношения", value: "1396" }, - { label: "Нефилимы", value: "1086" }, - { label: "Неформальный брак", value: "1355" }, - { label: "Нецензурная лексика", value: "1653" }, - { label: "Нечеловеческая мораль", value: "2551" }, - { label: "Нечеловеческие виды", value: "995" }, - { label: "Нечистая сила", value: "1464" }, - { label: "Нидерланды", value: "1141" }, - { label: "Низкая самооценка", value: "2014" }, - { label: "Низкое фэнтези", value: "2007" }, - { label: "Нимфы", value: "1477" }, - { label: "Ниндзя", value: "471" }, - { label: "Новая жизнь", value: "2018" }, - { label: "Новая Зеландия", value: "1789" }, - { label: "Новеллизация", value: "262" }, - { label: "Новые отношения", value: "1827" }, - { label: "Новый год", value: "206" }, - { label: "Новый канон ЗВ", value: "1948" }, - { label: "Норны", value: "2206" }, - { label: "Ностальгия", value: "878" }, - { label: "Ночное видение", value: "2006" }, - { label: "Ночные клубы", value: "531" }, - { label: "Нуар", value: "137" }, - { label: "Няни", value: "570" }, - { label: "Обездвиживание", value: "116" }, - { label: "Обещания / Клятвы", value: "1823" }, - { label: "Обиды", value: "905" }, - { label: "Обман / Заблуждение", value: "650" }, - { label: "Обмен квами", value: "2572" }, - { label: "Обмен ролями", value: "1055" }, - { label: "Обмен телами", value: "49" }, - { label: "Обмороки", value: "2484" }, - { label: "Обнажение", value: "970" }, - { label: "Оборотни", value: "28" }, - { label: "Обоснованный ООС", value: "2549" }, - { label: "Образ тела", value: "2289" }, - { label: "Обратный омегаверс", value: "2435" }, - { label: "Обретенные семьи", value: "601" }, - { label: "Обреченные отношения", value: "653" }, - { label: "Обскуры", value: "2237" }, - { label: "Обсуждение кинков", value: "1072" }, - { label: "Обусловленная контекстом гомофобия", value: "2685" }, - { label: "Обусловленный контекстом расизм", value: "2686" }, - { label: "Обусловленный контекстом сексизм", value: "2687" }, - { label: "Общежития", value: "534" }, - { label: "Общественный транспорт", value: "615" }, - { label: "Объективация", value: "1837" }, - { label: "Объектофилия", value: "366" }, - { label: "Овипозиция", value: "269" }, - { label: "Огнестрельное оружие", value: "1189" }, - { label: "Ограбления", value: "2269" }, - { label: "Одержимость", value: "2248" }, - { label: "Одержимые", value: "496" }, - { label: "Один день", value: "2647" }, - { label: "Одиночество", value: "1015" }, - { label: "Одичавшие дети", value: "835" }, - { label: "Однолюбы", value: "2146" }, - { label: "Одноминутный канонический персонаж", value: "2184" }, - { label: "Однополый мир", value: "1462" }, - { label: "Ожидание", value: "2909" }, - { label: "ОЖП", value: "1657" }, - { label: "Окаменение", value: "2052" }, - { label: "Океания", value: "1058" }, - { label: "Оккультизм", value: "2374" }, - { label: "ОКР", value: "594" }, - { label: "Окулолинктус", value: "1470" }, - { label: "Олимпийские игры", value: "1127" }, - { label: "Олфактофилия", value: "1945" }, - { label: "Омегаверс", value: "1693" }, - { label: "Омегаверс: Альфа/Альфа", value: "2437" }, - { label: "Омегаверс: Альфа/Бета", value: "2741" }, - { label: "Омегаверс: Альфа/Бета/Омега", value: "2898" }, - { label: "Омегаверс: Альфа/Омега/Альфа", value: "2436" }, - { label: "Омегаверс: Бета/Бета", value: "2966" }, - { label: "Омегаверс: Больше трех полов", value: "2782" }, - { label: "Омегаверс: Омега/Альфа/Омега", value: "2544" }, - { label: "Омегаверс: Омега/Бета", value: "2965" }, - { label: "Омегаверс: Омега/Омега", value: "2564" }, - { label: "Омегаверс: Смена второго пола", value: "2776" }, - { label: "Омегаверс: f!Альфа/m!Омега", value: "2777" }, - { label: "ОМП", value: "1656" }, - { label: "Онейрокинез", value: "2067" }, - { label: "Онкологические заболевания", value: "540" }, - { label: "ООС", value: "1648" }, - { label: "Опасность", value: "2847" }, - { label: "Опера", value: "549" }, - { label: "Опричнина", value: "2252" }, - { label: "Оптимизм", value: "2420" }, - { label: "Оракулы / Провидцы", value: "2221" }, - { label: "Оральная фиксация", value: "2523" }, - { label: "Оргазм без стимуляции", value: "1000" }, - { label: "Организаторы свадеб", value: "858" }, - { label: "Орден Феникса", value: "1907" }, - { label: "Орки", value: "187" }, - { label: "Оружие массового поражения", value: "1193" }, - { label: "Освоение земель", value: "1824" }, - { label: "Оседлание", value: "1223" }, - { label: "Османская империя", value: "527" }, - { label: "Особняки / Резиденции", value: "500" }, - { label: "Осознанные сновидения", value: "530" }, - { label: "Остеокинез", value: "2845" }, - { label: "Острова", value: "799" }, - { label: "От антигероя к герою", value: "2173" }, - { label: "От антигероя к злодею", value: "2213" }, - { label: "От божественного существа к смертному", value: "2664" }, - { label: "От возлюбленных к врагам", value: "783" }, - { label: "От возлюбленных к друзьям", value: "782" }, - { label: "От врагов к возлюбленным", value: "346" }, - { label: "От врагов к друзьям", value: "347" }, - { label: "От врагов к друзьям к возлюбленным", value: "345" }, - { label: "От героя к антигерою", value: "2546" }, - { label: "От героя к злодею", value: "1027" }, - { label: "От героя к монстру", value: "2659" }, - { label: "От героя к обычному персонажу", value: "2584" }, - { label: "От друзей к возлюбленным", value: "344" }, - { label: "От друзей к врагам", value: "463" }, - { label: "От друзей к незнакомцам", value: "1870" }, - { label: "От злодея к антигерою", value: "2640" }, - { label: "От злодея к герою", value: "1026" }, - { label: "От напарников к друзьям к возлюбленным", value: "2460" }, - { label: "От нездоровых отношений к здоровым", value: "2301" }, - { label: "От незнакомцев к возлюбленным", value: "372" }, - { label: "От простонародья к аристократу", value: "2816" }, - { label: "От сексуальных партнеров к возлюбленным", value: "2069" }, - { label: "От смертного к божественному существу", value: "2665" }, - { label: "От супругов к возлюбленным", value: "2359" }, - { label: "Ответвление от канона", value: "2336" }, - { label: "Ответственность", value: "2468" }, - { label: "Отдаю на вдохновение", value: "2288" }, - { label: "Отечественная война 1812 года", value: "1162" }, - { label: "Отказ от чувств", value: "2227" }, - { label: "Отклонения от канона", value: "5" }, - { label: "Открытый финал", value: "185" }, - { label: "ОтМетки", value: "2945" }, - { label: "Отношения втайне", value: "285" }, - { label: "Отношения на расстоянии", value: "37" }, - { label: "Отношения на спор", value: "383" }, - { label: "Отношения наполовину", value: "1573" }, - { label: "Отпуск", value: "1828" }, - { label: "Отрицание", value: "1868" }, - { label: "Отрицание чувств", value: "648" }, - { label: "Отрицательный протагонист", value: "1442" }, - { label: "Отсутствие души", value: "1897" }, - { label: "Отшельники", value: "543" }, - { label: "Офисы", value: "74" }, - { label: "Официанты / Официантки", value: "2560" }, - { label: "Охота", value: "825" }, - { label: "Охота на разумных существ", value: "2784" }, - { label: "Охотники", value: "2036" }, - { label: "Охотники за головами", value: "2103" }, - { label: "Охотники на нечисть", value: "46" }, - { label: "Охранники", value: "1173" }, - { label: "Ошибки", value: "2554" }, - { label: "Ошибочная идентификация", value: "2490" }, - { label: "Паладины", value: "1194" }, - { label: "Пан-персонажи", value: "302" }, - { label: "Пандемия COVID-19", value: "2724" }, - { label: "Панические атаки", value: "751" }, - { label: "Паническое расстройство", value: "733" }, - { label: "Панки", value: "625" }, - { label: "Паразиты", value: "1877" }, - { label: "Паралич", value: "1941" }, - { label: "Параллельные миры", value: "378" }, - { label: "Паранойя", value: "1253" }, - { label: "Пари", value: "778" }, - { label: "Парки / Зоны отдыха", value: "1005" }, - { label: "Парки аттракционов / Ярмарки", value: "793" }, - { label: "Паркур", value: "553" }, - { label: "Пародия", value: "1676" }, - { label: "Партизаны", value: "850" }, - { label: "Парфюмеры", value: "1384" }, - { label: "Пасха", value: "562" }, - { label: "Патологическое бесстрашие", value: "1498" }, - { label: "Патологическое накопительство", value: "2691" }, - { label: "Патологоанатомы", value: "502" }, - { label: "Патриархат", value: "705" }, - { label: "Патриотические темы и мотивы", value: "2231" }, - { label: "Патронусы", value: "2390" }, - { label: "Патрули", value: "1819" }, - { label: "Пафос", value: "2589" }, - { label: "Пацифисты", value: "1939" }, - { label: "Пегасы", value: "1047" }, - { label: "Пеггинг", value: "159" }, - { label: "Пейзажная лирика", value: "2330" }, - { label: "Пекарни", value: "78" }, - { label: "Первая мировая", value: "487" }, - { label: "Первая чеченская война", value: "2428" }, - { label: "Первобытные времена", value: "465" }, - { label: "Первое апреля", value: "2315" }, - { label: "Первопроходцы", value: "351" }, - { label: "Первый контакт", value: "2093" }, - { label: "Первый поцелуй", value: "2790" }, - { label: "Первый раз", value: "1292" }, - { label: "Передача магических способностей", value: "2497" }, - { label: "Переезд", value: "658" }, - { label: "Перезапуск мира", value: "2710" }, - { label: "Перемирие", value: "1331" }, - { label: "Переписки и чаты (стилизация)", value: "38" }, - { label: "Перерыв в отношениях", value: "519" }, - { label: "Переселение душ", value: "1237" }, - { label: "Перестрелки", value: "2268" }, - { label: "Переходный возраст", value: "1297" }, - { label: "Период Эдо", value: "1140" }, - { label: "Персекуторный бред", value: "2136" }, - { label: "Персонажи-геи", value: "2141" }, - { label: "Персонажи-лесбиянки", value: "2142" }, - { label: "Персонификация", value: "994" }, - { label: "Персонификация смерти", value: "1046" }, - { label: "Перу", value: "2017" }, - { label: "Перфекционизм", value: "2730" }, - { label: "Пессимизм", value: "2709" }, - { label: "Пет-плей", value: "391" }, - { label: "Петербург: бикейм #Чимин Сергеевич", value: "1859" }, - { label: "Петтинг", value: "301" }, - { label: "Пещеры", value: "655" }, - { label: "Пижамные вечеринки", value: "1340" }, - { label: "Пикацизм", value: "1227" }, - { label: "Пикники", value: "1165" }, - { label: "Пионеры", value: "668" }, - { label: "Пираты", value: "72" }, - { label: "Пирокинез", value: "1180" }, - { label: "Пиромания", value: "899" }, - { label: "Пирсинг", value: "507" }, - { label: "Писатели", value: "196" }, - { label: "Письма", value: "2298" }, - { label: "Письма (стилизация)", value: "266" }, - { label: "Пиццерии / Дайнеры", value: "593" }, - { label: "Плавание", value: "1135" }, - { label: "Плантокинез", value: "2029" }, - { label: "Пластические операции", value: "1847" }, - { label: "Платонические отношения", value: "385" }, - { label: "Племена", value: "1336" }, - { label: "Плен", value: "290" }, - { label: "Плененные путешествием", value: "2615" }, - { label: "Плохая компания", value: "1502" }, - { label: "Плохие друзья", value: "2445" }, - { label: "Плохой хороший финал", value: "1265" }, - { label: "Плюшефилия", value: "520" }, - { label: "Пляжи", value: "766" }, - { label: "По разные стороны", value: "897" }, - { label: "Побег", value: "661" }, - { label: "Побег из дома", value: "2309" }, - { label: "Победа Волдеморта", value: "1887" }, - { label: "Победа Гриндевальда", value: "2398" }, - { label: "Повара", value: "2853" }, - { label: "Повелитель смерти", value: "1909" }, - { label: "Повествование в будущем времени", value: "2633" }, - { label: "Повествование в настоящем времени", value: "1826" }, - { label: "Повествование во втором лице", value: "166" }, - { label: "Повествование от нескольких лиц", value: "1157" }, - { label: "Повествование от первого лица", value: "1681" }, - { label: "Повседневность", value: "1677" }, - { label: "Повстанцы", value: "659" }, - { label: "Повторение судьбы", value: "1997" }, - { label: "Поглощение способностей", value: "2605" }, - { label: "Погони / Преследования", value: "1996" }, - { label: "Пограничный синдром", value: "710" }, - { label: "Погребение заживо", value: "1279" }, - { label: "Под одной крышей", value: "462" }, - { label: "Под старину (стилизация)", value: "2486" }, - { label: "Подарки", value: "1109" }, - { label: "Подвешивание", value: "914" }, - { label: "Подводный мир", value: "901" }, - { label: "Подземелья", value: "2314" }, - { label: "Подземный мир", value: "1453" }, - { label: "Подлость", value: "2625" }, - { label: "Подменыши", value: "2250" }, - { label: "Подпольные бои", value: "1389" }, - { label: "Подразумеваемая смерть персонажа", value: "2204" }, - { label: "Подростки", value: "218" }, - { label: "Подростковая беременность", value: "986" }, - { label: "Подростковая влюбленность", value: "581" }, - { label: "Подсознание", value: "2273" }, - { label: "Поедание разумных существ", value: "1795" }, - { label: "Пожарные", value: "2557" }, - { label: "Пожилые персонажи", value: "852" }, - { label: "Поза 69", value: "699" }, - { label: "Поиск работы", value: "2646" }, - { label: "Поиск родителей", value: "2033" }, - { label: "Поклонение телу", value: "712" }, - { label: "Покровительство", value: "2223" }, - { label: "Покушение на жизнь", value: "1057" }, - { label: "Покушение на свободу", value: "2655" }, - { label: "Полиамория", value: "1705" }, - { label: "Полиандрия", value: "761" }, - { label: "Полигиния", value: "760" }, - { label: "Политика", value: "1835" }, - { label: "Политики", value: "711" }, - { label: "Политические интриги", value: "183" }, - { label: "Полицейские", value: "14" }, - { label: "Половой диморфизм", value: "1780" }, - { label: "Полубоги", value: "437" }, - { label: "Полудемоны", value: "2872" }, - { label: "Полудраконы", value: "2859" }, - { label: "Полукровки", value: "368" }, - { label: "Польша", value: "777" }, - { label: "Полярные станции", value: "1493" }, - { label: "Помощь врагу", value: "1325" }, - { label: "Попаданцы: В реальный мир", value: "1499" }, - { label: "Попаданцы: В своем теле", value: "1412" }, - { label: "Попаданцы: В чужом теле", value: "1413" }, - { label: "Попаданцы: Из одного фандома в другой", value: "2699" }, - { label: "Попаданцы: Из фандома в ориджинал", value: "2193" }, - { label: "Попаданцы: По обмену", value: "2196" }, - { label: "Попаданчество", value: "1695" }, - { label: "Популярность", value: "2581" }, - { label: "Попытка изнасилования", value: "1440" }, - { label: "Порноактеры", value: "289" }, - { label: "Португалия", value: "1749" }, - { label: "Порты", value: "1028" }, - { label: "Поселки", value: "1298" }, - { label: "После таймскипа (Haikyuu!!)", value: "2906" }, - { label: "После таймскипа (Shingeki no Kyojin)", value: "2903" }, - { label: "Послевоенное время", value: "1080" }, - { label: "Последний раз", value: "2645" }, - { label: "Последний рубеж", value: "1178" }, - { label: "Последний шанс", value: "1209" }, - { label: "Последствия", value: "2491" }, - { label: "Последствия болезни", value: "2612" }, - { label: "Послеродовая депрессия", value: "988" }, - { label: "Посмертная любовь", value: "2670" }, - { label: "Посмертный персонаж", value: "2653" }, - { label: "Пост-Пацифист", value: "2408" }, - { label: "Пост-Рейхенбах", value: "1903" }, - { label: "Пост-третий сезон (Шерлок)", value: "1977" }, - { label: "Постапокалиптика", value: "1697" }, - { label: "ПостВБ", value: "2386" }, - { label: "Постканон", value: "6" }, - { label: "ПостРагнарек", value: "2537" }, - { label: "ПостХог", value: "1883" }, - { label: "ПостЭндгейм", value: "2179" }, - { label: "Потеря девственности", value: "1234" }, - { label: "Потеря конечностей", value: "1289" }, - { label: "Потеря магических способностей", value: "1430" }, - { label: "Потеря памяти", value: "36" }, - { label: "Потеря сверхспособностей", value: "1051" }, - { label: "Поттергад", value: "2417" }, - { label: "Похищение", value: "224" }, - { label: "Похороны", value: "786" }, - { label: "Почтовые работники", value: "1821" }, - { label: "Пошлый юмор", value: "1362" }, - { label: "Поэма", value: "1938" }, - { label: "Поэты", value: "357" }, - { label: "Пояс верности", value: "1779" }, - { label: "Правда или действие", value: "1982" }, - { label: "Практопия", value: "2249" }, - { label: "Пре-гет", value: "209" }, - { label: "Пре-Рош", value: "2047" }, - { label: "Пре-слэш", value: "207" }, - { label: "Пре-фемслэш", value: "208" }, - { label: "Предательство", value: "361" }, - { label: "Предвидение", value: "1876" }, - { label: "Предложение руки и сердца", value: "1081" }, - { label: "Предопределенность", value: "411" }, - { label: "Предрассудки", value: "2046" }, - { label: "Предсмертные сообщения", value: "2174" }, - { label: "Преканон", value: "7" }, - { label: "ПреКербер", value: "2726" }, - { label: "Прекрасная эпоха", value: "1809" }, - { label: "Прелюдия", value: "2395" }, - { label: "Пренебрежение гигиеной", value: "2921" }, - { label: "Пренебрежение жизнью", value: "2216" }, - { label: "Преодоление комплексов", value: "588" }, - { label: "Преподаватели", value: "403" }, - { label: "Преподаватель/Обучающийся", value: "2507" }, - { label: "Преступники", value: "1446" }, - { label: "Преступный мир", value: "12" }, - { label: "Прибрежные города", value: "984" }, - { label: "Приватный танец", value: "2786" }, - { label: "Привязанность", value: "2372" }, - { label: "Пригород", value: "452" }, - { label: "Приемные семьи", value: "844" }, - { label: "Признания в любви", value: "1049" }, - { label: "Призраки", value: "26" }, - { label: "Приключения", value: "21" }, - { label: "Приметы / Суеверия", value: "612" }, - { label: "Примирение", value: "660" }, - { label: "Принудительная феминизация", value: "656" }, - { label: "Принудительное лечение", value: "971" }, - { label: "Принудительные отношения", value: "2302" }, - { label: "Принудительный брак", value: "307" }, - { label: "Принудительный инцест", value: "2942" }, - { label: "Принуждение", value: "1323" }, - { label: "Принятие себя", value: "2754" }, - { label: "Прислуга", value: "804" }, - { label: "Прист-кинк", value: "342" }, - { label: "Приступы агрессии", value: "2788" }, - { label: "Притча", value: "1088" }, - { label: "Приюты для животных", value: "2345" }, - { label: "ПРЛ", value: "1092" }, - { label: "Проблемы доверия", value: "2208" }, - { label: "Проблемы с законом", value: "1007" }, - { label: "Проводники душ", value: "2348" }, - { label: "Программисты", value: "574" }, - { label: "Прогрессорство", value: "449" }, - { label: "Прогулки", value: "2462" }, - { label: "Продажа души", value: "887" }, - { label: "Прозопагнозия", value: "2013" }, - { label: "Производственный роман", value: "2119" }, - { label: "Проклятия", value: "875" }, - { label: "Пролапс", value: "2926" }, - { label: "Промискуитет", value: "1817" }, - { label: "Промышленные предприятия", value: "1456" }, - { label: "Пропавшие без вести", value: "2073" }, - { label: "Пропущенная сцена", value: "1702" }, - { label: "Пророчества", value: "834" }, - { label: "Просмотр порно", value: "2341" }, - { label: "Проституция", value: "178" }, - { label: "Протезы", value: "1581" }, - { label: "Противоположности", value: "948" }, - { label: "Противоречивые чувства", value: "2528" }, - { label: "Прошлое", value: "983" }, - { label: "Прощение", value: "1545" }, - { label: "Псевдо-инцест", value: "336" }, - { label: "Псевдо-мистика", value: "2159" }, - { label: "Псевдоисторический сеттинг", value: "1479" }, - { label: "Псионика", value: "2023" }, - { label: "Психиатрические больницы", value: "135" }, - { label: "Психические расстройства", value: "69" }, - { label: "Психоз", value: "1339" }, - { label: "Психологи / Психоаналитики", value: "611" }, - { label: "Психологическая война", value: "2595" }, - { label: "Психологические пытки", value: "2825" }, - { label: "Психологические травмы", value: "40" }, - { label: "Психологический мазохизм", value: "2212" }, - { label: "Психологический ужас", value: "1261" }, - { label: "Психологическое насилие", value: "836" }, - { label: "Психология", value: "1674" }, - { label: "Психопатия", value: "869" }, - { label: "Психосоматические расстройства", value: "1834" }, - { label: "Психотерапия", value: "958" }, - { label: "ПТСР", value: "425" }, - { label: "Публичное обнажение", value: "319" }, - { label: "Пурпурная проза", value: "599" }, - { label: "Пустоши", value: "1114" }, - { label: "Пустыни", value: "692" }, - { label: "Путешественники-исследователи", value: "888" }, - { label: "Путешествия", value: "894" }, - { label: "Пытки", value: "210" }, - { label: "Пьеса", value: "434" }, - { label: "Рабочие", value: "1771" }, - { label: "Рабство", value: "51" }, - { label: "Равнодушие", value: "2516" }, - { label: "Равные отношения", value: "2728" }, - { label: "Радикальная медицина", value: "731" }, - { label: "Радио", value: "974" }, - { label: "Разбойники", value: "473" }, - { label: "Развитие отношений", value: "1404" }, - { label: "Развод", value: "200" }, - { label: "Разговоры", value: "1401" }, - { label: "Раздвоение личности", value: "287" }, - { label: "Раздельное обучение", value: "1959" }, - { label: "Разлука / Прощания", value: "1104" }, - { label: "Разница в возрасте", value: "2" }, - { label: "Разница культур", value: "1079" }, - { label: "Разновозрастная дружба", value: "2402" }, - { label: "Разнополая дружба", value: "806" }, - { label: "Разоблачения", value: "2684" }, - { label: "Разочарования", value: "646" }, - { label: "Разрушение стереотипов", value: "2805" }, - { label: "Разрушение четвертой стены", value: "381" }, - { label: "Разрыв связи", value: "2582" }, - { label: "Разум роя", value: "1459" }, - { label: "Разумное оружие", value: "2675" }, - { label: "Разумные животные", value: "895" }, - { label: "Разумные растения", value: "1037" }, - { label: "Раптофилия", value: "729" }, - { label: "Расизм", value: "433" }, - { label: "Раскрытие личностей", value: "2120" }, - { label: "Раскрытие магии", value: "1878" }, - { label: "Расплата", value: "2464" }, - { label: "Рассказ в рассказе", value: "2077" }, - { label: "Расставание", value: "506" }, - { label: "Расстройства аутистического спектра", value: "541" }, - { label: "Расстройства цветового восприятия", value: "1330" }, - { label: "Расстройства шизофренического спектра", value: "912" }, - { label: "Растаманы", value: "857" }, - { label: "Расширенная Вселенная ЗВ", value: "1950" }, - { label: "Реабилитационные центры", value: "1467" }, - { label: "Реактивное расстройство привязанности", value: "2576" }, - { label: "Реакции (стилизация)", value: "1283" }, - { label: "Реализм", value: "1201" }, - { label: "Реалити-шоу", value: "2312" }, - { label: "Ревность", value: "544" }, - { label: "Революции", value: "585" }, - { label: "Регенерация", value: "1530" }, - { label: "Регресс", value: "450" }, - { label: "Регрессия возраста", value: "102" }, - { label: "Редкие заболевания", value: "1856" }, - { label: "Реинкарнация", value: "101" }, - { label: "Рейтинг за лексику", value: "276" }, - { label: "Рейтинг за насилие и/или жестокость", value: "277" }, - { label: "Рейтинг за секс", value: "278" }, - { label: "Религиозная нетерпимость", value: "2451" }, - { label: "Религиозные войны", value: "2911" }, - { label: "Религиозные темы и мотивы", value: "198" }, - { label: "Религиозный фанатизм", value: "2548" }, - { label: "Ренессанс", value: "239" }, - { label: "Репетиторы", value: "2413" }, - { label: "Репетиции", value: "2241" }, - { label: "Репродуктивное насилие", value: "2310" }, - { label: "Реставраторы", value: "2239" }, - { label: "Рестораны", value: "412" }, - { label: "Ретеллинг", value: "52" }, - { label: "Ретрофутуризм", value: "727" }, - { label: "Реформация", value: "2199" }, - { label: "Речь Посполитая", value: "1529" }, - { label: "Риветхеды", value: "2904" }, - { label: "Римминг", value: "170" }, - { label: "Риск", value: "2467" }, - { label: "Рискованная беременность", value: "998" }, - { label: "Ритуалы", value: "749" }, - { label: "Ритуальные услуги", value: "2362" }, - { label: "Роботы", value: "513" }, - { label: "Родео", value: "1314" }, - { label: "Родители-одиночки", value: "325" }, - { label: "Родительские чувства", value: "2308" }, - { label: "Родомагия", value: "1899" }, - { label: "Роды", value: "991" }, - { label: "Рождество", value: "211" }, - { label: "Розыгрыши", value: "1807" }, - { label: "Рокеры", value: "861" }, - { label: "Роковая женщина / Роковой мужчина", value: "982" }, - { label: "Роковое усилие", value: "2604" }, - { label: "Роллер-дерби", value: "1014" }, - { label: "Романтизация", value: "1898" }, - { label: "Романтика", value: "1664" }, - { label: "Романтическая дружба", value: "2307" }, - { label: "Ромком", value: "2768" }, - { label: "Российская империя", value: "475" }, - { label: "Россия", value: "701" }, - { label: "РПП", value: "286" }, - { label: "Румыния", value: "862" }, - { label: "Русалки", value: "32" }, - { label: "Русальная неделя", value: "1067" }, - { label: "Русреал", value: "53" }, - { label: "Руферы", value: "578" }, - { label: "Рыбалка", value: "855" }, - { label: "Рыцари", value: "426" }, - { label: "Рыцарские турниры", value: "2759" }, - { label: "С чистого листа", value: "987" }, - { label: "Савойя", value: "1036" }, - { label: "Садизм / Мазохизм", value: "461" }, - { label: "Садовники", value: "1348" }, - { label: "Сайз-кинк", value: "100" }, - { label: "Самобытные культуры", value: "663" }, - { label: "Самовнушение", value: "2169" }, - { label: "Самовставка", value: "1689" }, - { label: "Самозванцы", value: "1811" }, - { label: "Самоистязание", value: "1386" }, - { label: "Самоопределение / Самопознание", value: "1278" }, - { label: "Самопожертвование", value: "750" }, - { label: "Самосуд", value: "1402" }, - { label: "Самоуничижение", value: "2667" }, - { label: "Самураи", value: "472" }, - { label: "Санатории", value: "223" }, - { label: "Сандалпанк", value: "1986" }, - { label: "Сарказм", value: "2225" }, - { label: "Сатира", value: "1944" }, - { label: "Сатиры", value: "264" }, - { label: "Сахарный диабет", value: "2609" }, - { label: "Сборник драбблов", value: "39" }, - { label: "Сборник лимериков", value: "1989" }, - { label: "Сборник пирожков", value: "743" }, - { label: "Сборник стихотворений", value: "600" }, - { label: "Сборник японской поэзии", value: "1351" }, - { label: "Свадьба", value: "184" }, - { label: "Сверхразумы", value: "966" }, - { label: "Сверхскорость", value: "1902" }, - { label: "Сверхспособности", value: "597" }, - { label: "Светлые властелины", value: "2596" }, - { label: "Светские мероприятия", value: "973" }, - { label: "Свидание вслепую", value: "741" }, - { label: "Свидания", value: "1235" }, - { label: "Свидетели", value: "1242" }, - { label: "Свингерство", value: "203" }, - { label: "Свобода", value: "2166" }, - { label: "Свободные отношения", value: "255" }, - { label: "Сводничество", value: "568" }, - { label: "Сводные родственники", value: "1445" }, - { label: "Святые", value: "393" }, - { label: "Сгенерировано ИИ", value: "3014" }, - { label: "СДВГ", value: "539" }, - { label: "Севвитус", value: "1871" }, - { label: "Северная Корея", value: "2767" }, - { label: "Северус Снейп жив", value: "2406" }, - { label: "Сегрегация", value: "2158" }, - { label: "Секретари", value: "1170" }, - { label: "Секретные миссии", value: "1829" }, - { label: "Секс без обязательств", value: "387" }, - { label: "Секс в воде", value: "718" }, - { label: "Секс в нетрезвом виде", value: "1110" }, - { label: "Секс в одежде", value: "1013" }, - { label: "Секс в публичных местах", value: "123" }, - { label: "Секс в транспорте", value: "124" }, - { label: "Секс в церкви", value: "2968" }, - { label: "Секс во время беременности", value: "279" }, - { label: "Секс во время болезни", value: "1365" }, - { label: "Секс во время менструации", value: "153" }, - { label: "Секс на камеру", value: "1101" }, - { label: "Секс на природе", value: "1256" }, - { label: "Секс перед зеркалом", value: "1255" }, - { label: "Секс по расчету", value: "1212" }, - { label: "Секс по телефону", value: "133" }, - { label: "Секс при посторонних", value: "2514" }, - { label: "Секс с воображаемыми партнерами", value: "2276" }, - { label: "Секс с использованием магии", value: "1563" }, - { label: "Секс с использованием одурманивающих веществ", value: "2635" }, - { label: "Секс с использованием посторонних предметов", value: "1661" }, - { label: "Секс с использованием сверхспособностей", value: "2610" }, - { label: "Секс со смертельным исходом", value: "1171" }, - { label: "Секс-игрушки", value: "176" }, - { label: "Секс-индустрия", value: "447" }, - { label: "Секс-клубы / Секс-вечеринки", value: "936" }, - { label: "Секс-куклы", value: "1202" }, - { label: "Секс-магия", value: "1131" }, - { label: "Секс-машины", value: "1112" }, - { label: "Секс-шопы", value: "739" }, - { label: "Сексизм", value: "464" }, - { label: "Сексомния", value: "2024" }, - { label: "Секстинг", value: "2215" }, - { label: "Сексуальная неопытность", value: "1717" }, - { label: "Сексуальное насилие", value: "2495" }, - { label: "Сексуальное обучение", value: "967" }, - { label: "Сексуальное рабство", value: "1353" }, - { label: "Сексуальные фобии", value: "1537" }, - { label: "Селфхарм", value: "76" }, - { label: "Селфцест", value: "1708" }, - { label: "Семейная сага", value: "920" }, - { label: "Семейные тайны", value: "779" }, - { label: "Семейный бизнес", value: "2338" }, - { label: "Семьи", value: "220" }, - { label: "Сенсорная депривация", value: "282" }, - { label: "Сенсорная перегрузка", value: "2351" }, - { label: "Сентиментальность", value: "2567" }, - { label: "Серая мораль", value: "839" }, - { label: "Серая мышь", value: "1250" }, - { label: "Серая реальность", value: "2278" }, - { label: "Серийные насильники", value: "1942" }, - { label: "Серийные убийцы", value: "193" }, - { label: "Серфинг", value: "764" }, - { label: "Сиамские близнецы", value: "1252" }, - { label: "Сибари", value: "131" }, - { label: "Сиблинги", value: "617" }, - { label: "Сидение на лице", value: "713" }, - { label: "Силкпанк", value: "1813" }, - { label: "Симбиоз", value: "849" }, - { label: "Символизм", value: "538" }, - { label: "Симфорофилия", value: "492" }, - { label: "Синдром восьмиклассника", value: "2032" }, - { label: "Синдром выжившего", value: "1100" }, - { label: "Синдром Дауна", value: "2106" }, - { label: "Синдром Котара", value: "2765" }, - { label: "Синдром Питера Пэна", value: "687" }, - { label: "Синдром Туретта", value: "946" }, - { label: "Синестезия", value: "960" }, - { label: "Сирены", value: "1830" }, - { label: "Сириус Блэк жив", value: "2617" }, - { label: "Сироты", value: "689" }, - { label: "Ситком", value: "2553" }, - { label: "Сказка", value: "288" }, - { label: "Скайпанк", value: "976" }, - { label: "Скандинавия", value: "781" }, - { label: "Скандинавская мифология", value: "2304" }, - { label: "Сквиртинг", value: "950" }, - { label: "Сквозное видение", value: "1280" }, - { label: "Скейтбординг", value: "1395" }, - { label: "Скелеты", value: "1766" }, - { label: "Скинхэды", value: "1757" }, - { label: "Скрытая беременность", value: "2580" }, - { label: "Скрытность", value: "2577" }, - { label: "Скрытые способности", value: "2579" }, - { label: "Скрытый Этаж", value: "2763" }, - { label: "Скульпторы", value: "2976" }, - { label: "Слатшейминг", value: "2910" }, - { label: "Следующее поколение", value: "698" }, - { label: "Слежка", value: "2102" }, - { label: "Слепота", value: "413" }, - { label: "Слияние", value: "2324" }, - { label: "Словакия", value: "2938" }, - { label: "Словения", value: "1788" }, - { label: "Сложные отношения", value: "326" }, - { label: "Слом личности", value: "623" }, - { label: "Слоуберн", value: "19" }, - { label: "Служебные животные", value: "1506" }, - { label: "Служебные отношения", value: "1347" }, - { label: "Служебный роман", value: "75" }, - { label: "Слухи / Сплетни", value: "2809" }, - { label: "Случайные убийства", value: "2673" }, - { label: "Случайный брак", value: "304" }, - { label: "Случайный поцелуй", value: "2852" }, - { label: "Случайный секс", value: "1275" }, - { label: "Смена имени", value: "2094" }, - { label: "Смена мировоззрения", value: "2865" }, - { label: "Смена сущности", value: "1707" }, - { label: "Смертельные заболевания", value: "1132" }, - { label: "Смерть антагониста", value: "2650" }, - { label: "Смерть всех персонажей", value: "2611" }, - { label: "Смерть второстепенных персонажей", value: "1713" }, - { label: "Смерть животных", value: "2210" }, - { label: "Смерть основных персонажей", value: "1647" }, - { label: "СМИ", value: "2656" }, - { label: "Смирение", value: "2272" }, - { label: "Смотрины", value: "1495" }, - { label: "Смутное время", value: "2002" }, - { label: "Снайперы", value: "1762" }, - { label: "Сновидения", value: "490" }, - { label: "Сноубординг", value: "2180" }, - { label: "Соблазнение / Ухаживания", value: "616" }, - { label: "Собственничество", value: "908" }, - { label: "Советпанк", value: "397" }, - { label: "Советский Союз", value: "468" }, - { label: "Совместная кровать", value: "1229" }, - { label: "Совместное купание", value: "1301" }, - { label: "Современное искусство", value: "1087" }, - { label: "Современность", value: "80" }, - { label: "Согласование с каноном", value: "1388" }, - { label: "Сожаления", value: "550" }, - { label: "Создание общества", value: "2643" }, - { label: "Созидание", value: "2590" }, - { label: "Сокровища", value: "893" }, - { label: "Сокрытие убийства", value: "2311" }, - { label: "Солянка из Сансов", value: "2515" }, - { label: "Сомелье / Бармены", value: "865" }, - { label: "Сомнамбулизм", value: "1394" }, - { label: "Сомнения", value: "1544" }, - { label: "Сомнофилия", value: "95" }, - { label: "Сонный паралич", value: "1203" }, - { label: "Соперничество", value: "671" }, - { label: "Сорванная свадьба", value: "2044" }, - { label: "Сострадание", value: "885" }, - { label: "Состязания", value: "910" }, - { label: "Состязания с участием животных", value: "1154" }, - { label: "Сотриголова — отец Хитоши Шинсо", value: "2877" }, - { label: "Соулмейты", value: "1696" }, - { label: "Соулмейты: Действия", value: "2575" }, - { label: "Соулмейты: Изменение восприятия", value: "1400" }, - { label: "Соулмейты: Контакт через кожу", value: "2608" }, - { label: "Соулмейты: Ментальная связь", value: "2841" }, - { label: "Соулмейты: Общая боль", value: "2583" }, - { label: "Соулмейты: Общие эмоции", value: "2637" }, - { label: "Соулмейты: Опознавательные фразы", value: "1399" }, - { label: "Соулмейты: Предметы", value: "2563" }, - { label: "Соулмейты: Предпочтения", value: "2521" }, - { label: "Соулмейты: Разноцветные глаза", value: "2461" }, - { label: "Соулмейты: Ранения", value: "2134" }, - { label: "Соулмейты: Сны", value: "2414" }, - { label: "Соулмейты: Тактильный контакт", value: "2190" }, - { label: "Соулмейты: Татуировки", value: "2711" }, - { label: "Соулмейты: Физические проявления", value: "2864" }, - { label: "Социальная фантастика", value: "591" }, - { label: "Социальные темы и мотивы", value: "1017" }, - { label: "Социальные эксперименты", value: "2756" }, - { label: "Социальный паразитизм", value: "2444" }, - { label: "Социофобия", value: "826" }, - { label: "Спарринги", value: "1575" }, - { label: "Спасатели", value: "392" }, - { label: "Спасение жизни", value: "1299" }, - { label: "Спасение мира", value: "812" }, - { label: "Спектрофилия", value: "2634" }, - { label: "Спецагенты", value: "401" }, - { label: "Спиритизм", value: "1478" }, - { label: "Сплошинг", value: "424" }, - { label: "Спонтанный секс", value: "2918" }, - { label: "Спорт", value: "15" }, - { label: "Спортзалы", value: "1359" }, - { label: "Сражения", value: "2868" }, - { label: "Средневековье", value: "238" }, - { label: "Ссоры / Конфликты", value: "645" }, - { label: "Стаи", value: "2884" }, - { label: "Сталкеры / Диггеры (субкультура)", value: "1029" }, - { label: "Сталкинг", value: "605" }, - { label: "Становление героя", value: "680" }, - { label: "Стёб", value: "1690" }, - { label: "Стереотипы", value: "809" }, - { label: "Стимпанк", value: "25" }, - { label: "Стимуляция руками", value: "225" }, - { label: "Стихи", value: "1692" }, - { label: "Стихийные бедствия", value: "1352" }, - { label: "Стихотворные вставки", value: "2404" }, - { label: "СтихоТворцы", value: "3015" }, - { label: "Стокгольмский синдром / Лимский синдром", value: "1217" }, - { label: "Столетняя война", value: "2243" }, - { label: "Сторонний рассказчик", value: "1383" }, - { label: "Стоунпанк", value: "1987" }, - { label: "Страдания", value: "2009" }, - { label: "Странногеддон", value: "1882" }, - { label: "Страны Балтии", value: "1031" }, - { label: "Страпоны", value: "380" }, - { label: "Страсть", value: "1955" }, - { label: "Страх перед родителями", value: "2332" }, - { label: "Стрельба из лука", value: "1791" }, - { label: "Стримеры / Ютуберы", value: "1473" }, - { label: "Стрип-клубы", value: "79" }, - { label: "Стрит-арт", value: "1012" }, - { label: "Строители", value: "1774" }, - { label: "Студенты", value: "274" }, - { label: "Студенты по обмену", value: "859" }, - { label: "Студенческие городки", value: "753" }, - { label: "Субкультуры", value: "1061" }, - { label: "Субординация", value: "1099" }, - { label: "Суд", value: "827" }, - { label: "Суккубы / Инкубы", value: "334" }, - { label: "Сумасшествие", value: "1239" }, - { label: "Сунагакуре", value: "2476" }, - { label: "Супергерои", value: "67" }, - { label: "Суперзлодеи", value: "1205" }, - { label: "Супермаркеты", value: "843" }, - { label: "Суррогатная беременность", value: "1427" }, - { label: "Сухой закон", value: "2100" }, - { label: "Сухой оргазм", value: "2818" }, - { label: "Сфера ИТ", value: "2534" }, - { label: "Сфинксы", value: "1349" }, - { label: "Сценарий (стилизация)", value: "323" }, - { label: "Счастливый финал", value: "3" }, - { label: "США", value: "640" }, - { label: "Съемочная площадка", value: "410" }, - { label: "Сын Тьмы", value: "2175" }, - { label: "Сэнгоку Джидай", value: "2380" }, - { label: "Сэнгоку Дзидай", value: "2834" }, - { label: "Сэцубун", value: "2969" }, - { label: "Сюрреализм / Фантасмагория", value: "872" }, - { label: "Т/И / Л/И", value: "167" }, - { label: "Таджикистан", value: "2974" }, - { label: "Таиланд", value: "1034" }, - { label: "Тайна происхождения", value: "662" }, - { label: "Тайная личность", value: "194" }, - { label: "Тайная сущность", value: "1512" }, - { label: "Тайны / Секреты", value: "906" }, - { label: "Тайные друзья (игра)", value: "2908" }, - { label: "Тайные организации", value: "572" }, - { label: "Тайные поклонники", value: "1374" }, - { label: "Тайный НапиСанта", value: "2972" }, - { label: "Таксисты", value: "2679" }, - { label: "Тактильный голод", value: "2197" }, - { label: "Тактильный контакт", value: "2875" }, - { label: "Танабата", value: "2340" }, - { label: "Танцы", value: "108" }, - { label: "Танцы на пилоне", value: "1370" }, - { label: "Таро", value: "642" }, - { label: "Тату-салоны", value: "324" }, - { label: "Татуировки", value: "327" }, - { label: "Твинцест", value: "1652" }, - { label: "Творческий кризис", value: "2107" }, - { label: "Театры", value: "195" }, - { label: "Телекинез", value: "951" }, - { label: "Телепатия", value: "127" }, - { label: "Телепортация", value: "1794" }, - { label: "Телесные жидкости", value: "1220" }, - { label: "Телесные наказания", value: "132" }, - { label: "Телесный хоррор", value: "212" }, - { label: "Телохранители", value: "400" }, - { label: "Темная сторона (Гарри Поттер)", value: "1937" }, - { label: "Темная сторона Силы", value: "1946" }, - { label: "Темная Эра (Bungou Stray Dogs)", value: "2494" }, - { label: "Темное прошлое", value: "2229" }, - { label: "Темное фэнтези", value: "1963" }, - { label: "Темные властелины", value: "1224" }, - { label: "Темные эльфы", value: "2597" }, - { label: "Темный романтизм", value: "2128" }, - { label: "Темы ментального здоровья", value: "2344" }, - { label: "Темы этики и морали", value: "1106" }, - { label: "Теннис", value: "1066" }, - { label: "Тентакли", value: "129" }, - { label: "Теория инверсии", value: "1980" }, - { label: "Теория Одурманивания", value: "2493" }, - { label: "Термокинез", value: "2793" }, - { label: "Террористы", value: "556" }, - { label: "Теслапанк", value: "1988" }, - { label: "Техники", value: "1272" }, - { label: "Техногенные катастрофы", value: "526" }, - { label: "Технокинез", value: "1271" }, - { label: "Технологии", value: "1232" }, - { label: "Технотриллер", value: "649" }, - { label: "Течка / Гон", value: "283" }, - { label: "Тим Дрейк — Джокер-младший", value: "2454" }, - { label: "Тим Дрейк — Кэтлэд", value: "2453" }, - { label: "Тихий секс", value: "1185" }, - { label: "Товарищи по несчастью", value: "2666" }, - { label: "Токсичные родственники", value: "2008" }, - { label: "Том Реддл — не Темный Лорд", value: "2394" }, - { label: "Томбои", value: "565" }, - { label: "Томгерлз", value: "848" }, - { label: "Торговля органами", value: "1358" }, - { label: "Торговцы", value: "2098" }, - { label: "Торговые центры", value: "1416" }, - { label: "Тоталитаризм", value: "1169" }, - { label: "Травники / Травницы", value: "1211" }, - { label: "Трагедия", value: "919" }, - { label: "Трагикомедия", value: "1454" }, - { label: "Традиции", value: "1463" }, - { label: "Трансгендерные персонажи", value: "175" }, - { label: "Трансплантация", value: "2364" }, - { label: "Трансфобия", value: "420" }, - { label: "Тревожное расстройство личности", value: "1914" }, - { label: "Тревожность", value: "1318" }, - { label: "Трейлеры", value: "785" }, - { label: "Тренировки / Обучение", value: "876" }, - { label: "Третий лишний", value: "1176" }, - { label: "Третья мировая", value: "624" }, - { label: "Три корейских государства", value: "2322" }, - { label: "Трикстеры", value: "1507" }, - { label: "Триллер", value: "169" }, - { label: "Трилогия Юникрона", value: "2378" }, - { label: "Трипофобия", value: "2342" }, - { label: "Трисам", value: "758" }, - { label: "Трихотилломания", value: "2185" }, - { label: "Тролли", value: "190" }, - { label: "Трудные отношения с родителями", value: "1490" }, - { label: "Трудный характер", value: "2123" }, - { label: "Трудоголизм", value: "1848" }, - { label: "Трущобы", value: "2822" }, - { label: "Туалетный юмор", value: "1247" }, - { label: "Туберкулез", value: "2163" }, - { label: "Турция", value: "794" }, - { label: "Тщеславие", value: "2578" }, - { label: "Тюрьмы / Темницы", value: "117" }, - { label: "Тяжелое детство", value: "2614" }, - { label: "Убийства", value: "621" }, - { label: "Убийственная пара", value: "1918" }, - { label: "Убийца поневоле", value: "2671" }, - { label: "Уважение", value: "2688" }, - { label: "Ужасы", value: "1679" }, - { label: "Узбекистан", value: "1293" }, - { label: "Уизлигады", value: "1862" }, - { label: "Украина", value: "716" }, - { label: "Укрытия", value: "199" }, - { label: "Уличные артисты", value: "2550" }, - { label: "Уличные гонки", value: "880" }, - { label: "Умбракинез", value: "2355" }, - { label: "Универсалы", value: "657" }, - { label: "Унижения", value: "1319" }, - { label: "Упоминания аддикций", value: "2848" }, - { label: "Упоминания алкоголя", value: "2415" }, - { label: "Упоминания беременности", value: "2057" }, - { label: "Упоминания войны", value: "2707" }, - { label: "Упоминания жестокости", value: "2447" }, - { label: "Упоминания зоофилии", value: "2620" }, - { label: "Упоминания измены", value: "2876" }, - { label: "Упоминания изнасилования", value: "521" }, - { label: "Упоминания инвалидности", value: "2705" }, - { label: "Упоминания инцеста", value: "2285" }, - { label: "Упоминания каннибализма", value: "2427" }, - { label: "Упоминания курения", value: "2450" }, - { label: "Упоминания мужской беременности", value: "2226" }, - { label: "Упоминания наркотиков", value: "144" }, - { label: "Упоминания насилия", value: "2207" }, - { label: "Упоминания нездоровых отношений", value: "2892" }, - { label: "Упоминания проституции", value: "2618" }, - { label: "Упоминания пыток", value: "2410" }, - { label: "Упоминания расизма", value: "2737" }, - { label: "Упоминания религии", value: "1048" }, - { label: "Упоминания самоубийства", value: "77" }, - { label: "Упоминания секса", value: "2747" }, - { label: "Упоминания селфхарма", value: "2105" }, - { label: "Упоминания смертей", value: "2081" }, - { label: "Упоминания смертей животных", value: "2211" }, - { label: "Упоминания телесного хоррора", value: "2466" }, - { label: "Упоминания терроризма", value: "774" }, - { label: "Упоминания убийств", value: "1501" }, - { label: "Управление оргазмом", value: "118" }, - { label: "Управление стихиями", value: "2091" }, - { label: "Уретральное проникновение", value: "1469" }, - { label: "Уро-кинк", value: "762" }, - { label: "Условное бессмертие", value: "2117" }, - { label: "Уся / Сянься", value: "2714" }, - { label: "Утопия", value: "350" }, - { label: "Утраченная тройка", value: "2535" }, - { label: "Утренний секс", value: "2882" }, - { label: "Уход за персонажами с инвалидностью", value: "2133" }, - { label: "Ухудшение отношений", value: "1219" }, - { label: "Учебные заведения", value: "1694" }, - { label: "Ученые", value: "402" }, - { label: "Уют", value: "2738" }, - { label: "Фавориты", value: "2135" }, - { label: "Фамильяры", value: "1045" }, - { label: "Фан-Бинго", value: "2957" }, - { label: "Фан-Фанты", value: "2953" }, - { label: "Фандомная аналитика", value: "742" }, - { label: "Фандомная Битва", value: "253" }, - { label: "Фансервис", value: "2483" }, - { label: "Фантастика", value: "1670" }, - { label: "Фастберн", value: "2016" }, - { label: "Фастфуд / Закусочные", value: "1025" }, - { label: "Феи", value: "191" }, - { label: "Фельчинг", value: "2368" }, - { label: "Фемдом", value: "54" }, - { label: "Феминистские темы и мотивы", value: "523" }, - { label: "Феминитивы", value: "390" }, - { label: "Фениксы", value: "1215" }, - { label: "Феодализм", value: "1504" }, - { label: "Фермы / Ранчо", value: "329" }, - { label: "Фестивали", value: "1006" }, - { label: "Фехтование", value: "1775" }, - { label: "Фиггинг", value: "1439" }, - { label: "Фигурное катание", value: "460" }, - { label: "Физиотерапия", value: "2222" }, - { label: "Физическая сверхсила", value: "2880" }, - { label: "Фикрайтеры / Фикридеры", value: "1284" }, - { label: "Фиксированная раскладка", value: "1344" }, - { label: "Фиктивные отношения", value: "308" }, - { label: "Фиктивный брак", value: "34" }, - { label: "Философия", value: "1675" }, - { label: "Финляндия", value: "1133" }, - { label: "Фистинг", value: "151" }, - { label: "Флафф", value: "1667" }, - { label: "Флирт", value: "349" }, - { label: "Флот", value: "846" }, - { label: "Фобии", value: "977" }, - { label: "Фольклор и предания", value: "1273" }, - { label: "Форнифилия", value: "580" }, - { label: "Фотографы", value: "348" }, - { label: "Фотокинез", value: "1805" }, - { label: "Франция", value: "691" }, - { label: "Фред Уизли жив", value: "2941" }, - { label: "Фроттаж", value: "96" }, - { label: "Фуд-плей", value: "509" }, - { label: "Фуд-фетиш", value: "2168" }, - { label: "Фурри", value: "996" }, - { label: "Фут-фетиш", value: "432" }, - { label: "Футбол", value: "458" }, - { label: "Фэйри", value: "2028" }, - { label: "Фэнтези", value: "1669" }, - { label: "Хакеры", value: "340" }, - { label: "Ханами", value: "1155" }, - { label: "Ханахаки", value: "45" }, - { label: "Ханжество", value: "882" }, - { label: "Характерная для канона жестокость", value: "896" }, - { label: "Харассмент", value: "477" }, - { label: "Хиерофилия", value: "2366" }, - { label: "Химеры", value: "816" }, - { label: "Химическое оружие", value: "1186" }, - { label: "Химэдэрэ / Оджидэрэ", value: "1363" }, - { label: "Хип-хоп (субкультура)", value: "1822" }, - { label: "Хиппи", value: "784" }, - { label: "Хиромантия", value: "2749" }, - { label: "Хирургическая коррекция пола", value: "1249" }, - { label: "Хирургические операции", value: "1845" }, - { label: "Хобби", value: "1161" }, - { label: "Хогсмид", value: "2388" }, - { label: "Хоккей", value: "828" }, - { label: "Холодная война", value: "2140" }, - { label: "Холодное оружие", value: "1187" }, - { label: "Хорватия", value: "1433" }, - { label: "Хороший плохой финал", value: "1266" }, - { label: "Хронические заболевания", value: "1267" }, - { label: "Хронокинез", value: "1447" }, - { label: "Хронофантастика", value: "56" }, - { label: "Хтонические существа", value: "504" }, - { label: "Художники", value: "140" }, - { label: "Хуманизация", value: "177" }, - { label: "Хэллоуин", value: "205" }, - { label: "Хюльдры", value: "1397" }, - { label: "Цветочные магазины", value: "164" }, - { label: "Целители", value: "1107" }, - { label: "Церкви", value: "533" }, - { label: "Цикличность", value: "1979" }, - { label: "Цирки", value: "259" }, - { label: "Цукими", value: "1798" }, - { label: "Цундэрэ", value: "1137" }, - { label: "Цыгане", value: "1091" }, - { label: "Чайлдфри", value: "592" }, - { label: "Чайные церемонии", value: "2416" }, - { label: "Частичный ООС", value: "1409" }, - { label: "Частные детективы", value: "427" }, - { label: "Черная археология", value: "2503" }, - { label: "Черная мораль", value: "2438" }, - { label: "Чернобыльская катастрофа", value: "775" }, - { label: "Черный юмор", value: "66" }, - { label: "Чехия", value: "1021" }, - { label: "Чиновники", value: "1991" }, - { label: "Чирлидинг", value: "1431" }, - { label: "Чистокровные AU (Гарри Поттер)", value: "1905" }, - { label: "Чосон", value: "571" }, - { label: "Чувство вины", value: "902" }, - { label: "Чумные доктора", value: "1125" }, - { label: "Чхусок", value: "2973" }, - { label: "Шаманы", value: "484" }, - { label: "Шантаж", value: "569" }, - { label: "Шармбатон", value: "2733" }, - { label: "Швейцария", value: "1022" }, - { label: "Шейпшифтеры", value: "29" }, - { label: "Шелки", value: "2431" }, - { label: "Шифры", value: "2232" }, - { label: "Школьная иерархия", value: "2787" }, - { label: "Школьники", value: "2566" }, - { label: "Школьные годы Тома Реддла", value: "1913" }, - { label: "Школьные ОтМетки", value: "3017" }, - { label: "Школьные поездки", value: "2481" }, - { label: "Школьный роман", value: "2440" }, - { label: "Шопинг", value: "864" }, - { label: "Шоу-бизнес", value: "17" }, - { label: "Шпионы", value: "148" }, - { label: "Шрамирование", value: "891" }, - { label: "Шрамы", value: "328" }, - { label: "Эвтаназия", value: "1102" }, - { label: "Эгоизм", value: "2071" }, - { label: "Эдвардианская эпоха", value: "2975" }, - { label: "Эзотерические темы и мотивы", value: "2801" }, - { label: "Эйблизм", value: "453" }, - { label: "Эйдетическая память", value: "975" }, - { label: "Эйфория", value: "2458" }, - { label: "Экзамены", value: "2591" }, - { label: "Экзистенциальный кризис", value: "1200" }, - { label: "Экзорцизм", value: "494" }, - { label: "Экологи / Биологи", value: "631" }, - { label: "Экологический кризис", value: "485" }, - { label: "Экономический кризис", value: "1547" }, - { label: "Эксаудиризм", value: "2151" }, - { label: "Эксгибиционизм", value: "489" }, - { label: "Эксперимент", value: "1686" }, - { label: "Экстрасенсы", value: "371" }, - { label: "Экшн", value: "1673" }, - { label: "Электрокинез", value: "1857" }, - { label: "Электростимуляция", value: "807" }, - { label: "Элементы ангста", value: "2696" }, - { label: "Элементы гета", value: "1714" }, - { label: "Элементы дарка", value: "2703" }, - { label: "Элементы детектива", value: "2698" }, - { label: "Элементы драмы", value: "2697" }, - { label: "Элементы других видов отношений", value: "454" }, - { label: "Элементы мистики", value: "1324" }, - { label: "Элементы психологии", value: "2824" }, - { label: "Элементы романтики", value: "2742" }, - { label: "Элементы слэша", value: "1715" }, - { label: "Элементы ужасов", value: "2721" }, - { label: "Элементы фемслэша", value: "1716" }, - { label: "Элементы флаффа", value: "2731" }, - { label: "Элементы юмора / Элементы стёба", value: "2011" }, - { label: "Эльфы", value: "44" }, - { label: "Эметофилия", value: "1434" }, - { label: "Эмо", value: "641" }, - { label: "Эмоциональная одержимость", value: "1270" }, - { label: "Эмпатия", value: "1177" }, - { label: "Энергетический вампиризм", value: "818" }, - { label: "Эпидемии", value: "619" }, - { label: "Эпизод 23.5", value: "2469" }, - { label: "Эпилепсия", value: "1511" }, - { label: "Эпилог? Какой эпилог?", value: "2651" }, - { label: "Эпоха Бакумацу", value: "1472" }, - { label: "Эпоха Мэйдзи", value: "1435" }, - { label: "Эпоха Тайсё", value: "2015" }, - { label: "Эрик Киллмонгер жив", value: "2897" }, - { label: "Эрогуро", value: "1731" }, - { label: "Эротическая дрессировка", value: "2026" }, - { label: "Эротическая лактация", value: "284" }, - { label: "Эротическая мумификация", value: "415" }, - { label: "Эротическая сверхстимуляция", value: "1245" }, - { label: "Эротические наказания", value: "125" }, - { label: "Эротические ролевые игры", value: "111" }, - { label: "Эротические сны", value: "128" }, - { label: "Эротические фантазии", value: "561" }, - { label: "Эротический массаж", value: "1518" }, - { label: "Эротический перенос", value: "744" }, - { label: "Эротомания", value: "2922" }, - { label: "Эскапизм", value: "1552" }, - { label: "Эскорты", value: "922" }, - { label: "Эстетика", value: "2021" }, - { label: "ЭТЕРНА", value: "2948" }, - { label: "Этнические анклавы", value: "2732" }, - { label: "Этническое фэнтези", value: "667" }, - { label: "Эффект бабочки", value: "1572" }, - { label: "Ювелиры", value: "2964" }, - { label: "Южная Америка", value: "832" }, - { label: "Южная готика", value: "1077" }, - { label: "Южная Корея", value: "674" }, - { label: "Юмор", value: "1666" }, - { label: "Юристы", value: "333" }, - { label: "Я никогда не... (игра)", value: "1436" }, - { label: "Явное согласие", value: "1569" }, - { label: "Ядерная война", value: "889" }, - { label: "Язык жестов", value: "2962" }, - { label: "Язык животных", value: "1327" }, - { label: "Язык растений", value: "2622" }, - { label: "Язык цветов", value: "272" }, - { label: "Ямадад", value: "2916" }, - { label: "Янгирэ", value: "1257" }, - { label: "Яндэрэ", value: "1136" }, - { label: "Яогуаи", value: "2347" }, - { label: "Япония", value: "665" }, - { label: "Японские фестивали", value: "2555" }, - { label: "Ящеролюди", value: "682" }, - { label: "Abuse/Comfort", value: "2599" }, - { label: "Aged down", value: "229" }, - { label: "Aged up", value: "4" }, - { label: "Artfic", value: "68" }, - { label: "AU", value: "1683" }, - { label: "AU: Альтернативные способности", value: "2086" }, - { label: "AU: Без апокалипсиса", value: "2051" }, - { label: "AU: Без войны", value: "2305" }, - { label: "AU: Без магии", value: "1052" }, - { label: "AU: Без мистики", value: "2489" }, - { label: "AU: Без сверхспособностей", value: "1053" }, - { label: "AU: Без супергероев", value: "1152" }, - { label: "AU: Все гражданские", value: "2797" }, - { label: "AU: Все друзья", value: "2552" }, - { label: "AU: Все люди", value: "1151" }, - { label: "AU: Все хорошо", value: "2124" }, - { label: "AU: Другая страна", value: "2165" }, - { label: "AU: Другая эпоха", value: "2452" }, - { label: "AU: Другое детство", value: "2360" }, - { label: "AU: Другое знакомство", value: "2217" }, - { label: "AU: Другое семейное положение", value: "2902" }, - { label: "AU: Не родственники", value: "2167" }, - { label: "AU: Профессиональные герои (BNHA)", value: "2924" }, - { label: "AU: Родственники", value: "2164" }, - { label: "AU: Школа", value: "2532" }, - { label: "AU: Age swap", value: "917" }, - { label: "AU: Kitty Gang", value: "2928" }, - { label: "AU: Race swap", value: "933" }, - { label: "AU: Reverse", value: "1156" }, - { label: "AU: Role swap", value: "1054" }, - { label: "Authors Time", value: "2959" }, - { label: "BDSM", value: "1646" }, - { label: "BDSM-вселенная", value: "1078" }, - { label: "BDSM: Домспейс", value: "1444" }, - { label: "BDSM: Дроп", value: "1411" }, - { label: "BDSM: Сабспейс", value: "1410" }, - { label: "BDSM: Aftercare", value: "1523" }, - { label: "BEAST (Bungou Stray Dogs)", value: "2457" }, - { label: "Blackwatch", value: "2116" }, - { label: "Body inflation", value: "389" }, - { label: "Character study", value: "363" }, - { label: "Cockwarming", value: "318" }, - { label: "Dark academia", value: "2430" }, - { label: "DDLG / DDLB / MDLG / MDLB", value: "1033" }, - { label: "Dirty talk", value: "139" }, - { label: "Doggy style", value: "1361" }, - { label: "Dollification", value: "2780" }, - { label: "Dramione Authors Tournament", value: "2950" }, - { label: "EIQ", value: "271" }, - { label: "ER", value: "1687" }, - { label: "Final Haikyuu Quest", value: "2652" }, - { label: "Fix-it", value: "1" }, - { label: "Frenemies", value: "666" }, - { label: "Gangbang", value: "315" }, - { label: "Glory hole", value: "2188" }, - { label: "Golden Age Overwatch", value: "2112" }, - { label: "GOLockdown", value: "2391" }, - { label: "Gun play", value: "252" }, - { label: "Hurt/Comfort", value: "1682" }, - { label: "Hydra!AU", value: "1931" }, - { label: "Ironqrow week", value: "2209" }, - { label: "Kimetsu Gakuen", value: "2574" }, - { label: "Knife play", value: "152" }, - { label: "KPOP-AUFEST", value: "1851" }, - { label: "LARP", value: "1510" }, - { label: "LOVE ОтМетки", value: "2981" }, - { label: "Maleval week", value: "2382" }, - { label: "Marvel ГФИ", value: "2896" }, - { label: "Mate or die", value: "375" }, - { label: "Mirror!verse (Звездный путь)", value: "2899" }, - { label: "Moresome", value: "759" }, - { label: "N раз", value: "263" }, - { label: "Omnic Crisis Era", value: "2111" }, - { label: "ORIGINAL'S 2021", value: "2939" }, - { label: "Parentlock", value: "2053" }, - { label: "Patronus of Dramione", value: "2958" }, - { label: "Post-Fall of Overwatch", value: "2114" }, - { label: "Post-Recall", value: "2115" }, - { label: "PWP", value: "1680" }, - { label: "Rape/Revenge", value: "2195" }, - { label: "Reverse Falls", value: "1879" }, - { label: "RofR ДраМиВояж", value: "2954" }, - { label: "RST", value: "815" }, - { label: "RTFM", value: "2327" }, - { label: "Service top / Power bottom", value: "563" }, - { label: "Shingeki no Kyojin High School AU", value: "2037" }, - { label: "Shingeki! Kyojin Chuugakkou", value: "2038" }, - { label: "Sickfic", value: "422" }, - { label: "Songfic", value: "1684" }, - { label: "Spider-son & Iron-dad", value: "1885" }, - { label: "Sugar daddy", value: "106" }, - { label: "Sugar mommy", value: "160" }, - { label: "Superfamily (Мстители)", value: "2623" }, - { label: "Superior Iron Man", value: "2676" }, - { label: "Talon (Overwatch)", value: "2110" }, - { label: "The Big Four / The Super Six", value: "1954" }, - { label: "Trench AU", value: "2387" }, - { label: "TYL", value: "1933" }, - { label: "Underage", value: "1658" }, - { label: "URT", value: "2761" }, - { label: "UST", value: "1709" }, - { label: "Versefic", value: "2729" }, - { label: "Videofic", value: "2480" }, - { label: "Womance", value: "265" }, - { label: "X век", value: "240" }, - { label: "XI век", value: "241" }, - { label: "XII век", value: "242" }, - { label: "XIII век", value: "243" }, - { label: "XIV век", value: "244" }, - { label: "XIX век", value: "249" }, - { label: "XV век", value: "245" }, - { label: "XVI век", value: "246" }, - { label: "XVII век", value: "247" }, - { label: "XVIII век", value: "248" }, - { label: "XX век", value: "1004" }, + { label: 'Все', value: '' }, + { label: '#1fic1week', value: '884' }, + { label: '#7daystowrite', value: '2961' }, + { label: '#Бинго_ТФ', value: '1458' }, + { label: '#миниатюры3days', value: '2118' }, + { label: '#Не_Тайный_Санта', value: '2760' }, + { label: '#Писательские фанты-блиц', value: '2613' }, + { label: '#cosmowrite', value: '2284' }, + { label: '#femKHRweek', value: '2658' }, + { label: '#FourShips', value: '2588' }, + { label: '#Goretober', value: '1861' }, + { label: '#InternationalVampireDay / #WorldDraculaDay', value: '2048' }, + { label: '#KattWeek', value: '2717' }, + { label: '#Kinktober', value: '1867' }, + { label: '#LotorWeek', value: '2657' }, + { label: '#MerMay', value: '1122' }, + { label: '#NaNoWriMo', value: '1852' }, + { label: '#nsfweruriweek', value: '2944' }, + { label: '#RCMiaChallenge', value: '2700' }, + { label: '#SheithMonth', value: '2432' }, + { label: '#SixDrabbles', value: '2293' }, + { label: '#Writober', value: '1831' }, + { label: '1900-е годы', value: '369' }, + { label: '1910-е годы', value: '213' }, + { label: '1920-е годы', value: '81' }, + { label: '1930-е годы', value: '82' }, + { label: '1940-е годы', value: '83' }, + { label: '1950-е годы', value: '84' }, + { label: '1960-е годы', value: '85' }, + { label: '1970-е годы', value: '86' }, + { label: '1980-е годы', value: '87' }, + { label: '1990-е годы', value: '88' }, + { label: '2000-е годы', value: '590' }, + { label: '2010-е годы', value: '2562' }, + { label: '2014 год', value: '2977' }, + { label: '2P', value: '1910' }, + { label: '9 мая', value: '1019' }, + { label: 'Аборт / Выкидыш', value: '306' }, + { label: 'Аватары', value: '2401' }, + { label: 'Авиатранспорт', value: '962' }, + { label: 'Авиация', value: '455' }, + { label: 'Аврорат', value: '1906' }, + { label: 'Австралия', value: '767' }, + { label: 'Австрия', value: '1024' }, + { label: 'Автомастерские', value: '788' }, + { label: 'Авторская пунктуация', value: '2886' }, + { label: 'Авторские неологизмы', value: '1123' }, + { label: 'Автоспорт', value: '856' }, + { label: 'Автостоп', value: '1429' }, + { label: 'Аддикции', value: '1230' }, + { label: 'Адреналиновая зависимость', value: '1480' }, + { label: 'Азартные игры', value: '1009' }, + { label: 'Азкабан', value: '1922' }, + { label: 'Аквакинез', value: '1380' }, + { label: 'Аквапарки', value: '789' }, + { label: 'Акростих', value: '911' }, + { label: 'Актеры', value: '408' }, + { label: 'Акустикофилия', value: '903' }, + { label: 'Алекситимия', value: '2020' }, + { label: 'Алкоголь', value: '201' }, + { label: 'Алкогольные игры', value: '1534' }, + { label: 'Аллергии', value: '746' }, + { label: 'Алхимия', value: '1174' }, + { label: 'Альбинизм', value: '1213' }, + { label: 'Альпинизм', value: '1448' }, + { label: 'Альтернативная мировая история', value: '442' }, + { label: 'Альтернативное размножение', value: '2751' }, + { label: 'Альтернативные судьбы', value: '2839' }, + { label: 'Амазонки', value: '1424' }, + { label: 'Амегакуре', value: '2571' }, + { label: 'Американский футбол', value: '808' }, + { label: 'Ампутация', value: '704' }, + { label: 'Амфибии', value: '2792' }, + { label: 'Анальгезия', value: '1254' }, + { label: 'Анальный оргазм', value: '999' }, + { label: 'Анальный секс', value: '215' }, + { label: 'Анархия', value: '1175' }, + { label: 'Анахронизмы', value: '780' }, + { label: 'Ангелы', value: '91' }, + { label: 'Ангелы-хранители', value: '1044' }, + { label: 'Ангст', value: '1665' }, + { label: 'Андрогинная внешность', value: '2602' }, + { label: 'Андроиды', value: '155' }, + { label: 'Анимагия', value: '2519' }, + { label: 'Анимализм', value: '1443' }, + { label: 'Аномальные зоны', value: '822' }, + { label: 'Анонимный секс', value: '112' }, + { label: 'Анрождение', value: '1246' }, + { label: 'Антарктида', value: '2205' }, + { label: 'Анти-Сью (Анти-Стью)', value: '1150' }, + { label: 'Антигерои', value: '418' }, + { label: 'Антизлодеи', value: '1833' }, + { label: 'Антикварные магазины', value: '1496' }, + { label: 'Антисоциальное расстройство личности', value: '734' }, + { label: 'Антиутопия', value: '1700' }, + { label: 'Античность', value: '237' }, + { label: 'Апатия', value: '1850' }, + { label: 'Аптеки', value: '1240' }, + { label: 'Аргентина', value: '1146' }, + { label: 'Арены', value: '940' }, + { label: 'Аристократия', value: '16' }, + { label: 'Арктика', value: '2426' }, + { label: 'Армия', value: '22' }, + { label: 'Аромантичные персонажи', value: '339' }, + { label: 'Артефакты', value: '924' }, + { label: 'Археологи', value: '405' }, + { label: 'Архивы', value: '2594' }, + { label: 'Архитекторы', value: '1011' }, + { label: 'Асексофобия', value: '1120' }, + { label: 'Асексуальные персонажи', value: '168' }, + { label: 'Аскетизм', value: '606' }, + { label: 'Асоциальность', value: '2019' }, + { label: 'Ассистенты', value: '2641' }, + { label: 'Астма', value: '1121' }, + { label: 'Астральное проецирование', value: '2530' }, + { label: 'Астральные миры', value: '1379' }, + { label: 'Астрология', value: '476' }, + { label: 'Астронавты', value: '2220' }, + { label: 'Астрономия', value: '1372' }, + { label: 'Асфиксия', value: '104' }, + { label: 'Атеисты', value: '669' }, + { label: 'Атлантида', value: '1144' }, + { label: 'Атмокинез', value: '1964' }, + { label: 'Атмосферная зарисовка', value: '2802' }, + { label: 'Атомные электростанции', value: '2192' }, + { label: 'Аудиокинез', value: '2849' }, + { label: 'Аукционы', value: '607' }, + { label: 'Аутоассасинофилия', value: '2025' }, + { label: 'Аутоканнибализм', value: '2593' }, + { label: 'Аутофелляция / Аутокуннилингус', value: '610' }, + { label: 'Африка', value: '791' }, + { label: 'Афродизиаки', value: '103' }, + { label: 'Аффект', value: '938' }, + { label: 'Аэрокинез', value: '2850' }, + { label: 'Байкеры', value: '522' }, + { label: 'Байронические герои', value: '2900' }, + { label: 'Балет', value: '404' }, + { label: 'Баллада', value: '2318' }, + { label: 'Банки / Биржи', value: '937' }, + { label: 'Банши', value: '250' }, + { label: 'Барти Крауч-младший — не Пожиратель Смерти', value: '2795' }, + { label: 'Бары', value: '142' }, + { label: 'Баскетбол', value: '829' }, + { label: 'Баски', value: '1763' }, + { label: 'Бастарды', value: '603' }, + { label: 'Батохаки', value: '2830' }, + { label: 'Бедность', value: '979' }, + { label: 'Без Волдеморта', value: '2160' }, + { label: 'Без диалогов (стилизация)', value: '2885' }, + { label: 'Без золотого трио', value: '2648' }, + { label: 'Без Избранного (Гарри Поттер)', value: '2762' }, + { label: 'Без канонических персонажей', value: '2694' }, + { label: 'Без Таноса', value: '2381' }, + { label: 'БезГражданки', value: '2418' }, + { label: 'Бездомные', value: '916' }, + { label: 'Безответственность', value: '2152' }, + { label: 'Безумные ученые', value: '2559' }, + { label: 'Безэмоциональность', value: '1488' }, + { label: 'Бейсбол', value: '1489' }, + { label: 'Беларусь', value: '1030' }, + { label: 'Белый день', value: '683' }, + { label: 'Белый стих', value: '1998' }, + { label: 'Бельгия', value: '1039' }, + { label: 'Беременность', value: '1712' }, + { label: 'Бермудский треугольник', value: '1158' }, + { label: 'Берсерки', value: '566' }, + { label: 'Бесконтактный секс', value: '2214' }, + { label: 'Бесплодие', value: '719' }, + { label: 'Беспокойство', value: '2888' }, + { label: 'Беспризорники', value: '604' }, + { label: 'Беспричудный Изуку', value: '2422' }, + { label: 'Бессмертие', value: '219' }, + { label: 'Бессмертный конкурс 2023', value: '3016' }, + { label: 'Бестелесность', value: '2607' }, + { label: 'Би-персонажи', value: '300' }, + { label: 'Библейские темы и мотивы', value: '1468' }, + { label: 'Библиотекари', value: '2631' }, + { label: 'Библиотеки', value: '310' }, + { label: 'Бизнесмены / Бизнесвумен', value: '573' }, + { label: 'Биологическое оружие', value: '1188' }, + { label: 'Биопанк', value: '281' }, + { label: 'Биороботы', value: '2256' }, + { label: 'Биполярное расстройство', value: '690' }, + { label: 'Битва за Хогвартс', value: '2774' }, + { label: 'Битва Историй', value: '2960' }, + { label: 'Благородные разбойники', value: '952' }, + { label: 'Благотворительность', value: '613' }, + { label: 'Бладплей', value: '109' }, + { label: 'Ближний Восток', value: '1041' }, + { label: 'Близкие враги', value: '721' }, + { label: 'Близнецы', value: '295' }, + { label: 'Блиц-свидания', value: '2511' }, + { label: 'Блогеры', value: '1471' }, + { label: 'Блокадный Ленинград', value: '1111' }, + { label: 'Блэкигуд', value: '2905' }, + { label: 'Богачи', value: '1549' }, + { label: 'Боги / Божественные сущности', value: '149' }, + { label: 'Боди-арт', value: '2260' }, + { label: 'Бодипозитив', value: '1153' }, + { label: 'Боевая пара', value: '1919' }, + { label: 'Боевые искусства', value: '918' }, + { label: 'Бои без правил', value: '1390' }, + { label: 'Бокс', value: '765' }, + { label: 'Болгария', value: '1786' }, + { label: 'Болезнь Альцгеймера', value: '2533' }, + { label: 'Боль', value: '1803' }, + { label: 'Больницы', value: '20' }, + { label: 'Большая Игра профессора Дамблдора', value: '2890' }, + { label: 'Бордели', value: '120' }, + { label: 'Борьба за власть', value: '2300' }, + { label: 'Борьба за отношения', value: '1251' }, + { label: 'Борьба за справедливость', value: '2335' }, + { label: 'Боязнь боли', value: '2441' }, + { label: 'Боязнь воды', value: '1084' }, + { label: 'Боязнь врачей', value: '2063' }, + { label: 'Боязнь высоты', value: '1085' }, + { label: 'Боязнь грозы', value: '1124' }, + { label: 'Боязнь громких звуков', value: '1090' }, + { label: 'Боязнь грязи', value: '1277' }, + { label: 'Боязнь замкнутых пространств', value: '821' }, + { label: 'Боязнь зеркал', value: '2739' }, + { label: 'Боязнь клоунов', value: '2970' }, + { label: 'Боязнь крови', value: '1312' }, + { label: 'Боязнь людей', value: '1082' }, + { label: 'Боязнь мужчин', value: '1264' }, + { label: 'Боязнь насекомых / Боязнь пауков', value: '1119' }, + { label: 'Боязнь неудачи', value: '2713' }, + { label: 'Боязнь огня', value: '1258' }, + { label: 'Боязнь одиночества', value: '1491' }, + { label: 'Боязнь открытых пространств', value: '965' }, + { label: 'Боязнь привязанности', value: '1343' }, + { label: 'Боязнь призраков', value: '2770' }, + { label: 'Боязнь прикосновений', value: '823' }, + { label: 'Боязнь сексуальных домогательств', value: '2319' }, + { label: 'Боязнь смерти', value: '1515' }, + { label: 'Боязнь сна', value: '2262' }, + { label: 'Боязнь темноты', value: '1494' }, + { label: 'Боязнь тишины', value: '2233' }, + { label: 'Бразилия', value: '833' }, + { label: 'Брак по договоренности', value: '35' }, + { label: 'Брак по расчету', value: '303' }, + { label: 'БРД', value: '2907' }, + { label: 'Бродячие артисты', value: '2661' }, + { label: 'Бродячие певцы', value: '1497' }, + { label: 'Броманс', value: '171' }, + { label: 'Будущее', value: '227' }, + { label: 'Буллёзный эпидермолиз', value: '2725' }, + { label: 'Буллинг', value: '436' }, + { label: 'Бухгалтеры', value: '2509' }, + { label: 'Буч и фэм', value: '486' }, + { label: 'Бывшие', value: '311' }, + { label: 'Бывшие враги', value: '2812' }, + { label: 'Бывшие серийные убийцы', value: '2498' }, + { label: 'Бытовое фэнтези', value: '2702' }, + { label: 'В одном теле', value: '1425' }, + { label: 'В поисках отношений', value: '1576' }, + { label: 'В. А. О. Н.', value: '2377' }, + { label: 'Вагинальный секс', value: '395' }, + { label: 'Ваканда', value: '2668' }, + { label: 'Валькирии', value: '557' }, + { label: 'Вампиры', value: '27' }, + { label: 'Василиски', value: '863' }, + { label: 'Вдовство', value: '2030' }, + { label: 'Вебкам-модели', value: '2299' }, + { label: 'Вегетарианцы / Веганы', value: '577' }, + { label: 'Ведьмы / Колдуны', value: '33' }, + { label: 'Великая депрессия', value: '2224' }, + { label: 'Великая французская революция', value: '2000' }, + { label: 'Великобритания', value: '673' }, + { label: 'Великое Княжество Литовское', value: '2587' }, + { label: 'Великолепный мерзавец', value: '1244' }, + { label: 'Велоспорт', value: '2978' }, + { label: 'Венгрия', value: '1432' }, + { label: 'Вендиго', value: '251' }, + { label: 'Верность', value: '459' }, + { label: 'Вертикальный инцест', value: '337' }, + { label: 'Вестерн', value: '2329' }, + { label: 'Ветераны', value: '555' }, + { label: 'Ветеринары', value: '629' }, + { label: 'Вечеринки', value: '854' }, + { label: 'Вечная молодость', value: '2405' }, + { label: 'Вещие сны', value: '1993' }, + { label: 'Взаимопонимание', value: '2887' }, + { label: 'Взросление', value: '909' }, + { label: 'Вигиланты', value: '1514' }, + { label: 'Видеоигры', value: '2508' }, + { label: 'Визажисты / Гримеры', value: '1801' }, + { label: 'Викинги', value: '676' }, + { label: 'Виктимблейминг', value: '331' }, + { label: 'Викторианская эпоха', value: '469' }, + { label: 'Виртуальная реальность', value: '803' }, + { label: 'Виртуальный секс', value: '268' }, + { label: 'Витилиго', value: '1167' }, + { label: 'Вице-королевство Новая Испания', value: '2603' }, + { label: 'ВИЧ / СПИД', value: '542' }, + { label: 'Влюбленность', value: '1303' }, + { label: 'Вне закона', value: '1089' }, + { label: 'Внутренний сексизм', value: '2155' }, + { label: 'Внутренняя гомофобия', value: '2154' }, + { label: 'Внутренняя трансфобия', value: '2716' }, + { label: 'Водители / Шоферы', value: '879' }, + { label: 'Военные', value: '313' }, + { label: 'Военные городки', value: '2095' }, + { label: 'Военные преступления', value: '2356' }, + { label: 'Возвращение', value: '1522' }, + { label: 'Возвращение Артура', value: '2570' }, + { label: 'Воздержание', value: '921' }, + { label: 'Воздушная атлетика', value: '2811' }, + { label: 'Война', value: '232' }, + { label: 'Война в Афганистане', value: '1050' }, + { label: 'Война во Вьетнаме', value: '1332' }, + { label: 'Война за независимость США', value: '2065' }, + { label: 'Война миров', value: '1222' }, + { label: 'Волдигуд', value: '1874' }, + { label: 'Волейбол', value: '1095' }, + { label: 'Волк в овечьей шкуре', value: '2228' }, + { label: 'Волонтеры', value: '900' }, + { label: 'Волшебники / Волшебницы', value: '93' }, + { label: 'Воображаемые друзья', value: '738' }, + { label: 'Ворарефилия', value: '157' }, + { label: 'Воры', value: '320' }, + { label: 'Воскрешение', value: '256' }, + { label: 'Воспоминания', value: '1118' }, + { label: 'Воссоединение', value: '498' }, + { label: 'Восстание машин', value: '817' }, + { label: 'Восточное побережье', value: '770' }, + { label: 'Восьмое марта', value: '493' }, + { label: 'Вражда', value: '681' }, + { label: 'Вражда семей', value: '2867' }, + { label: 'Врачи', value: '374' }, + { label: 'Времена Мародеров', value: '1872' }, + { label: 'Времена Основателей', value: '1891' }, + { label: 'Временная смерть персонажа', value: '439' }, + { label: 'Временное бессмертие', value: '2755' }, + { label: 'Временное воскрешение персонажей', value: '2649' }, + { label: 'Временные петли', value: '147' }, + { label: 'Временные превращения', value: '927' }, + { label: 'Врожденные заболевания', value: '1781' }, + { label: 'Всадники Апокалипсиса', value: '1326' }, + { label: 'Все живы / Никто не умер', value: '535' }, + { label: 'Все за Одного — отец Изуку', value: '2421' }, + { label: 'Всезнающий рассказчик', value: '1368' }, + { label: 'Вселение в тело', value: '1181' }, + { label: 'Вторая мировая', value: '488' }, + { label: 'Вторжение пришельцев', value: '755' }, + { label: 'Вторичный стыд', value: '1393' }, + { label: 'Второй шанс', value: '1799' }, + { label: 'Второстепенные оригинальные персонажи', value: '1536' }, + { label: 'Вуайеризм', value: '174' }, + { label: 'Вуду', value: '678' }, + { label: 'Выбор', value: '2808' }, + { label: 'Выгорание', value: '2914' }, + { label: 'Выживание', value: '216' }, + { label: 'Вымирающий вид', value: '2373' }, + { label: 'Вымышленная анатомия', value: '1317' }, + { label: 'Вымышленная география', value: '1294' }, + { label: 'Вымышленная религия', value: '2062' }, + { label: 'Вымышленная физика', value: '2132' }, + { label: 'Вымышленные виды спорта', value: '1777' }, + { label: 'Вымышленные заболевания', value: '1295' }, + { label: 'Вымышленные науки', value: '1333' }, + { label: 'Вымышленные праздники', value: '1769' }, + { label: 'Вымышленные профессии', value: '1391' }, + { label: 'Вымышленные существа', value: '1685' }, + { label: 'Вымышленные языки', value: '536' }, + { label: 'Выпускные', value: '717' }, + { label: 'Высшее общество', value: '1093' }, + { label: 'Высшие учебные заведения', value: '97' }, + { label: 'Выученная беспомощность', value: '2772' }, + { label: 'Выхаживание', value: '511' }, + { label: 'Выход из нездоровых отношений', value: '2891' }, + { label: 'Гадалки / Ясновидящие', value: '2662' }, + { label: 'Гадкий утенок', value: '1225' }, + { label: 'Газлайтинг', value: '1418' }, + { label: 'Галлюцинации / Иллюзии', value: '365' }, + { label: 'Гаремник', value: '50' }, + { label: 'Гаремы', value: '115' }, + { label: 'Гарпии', value: '1482' }, + { label: 'Гарри Поттер и Драко Малфой — друзья', value: '1923' }, + { label: 'Гастроли', value: '2487' }, + { label: 'Гедонизм', value: '736' }, + { label: 'Гей-клубы', value: '2482' }, + { label: 'Геймеры', value: '356' }, + { label: 'Геймлит', value: '23' }, + { label: 'Гейши / Кисэн', value: '332' }, + { label: 'Гемофилия', value: '1341' }, + { label: 'Гендерная дисфория', value: '431' }, + { label: 'Гендерная интрига', value: '55' }, + { label: 'Гендерное неравенство', value: '2568' }, + { label: 'Гендерный нонконформизм', value: '1417' }, + { label: 'Гендерсвап', value: '1691' }, + { label: 'Генетика / Эволюция', value: '2131' }, + { label: 'Гении', value: '445' }, + { label: 'Генитальные пытки', value: '2104' }, + { label: 'Геноцид', value: '720' }, + { label: 'Геокинез', value: '2889' }, + { label: 'Геологи / Географы', value: '934' }, + { label: 'Германия', value: '688' }, + { label: 'Германия — не Священная Римская Империя', value: '2470' }, + { label: 'Герой поневоле', value: '2145' }, + { label: 'Геронтофилия', value: '162' }, + { label: 'Гетерохромия', value: '1210' }, + { label: 'Гетто', value: '1783' }, + { label: 'Гибристофилия', value: '1843' }, + { label: 'Гиганты', value: '735' }, + { label: 'Гидра трэш-пати', value: '2804' }, + { label: 'Гики', value: '482' }, + { label: 'Гильдии', value: '957' }, + { label: 'Гимнастика', value: '1974' }, + { label: 'Гиперакузия', value: '2866' }, + { label: 'Гиперсексуальность', value: '945' }, + { label: 'Гипноз', value: '700' }, + { label: 'Гладиаторы', value: '677' }, + { label: 'Глобальные катастрофы', value: '525' }, + { label: 'Глухота', value: '595' }, + { label: 'Гнев', value: '2806' }, + { label: 'Гнездование', value: '1452' }, + { label: 'Гномы', value: '138' }, + { label: 'Гоблины', value: '188' }, + { label: 'Големы', value: '1208' }, + { label: 'Голод', value: '868' }, + { label: 'Голодные Фэндомные Игры', value: '2895' }, + { label: 'Гомофобия', value: '428' }, + { label: 'Гомункулы', value: '1366' }, + { label: 'Горе / Утрата', value: '1113' }, + { label: 'Горизонтальный инцест', value: '338' }, + { label: 'Городские легенды', value: '1832' }, + { label: 'Горы', value: '702' }, + { label: 'Горячие источники', value: '2813' }, + { label: 'Гостевой брак', value: '1354' }, + { label: 'Гостиницы', value: '384' }, + { label: 'Готический роман', value: '963' }, + { label: 'Готы', value: '575' }, + { label: 'Гравиокинез', value: '1985' }, + { label: 'Гражданская война', value: '370' }, + { label: 'Гражданская война в России', value: '1282' }, + { label: 'Гражданская война в США', value: '1018' }, + { label: 'Графичные описания', value: '2854' }, + { label: 'Грейнджергад', value: '1892' }, + { label: 'Грейромантичные персонажи', value: '707' }, + { label: 'Гремлины', value: '189' }, + { label: 'Греция', value: '1040' }, + { label: 'Грифоны', value: '335' }, + { label: 'Громкий секс', value: '1184' }, + { label: 'Грубый секс', value: '121' }, + { label: 'Грудное кормление', value: '296' }, + { label: 'Групи', value: '2925' }, + { label: 'Групповое изнасилование', value: '2203' }, + { label: 'Групповой секс', value: '1654' }, + { label: 'Грязный реализм', value: '634' }, + { label: 'Гувернантки', value: '1179' }, + { label: 'Гули', value: '367' }, + { label: 'Гэвин Рид — андроид', value: '2692' }, + { label: 'Даб-кон', value: '57' }, + { label: 'Даби — не Тойя Тодороки', value: '2821' }, + { label: 'Даби — Тойя Тодороки', value: '2423' }, + { label: 'Дадзава', value: '2915' }, + { label: 'Дадмайт', value: '2917' }, + { label: 'Дайверы', value: '939' }, + { label: 'Дайвинг', value: '2823' }, + { label: 'Дама в беде', value: '1259' }, + { label: 'Дамбигад', value: '1863' }, + { label: 'Дамбигуд', value: '1873' }, + { label: 'Дампиры', value: '2860' }, + { label: 'Дандэрэ', value: '1364' }, + { label: 'Дарк', value: '1678' }, + { label: 'Даркнет', value: '1287' }, + { label: 'Дачи', value: '930' }, + { label: 'Двойники', value: '305' }, + { label: 'Двойное проникновение', value: '107' }, + { label: 'Двойной кноттинг', value: '2619' }, + { label: 'Двойные агенты', value: '441' }, + { label: 'Дворцовые интриги', value: '2075' }, + { label: 'Девиантное поведение', value: '1814' }, + { label: 'Девичники / Мальчишники', value: '1191' }, + { label: 'Девочки из Эквестрии', value: '2912' }, + { label: 'Дежавю', value: '2049' }, + { label: 'Декаданс', value: '2353' }, + { label: 'Деконструкция', value: '1322' }, + { label: 'Деми-персонажи', value: '723' }, + { label: 'Демиурги', value: '819' }, + { label: 'Демонология', value: '2485' }, + { label: 'Демоны', value: '73' }, + { label: 'Демоны-хранители', value: '2045' }, + { label: 'Дендрофилия', value: '491' }, + { label: 'День благодарения', value: '754' }, + { label: 'День знаний', value: '1075' }, + { label: 'День матери', value: '2979' }, + { label: 'День мертвых', value: '1074' }, + { label: 'День независимости США', value: '1008' }, + { label: 'День рождения', value: '298' }, + { label: 'День святого Валентина', value: '204' }, + { label: 'День святого Патрика', value: '1129' }, + { label: 'День смерти', value: '2251' }, + { label: 'День учителя', value: '1076' }, + { label: 'Деперсонализация', value: '1197' }, + { label: 'Депрессия', value: '354' }, + { label: 'Депривация сна', value: '551' }, + { label: 'Дереализация', value: '1198' }, + { label: 'Деревни', value: '364' }, + { label: 'Дерматозойный бред', value: '2779' }, + { label: 'Деспоты / Тираны', value: '1345' }, + { label: 'Детектив', value: '1672' }, + { label: 'Дети', value: '217' }, + { label: 'Детоненавистничество', value: '1836' }, + { label: 'Детская влюбленность', value: '186' }, + { label: 'Детские дома', value: '355' }, + { label: 'Детские лагеря', value: '222' }, + { label: 'Детские сады', value: '567' }, + { label: 'Дефекты зрения', value: '2639' }, + { label: 'Дефекты речи', value: '737' }, + { label: 'Джинны / Пери', value: '456' }, + { label: 'Джон Ватсон — преступник', value: '1932' }, + { label: 'Джуманджи — не игровой мир', value: '2799' }, + { label: 'Диабет', value: '820' }, + { label: 'Диалоги (стилизация)', value: '1139' }, + { label: 'Дизайнеры', value: '1875' }, + { label: 'Дизельпанк', value: '294' }, + { label: 'Дикие животные', value: '622' }, + { label: 'Дикий Запад', value: '293' }, + { label: 'Дилетанты', value: '2443' }, + { label: 'Динозавры', value: '670' }, + { label: 'Дипломатия', value: '1790' }, + { label: 'Дисбаланс власти', value: '732' }, + { label: 'Дискриминация', value: '2230' }, + { label: 'Дискриминация по внешности', value: '1414' }, + { label: 'Дискриминация по возрасту', value: '2843' }, + { label: 'Дислексия', value: '860' }, + { label: 'Дисморфофобия', value: '1108' }, + { label: 'Диссоциативное расстройство идентичности', value: '2127' }, + { label: 'Дистанционное обучение', value: '2592' }, + { label: 'Дисфункциональные семьи', value: '602' }, + { label: 'Дневники (стилизация)', value: '214' }, + { label: 'Добрый Сальери', value: '2522' }, + { label: 'Доверие', value: '1117' }, + { label: 'Договоры / Сделки', value: '2137' }, + { label: 'Документы / Отчеты (стилизация)', value: '752' }, + { label: 'Долголетие', value: '2130' }, + { label: 'Домашнее насилие', value: '42' }, + { label: 'Домашние животные', value: '517' }, + { label: 'Домашний арест', value: '2526' }, + { label: 'Домашний ремонт', value: '867' }, + { label: 'Домовые', value: '1423' }, + { label: 'Допросы', value: '2654' }, + { label: 'Дорожное приключение', value: '47' }, + { label: 'Драббл', value: '2935' }, + { label: 'Драки', value: '1367' }, + { label: 'Драконы', value: '43' }, + { label: 'Драма', value: '1668' }, + { label: 'Драуги', value: '2264' }, + { label: 'Древний Восток', value: '1940' }, + { label: 'Древний Египет', value: '474' }, + { label: 'Древний Китай', value: '1438' }, + { label: 'Древняя Русь', value: '466' }, + { label: 'Дремлющее зло', value: '2719' }, + { label: 'Дремлющие способности', value: '2084' }, + { label: 'Другая магическая школа', value: '1864' }, + { label: 'Другие планеты', value: '1415' }, + { label: 'Другой избранный', value: '2054' }, + { label: 'Другой факультет', value: '1866' }, + { label: 'Дружба', value: '1701' }, + { label: 'Дружба втайне', value: '2303' }, + { label: 'Дружба по расчету', value: '2331' }, + { label: 'Друзья детства', value: '343' }, + { label: 'Друзья по переписке', value: '1466' }, + { label: 'Друзья поневоле', value: '627' }, + { label: 'Друзья с привилегиями', value: '113' }, + { label: 'Друиды', value: '1199' }, + { label: 'Дрэг (субкультура)', value: '1248' }, + { label: 'Дурмстранг', value: '2389' }, + { label: 'Дурслигуд', value: '2058' }, + { label: 'Духи природы', value: '503' }, + { label: 'Духовенство', value: '478' }, + { label: 'Духовная связь', value: '2090' }, + { label: 'Духокинез', value: '2500' }, + { label: 'Дуэли', value: '446' }, + { label: 'Дьяволы', value: '2369' }, + { label: 'Дэдди-кинк', value: '165' }, + { label: 'Еда / Кулинария', value: '1195' }, + { label: 'Единороги', value: '373' }, + { label: 'Ёкаи', value: '416' }, + { label: 'Естественные враги', value: '728' }, + { label: 'Жамевю', value: '2050' }, + { label: 'Жаргон', value: '1016' }, + { label: 'Ждун Фест', value: '2936' }, + { label: 'Железнодорожники', value: '2680' }, + { label: 'Железные дороги', value: '811' }, + { label: 'Женская дружба', value: '2156' }, + { label: 'Жертвы обстоятельств', value: '1475' }, + { label: 'Жестокое обращение с животными', value: '722' }, + { label: 'Жестокость', value: '1710' }, + { label: 'Живой сосуд', value: '2072' }, + { label: 'Животные-компаньоны', value: '2371' }, + { label: 'Живые машины', value: '961' }, + { label: 'ЖиП', value: '143' }, + { label: 'Журналисты', value: '438' }, + { label: 'Заболевания', value: '1268' }, + { label: 'Заболевания крови', value: '2254' }, + { label: 'Заболевания сердца', value: '1767' }, + { label: 'Забота / Поддержка', value: '1302' }, + { label: 'Заброшенные города', value: '1276' }, + { label: 'Заброшенные здания', value: '1483' }, + { label: 'Забытый герой', value: '2337' }, + { label: 'Завещание', value: '1262' }, + { label: 'Зависимое расстройство личности', value: '2349' }, + { label: 'Зависть', value: '990' }, + { label: 'Завоевание', value: '978' }, + { label: 'Загробный мир', value: '275' }, + { label: 'Закавказье', value: '2819' }, + { label: 'Заклятые друзья', value: '1316' }, + { label: 'Закрытые города', value: '1481' }, + { label: 'Закрытые учебные заведения', value: '98' }, + { label: 'Закрытый детектив', value: '2616' }, + { label: 'Заложники', value: '845' }, + { label: 'Замки', value: '654' }, + { label: 'Замкнутое пространство', value: '2306' }, + { label: 'Замкнутый мир', value: '874' }, + { label: 'Занавесочная история', value: '1688' }, + { label: 'Запахи', value: '1385' }, + { label: 'Заповедники', value: '618' }, + { label: 'Запредельно одаренный персонаж', value: '1241' }, + { label: 'Запрет на магию', value: '2663' }, + { label: 'Запретные отношения', value: '362' }, + { label: 'Засосы / Укусы', value: '913' }, + { label: 'Застенчивость', value: '2807' }, + { label: 'Затворники', value: '1059' }, + { label: 'Затерянные миры', value: '2448' }, + { label: 'Защита любимого', value: '2492' }, + { label: 'Защитники природы', value: '628' }, + { label: 'Защищенный секс', value: '636' }, + { label: 'Звездные слезы', value: '2971' }, + { label: 'Здоровые механизмы преодоления', value: '2147' }, + { label: 'Здоровые отношения', value: '1853' }, + { label: 'Зеркала', value: '724' }, + { label: 'Зимняя Фандомная Битва', value: '254' }, + { label: 'Злобные бывшие', value: '518' }, + { label: 'Зловещая находка', value: '2275' }, + { label: 'Знаки зодиака', value: '643' }, + { label: 'Знаменитости', value: '1360' }, + { label: 'Золотая клетка', value: '480' }, + { label: 'Золотая молодежь', value: '796' }, + { label: 'Золотая Орда', value: '1143' }, + { label: 'Золушка', value: '1226' }, + { label: 'Зомби', value: '31' }, + { label: 'Зоопарки', value: '792' }, + { label: 'Зоофилия', value: '1662' }, + { label: 'Зоофобии', value: '2715' }, + { label: 'ЗППП', value: '630' }, + { label: 'ЗПР', value: '2259' }, + { label: 'Зрелые персонажи', value: '851' }, + { label: 'Иван Купала', value: '1068' }, + { label: 'Игровое расстройство', value: '949' }, + { label: 'Игрушки', value: '1346' }, + { label: 'Игры на выживание', value: '554' }, + { label: 'Игры на раздевание', value: '1535' }, + { label: 'Игры с сосками', value: '423' }, + { label: 'Игры с температурой', value: '230' }, + { label: 'Идеализация', value: '1540' }, + { label: 'Иерархический строй', value: '652' }, + { label: 'Избранные', value: '847' }, + { label: 'Избыточный физиологизм', value: '2334' }, + { label: 'Изгнанники', value: '871' }, + { label: 'Измена', value: '62' }, + { label: 'Измененное состояние сознания', value: '2242' }, + { label: 'Изменились за лето', value: '1908' }, + { label: 'Изнасилование', value: '1650' }, + { label: 'Изоляция', value: '1296' }, + { label: 'Израиль', value: '1094' }, + { label: 'Изуку Мидория — злодей', value: '2472' }, + { label: 'Иллюзионисты', value: '532' }, + { label: 'Имейджин', value: '838' }, + { label: 'Импотенция', value: '632' }, + { label: 'Импринтинг', value: '635' }, + { label: 'Импрореал', value: '2955' }, + { label: 'Импрофест', value: '2947' }, + { label: 'Имя вместо цвета', value: '2919' }, + { label: 'Инвалидность', value: '197' }, + { label: 'Индия', value: '1002' }, + { label: 'Инквизиция', value: '1130' }, + { label: 'Инопланетяне', value: '58' }, + { label: 'Инсектоиды / Арахноиды', value: '953' }, + { label: 'Инсектофилия / Арахнофилия', value: '664' }, + { label: 'Инсценированная смерть персонажа', value: '440' }, + { label: 'Интерактивная работа', value: '419' }, + { label: 'Интервью (стилизация)', value: '2446' }, + { label: 'Интернет', value: '1286' }, + { label: 'Интерсекс-персонажи', value: '299' }, + { label: 'Интрамаммарный секс', value: '840' }, + { label: 'Инфантильность', value: '1300' }, + { label: 'Инцест', value: '1651' }, + { label: 'Ипохондрия', value: '2178' }, + { label: 'Ирландия', value: '1035' }, + { label: 'Иронический детектив', value: '2638' }, + { label: 'Искра Стайлз Стилински', value: '1975' }, + { label: 'Искупление', value: '877' }, + { label: 'Искусственно вызванные чувства', value: '626' }, + { label: 'Искусственные интеллекты', value: '150' }, + { label: 'Искусственные существа', value: '2277' }, + { label: 'Искусство', value: '2257' }, + { label: 'Искусствоведы', value: '2873' }, + { label: 'Искушение', value: '776' }, + { label: 'Испания', value: '1020' }, + { label: 'Исповеди', value: '1342' }, + { label: 'Исправительные лагеря', value: '2439' }, + { label: 'Истинные', value: '1855' }, + { label: 'Историческая Хеталия', value: '1865' }, + { label: 'Исторические эпохи', value: '1698' }, + { label: 'Исторический Кантриболс', value: '2913' }, + { label: 'Историческое допущение', value: '2681' }, + { label: 'Исцеление', value: '802' }, + { label: 'Италия', value: '1003' }, + { label: 'Ихтиоморфы', value: '972' }, + { label: 'Йога', value: '483' }, + { label: 'Йоль', value: '2143' }, + { label: 'Кабаре', value: '457' }, + { label: 'Кабэ-дон', value: '2626' }, + { label: 'Казахстан', value: '1260' }, + { label: 'Казино', value: '620' }, + { label: 'Казнь', value: '548' }, + { label: 'Как ориджинал', value: '1032' }, + { label: 'Каламбуры', value: '881' }, + { label: 'Каминг-аут', value: '559' }, + { label: 'Канада', value: '695' }, + { label: 'Каникулы', value: '805' }, + { label: 'Каннибализм', value: '1706' }, + { label: 'Канонная смерть персонажа', value: '564' }, + { label: 'Капитан Гидра', value: '1928' }, + { label: 'Карантинные зоны', value: '1290' }, + { label: 'Карантины', value: '2734' }, + { label: 'Караоке', value: '886' }, + { label: 'Карательная психиатрия', value: '730' }, + { label: 'Карибский бассейн', value: '1172' }, + { label: 'Карма', value: '647' }, + { label: 'Карьеризм', value: '956' }, + { label: 'Каскадеры', value: '2267' }, + { label: 'Катарсис', value: '824' }, + { label: 'Кафе / Кофейни / Чайные', value: '13' }, + { label: 'Квай-Гад', value: '2894' }, + { label: 'Квантовая физика', value: '2744' }, + { label: 'Квисины', value: '2722' }, + { label: 'Кемономими', value: '997' }, + { label: 'Кемпинг', value: '182' }, + { label: 'Кентавры', value: '64' }, + { label: 'Кибербуллинг', value: '1858' }, + { label: 'Киберпанк', value: '24' }, + { label: 'Киберспорт', value: '745' }, + { label: 'Кибертрон', value: '2775' }, + { label: 'Киборги', value: '154' }, + { label: 'Киевская Русь', value: '515' }, + { label: 'Кинк на беременность', value: '2281' }, + { label: 'Кинк на волосы', value: '2089' }, + { label: 'Кинк на волосы на теле', value: '1337' }, + { label: 'Кинк на девственность', value: '2282' }, + { label: 'Кинк на инвалидность', value: '633' }, + { label: 'Кинк на интеллект', value: '2798' }, + { label: 'Кинк на клыки', value: '2325' }, + { label: 'Кинк на колготки', value: '2644' }, + { label: 'Кинк на крылья', value: '2035' }, + { label: 'Кинк на латекс', value: '134' }, + { label: 'Кинк на мольбы', value: '1543' }, + { label: 'Кинк на наручники', value: '1065' }, + { label: 'Кинк на нижнее белье', value: '145' }, + { label: 'Кинк на очки', value: '1723' }, + { label: 'Кинк на ошейники', value: '388' }, + { label: 'Кинк на полноту', value: '552' }, + { label: 'Кинк на похвалу', value: '1042' }, + { label: 'Кинк на протезы', value: '2682' }, + { label: 'Кинк на рога', value: '2320' }, + { label: 'Кинк на руки', value: '915' }, + { label: 'Кинк на сердцебиение', value: '941' }, + { label: 'Кинк на силу', value: '1476' }, + { label: 'Кинк на слезы', value: '1373' }, + { label: 'Кинк на служение', value: '2980' }, + { label: 'Кинк на страх', value: '2246' }, + { label: 'Кинк на стыд', value: '747' }, + { label: 'Кинк на унижение', value: '2291' }, + { label: 'Кинк на униформу', value: '1350' }, + { label: 'Кинк на уши', value: '2370' }, + { label: 'Кинк на формальную одежду', value: '1064' }, + { label: 'Кинк на чужую одежду', value: '2920' }, + { label: 'Кинк на чулки', value: '810' }, + { label: 'Кинк на шрамы', value: '2831' }, + { label: 'Кинк на щекотку', value: '2396' }, + { label: 'Кинк-бинго', value: '2290' }, + { label: 'Кинки / Фетиши', value: '1659' }, + { label: 'Кинотеатры', value: '981' }, + { label: 'Киригакуре', value: '2475' }, + { label: 'Китай', value: '757' }, + { label: 'Кладбища', value: '501' }, + { label: 'Кланы', value: '2879' }, + { label: 'Классизм', value: '1927' }, + { label: 'Клейма', value: '1548' }, + { label: 'Клептомания', value: '1160' }, + { label: 'Клетка Люцифера', value: '1884' }, + { label: 'Клетки', value: '1069' }, + { label: 'Клиническая смерть', value: '2068' }, + { label: 'Клокпанк / Ветропанк', value: '1812' }, + { label: 'Клоны', value: '358' }, + { label: 'Клоуны', value: '1520' }, + { label: 'Клубы по интересам', value: '1369' }, + { label: 'Клюква', value: '1001' }, + { label: 'Кляпы / Затыкание рта', value: '379' }, + { label: 'Книги', value: '2746' }, + { label: 'Книжные магазины', value: '444' }, + { label: 'Кноттинг', value: '99' }, + { label: 'Ковбои', value: '330' }, + { label: 'Кода', value: '1943' }, + { label: 'Кожаная одежда', value: '1517' }, + { label: 'Колдовстворец', value: '2600' }, + { label: 'Кома', value: '516' }, + { label: 'Командная работа', value: '708' }, + { label: 'Комики', value: '1315' }, + { label: 'Комиксисты / Мангаки', value: '576' }, + { label: 'Коммунальные квартиры', value: '1437' }, + { label: 'Коммуны', value: '797' }, + { label: 'Комнатные растения', value: '1792' }, + { label: 'Комплекс Бога', value: '2087' }, + { label: 'Комплекс превосходства', value: '2191' }, + { label: 'Комплексы', value: '2529' }, + { label: 'Конкурс от BUBBLE', value: '2937' }, + { label: 'Конный спорт', value: '1378' }, + { label: 'Конохагакуре', value: '2477' }, + { label: 'Конспирология', value: '1911' }, + { label: 'Контрабандисты', value: '2363' }, + { label: 'Контроль / Подчинение', value: '932' }, + { label: 'Контроль памяти', value: '2540' }, + { label: 'Контроль сознания', value: '257' }, + { label: 'Конфликт мировоззрений', value: '2678' }, + { label: 'Концентрационные лагеря', value: '547' }, + { label: 'Концерты / Выступления', value: '1236' }, + { label: 'Конюшни', value: '514' }, + { label: 'Копирование сознания', value: '1231' }, + { label: 'Копирование способностей', value: '1825' }, + { label: 'Копро-кинк', value: '763' }, + { label: 'Корё', value: '1142' }, + { label: 'Корейская война', value: '2187' }, + { label: 'Коренные народы', value: '1505' }, + { label: 'Королевства', value: '2794' }, + { label: 'Корпорация BOBSOC', value: '2940' }, + { label: 'Корректирующее изнасилование', value: '2856' }, + { label: 'Коррупция', value: '2150' }, + { label: 'Косвенные убийства', value: '2846' }, + { label: 'Космоопера', value: '48' }, + { label: 'Космос', value: '236' }, + { label: 'Косплееры', value: '944' }, + { label: 'Кочевники', value: '1793' }, + { label: 'Кошмары', value: '1228' }, + { label: 'Красная нить судьбы', value: '608' }, + { label: 'Крепости / Башни', value: '2321' }, + { label: 'Крестовые походы', value: '2070' }, + { label: 'Крестражи', value: '2238' }, + { label: 'Кризис гендера', value: '1406' }, + { label: 'Кризис ориентации', value: '1405' }, + { label: 'Криминалистическая экспертиза', value: '2361' }, + { label: 'Криминальная пара', value: '1920' }, + { label: 'Криокинез', value: '1804' }, + { label: 'Криптиды', value: '60' }, + { label: 'Криптоистория', value: '179' }, + { label: 'Кровь / Травмы', value: '800' }, + { label: 'Кроссдрессинг', value: '65' }, + { label: 'Кроссовер', value: '10' }, + { label: 'Кроули — Архангел Рафаэль', value: '2471' }, + { label: 'Круизы', value: '2488' }, + { label: 'Крушение «Титаника»', value: '2200' }, + { label: 'Крылатые', value: '92' }, + { label: 'Крэк', value: '1408' }, + { label: 'Ксенофилия', value: '1711' }, + { label: 'Ксенофобия', value: '589' }, + { label: 'Куба', value: '773' }, + { label: 'Кудэрэ', value: '1216' }, + { label: 'Куклы', value: '1221' }, + { label: 'Куколдинг', value: '524' }, + { label: 'Культ Дракона', value: '2244' }, + { label: 'Кумогакуре', value: '2474' }, + { label: 'Куннилингус', value: '172' }, + { label: 'Купальни', value: '964' }, + { label: 'Курение', value: '202' }, + { label: 'Курортный роман', value: '181' }, + { label: 'Курьеры', value: '2735' }, + { label: 'Лабиринты', value: '1981' }, + { label: 'Лаборатории', value: '1115' }, + { label: 'Лабораторные опыты', value: '146' }, + { label: 'Лав-отели', value: '801' }, + { label: 'Лавандовый брак', value: '1357' }, + { label: 'Лакросс', value: '830' }, + { label: 'Лапслок', value: '267' }, + { label: 'Латентная гомосексуальность', value: '2339' }, + { label: 'ЛГБТ-родители', value: '2723' }, + { label: 'Лебединая верность', value: '537' }, + { label: 'Левитация', value: '1281' }, + { label: 'Легкая атлетика', value: '1797' }, + { label: 'Лекарственная зависимость', value: '2632' }, + { label: 'Леса', value: '703' }, + { label: 'Летние ОтМетки', value: '2956' }, + { label: 'Летние школы', value: '2803' }, + { label: 'Лето девяносто девятого', value: '1990' }, + { label: 'Лжебоги', value: '2869' }, + { label: 'Лингвистический кинк', value: '2042' }, + { label: 'Лирика', value: '2005' }, + { label: 'Лихтенштейн', value: '1023' }, + { label: 'Личность против системы', value: '1912' }, + { label: 'Ложная беременность', value: '1428' }, + { label: 'Ложные воспоминания', value: '376' }, + { label: 'Ложные обвинения', value: '1204' }, + { label: 'Локальный постапокалипсис', value: '2455' }, + { label: 'Лолита (субкультура)', value: '1207' }, + { label: 'Ломка', value: '1846' }, + { label: 'Лошади', value: '1083' }, + { label: 'Лугнасад', value: '2621' }, + { label: 'Лудомания', value: '947' }, + { label: 'Лучники', value: '1521' }, + { label: 'Любить луну', value: '2901' }, + { label: 'Любовная магия', value: '2317' }, + { label: 'Любовный магнит', value: '1407' }, + { label: 'Любовный многоугольник', value: '63' }, + { label: 'Любовь с первого взгляда', value: '813' }, + { label: 'Любовь/Ненависть', value: '1703' }, + { label: 'Люди', value: '1538' }, + { label: 'Людоеды', value: '1288' }, + { label: 'Магазины', value: '1063' }, + { label: 'Магическая связь', value: '2720' }, + { label: 'Магические клятвы', value: '2245' }, + { label: 'Магические лавки', value: '2629' }, + { label: 'Магические учебные заведения', value: '1338' }, + { label: 'Магический реализм', value: '9' }, + { label: 'Магия', value: '1419' }, + { label: 'Магия крови', value: '2857' }, + { label: 'Маленькие города', value: '221' }, + { label: 'Малфоигуд', value: '1890' }, + { label: 'Мамми-кинк', value: '273' }, + { label: 'Мандалорцы', value: '2870' }, + { label: 'Манекены', value: '1509' }, + { label: 'Манипуляции', value: '992' }, + { label: 'Марафонный секс', value: '316' }, + { label: 'Маргиналы', value: '394' }, + { label: 'Марди Гра', value: '2407' }, + { label: 'Маскарады / Балы', value: '443' }, + { label: 'Массаж', value: '2218' }, + { label: 'Мастурбация', value: '226' }, + { label: 'Матриархат', value: '706' }, + { label: 'Матчасть', value: '2328' }, + { label: 'Махотокоро', value: '2943' }, + { label: 'Маяки', value: '1356' }, + { label: 'Мегаломания', value: '985' }, + { label: 'Мегаполисы', value: '451' }, + { label: 'Медицинский кинк', value: '748' }, + { label: 'Медицинское использование наркотиков', value: '925' }, + { label: 'Медовый месяц', value: '2235' }, + { label: 'Медсестры / Медбратья', value: '2236' }, + { label: 'Межбедренный секс', value: '931' }, + { label: 'Междумирье', value: '2280' }, + { label: 'Межэтнические отношения', value: '2121' }, + { label: 'Мейлдом', value: '59' }, + { label: 'Мексика', value: '768' }, + { label: 'Меланхолия', value: '2108' }, + { label: 'Мелодрама', value: '2881' }, + { label: 'Менструация', value: '1570' }, + { label: 'Ментальная тюрьма', value: '2294' }, + { label: 'Месть', value: '907' }, + { label: 'Металлисты', value: '1206' }, + { label: 'Металлокинез', value: '1532' }, + { label: 'Метафизические существа', value: '2097' }, + { label: 'Метки', value: '1398' }, + { label: 'Метросексуалы', value: '558' }, + { label: 'Механофилия / Технофилия', value: '587' }, + { label: 'Мечники', value: '2783' }, + { label: 'Мечты', value: '993' }, + { label: 'Миддлпанк', value: '2076' }, + { label: 'Мизофилия', value: '1970' }, + { label: 'Микро / Макро', value: '407' }, + { label: 'Микрофикшен', value: '417' }, + { label: 'Мимикрия', value: '2083' }, + { label: 'Минет', value: '231' }, + { label: 'Министерство Магии', value: '1917' }, + { label: 'Минотавры', value: '2820' }, + { label: 'Мир без гомофобии', value: '2660' }, + { label: 'Мир Желаний (Однажды в сказке)', value: '2923' }, + { label: 'Мир Института Войны', value: '2379' }, + { label: 'Мироустройство', value: '989' }, + { label: 'Мистерия', value: '2012' }, + { label: 'Мистика', value: '1671' }, + { label: 'Мистическая беременность', value: '1784' }, + { label: 'Мистические защитники', value: '2162' }, + { label: 'Мифы и мифология', value: '18' }, + { label: 'Мнимая беременность', value: '1096' }, + { label: 'Мнимая проза', value: '1098' }, + { label: 'Мнимая свобода', value: '2708' }, + { label: 'Многодетные семьи', value: '2785' }, + { label: 'Множественное проникновение', value: '2059' }, + { label: 'Множественные оргазмы', value: '317' }, + { label: 'Множественные финалы', value: '312' }, + { label: 'Могучие Рейнджеры: Дино Заряд', value: '2931' }, + { label: 'Могучие Рейнджеры: Мегафорс', value: '2930' }, + { label: 'Могучие Рейнджеры: Мистическая Сила', value: '2933' }, + { label: 'Могучие Рейнджеры: Самураи', value: '2929' }, + { label: 'Могучие Рейнджеры: Успеть на помощь', value: '2934' }, + { label: 'Могучие Рейнджеры: Ярость Джунглей', value: '2932' }, + { label: 'Модели', value: '399' }, + { label: 'Модельеры', value: '598' }, + { label: 'Модельный бизнес', value: '435' }, + { label: 'Модификации тела', value: '890' }, + { label: 'Молдова', value: '2967' }, + { label: 'Молчание', value: '2465' }, + { label: 'Монастыри', value: '352' }, + { label: 'Монахи', value: '353' }, + { label: 'Монолог', value: '2333' }, + { label: 'Монстрофилия', value: '94' }, + { label: 'Монстры', value: '529' }, + { label: 'Моральные дилеммы', value: '959' }, + { label: 'Морлед', value: '2255' }, + { label: 'Моря / Океаны', value: '955' }, + { label: 'Моряки', value: '406' }, + { label: 'Мотоспорт', value: '1461' }, + { label: 'Мужская беременность', value: '1660' }, + { label: 'Мужская дружба', value: '2157' }, + { label: 'Мужское грудное кормление', value: '297' }, + { label: 'Музеи / Галереи', value: '795' }, + { label: 'Музы', value: '2001' }, + { label: 'Музыканты', value: '141' }, + { label: 'Муковисцидоз', value: '2764' }, + { label: 'Мультикроссовер', value: '2247' }, + { label: 'Мумии', value: '579' }, + { label: 'Мутанты', value: '740' }, + { label: 'Мэри Сью (Марти Стью)', value: '1655' }, + { label: 'Мятежи / Восстания', value: '584' }, + { label: 'На грани жизни и смерти', value: '2545' }, + { label: 'Наблюдатели', value: '1816' }, + { label: 'Навязчивая опека', value: '954' }, + { label: 'Навязчивые мысли', value: '2851' }, + { label: 'Наги', value: '90' }, + { label: 'Надежда', value: '2627' }, + { label: 'Наемники', value: '512' }, + { label: 'Наемные убийцы', value: '192' }, + { label: 'Названные сиблинги', value: '2927' }, + { label: 'Намеки на отношения', value: '1387' }, + { label: 'Намеки на секс', value: '1382' }, + { label: 'Напарники', value: '1291' }, + { label: 'Наперегонки со временем', value: '2240' }, + { label: 'НапиСанта', value: '2946' }, + { label: 'Наполеоновские войны', value: '470' }, + { label: 'Нарколепсия', value: '586' }, + { label: 'Наркоторговля', value: '1116' }, + { label: 'Нарушение этических норм', value: '968' }, + { label: 'Нарциссизм', value: '546' }, + { label: 'Насекомые', value: '2358' }, + { label: 'Насилие', value: '1649' }, + { label: 'Насилие над детьми', value: '726' }, + { label: 'Наследие (Гарри Поттер)', value: '1935' }, + { label: 'Наследство', value: '2172' }, + { label: 'Наставничество', value: '935' }, + { label: 'Настольные игры', value: '1513' }, + { label: 'Настольные ролевые игры', value: '644' }, + { label: 'Натуризм / Нудизм', value: '1810' }, + { label: 'Наука', value: '2624' }, + { label: 'Научная фантастика', value: '596' }, + { label: 'Научно-исследовательские организации', value: '1503' }, + { label: 'Научное фэнтези', value: '180' }, + { label: 'Начало отношений', value: '1328' }, + { label: 'Небинарные персонажи', value: '314' }, + { label: 'Невезение', value: '2689' }, + { label: 'Невзаимные предначертанные', value: '1484' }, + { label: 'Невзаимные чувства', value: '105' }, + { label: 'Невидимость', value: '359' }, + { label: 'Невидимый мир', value: '2286' }, + { label: 'Неграфичные описания', value: '2855' }, + { label: 'Недоверие', value: '1842' }, + { label: 'Недопонимания', value: '614' }, + { label: 'Недостатки внешности', value: '923' }, + { label: 'Нежелательная беременность', value: '715' }, + { label: 'Нежелательные сверхспособности', value: '1335' }, + { label: 'Нежелательные чувства', value: '2078' }, + { label: 'Нежный секс', value: '280' }, + { label: 'Незащищенный секс', value: '497' }, + { label: 'Нездоровые механизмы преодоления', value: '1528' }, + { label: 'Нездоровые отношения', value: '71' }, + { label: 'Нездоровый образ жизни', value: '2148' }, + { label: 'Нездоровый BDSM', value: '1062' }, + { label: 'Неидеальный омегаверс', value: '2712' }, + { label: 'Неизвестность', value: '2826' }, + { label: 'Неизвестные родственники', value: '2752' }, + { label: 'Неизлечимые заболевания', value: '1269' }, + { label: 'Нейтрализация сверхспособностей', value: '2343' }, + { label: 'Некромаги', value: '679' }, + { label: 'Некрофилия', value: '1663' }, + { label: 'Нелинейное повествование', value: '1699' }, + { label: 'Неловкий секс', value: '2883' }, + { label: 'Неловкость', value: '2459' }, + { label: 'Нелюбящие родители', value: '2817' }, + { label: 'Немертвые', value: '495' }, + { label: 'Немота', value: '414' }, + { label: 'Ненависть', value: '685' }, + { label: 'Ненависть к себе', value: '1441' }, + { label: 'Ненависть с первого взгляда', value: '814' }, + { label: 'Ненадежный рассказчик', value: '322' }, + { label: 'Ненасильственная смерть', value: '2531' }, + { label: 'Необитаемые острова', value: '1196' }, + { label: 'Необратимые превращения', value: '926' }, + { label: 'Неозвученные чувства', value: '258' }, + { label: 'Неприятие отношений', value: '2056' }, + { label: 'Неравные отношения', value: '545' }, + { label: 'Неравный брак', value: '409' }, + { label: 'Неразрывная связь', value: '2569' }, + { label: 'Нервный срыв', value: '1190' }, + { label: 'Нерды', value: '481' }, + { label: 'Нереалистичность', value: '2350' }, + { label: 'Несексуальная близость', value: '2874' }, + { label: 'Нестандартная поэзия', value: '1704' }, + { label: 'Несчастливые отношения', value: '2080' }, + { label: 'Несчастливый финал', value: '61' }, + { label: 'Несчастные случаи', value: '499' }, + { label: 'Нетаймори', value: '2323' }, + { label: 'Неторопливое повествование', value: '1815' }, + { label: 'Нетрадиционное использование Силы', value: '2520' }, + { + label: 'Неумышленное употребление наркотических веществ', + value: '1192', + }, + { label: 'Неуставные отношения', value: '1396' }, + { label: 'Нефилимы', value: '1086' }, + { label: 'Неформальный брак', value: '1355' }, + { label: 'Нецензурная лексика', value: '1653' }, + { label: 'Нечеловеческая мораль', value: '2551' }, + { label: 'Нечеловеческие виды', value: '995' }, + { label: 'Нечистая сила', value: '1464' }, + { label: 'Нидерланды', value: '1141' }, + { label: 'Низкая самооценка', value: '2014' }, + { label: 'Низкое фэнтези', value: '2007' }, + { label: 'Нимфы', value: '1477' }, + { label: 'Ниндзя', value: '471' }, + { label: 'Новая жизнь', value: '2018' }, + { label: 'Новая Зеландия', value: '1789' }, + { label: 'Новеллизация', value: '262' }, + { label: 'Новые отношения', value: '1827' }, + { label: 'Новый год', value: '206' }, + { label: 'Новый канон ЗВ', value: '1948' }, + { label: 'Норны', value: '2206' }, + { label: 'Ностальгия', value: '878' }, + { label: 'Ночное видение', value: '2006' }, + { label: 'Ночные клубы', value: '531' }, + { label: 'Нуар', value: '137' }, + { label: 'Няни', value: '570' }, + { label: 'Обездвиживание', value: '116' }, + { label: 'Обещания / Клятвы', value: '1823' }, + { label: 'Обиды', value: '905' }, + { label: 'Обман / Заблуждение', value: '650' }, + { label: 'Обмен квами', value: '2572' }, + { label: 'Обмен ролями', value: '1055' }, + { label: 'Обмен телами', value: '49' }, + { label: 'Обмороки', value: '2484' }, + { label: 'Обнажение', value: '970' }, + { label: 'Оборотни', value: '28' }, + { label: 'Обоснованный ООС', value: '2549' }, + { label: 'Образ тела', value: '2289' }, + { label: 'Обратный омегаверс', value: '2435' }, + { label: 'Обретенные семьи', value: '601' }, + { label: 'Обреченные отношения', value: '653' }, + { label: 'Обскуры', value: '2237' }, + { label: 'Обсуждение кинков', value: '1072' }, + { label: 'Обусловленная контекстом гомофобия', value: '2685' }, + { label: 'Обусловленный контекстом расизм', value: '2686' }, + { label: 'Обусловленный контекстом сексизм', value: '2687' }, + { label: 'Общежития', value: '534' }, + { label: 'Общественный транспорт', value: '615' }, + { label: 'Объективация', value: '1837' }, + { label: 'Объектофилия', value: '366' }, + { label: 'Овипозиция', value: '269' }, + { label: 'Огнестрельное оружие', value: '1189' }, + { label: 'Ограбления', value: '2269' }, + { label: 'Одержимость', value: '2248' }, + { label: 'Одержимые', value: '496' }, + { label: 'Один день', value: '2647' }, + { label: 'Одиночество', value: '1015' }, + { label: 'Одичавшие дети', value: '835' }, + { label: 'Однолюбы', value: '2146' }, + { label: 'Одноминутный канонический персонаж', value: '2184' }, + { label: 'Однополый мир', value: '1462' }, + { label: 'Ожидание', value: '2909' }, + { label: 'ОЖП', value: '1657' }, + { label: 'Окаменение', value: '2052' }, + { label: 'Океания', value: '1058' }, + { label: 'Оккультизм', value: '2374' }, + { label: 'ОКР', value: '594' }, + { label: 'Окулолинктус', value: '1470' }, + { label: 'Олимпийские игры', value: '1127' }, + { label: 'Олфактофилия', value: '1945' }, + { label: 'Омегаверс', value: '1693' }, + { label: 'Омегаверс: Альфа/Альфа', value: '2437' }, + { label: 'Омегаверс: Альфа/Бета', value: '2741' }, + { label: 'Омегаверс: Альфа/Бета/Омега', value: '2898' }, + { label: 'Омегаверс: Альфа/Омега/Альфа', value: '2436' }, + { label: 'Омегаверс: Бета/Бета', value: '2966' }, + { label: 'Омегаверс: Больше трех полов', value: '2782' }, + { label: 'Омегаверс: Омега/Альфа/Омега', value: '2544' }, + { label: 'Омегаверс: Омега/Бета', value: '2965' }, + { label: 'Омегаверс: Омега/Омега', value: '2564' }, + { label: 'Омегаверс: Смена второго пола', value: '2776' }, + { label: 'Омегаверс: f!Альфа/m!Омега', value: '2777' }, + { label: 'ОМП', value: '1656' }, + { label: 'Онейрокинез', value: '2067' }, + { label: 'Онкологические заболевания', value: '540' }, + { label: 'ООС', value: '1648' }, + { label: 'Опасность', value: '2847' }, + { label: 'Опера', value: '549' }, + { label: 'Опричнина', value: '2252' }, + { label: 'Оптимизм', value: '2420' }, + { label: 'Оракулы / Провидцы', value: '2221' }, + { label: 'Оральная фиксация', value: '2523' }, + { label: 'Оргазм без стимуляции', value: '1000' }, + { label: 'Организаторы свадеб', value: '858' }, + { label: 'Орден Феникса', value: '1907' }, + { label: 'Орки', value: '187' }, + { label: 'Оружие массового поражения', value: '1193' }, + { label: 'Освоение земель', value: '1824' }, + { label: 'Оседлание', value: '1223' }, + { label: 'Османская империя', value: '527' }, + { label: 'Особняки / Резиденции', value: '500' }, + { label: 'Осознанные сновидения', value: '530' }, + { label: 'Остеокинез', value: '2845' }, + { label: 'Острова', value: '799' }, + { label: 'От антигероя к герою', value: '2173' }, + { label: 'От антигероя к злодею', value: '2213' }, + { label: 'От божественного существа к смертному', value: '2664' }, + { label: 'От возлюбленных к врагам', value: '783' }, + { label: 'От возлюбленных к друзьям', value: '782' }, + { label: 'От врагов к возлюбленным', value: '346' }, + { label: 'От врагов к друзьям', value: '347' }, + { label: 'От врагов к друзьям к возлюбленным', value: '345' }, + { label: 'От героя к антигерою', value: '2546' }, + { label: 'От героя к злодею', value: '1027' }, + { label: 'От героя к монстру', value: '2659' }, + { label: 'От героя к обычному персонажу', value: '2584' }, + { label: 'От друзей к возлюбленным', value: '344' }, + { label: 'От друзей к врагам', value: '463' }, + { label: 'От друзей к незнакомцам', value: '1870' }, + { label: 'От злодея к антигерою', value: '2640' }, + { label: 'От злодея к герою', value: '1026' }, + { label: 'От напарников к друзьям к возлюбленным', value: '2460' }, + { label: 'От нездоровых отношений к здоровым', value: '2301' }, + { label: 'От незнакомцев к возлюбленным', value: '372' }, + { label: 'От простонародья к аристократу', value: '2816' }, + { label: 'От сексуальных партнеров к возлюбленным', value: '2069' }, + { label: 'От смертного к божественному существу', value: '2665' }, + { label: 'От супругов к возлюбленным', value: '2359' }, + { label: 'Ответвление от канона', value: '2336' }, + { label: 'Ответственность', value: '2468' }, + { label: 'Отдаю на вдохновение', value: '2288' }, + { label: 'Отечественная война 1812 года', value: '1162' }, + { label: 'Отказ от чувств', value: '2227' }, + { label: 'Отклонения от канона', value: '5' }, + { label: 'Открытый финал', value: '185' }, + { label: 'ОтМетки', value: '2945' }, + { label: 'Отношения втайне', value: '285' }, + { label: 'Отношения на расстоянии', value: '37' }, + { label: 'Отношения на спор', value: '383' }, + { label: 'Отношения наполовину', value: '1573' }, + { label: 'Отпуск', value: '1828' }, + { label: 'Отрицание', value: '1868' }, + { label: 'Отрицание чувств', value: '648' }, + { label: 'Отрицательный протагонист', value: '1442' }, + { label: 'Отсутствие души', value: '1897' }, + { label: 'Отшельники', value: '543' }, + { label: 'Офисы', value: '74' }, + { label: 'Официанты / Официантки', value: '2560' }, + { label: 'Охота', value: '825' }, + { label: 'Охота на разумных существ', value: '2784' }, + { label: 'Охотники', value: '2036' }, + { label: 'Охотники за головами', value: '2103' }, + { label: 'Охотники на нечисть', value: '46' }, + { label: 'Охранники', value: '1173' }, + { label: 'Ошибки', value: '2554' }, + { label: 'Ошибочная идентификация', value: '2490' }, + { label: 'Паладины', value: '1194' }, + { label: 'Пан-персонажи', value: '302' }, + { label: 'Пандемия COVID-19', value: '2724' }, + { label: 'Панические атаки', value: '751' }, + { label: 'Паническое расстройство', value: '733' }, + { label: 'Панки', value: '625' }, + { label: 'Паразиты', value: '1877' }, + { label: 'Паралич', value: '1941' }, + { label: 'Параллельные миры', value: '378' }, + { label: 'Паранойя', value: '1253' }, + { label: 'Пари', value: '778' }, + { label: 'Парки / Зоны отдыха', value: '1005' }, + { label: 'Парки аттракционов / Ярмарки', value: '793' }, + { label: 'Паркур', value: '553' }, + { label: 'Пародия', value: '1676' }, + { label: 'Партизаны', value: '850' }, + { label: 'Парфюмеры', value: '1384' }, + { label: 'Пасха', value: '562' }, + { label: 'Патологическое бесстрашие', value: '1498' }, + { label: 'Патологическое накопительство', value: '2691' }, + { label: 'Патологоанатомы', value: '502' }, + { label: 'Патриархат', value: '705' }, + { label: 'Патриотические темы и мотивы', value: '2231' }, + { label: 'Патронусы', value: '2390' }, + { label: 'Патрули', value: '1819' }, + { label: 'Пафос', value: '2589' }, + { label: 'Пацифисты', value: '1939' }, + { label: 'Пегасы', value: '1047' }, + { label: 'Пеггинг', value: '159' }, + { label: 'Пейзажная лирика', value: '2330' }, + { label: 'Пекарни', value: '78' }, + { label: 'Первая мировая', value: '487' }, + { label: 'Первая чеченская война', value: '2428' }, + { label: 'Первобытные времена', value: '465' }, + { label: 'Первое апреля', value: '2315' }, + { label: 'Первопроходцы', value: '351' }, + { label: 'Первый контакт', value: '2093' }, + { label: 'Первый поцелуй', value: '2790' }, + { label: 'Первый раз', value: '1292' }, + { label: 'Передача магических способностей', value: '2497' }, + { label: 'Переезд', value: '658' }, + { label: 'Перезапуск мира', value: '2710' }, + { label: 'Перемирие', value: '1331' }, + { label: 'Переписки и чаты (стилизация)', value: '38' }, + { label: 'Перерыв в отношениях', value: '519' }, + { label: 'Переселение душ', value: '1237' }, + { label: 'Перестрелки', value: '2268' }, + { label: 'Переходный возраст', value: '1297' }, + { label: 'Период Эдо', value: '1140' }, + { label: 'Персекуторный бред', value: '2136' }, + { label: 'Персонажи-геи', value: '2141' }, + { label: 'Персонажи-лесбиянки', value: '2142' }, + { label: 'Персонификация', value: '994' }, + { label: 'Персонификация смерти', value: '1046' }, + { label: 'Перу', value: '2017' }, + { label: 'Перфекционизм', value: '2730' }, + { label: 'Пессимизм', value: '2709' }, + { label: 'Пет-плей', value: '391' }, + { label: 'Петербург: бикейм #Чимин Сергеевич', value: '1859' }, + { label: 'Петтинг', value: '301' }, + { label: 'Пещеры', value: '655' }, + { label: 'Пижамные вечеринки', value: '1340' }, + { label: 'Пикацизм', value: '1227' }, + { label: 'Пикники', value: '1165' }, + { label: 'Пионеры', value: '668' }, + { label: 'Пираты', value: '72' }, + { label: 'Пирокинез', value: '1180' }, + { label: 'Пиромания', value: '899' }, + { label: 'Пирсинг', value: '507' }, + { label: 'Писатели', value: '196' }, + { label: 'Письма', value: '2298' }, + { label: 'Письма (стилизация)', value: '266' }, + { label: 'Пиццерии / Дайнеры', value: '593' }, + { label: 'Плавание', value: '1135' }, + { label: 'Плантокинез', value: '2029' }, + { label: 'Пластические операции', value: '1847' }, + { label: 'Платонические отношения', value: '385' }, + { label: 'Племена', value: '1336' }, + { label: 'Плен', value: '290' }, + { label: 'Плененные путешествием', value: '2615' }, + { label: 'Плохая компания', value: '1502' }, + { label: 'Плохие друзья', value: '2445' }, + { label: 'Плохой хороший финал', value: '1265' }, + { label: 'Плюшефилия', value: '520' }, + { label: 'Пляжи', value: '766' }, + { label: 'По разные стороны', value: '897' }, + { label: 'Побег', value: '661' }, + { label: 'Побег из дома', value: '2309' }, + { label: 'Победа Волдеморта', value: '1887' }, + { label: 'Победа Гриндевальда', value: '2398' }, + { label: 'Повара', value: '2853' }, + { label: 'Повелитель смерти', value: '1909' }, + { label: 'Повествование в будущем времени', value: '2633' }, + { label: 'Повествование в настоящем времени', value: '1826' }, + { label: 'Повествование во втором лице', value: '166' }, + { label: 'Повествование от нескольких лиц', value: '1157' }, + { label: 'Повествование от первого лица', value: '1681' }, + { label: 'Повседневность', value: '1677' }, + { label: 'Повстанцы', value: '659' }, + { label: 'Повторение судьбы', value: '1997' }, + { label: 'Поглощение способностей', value: '2605' }, + { label: 'Погони / Преследования', value: '1996' }, + { label: 'Пограничный синдром', value: '710' }, + { label: 'Погребение заживо', value: '1279' }, + { label: 'Под одной крышей', value: '462' }, + { label: 'Под старину (стилизация)', value: '2486' }, + { label: 'Подарки', value: '1109' }, + { label: 'Подвешивание', value: '914' }, + { label: 'Подводный мир', value: '901' }, + { label: 'Подземелья', value: '2314' }, + { label: 'Подземный мир', value: '1453' }, + { label: 'Подлость', value: '2625' }, + { label: 'Подменыши', value: '2250' }, + { label: 'Подпольные бои', value: '1389' }, + { label: 'Подразумеваемая смерть персонажа', value: '2204' }, + { label: 'Подростки', value: '218' }, + { label: 'Подростковая беременность', value: '986' }, + { label: 'Подростковая влюбленность', value: '581' }, + { label: 'Подсознание', value: '2273' }, + { label: 'Поедание разумных существ', value: '1795' }, + { label: 'Пожарные', value: '2557' }, + { label: 'Пожилые персонажи', value: '852' }, + { label: 'Поза 69', value: '699' }, + { label: 'Поиск работы', value: '2646' }, + { label: 'Поиск родителей', value: '2033' }, + { label: 'Поклонение телу', value: '712' }, + { label: 'Покровительство', value: '2223' }, + { label: 'Покушение на жизнь', value: '1057' }, + { label: 'Покушение на свободу', value: '2655' }, + { label: 'Полиамория', value: '1705' }, + { label: 'Полиандрия', value: '761' }, + { label: 'Полигиния', value: '760' }, + { label: 'Политика', value: '1835' }, + { label: 'Политики', value: '711' }, + { label: 'Политические интриги', value: '183' }, + { label: 'Полицейские', value: '14' }, + { label: 'Половой диморфизм', value: '1780' }, + { label: 'Полубоги', value: '437' }, + { label: 'Полудемоны', value: '2872' }, + { label: 'Полудраконы', value: '2859' }, + { label: 'Полукровки', value: '368' }, + { label: 'Польша', value: '777' }, + { label: 'Полярные станции', value: '1493' }, + { label: 'Помощь врагу', value: '1325' }, + { label: 'Попаданцы: В реальный мир', value: '1499' }, + { label: 'Попаданцы: В своем теле', value: '1412' }, + { label: 'Попаданцы: В чужом теле', value: '1413' }, + { label: 'Попаданцы: Из одного фандома в другой', value: '2699' }, + { label: 'Попаданцы: Из фандома в ориджинал', value: '2193' }, + { label: 'Попаданцы: По обмену', value: '2196' }, + { label: 'Попаданчество', value: '1695' }, + { label: 'Популярность', value: '2581' }, + { label: 'Попытка изнасилования', value: '1440' }, + { label: 'Порноактеры', value: '289' }, + { label: 'Португалия', value: '1749' }, + { label: 'Порты', value: '1028' }, + { label: 'Поселки', value: '1298' }, + { label: 'После таймскипа (Haikyuu!!)', value: '2906' }, + { label: 'После таймскипа (Shingeki no Kyojin)', value: '2903' }, + { label: 'Послевоенное время', value: '1080' }, + { label: 'Последний раз', value: '2645' }, + { label: 'Последний рубеж', value: '1178' }, + { label: 'Последний шанс', value: '1209' }, + { label: 'Последствия', value: '2491' }, + { label: 'Последствия болезни', value: '2612' }, + { label: 'Послеродовая депрессия', value: '988' }, + { label: 'Посмертная любовь', value: '2670' }, + { label: 'Посмертный персонаж', value: '2653' }, + { label: 'Пост-Пацифист', value: '2408' }, + { label: 'Пост-Рейхенбах', value: '1903' }, + { label: 'Пост-третий сезон (Шерлок)', value: '1977' }, + { label: 'Постапокалиптика', value: '1697' }, + { label: 'ПостВБ', value: '2386' }, + { label: 'Постканон', value: '6' }, + { label: 'ПостРагнарек', value: '2537' }, + { label: 'ПостХог', value: '1883' }, + { label: 'ПостЭндгейм', value: '2179' }, + { label: 'Потеря девственности', value: '1234' }, + { label: 'Потеря конечностей', value: '1289' }, + { label: 'Потеря магических способностей', value: '1430' }, + { label: 'Потеря памяти', value: '36' }, + { label: 'Потеря сверхспособностей', value: '1051' }, + { label: 'Поттергад', value: '2417' }, + { label: 'Похищение', value: '224' }, + { label: 'Похороны', value: '786' }, + { label: 'Почтовые работники', value: '1821' }, + { label: 'Пошлый юмор', value: '1362' }, + { label: 'Поэма', value: '1938' }, + { label: 'Поэты', value: '357' }, + { label: 'Пояс верности', value: '1779' }, + { label: 'Правда или действие', value: '1982' }, + { label: 'Практопия', value: '2249' }, + { label: 'Пре-гет', value: '209' }, + { label: 'Пре-Рош', value: '2047' }, + { label: 'Пре-слэш', value: '207' }, + { label: 'Пре-фемслэш', value: '208' }, + { label: 'Предательство', value: '361' }, + { label: 'Предвидение', value: '1876' }, + { label: 'Предложение руки и сердца', value: '1081' }, + { label: 'Предопределенность', value: '411' }, + { label: 'Предрассудки', value: '2046' }, + { label: 'Предсмертные сообщения', value: '2174' }, + { label: 'Преканон', value: '7' }, + { label: 'ПреКербер', value: '2726' }, + { label: 'Прекрасная эпоха', value: '1809' }, + { label: 'Прелюдия', value: '2395' }, + { label: 'Пренебрежение гигиеной', value: '2921' }, + { label: 'Пренебрежение жизнью', value: '2216' }, + { label: 'Преодоление комплексов', value: '588' }, + { label: 'Преподаватели', value: '403' }, + { label: 'Преподаватель/Обучающийся', value: '2507' }, + { label: 'Преступники', value: '1446' }, + { label: 'Преступный мир', value: '12' }, + { label: 'Прибрежные города', value: '984' }, + { label: 'Приватный танец', value: '2786' }, + { label: 'Привязанность', value: '2372' }, + { label: 'Пригород', value: '452' }, + { label: 'Приемные семьи', value: '844' }, + { label: 'Признания в любви', value: '1049' }, + { label: 'Призраки', value: '26' }, + { label: 'Приключения', value: '21' }, + { label: 'Приметы / Суеверия', value: '612' }, + { label: 'Примирение', value: '660' }, + { label: 'Принудительная феминизация', value: '656' }, + { label: 'Принудительное лечение', value: '971' }, + { label: 'Принудительные отношения', value: '2302' }, + { label: 'Принудительный брак', value: '307' }, + { label: 'Принудительный инцест', value: '2942' }, + { label: 'Принуждение', value: '1323' }, + { label: 'Принятие себя', value: '2754' }, + { label: 'Прислуга', value: '804' }, + { label: 'Прист-кинк', value: '342' }, + { label: 'Приступы агрессии', value: '2788' }, + { label: 'Притча', value: '1088' }, + { label: 'Приюты для животных', value: '2345' }, + { label: 'ПРЛ', value: '1092' }, + { label: 'Проблемы доверия', value: '2208' }, + { label: 'Проблемы с законом', value: '1007' }, + { label: 'Проводники душ', value: '2348' }, + { label: 'Программисты', value: '574' }, + { label: 'Прогрессорство', value: '449' }, + { label: 'Прогулки', value: '2462' }, + { label: 'Продажа души', value: '887' }, + { label: 'Прозопагнозия', value: '2013' }, + { label: 'Производственный роман', value: '2119' }, + { label: 'Проклятия', value: '875' }, + { label: 'Пролапс', value: '2926' }, + { label: 'Промискуитет', value: '1817' }, + { label: 'Промышленные предприятия', value: '1456' }, + { label: 'Пропавшие без вести', value: '2073' }, + { label: 'Пропущенная сцена', value: '1702' }, + { label: 'Пророчества', value: '834' }, + { label: 'Просмотр порно', value: '2341' }, + { label: 'Проституция', value: '178' }, + { label: 'Протезы', value: '1581' }, + { label: 'Противоположности', value: '948' }, + { label: 'Противоречивые чувства', value: '2528' }, + { label: 'Прошлое', value: '983' }, + { label: 'Прощение', value: '1545' }, + { label: 'Псевдо-инцест', value: '336' }, + { label: 'Псевдо-мистика', value: '2159' }, + { label: 'Псевдоисторический сеттинг', value: '1479' }, + { label: 'Псионика', value: '2023' }, + { label: 'Психиатрические больницы', value: '135' }, + { label: 'Психические расстройства', value: '69' }, + { label: 'Психоз', value: '1339' }, + { label: 'Психологи / Психоаналитики', value: '611' }, + { label: 'Психологическая война', value: '2595' }, + { label: 'Психологические пытки', value: '2825' }, + { label: 'Психологические травмы', value: '40' }, + { label: 'Психологический мазохизм', value: '2212' }, + { label: 'Психологический ужас', value: '1261' }, + { label: 'Психологическое насилие', value: '836' }, + { label: 'Психология', value: '1674' }, + { label: 'Психопатия', value: '869' }, + { label: 'Психосоматические расстройства', value: '1834' }, + { label: 'Психотерапия', value: '958' }, + { label: 'ПТСР', value: '425' }, + { label: 'Публичное обнажение', value: '319' }, + { label: 'Пурпурная проза', value: '599' }, + { label: 'Пустоши', value: '1114' }, + { label: 'Пустыни', value: '692' }, + { label: 'Путешественники-исследователи', value: '888' }, + { label: 'Путешествия', value: '894' }, + { label: 'Пытки', value: '210' }, + { label: 'Пьеса', value: '434' }, + { label: 'Рабочие', value: '1771' }, + { label: 'Рабство', value: '51' }, + { label: 'Равнодушие', value: '2516' }, + { label: 'Равные отношения', value: '2728' }, + { label: 'Радикальная медицина', value: '731' }, + { label: 'Радио', value: '974' }, + { label: 'Разбойники', value: '473' }, + { label: 'Развитие отношений', value: '1404' }, + { label: 'Развод', value: '200' }, + { label: 'Разговоры', value: '1401' }, + { label: 'Раздвоение личности', value: '287' }, + { label: 'Раздельное обучение', value: '1959' }, + { label: 'Разлука / Прощания', value: '1104' }, + { label: 'Разница в возрасте', value: '2' }, + { label: 'Разница культур', value: '1079' }, + { label: 'Разновозрастная дружба', value: '2402' }, + { label: 'Разнополая дружба', value: '806' }, + { label: 'Разоблачения', value: '2684' }, + { label: 'Разочарования', value: '646' }, + { label: 'Разрушение стереотипов', value: '2805' }, + { label: 'Разрушение четвертой стены', value: '381' }, + { label: 'Разрыв связи', value: '2582' }, + { label: 'Разум роя', value: '1459' }, + { label: 'Разумное оружие', value: '2675' }, + { label: 'Разумные животные', value: '895' }, + { label: 'Разумные растения', value: '1037' }, + { label: 'Раптофилия', value: '729' }, + { label: 'Расизм', value: '433' }, + { label: 'Раскрытие личностей', value: '2120' }, + { label: 'Раскрытие магии', value: '1878' }, + { label: 'Расплата', value: '2464' }, + { label: 'Рассказ в рассказе', value: '2077' }, + { label: 'Расставание', value: '506' }, + { label: 'Расстройства аутистического спектра', value: '541' }, + { label: 'Расстройства цветового восприятия', value: '1330' }, + { label: 'Расстройства шизофренического спектра', value: '912' }, + { label: 'Растаманы', value: '857' }, + { label: 'Расширенная Вселенная ЗВ', value: '1950' }, + { label: 'Реабилитационные центры', value: '1467' }, + { label: 'Реактивное расстройство привязанности', value: '2576' }, + { label: 'Реакции (стилизация)', value: '1283' }, + { label: 'Реализм', value: '1201' }, + { label: 'Реалити-шоу', value: '2312' }, + { label: 'Ревность', value: '544' }, + { label: 'Революции', value: '585' }, + { label: 'Регенерация', value: '1530' }, + { label: 'Регресс', value: '450' }, + { label: 'Регрессия возраста', value: '102' }, + { label: 'Редкие заболевания', value: '1856' }, + { label: 'Реинкарнация', value: '101' }, + { label: 'Рейтинг за лексику', value: '276' }, + { label: 'Рейтинг за насилие и/или жестокость', value: '277' }, + { label: 'Рейтинг за секс', value: '278' }, + { label: 'Религиозная нетерпимость', value: '2451' }, + { label: 'Религиозные войны', value: '2911' }, + { label: 'Религиозные темы и мотивы', value: '198' }, + { label: 'Религиозный фанатизм', value: '2548' }, + { label: 'Ренессанс', value: '239' }, + { label: 'Репетиторы', value: '2413' }, + { label: 'Репетиции', value: '2241' }, + { label: 'Репродуктивное насилие', value: '2310' }, + { label: 'Реставраторы', value: '2239' }, + { label: 'Рестораны', value: '412' }, + { label: 'Ретеллинг', value: '52' }, + { label: 'Ретрофутуризм', value: '727' }, + { label: 'Реформация', value: '2199' }, + { label: 'Речь Посполитая', value: '1529' }, + { label: 'Риветхеды', value: '2904' }, + { label: 'Римминг', value: '170' }, + { label: 'Риск', value: '2467' }, + { label: 'Рискованная беременность', value: '998' }, + { label: 'Ритуалы', value: '749' }, + { label: 'Ритуальные услуги', value: '2362' }, + { label: 'Роботы', value: '513' }, + { label: 'Родео', value: '1314' }, + { label: 'Родители-одиночки', value: '325' }, + { label: 'Родительские чувства', value: '2308' }, + { label: 'Родомагия', value: '1899' }, + { label: 'Роды', value: '991' }, + { label: 'Рождество', value: '211' }, + { label: 'Розыгрыши', value: '1807' }, + { label: 'Рокеры', value: '861' }, + { label: 'Роковая женщина / Роковой мужчина', value: '982' }, + { label: 'Роковое усилие', value: '2604' }, + { label: 'Роллер-дерби', value: '1014' }, + { label: 'Романтизация', value: '1898' }, + { label: 'Романтика', value: '1664' }, + { label: 'Романтическая дружба', value: '2307' }, + { label: 'Ромком', value: '2768' }, + { label: 'Российская империя', value: '475' }, + { label: 'Россия', value: '701' }, + { label: 'РПП', value: '286' }, + { label: 'Румыния', value: '862' }, + { label: 'Русалки', value: '32' }, + { label: 'Русальная неделя', value: '1067' }, + { label: 'Русреал', value: '53' }, + { label: 'Руферы', value: '578' }, + { label: 'Рыбалка', value: '855' }, + { label: 'Рыцари', value: '426' }, + { label: 'Рыцарские турниры', value: '2759' }, + { label: 'С чистого листа', value: '987' }, + { label: 'Савойя', value: '1036' }, + { label: 'Садизм / Мазохизм', value: '461' }, + { label: 'Садовники', value: '1348' }, + { label: 'Сайз-кинк', value: '100' }, + { label: 'Самобытные культуры', value: '663' }, + { label: 'Самовнушение', value: '2169' }, + { label: 'Самовставка', value: '1689' }, + { label: 'Самозванцы', value: '1811' }, + { label: 'Самоистязание', value: '1386' }, + { label: 'Самоопределение / Самопознание', value: '1278' }, + { label: 'Самопожертвование', value: '750' }, + { label: 'Самосуд', value: '1402' }, + { label: 'Самоуничижение', value: '2667' }, + { label: 'Самураи', value: '472' }, + { label: 'Санатории', value: '223' }, + { label: 'Сандалпанк', value: '1986' }, + { label: 'Сарказм', value: '2225' }, + { label: 'Сатира', value: '1944' }, + { label: 'Сатиры', value: '264' }, + { label: 'Сахарный диабет', value: '2609' }, + { label: 'Сборник драбблов', value: '39' }, + { label: 'Сборник лимериков', value: '1989' }, + { label: 'Сборник пирожков', value: '743' }, + { label: 'Сборник стихотворений', value: '600' }, + { label: 'Сборник японской поэзии', value: '1351' }, + { label: 'Свадьба', value: '184' }, + { label: 'Сверхразумы', value: '966' }, + { label: 'Сверхскорость', value: '1902' }, + { label: 'Сверхспособности', value: '597' }, + { label: 'Светлые властелины', value: '2596' }, + { label: 'Светские мероприятия', value: '973' }, + { label: 'Свидание вслепую', value: '741' }, + { label: 'Свидания', value: '1235' }, + { label: 'Свидетели', value: '1242' }, + { label: 'Свингерство', value: '203' }, + { label: 'Свобода', value: '2166' }, + { label: 'Свободные отношения', value: '255' }, + { label: 'Сводничество', value: '568' }, + { label: 'Сводные родственники', value: '1445' }, + { label: 'Святые', value: '393' }, + { label: 'Сгенерировано ИИ', value: '3014' }, + { label: 'СДВГ', value: '539' }, + { label: 'Севвитус', value: '1871' }, + { label: 'Северная Корея', value: '2767' }, + { label: 'Северус Снейп жив', value: '2406' }, + { label: 'Сегрегация', value: '2158' }, + { label: 'Секретари', value: '1170' }, + { label: 'Секретные миссии', value: '1829' }, + { label: 'Секс без обязательств', value: '387' }, + { label: 'Секс в воде', value: '718' }, + { label: 'Секс в нетрезвом виде', value: '1110' }, + { label: 'Секс в одежде', value: '1013' }, + { label: 'Секс в публичных местах', value: '123' }, + { label: 'Секс в транспорте', value: '124' }, + { label: 'Секс в церкви', value: '2968' }, + { label: 'Секс во время беременности', value: '279' }, + { label: 'Секс во время болезни', value: '1365' }, + { label: 'Секс во время менструации', value: '153' }, + { label: 'Секс на камеру', value: '1101' }, + { label: 'Секс на природе', value: '1256' }, + { label: 'Секс перед зеркалом', value: '1255' }, + { label: 'Секс по расчету', value: '1212' }, + { label: 'Секс по телефону', value: '133' }, + { label: 'Секс при посторонних', value: '2514' }, + { label: 'Секс с воображаемыми партнерами', value: '2276' }, + { label: 'Секс с использованием магии', value: '1563' }, + { + label: 'Секс с использованием одурманивающих веществ', + value: '2635', + }, + { label: 'Секс с использованием посторонних предметов', value: '1661' }, + { label: 'Секс с использованием сверхспособностей', value: '2610' }, + { label: 'Секс со смертельным исходом', value: '1171' }, + { label: 'Секс-игрушки', value: '176' }, + { label: 'Секс-индустрия', value: '447' }, + { label: 'Секс-клубы / Секс-вечеринки', value: '936' }, + { label: 'Секс-куклы', value: '1202' }, + { label: 'Секс-магия', value: '1131' }, + { label: 'Секс-машины', value: '1112' }, + { label: 'Секс-шопы', value: '739' }, + { label: 'Сексизм', value: '464' }, + { label: 'Сексомния', value: '2024' }, + { label: 'Секстинг', value: '2215' }, + { label: 'Сексуальная неопытность', value: '1717' }, + { label: 'Сексуальное насилие', value: '2495' }, + { label: 'Сексуальное обучение', value: '967' }, + { label: 'Сексуальное рабство', value: '1353' }, + { label: 'Сексуальные фобии', value: '1537' }, + { label: 'Селфхарм', value: '76' }, + { label: 'Селфцест', value: '1708' }, + { label: 'Семейная сага', value: '920' }, + { label: 'Семейные тайны', value: '779' }, + { label: 'Семейный бизнес', value: '2338' }, + { label: 'Семьи', value: '220' }, + { label: 'Сенсорная депривация', value: '282' }, + { label: 'Сенсорная перегрузка', value: '2351' }, + { label: 'Сентиментальность', value: '2567' }, + { label: 'Серая мораль', value: '839' }, + { label: 'Серая мышь', value: '1250' }, + { label: 'Серая реальность', value: '2278' }, + { label: 'Серийные насильники', value: '1942' }, + { label: 'Серийные убийцы', value: '193' }, + { label: 'Серфинг', value: '764' }, + { label: 'Сиамские близнецы', value: '1252' }, + { label: 'Сибари', value: '131' }, + { label: 'Сиблинги', value: '617' }, + { label: 'Сидение на лице', value: '713' }, + { label: 'Силкпанк', value: '1813' }, + { label: 'Симбиоз', value: '849' }, + { label: 'Символизм', value: '538' }, + { label: 'Симфорофилия', value: '492' }, + { label: 'Синдром восьмиклассника', value: '2032' }, + { label: 'Синдром выжившего', value: '1100' }, + { label: 'Синдром Дауна', value: '2106' }, + { label: 'Синдром Котара', value: '2765' }, + { label: 'Синдром Питера Пэна', value: '687' }, + { label: 'Синдром Туретта', value: '946' }, + { label: 'Синестезия', value: '960' }, + { label: 'Сирены', value: '1830' }, + { label: 'Сириус Блэк жив', value: '2617' }, + { label: 'Сироты', value: '689' }, + { label: 'Ситком', value: '2553' }, + { label: 'Сказка', value: '288' }, + { label: 'Скайпанк', value: '976' }, + { label: 'Скандинавия', value: '781' }, + { label: 'Скандинавская мифология', value: '2304' }, + { label: 'Сквиртинг', value: '950' }, + { label: 'Сквозное видение', value: '1280' }, + { label: 'Скейтбординг', value: '1395' }, + { label: 'Скелеты', value: '1766' }, + { label: 'Скинхэды', value: '1757' }, + { label: 'Скрытая беременность', value: '2580' }, + { label: 'Скрытность', value: '2577' }, + { label: 'Скрытые способности', value: '2579' }, + { label: 'Скрытый Этаж', value: '2763' }, + { label: 'Скульпторы', value: '2976' }, + { label: 'Слатшейминг', value: '2910' }, + { label: 'Следующее поколение', value: '698' }, + { label: 'Слежка', value: '2102' }, + { label: 'Слепота', value: '413' }, + { label: 'Слияние', value: '2324' }, + { label: 'Словакия', value: '2938' }, + { label: 'Словения', value: '1788' }, + { label: 'Сложные отношения', value: '326' }, + { label: 'Слом личности', value: '623' }, + { label: 'Слоуберн', value: '19' }, + { label: 'Служебные животные', value: '1506' }, + { label: 'Служебные отношения', value: '1347' }, + { label: 'Служебный роман', value: '75' }, + { label: 'Слухи / Сплетни', value: '2809' }, + { label: 'Случайные убийства', value: '2673' }, + { label: 'Случайный брак', value: '304' }, + { label: 'Случайный поцелуй', value: '2852' }, + { label: 'Случайный секс', value: '1275' }, + { label: 'Смена имени', value: '2094' }, + { label: 'Смена мировоззрения', value: '2865' }, + { label: 'Смена сущности', value: '1707' }, + { label: 'Смертельные заболевания', value: '1132' }, + { label: 'Смерть антагониста', value: '2650' }, + { label: 'Смерть всех персонажей', value: '2611' }, + { label: 'Смерть второстепенных персонажей', value: '1713' }, + { label: 'Смерть животных', value: '2210' }, + { label: 'Смерть основных персонажей', value: '1647' }, + { label: 'СМИ', value: '2656' }, + { label: 'Смирение', value: '2272' }, + { label: 'Смотрины', value: '1495' }, + { label: 'Смутное время', value: '2002' }, + { label: 'Снайперы', value: '1762' }, + { label: 'Сновидения', value: '490' }, + { label: 'Сноубординг', value: '2180' }, + { label: 'Соблазнение / Ухаживания', value: '616' }, + { label: 'Собственничество', value: '908' }, + { label: 'Советпанк', value: '397' }, + { label: 'Советский Союз', value: '468' }, + { label: 'Совместная кровать', value: '1229' }, + { label: 'Совместное купание', value: '1301' }, + { label: 'Современное искусство', value: '1087' }, + { label: 'Современность', value: '80' }, + { label: 'Согласование с каноном', value: '1388' }, + { label: 'Сожаления', value: '550' }, + { label: 'Создание общества', value: '2643' }, + { label: 'Созидание', value: '2590' }, + { label: 'Сокровища', value: '893' }, + { label: 'Сокрытие убийства', value: '2311' }, + { label: 'Солянка из Сансов', value: '2515' }, + { label: 'Сомелье / Бармены', value: '865' }, + { label: 'Сомнамбулизм', value: '1394' }, + { label: 'Сомнения', value: '1544' }, + { label: 'Сомнофилия', value: '95' }, + { label: 'Сонный паралич', value: '1203' }, + { label: 'Соперничество', value: '671' }, + { label: 'Сорванная свадьба', value: '2044' }, + { label: 'Сострадание', value: '885' }, + { label: 'Состязания', value: '910' }, + { label: 'Состязания с участием животных', value: '1154' }, + { label: 'Сотриголова — отец Хитоши Шинсо', value: '2877' }, + { label: 'Соулмейты', value: '1696' }, + { label: 'Соулмейты: Действия', value: '2575' }, + { label: 'Соулмейты: Изменение восприятия', value: '1400' }, + { label: 'Соулмейты: Контакт через кожу', value: '2608' }, + { label: 'Соулмейты: Ментальная связь', value: '2841' }, + { label: 'Соулмейты: Общая боль', value: '2583' }, + { label: 'Соулмейты: Общие эмоции', value: '2637' }, + { label: 'Соулмейты: Опознавательные фразы', value: '1399' }, + { label: 'Соулмейты: Предметы', value: '2563' }, + { label: 'Соулмейты: Предпочтения', value: '2521' }, + { label: 'Соулмейты: Разноцветные глаза', value: '2461' }, + { label: 'Соулмейты: Ранения', value: '2134' }, + { label: 'Соулмейты: Сны', value: '2414' }, + { label: 'Соулмейты: Тактильный контакт', value: '2190' }, + { label: 'Соулмейты: Татуировки', value: '2711' }, + { label: 'Соулмейты: Физические проявления', value: '2864' }, + { label: 'Социальная фантастика', value: '591' }, + { label: 'Социальные темы и мотивы', value: '1017' }, + { label: 'Социальные эксперименты', value: '2756' }, + { label: 'Социальный паразитизм', value: '2444' }, + { label: 'Социофобия', value: '826' }, + { label: 'Спарринги', value: '1575' }, + { label: 'Спасатели', value: '392' }, + { label: 'Спасение жизни', value: '1299' }, + { label: 'Спасение мира', value: '812' }, + { label: 'Спектрофилия', value: '2634' }, + { label: 'Спецагенты', value: '401' }, + { label: 'Спиритизм', value: '1478' }, + { label: 'Сплошинг', value: '424' }, + { label: 'Спонтанный секс', value: '2918' }, + { label: 'Спорт', value: '15' }, + { label: 'Спортзалы', value: '1359' }, + { label: 'Сражения', value: '2868' }, + { label: 'Средневековье', value: '238' }, + { label: 'Ссоры / Конфликты', value: '645' }, + { label: 'Стаи', value: '2884' }, + { label: 'Сталкеры / Диггеры (субкультура)', value: '1029' }, + { label: 'Сталкинг', value: '605' }, + { label: 'Становление героя', value: '680' }, + { label: 'Стёб', value: '1690' }, + { label: 'Стереотипы', value: '809' }, + { label: 'Стимпанк', value: '25' }, + { label: 'Стимуляция руками', value: '225' }, + { label: 'Стихи', value: '1692' }, + { label: 'Стихийные бедствия', value: '1352' }, + { label: 'Стихотворные вставки', value: '2404' }, + { label: 'СтихоТворцы', value: '3015' }, + { label: 'Стокгольмский синдром / Лимский синдром', value: '1217' }, + { label: 'Столетняя война', value: '2243' }, + { label: 'Сторонний рассказчик', value: '1383' }, + { label: 'Стоунпанк', value: '1987' }, + { label: 'Страдания', value: '2009' }, + { label: 'Странногеддон', value: '1882' }, + { label: 'Страны Балтии', value: '1031' }, + { label: 'Страпоны', value: '380' }, + { label: 'Страсть', value: '1955' }, + { label: 'Страх перед родителями', value: '2332' }, + { label: 'Стрельба из лука', value: '1791' }, + { label: 'Стримеры / Ютуберы', value: '1473' }, + { label: 'Стрип-клубы', value: '79' }, + { label: 'Стрит-арт', value: '1012' }, + { label: 'Строители', value: '1774' }, + { label: 'Студенты', value: '274' }, + { label: 'Студенты по обмену', value: '859' }, + { label: 'Студенческие городки', value: '753' }, + { label: 'Субкультуры', value: '1061' }, + { label: 'Субординация', value: '1099' }, + { label: 'Суд', value: '827' }, + { label: 'Суккубы / Инкубы', value: '334' }, + { label: 'Сумасшествие', value: '1239' }, + { label: 'Сунагакуре', value: '2476' }, + { label: 'Супергерои', value: '67' }, + { label: 'Суперзлодеи', value: '1205' }, + { label: 'Супермаркеты', value: '843' }, + { label: 'Суррогатная беременность', value: '1427' }, + { label: 'Сухой закон', value: '2100' }, + { label: 'Сухой оргазм', value: '2818' }, + { label: 'Сфера ИТ', value: '2534' }, + { label: 'Сфинксы', value: '1349' }, + { label: 'Сценарий (стилизация)', value: '323' }, + { label: 'Счастливый финал', value: '3' }, + { label: 'США', value: '640' }, + { label: 'Съемочная площадка', value: '410' }, + { label: 'Сын Тьмы', value: '2175' }, + { label: 'Сэнгоку Джидай', value: '2380' }, + { label: 'Сэнгоку Дзидай', value: '2834' }, + { label: 'Сэцубун', value: '2969' }, + { label: 'Сюрреализм / Фантасмагория', value: '872' }, + { label: 'Т/И / Л/И', value: '167' }, + { label: 'Таджикистан', value: '2974' }, + { label: 'Таиланд', value: '1034' }, + { label: 'Тайна происхождения', value: '662' }, + { label: 'Тайная личность', value: '194' }, + { label: 'Тайная сущность', value: '1512' }, + { label: 'Тайны / Секреты', value: '906' }, + { label: 'Тайные друзья (игра)', value: '2908' }, + { label: 'Тайные организации', value: '572' }, + { label: 'Тайные поклонники', value: '1374' }, + { label: 'Тайный НапиСанта', value: '2972' }, + { label: 'Таксисты', value: '2679' }, + { label: 'Тактильный голод', value: '2197' }, + { label: 'Тактильный контакт', value: '2875' }, + { label: 'Танабата', value: '2340' }, + { label: 'Танцы', value: '108' }, + { label: 'Танцы на пилоне', value: '1370' }, + { label: 'Таро', value: '642' }, + { label: 'Тату-салоны', value: '324' }, + { label: 'Татуировки', value: '327' }, + { label: 'Твинцест', value: '1652' }, + { label: 'Творческий кризис', value: '2107' }, + { label: 'Театры', value: '195' }, + { label: 'Телекинез', value: '951' }, + { label: 'Телепатия', value: '127' }, + { label: 'Телепортация', value: '1794' }, + { label: 'Телесные жидкости', value: '1220' }, + { label: 'Телесные наказания', value: '132' }, + { label: 'Телесный хоррор', value: '212' }, + { label: 'Телохранители', value: '400' }, + { label: 'Темная сторона (Гарри Поттер)', value: '1937' }, + { label: 'Темная сторона Силы', value: '1946' }, + { label: 'Темная Эра (Bungou Stray Dogs)', value: '2494' }, + { label: 'Темное прошлое', value: '2229' }, + { label: 'Темное фэнтези', value: '1963' }, + { label: 'Темные властелины', value: '1224' }, + { label: 'Темные эльфы', value: '2597' }, + { label: 'Темный романтизм', value: '2128' }, + { label: 'Темы ментального здоровья', value: '2344' }, + { label: 'Темы этики и морали', value: '1106' }, + { label: 'Теннис', value: '1066' }, + { label: 'Тентакли', value: '129' }, + { label: 'Теория инверсии', value: '1980' }, + { label: 'Теория Одурманивания', value: '2493' }, + { label: 'Термокинез', value: '2793' }, + { label: 'Террористы', value: '556' }, + { label: 'Теслапанк', value: '1988' }, + { label: 'Техники', value: '1272' }, + { label: 'Техногенные катастрофы', value: '526' }, + { label: 'Технокинез', value: '1271' }, + { label: 'Технологии', value: '1232' }, + { label: 'Технотриллер', value: '649' }, + { label: 'Течка / Гон', value: '283' }, + { label: 'Тим Дрейк — Джокер-младший', value: '2454' }, + { label: 'Тим Дрейк — Кэтлэд', value: '2453' }, + { label: 'Тихий секс', value: '1185' }, + { label: 'Товарищи по несчастью', value: '2666' }, + { label: 'Токсичные родственники', value: '2008' }, + { label: 'Том Реддл — не Темный Лорд', value: '2394' }, + { label: 'Томбои', value: '565' }, + { label: 'Томгерлз', value: '848' }, + { label: 'Торговля органами', value: '1358' }, + { label: 'Торговцы', value: '2098' }, + { label: 'Торговые центры', value: '1416' }, + { label: 'Тоталитаризм', value: '1169' }, + { label: 'Травники / Травницы', value: '1211' }, + { label: 'Трагедия', value: '919' }, + { label: 'Трагикомедия', value: '1454' }, + { label: 'Традиции', value: '1463' }, + { label: 'Трансгендерные персонажи', value: '175' }, + { label: 'Трансплантация', value: '2364' }, + { label: 'Трансфобия', value: '420' }, + { label: 'Тревожное расстройство личности', value: '1914' }, + { label: 'Тревожность', value: '1318' }, + { label: 'Трейлеры', value: '785' }, + { label: 'Тренировки / Обучение', value: '876' }, + { label: 'Третий лишний', value: '1176' }, + { label: 'Третья мировая', value: '624' }, + { label: 'Три корейских государства', value: '2322' }, + { label: 'Трикстеры', value: '1507' }, + { label: 'Триллер', value: '169' }, + { label: 'Трилогия Юникрона', value: '2378' }, + { label: 'Трипофобия', value: '2342' }, + { label: 'Трисам', value: '758' }, + { label: 'Трихотилломания', value: '2185' }, + { label: 'Тролли', value: '190' }, + { label: 'Трудные отношения с родителями', value: '1490' }, + { label: 'Трудный характер', value: '2123' }, + { label: 'Трудоголизм', value: '1848' }, + { label: 'Трущобы', value: '2822' }, + { label: 'Туалетный юмор', value: '1247' }, + { label: 'Туберкулез', value: '2163' }, + { label: 'Турция', value: '794' }, + { label: 'Тщеславие', value: '2578' }, + { label: 'Тюрьмы / Темницы', value: '117' }, + { label: 'Тяжелое детство', value: '2614' }, + { label: 'Убийства', value: '621' }, + { label: 'Убийственная пара', value: '1918' }, + { label: 'Убийца поневоле', value: '2671' }, + { label: 'Уважение', value: '2688' }, + { label: 'Ужасы', value: '1679' }, + { label: 'Узбекистан', value: '1293' }, + { label: 'Уизлигады', value: '1862' }, + { label: 'Украина', value: '716' }, + { label: 'Укрытия', value: '199' }, + { label: 'Уличные артисты', value: '2550' }, + { label: 'Уличные гонки', value: '880' }, + { label: 'Умбракинез', value: '2355' }, + { label: 'Универсалы', value: '657' }, + { label: 'Унижения', value: '1319' }, + { label: 'Упоминания аддикций', value: '2848' }, + { label: 'Упоминания алкоголя', value: '2415' }, + { label: 'Упоминания беременности', value: '2057' }, + { label: 'Упоминания войны', value: '2707' }, + { label: 'Упоминания жестокости', value: '2447' }, + { label: 'Упоминания зоофилии', value: '2620' }, + { label: 'Упоминания измены', value: '2876' }, + { label: 'Упоминания изнасилования', value: '521' }, + { label: 'Упоминания инвалидности', value: '2705' }, + { label: 'Упоминания инцеста', value: '2285' }, + { label: 'Упоминания каннибализма', value: '2427' }, + { label: 'Упоминания курения', value: '2450' }, + { label: 'Упоминания мужской беременности', value: '2226' }, + { label: 'Упоминания наркотиков', value: '144' }, + { label: 'Упоминания насилия', value: '2207' }, + { label: 'Упоминания нездоровых отношений', value: '2892' }, + { label: 'Упоминания проституции', value: '2618' }, + { label: 'Упоминания пыток', value: '2410' }, + { label: 'Упоминания расизма', value: '2737' }, + { label: 'Упоминания религии', value: '1048' }, + { label: 'Упоминания самоубийства', value: '77' }, + { label: 'Упоминания секса', value: '2747' }, + { label: 'Упоминания селфхарма', value: '2105' }, + { label: 'Упоминания смертей', value: '2081' }, + { label: 'Упоминания смертей животных', value: '2211' }, + { label: 'Упоминания телесного хоррора', value: '2466' }, + { label: 'Упоминания терроризма', value: '774' }, + { label: 'Упоминания убийств', value: '1501' }, + { label: 'Управление оргазмом', value: '118' }, + { label: 'Управление стихиями', value: '2091' }, + { label: 'Уретральное проникновение', value: '1469' }, + { label: 'Уро-кинк', value: '762' }, + { label: 'Условное бессмертие', value: '2117' }, + { label: 'Уся / Сянься', value: '2714' }, + { label: 'Утопия', value: '350' }, + { label: 'Утраченная тройка', value: '2535' }, + { label: 'Утренний секс', value: '2882' }, + { label: 'Уход за персонажами с инвалидностью', value: '2133' }, + { label: 'Ухудшение отношений', value: '1219' }, + { label: 'Учебные заведения', value: '1694' }, + { label: 'Ученые', value: '402' }, + { label: 'Уют', value: '2738' }, + { label: 'Фавориты', value: '2135' }, + { label: 'Фамильяры', value: '1045' }, + { label: 'Фан-Бинго', value: '2957' }, + { label: 'Фан-Фанты', value: '2953' }, + { label: 'Фандомная аналитика', value: '742' }, + { label: 'Фандомная Битва', value: '253' }, + { label: 'Фансервис', value: '2483' }, + { label: 'Фантастика', value: '1670' }, + { label: 'Фастберн', value: '2016' }, + { label: 'Фастфуд / Закусочные', value: '1025' }, + { label: 'Феи', value: '191' }, + { label: 'Фельчинг', value: '2368' }, + { label: 'Фемдом', value: '54' }, + { label: 'Феминистские темы и мотивы', value: '523' }, + { label: 'Феминитивы', value: '390' }, + { label: 'Фениксы', value: '1215' }, + { label: 'Феодализм', value: '1504' }, + { label: 'Фермы / Ранчо', value: '329' }, + { label: 'Фестивали', value: '1006' }, + { label: 'Фехтование', value: '1775' }, + { label: 'Фиггинг', value: '1439' }, + { label: 'Фигурное катание', value: '460' }, + { label: 'Физиотерапия', value: '2222' }, + { label: 'Физическая сверхсила', value: '2880' }, + { label: 'Фикрайтеры / Фикридеры', value: '1284' }, + { label: 'Фиксированная раскладка', value: '1344' }, + { label: 'Фиктивные отношения', value: '308' }, + { label: 'Фиктивный брак', value: '34' }, + { label: 'Философия', value: '1675' }, + { label: 'Финляндия', value: '1133' }, + { label: 'Фистинг', value: '151' }, + { label: 'Флафф', value: '1667' }, + { label: 'Флирт', value: '349' }, + { label: 'Флот', value: '846' }, + { label: 'Фобии', value: '977' }, + { label: 'Фольклор и предания', value: '1273' }, + { label: 'Форнифилия', value: '580' }, + { label: 'Фотографы', value: '348' }, + { label: 'Фотокинез', value: '1805' }, + { label: 'Франция', value: '691' }, + { label: 'Фред Уизли жив', value: '2941' }, + { label: 'Фроттаж', value: '96' }, + { label: 'Фуд-плей', value: '509' }, + { label: 'Фуд-фетиш', value: '2168' }, + { label: 'Фурри', value: '996' }, + { label: 'Фут-фетиш', value: '432' }, + { label: 'Футбол', value: '458' }, + { label: 'Фэйри', value: '2028' }, + { label: 'Фэнтези', value: '1669' }, + { label: 'Хакеры', value: '340' }, + { label: 'Ханами', value: '1155' }, + { label: 'Ханахаки', value: '45' }, + { label: 'Ханжество', value: '882' }, + { label: 'Характерная для канона жестокость', value: '896' }, + { label: 'Харассмент', value: '477' }, + { label: 'Хиерофилия', value: '2366' }, + { label: 'Химеры', value: '816' }, + { label: 'Химическое оружие', value: '1186' }, + { label: 'Химэдэрэ / Оджидэрэ', value: '1363' }, + { label: 'Хип-хоп (субкультура)', value: '1822' }, + { label: 'Хиппи', value: '784' }, + { label: 'Хиромантия', value: '2749' }, + { label: 'Хирургическая коррекция пола', value: '1249' }, + { label: 'Хирургические операции', value: '1845' }, + { label: 'Хобби', value: '1161' }, + { label: 'Хогсмид', value: '2388' }, + { label: 'Хоккей', value: '828' }, + { label: 'Холодная война', value: '2140' }, + { label: 'Холодное оружие', value: '1187' }, + { label: 'Хорватия', value: '1433' }, + { label: 'Хороший плохой финал', value: '1266' }, + { label: 'Хронические заболевания', value: '1267' }, + { label: 'Хронокинез', value: '1447' }, + { label: 'Хронофантастика', value: '56' }, + { label: 'Хтонические существа', value: '504' }, + { label: 'Художники', value: '140' }, + { label: 'Хуманизация', value: '177' }, + { label: 'Хэллоуин', value: '205' }, + { label: 'Хюльдры', value: '1397' }, + { label: 'Цветочные магазины', value: '164' }, + { label: 'Целители', value: '1107' }, + { label: 'Церкви', value: '533' }, + { label: 'Цикличность', value: '1979' }, + { label: 'Цирки', value: '259' }, + { label: 'Цукими', value: '1798' }, + { label: 'Цундэрэ', value: '1137' }, + { label: 'Цыгане', value: '1091' }, + { label: 'Чайлдфри', value: '592' }, + { label: 'Чайные церемонии', value: '2416' }, + { label: 'Частичный ООС', value: '1409' }, + { label: 'Частные детективы', value: '427' }, + { label: 'Черная археология', value: '2503' }, + { label: 'Черная мораль', value: '2438' }, + { label: 'Чернобыльская катастрофа', value: '775' }, + { label: 'Черный юмор', value: '66' }, + { label: 'Чехия', value: '1021' }, + { label: 'Чиновники', value: '1991' }, + { label: 'Чирлидинг', value: '1431' }, + { label: 'Чистокровные AU (Гарри Поттер)', value: '1905' }, + { label: 'Чосон', value: '571' }, + { label: 'Чувство вины', value: '902' }, + { label: 'Чумные доктора', value: '1125' }, + { label: 'Чхусок', value: '2973' }, + { label: 'Шаманы', value: '484' }, + { label: 'Шантаж', value: '569' }, + { label: 'Шармбатон', value: '2733' }, + { label: 'Швейцария', value: '1022' }, + { label: 'Шейпшифтеры', value: '29' }, + { label: 'Шелки', value: '2431' }, + { label: 'Шифры', value: '2232' }, + { label: 'Школьная иерархия', value: '2787' }, + { label: 'Школьники', value: '2566' }, + { label: 'Школьные годы Тома Реддла', value: '1913' }, + { label: 'Школьные ОтМетки', value: '3017' }, + { label: 'Школьные поездки', value: '2481' }, + { label: 'Школьный роман', value: '2440' }, + { label: 'Шопинг', value: '864' }, + { label: 'Шоу-бизнес', value: '17' }, + { label: 'Шпионы', value: '148' }, + { label: 'Шрамирование', value: '891' }, + { label: 'Шрамы', value: '328' }, + { label: 'Эвтаназия', value: '1102' }, + { label: 'Эгоизм', value: '2071' }, + { label: 'Эдвардианская эпоха', value: '2975' }, + { label: 'Эзотерические темы и мотивы', value: '2801' }, + { label: 'Эйблизм', value: '453' }, + { label: 'Эйдетическая память', value: '975' }, + { label: 'Эйфория', value: '2458' }, + { label: 'Экзамены', value: '2591' }, + { label: 'Экзистенциальный кризис', value: '1200' }, + { label: 'Экзорцизм', value: '494' }, + { label: 'Экологи / Биологи', value: '631' }, + { label: 'Экологический кризис', value: '485' }, + { label: 'Экономический кризис', value: '1547' }, + { label: 'Эксаудиризм', value: '2151' }, + { label: 'Эксгибиционизм', value: '489' }, + { label: 'Эксперимент', value: '1686' }, + { label: 'Экстрасенсы', value: '371' }, + { label: 'Экшн', value: '1673' }, + { label: 'Электрокинез', value: '1857' }, + { label: 'Электростимуляция', value: '807' }, + { label: 'Элементы ангста', value: '2696' }, + { label: 'Элементы гета', value: '1714' }, + { label: 'Элементы дарка', value: '2703' }, + { label: 'Элементы детектива', value: '2698' }, + { label: 'Элементы драмы', value: '2697' }, + { label: 'Элементы других видов отношений', value: '454' }, + { label: 'Элементы мистики', value: '1324' }, + { label: 'Элементы психологии', value: '2824' }, + { label: 'Элементы романтики', value: '2742' }, + { label: 'Элементы слэша', value: '1715' }, + { label: 'Элементы ужасов', value: '2721' }, + { label: 'Элементы фемслэша', value: '1716' }, + { label: 'Элементы флаффа', value: '2731' }, + { label: 'Элементы юмора / Элементы стёба', value: '2011' }, + { label: 'Эльфы', value: '44' }, + { label: 'Эметофилия', value: '1434' }, + { label: 'Эмо', value: '641' }, + { label: 'Эмоциональная одержимость', value: '1270' }, + { label: 'Эмпатия', value: '1177' }, + { label: 'Энергетический вампиризм', value: '818' }, + { label: 'Эпидемии', value: '619' }, + { label: 'Эпизод 23.5', value: '2469' }, + { label: 'Эпилепсия', value: '1511' }, + { label: 'Эпилог? Какой эпилог?', value: '2651' }, + { label: 'Эпоха Бакумацу', value: '1472' }, + { label: 'Эпоха Мэйдзи', value: '1435' }, + { label: 'Эпоха Тайсё', value: '2015' }, + { label: 'Эрик Киллмонгер жив', value: '2897' }, + { label: 'Эрогуро', value: '1731' }, + { label: 'Эротическая дрессировка', value: '2026' }, + { label: 'Эротическая лактация', value: '284' }, + { label: 'Эротическая мумификация', value: '415' }, + { label: 'Эротическая сверхстимуляция', value: '1245' }, + { label: 'Эротические наказания', value: '125' }, + { label: 'Эротические ролевые игры', value: '111' }, + { label: 'Эротические сны', value: '128' }, + { label: 'Эротические фантазии', value: '561' }, + { label: 'Эротический массаж', value: '1518' }, + { label: 'Эротический перенос', value: '744' }, + { label: 'Эротомания', value: '2922' }, + { label: 'Эскапизм', value: '1552' }, + { label: 'Эскорты', value: '922' }, + { label: 'Эстетика', value: '2021' }, + { label: 'ЭТЕРНА', value: '2948' }, + { label: 'Этнические анклавы', value: '2732' }, + { label: 'Этническое фэнтези', value: '667' }, + { label: 'Эффект бабочки', value: '1572' }, + { label: 'Ювелиры', value: '2964' }, + { label: 'Южная Америка', value: '832' }, + { label: 'Южная готика', value: '1077' }, + { label: 'Южная Корея', value: '674' }, + { label: 'Юмор', value: '1666' }, + { label: 'Юристы', value: '333' }, + { label: 'Я никогда не... (игра)', value: '1436' }, + { label: 'Явное согласие', value: '1569' }, + { label: 'Ядерная война', value: '889' }, + { label: 'Язык жестов', value: '2962' }, + { label: 'Язык животных', value: '1327' }, + { label: 'Язык растений', value: '2622' }, + { label: 'Язык цветов', value: '272' }, + { label: 'Ямадад', value: '2916' }, + { label: 'Янгирэ', value: '1257' }, + { label: 'Яндэрэ', value: '1136' }, + { label: 'Яогуаи', value: '2347' }, + { label: 'Япония', value: '665' }, + { label: 'Японские фестивали', value: '2555' }, + { label: 'Ящеролюди', value: '682' }, + { label: 'Abuse/Comfort', value: '2599' }, + { label: 'Aged down', value: '229' }, + { label: 'Aged up', value: '4' }, + { label: 'Artfic', value: '68' }, + { label: 'AU', value: '1683' }, + { label: 'AU: Альтернативные способности', value: '2086' }, + { label: 'AU: Без апокалипсиса', value: '2051' }, + { label: 'AU: Без войны', value: '2305' }, + { label: 'AU: Без магии', value: '1052' }, + { label: 'AU: Без мистики', value: '2489' }, + { label: 'AU: Без сверхспособностей', value: '1053' }, + { label: 'AU: Без супергероев', value: '1152' }, + { label: 'AU: Все гражданские', value: '2797' }, + { label: 'AU: Все друзья', value: '2552' }, + { label: 'AU: Все люди', value: '1151' }, + { label: 'AU: Все хорошо', value: '2124' }, + { label: 'AU: Другая страна', value: '2165' }, + { label: 'AU: Другая эпоха', value: '2452' }, + { label: 'AU: Другое детство', value: '2360' }, + { label: 'AU: Другое знакомство', value: '2217' }, + { label: 'AU: Другое семейное положение', value: '2902' }, + { label: 'AU: Не родственники', value: '2167' }, + { label: 'AU: Профессиональные герои (BNHA)', value: '2924' }, + { label: 'AU: Родственники', value: '2164' }, + { label: 'AU: Школа', value: '2532' }, + { label: 'AU: Age swap', value: '917' }, + { label: 'AU: Kitty Gang', value: '2928' }, + { label: 'AU: Race swap', value: '933' }, + { label: 'AU: Reverse', value: '1156' }, + { label: 'AU: Role swap', value: '1054' }, + { label: 'Authors Time', value: '2959' }, + { label: 'BDSM', value: '1646' }, + { label: 'BDSM-вселенная', value: '1078' }, + { label: 'BDSM: Домспейс', value: '1444' }, + { label: 'BDSM: Дроп', value: '1411' }, + { label: 'BDSM: Сабспейс', value: '1410' }, + { label: 'BDSM: Aftercare', value: '1523' }, + { label: 'BEAST (Bungou Stray Dogs)', value: '2457' }, + { label: 'Blackwatch', value: '2116' }, + { label: 'Body inflation', value: '389' }, + { label: 'Character study', value: '363' }, + { label: 'Cockwarming', value: '318' }, + { label: 'Dark academia', value: '2430' }, + { label: 'DDLG / DDLB / MDLG / MDLB', value: '1033' }, + { label: 'Dirty talk', value: '139' }, + { label: 'Doggy style', value: '1361' }, + { label: 'Dollification', value: '2780' }, + { label: 'Dramione Authors Tournament', value: '2950' }, + { label: 'EIQ', value: '271' }, + { label: 'ER', value: '1687' }, + { label: 'Final Haikyuu Quest', value: '2652' }, + { label: 'Fix-it', value: '1' }, + { label: 'Frenemies', value: '666' }, + { label: 'Gangbang', value: '315' }, + { label: 'Glory hole', value: '2188' }, + { label: 'Golden Age Overwatch', value: '2112' }, + { label: 'GOLockdown', value: '2391' }, + { label: 'Gun play', value: '252' }, + { label: 'Hurt/Comfort', value: '1682' }, + { label: 'Hydra!AU', value: '1931' }, + { label: 'Ironqrow week', value: '2209' }, + { label: 'Kimetsu Gakuen', value: '2574' }, + { label: 'Knife play', value: '152' }, + { label: 'KPOP-AUFEST', value: '1851' }, + { label: 'LARP', value: '1510' }, + { label: 'LOVE ОтМетки', value: '2981' }, + { label: 'Maleval week', value: '2382' }, + { label: 'Marvel ГФИ', value: '2896' }, + { label: 'Mate or die', value: '375' }, + { label: 'Mirror!verse (Звездный путь)', value: '2899' }, + { label: 'Moresome', value: '759' }, + { label: 'N раз', value: '263' }, + { label: 'Omnic Crisis Era', value: '2111' }, + { label: "ORIGINAL'S 2021", value: '2939' }, + { label: 'Parentlock', value: '2053' }, + { label: 'Patronus of Dramione', value: '2958' }, + { label: 'Post-Fall of Overwatch', value: '2114' }, + { label: 'Post-Recall', value: '2115' }, + { label: 'PWP', value: '1680' }, + { label: 'Rape/Revenge', value: '2195' }, + { label: 'Reverse Falls', value: '1879' }, + { label: 'RofR ДраМиВояж', value: '2954' }, + { label: 'RST', value: '815' }, + { label: 'RTFM', value: '2327' }, + { label: 'Service top / Power bottom', value: '563' }, + { label: 'Shingeki no Kyojin High School AU', value: '2037' }, + { label: 'Shingeki! Kyojin Chuugakkou', value: '2038' }, + { label: 'Sickfic', value: '422' }, + { label: 'Songfic', value: '1684' }, + { label: 'Spider-son & Iron-dad', value: '1885' }, + { label: 'Sugar daddy', value: '106' }, + { label: 'Sugar mommy', value: '160' }, + { label: 'Superfamily (Мстители)', value: '2623' }, + { label: 'Superior Iron Man', value: '2676' }, + { label: 'Talon (Overwatch)', value: '2110' }, + { label: 'The Big Four / The Super Six', value: '1954' }, + { label: 'Trench AU', value: '2387' }, + { label: 'TYL', value: '1933' }, + { label: 'Underage', value: '1658' }, + { label: 'URT', value: '2761' }, + { label: 'UST', value: '1709' }, + { label: 'Versefic', value: '2729' }, + { label: 'Videofic', value: '2480' }, + { label: 'Womance', value: '265' }, + { label: 'X век', value: '240' }, + { label: 'XI век', value: '241' }, + { label: 'XII век', value: '242' }, + { label: 'XIII век', value: '243' }, + { label: 'XIV век', value: '244' }, + { label: 'XIX век', value: '249' }, + { label: 'XV век', value: '245' }, + { label: 'XVI век', value: '246' }, + { label: 'XVII век', value: '247' }, + { label: 'XVIII век', value: '248' }, + { label: 'XX век', value: '1004' }, ], type: FilterTypes.Picker, }, diff --git a/plugins/russian/jaomix.ts b/plugins/russian/jaomix.ts index bef5d04..696bdd3 100644 --- a/plugins/russian/jaomix.ts +++ b/plugins/russian/jaomix.ts @@ -1,16 +1,16 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; class Jaomix implements Plugin.PluginBase { - id = "jaomix.ru"; - name = "Jaomix"; - site = "https://jaomix.ru"; - version = "1.0.0"; - icon = "src/ru/jaomix/icon.png"; + id = 'jaomix.ru'; + name = 'Jaomix'; + site = 'https://jaomix.ru'; + version = '1.0.0'; + icon = 'src/ru/jaomix/icon.png'; async popularNovels( pageNo: number, @@ -19,109 +19,116 @@ class Jaomix implements Plugin.PluginBase { filters, }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/?searchrn"; + let url = this.site + '/?searchrn'; if (filters?.lang?.value?.length) { url += filters.lang.value .map((lang, idx) => `&lang[${idx}]=${lang}`) - .join(""); + .join(''); } if (filters?.genre?.value?.include?.length) { url += filters.genre.value.include .map((genre, idx) => `&genre[${idx}]=${genre}`) - .join(""); + .join(''); } if (filters?.genre?.value?.exclude?.length) { url += filters.genre.value.exclude .map((genre, idx) => `&delgenre[${idx}]=del ${genre}`) - .join(""); + .join(''); } - url += "&sortcountchapt=" + (filters?.sortcountchapt?.value || "1"); - url += "&sortdaycreate=" + (filters?.sortdaycreate?.value || "1"); - url += "&sortby=" + (showLatestNovels ? "upd" : filters?.sortby?.value || "topweek"); - url += "&gpage=" + pageNo; + url += '&sortcountchapt=' + (filters?.sortcountchapt?.value || '1'); + url += '&sortdaycreate=' + (filters?.sortdaycreate?.value || '1'); + url += + '&sortby=' + + (showLatestNovels ? 'upd' : filters?.sortby?.value || 'topweek'); + url += '&gpage=' + pageNo; - const body = await fetchApi(url).then((res) => res.text()); + const body = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio('div[class="block-home"] > div[class="one"]') - .each((index, element) => { + loadedCheerio('div[class="block-home"] > div[class="one"]').each( + (index, element) => { const name = loadedCheerio(element) .find('div[class="img-home"] > a') - .attr("title"); + .attr('title'); const cover = loadedCheerio(element) .find('div[class="img-home"] > a > img') - .attr("src") - ?.replace("-150x150", ""); + .attr('src') + ?.replace('-150x150', ''); const url = loadedCheerio(element) .find('div[class="img-home"] > a') - .attr("href"); + .attr('href'); if (!name || !url) return; - novels.push({ name, cover, path: url.replace(this.site, "") }); - }); + novels.push({ name, cover, path: url.replace(this.site, '') }); + }, + ); return novels; } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const body = await fetchApi(this.site + novelPath).then(res => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, name: loadedCheerio('div[class="desc-book"] > h1').text().trim(), - cover: loadedCheerio('div[class="img-book"] > img').attr("src"), + cover: loadedCheerio('div[class="img-book"] > img').attr('src'), summary: loadedCheerio('div[id="desc-tab"]').text().trim(), }; - loadedCheerio("#info-book > p").each(function () { - let text = loadedCheerio(this).text().replace(/,/g, "").split(" "); - if (text[0] === "Автор:") { - novel.author = text.splice(1).join(" "); - } else if (text[0] === "Жанры:") { - novel.genres = text.splice(1).join(","); - } else if (text[0] === "Статус:") { - novel.status = text.includes("продолжается") + loadedCheerio('#info-book > p').each(function () { + let text = loadedCheerio(this).text().replace(/,/g, '').split(' '); + if (text[0] === 'Автор:') { + novel.author = text.splice(1).join(' '); + } else if (text[0] === 'Жанры:') { + novel.genres = text.splice(1).join(','); + } else if (text[0] === 'Статус:') { + novel.status = text.includes('продолжается') ? NovelStatus.Ongoing : NovelStatus.Completed; } }); const chapters: Plugin.ChapterItem[] = []; - const totalChapters = loadedCheerio(".download-chapter div.title").length; + const totalChapters = loadedCheerio('.download-chapter div.title').length; - loadedCheerio(".download-chapter div.title").each((chapterIndex, element) => { - const name = loadedCheerio(element).find("a").attr("title"); - const url = loadedCheerio(element).find("a").attr("href"); - if (!name || !url) return; + loadedCheerio('.download-chapter div.title').each( + (chapterIndex, element) => { + const name = loadedCheerio(element).find('a').attr('title'); + const url = loadedCheerio(element).find('a').attr('href'); + if (!name || !url) return; - const releaseDate = loadedCheerio(element).find("time").text(); - chapters.push({ - name, - path: url.replace(this.site, ""), - releaseTime: this.parseDate(releaseDate), - chapterNumber: totalChapters - chapterIndex, - }); - }); + const releaseDate = loadedCheerio(element).find('time').text(); + chapters.push({ + name, + path: url.replace(this.site, ''), + releaseTime: this.parseDate(releaseDate), + chapterNumber: totalChapters - chapterIndex, + }); + }, + ); novel.chapters = chapters.reverse(); return novel; } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); loadedCheerio('div[class="adblock-service"]').remove(); const chapterText = loadedCheerio('div[class="entry-content"]').html(); - return chapterText || ""; + return chapterText || ''; } async searchNovels( @@ -129,34 +136,38 @@ class Jaomix implements Plugin.PluginBase { pageNo: number | undefined = 1, ): Promise { const url = - this.site + "/?searchrn=" + searchTerm + - "&but=Поиск по названию&sortby=upd&gpage=" + pageNo; - const body = await fetchApi(url).then((res) => res.text()); + this.site + + '/?searchrn=' + + searchTerm + + '&but=Поиск по названию&sortby=upd&gpage=' + + pageNo; + const body = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - loadedCheerio('div[class="block-home"] > div[class="one"]') - .each((index, element) => { + loadedCheerio('div[class="block-home"] > div[class="one"]').each( + (index, element) => { const name = loadedCheerio(element) .find('div[class="img-home"] > a') - .attr("title"); + .attr('title'); const cover = loadedCheerio(element) .find('div[class="img-home"] > a > img') - .attr("src") - ?.replace("-150x150", ""); + .attr('src') + ?.replace('-150x150', ''); const url = loadedCheerio(element) .find('div[class="img-home"] > a') - .attr("href"); + .attr('href'); if (!name || !url) return; - novels.push({ name, cover, path: url.replace(this.site, "") }); - }); + novels.push({ name, cover, path: url.replace(this.site, '') }); + }, + ); return novels; } - parseDate = (dateString: string | undefined = "") => { + parseDate = (dateString: string | undefined = '') => { const months: { [key: string]: number } = { Янв: 1, Фев: 2, @@ -172,9 +183,11 @@ class Jaomix implements Plugin.PluginBase { Дек: 12, }; - const [time, day, month, year] = dateString.split(" "); + const [time, day, month, year] = dateString.split(' '); if (time && day && months[month] && year) { - return dayjs(year + "-" + months[month] + "-" + day + " " + time).format("LLL"); + return dayjs(year + '-' + months[month] + '-' + day + ' ' + time).format( + 'LLL', + ); } return dateString || null; @@ -184,110 +197,110 @@ class Jaomix implements Plugin.PluginBase { filters = { sortby: { - label: "Сортировка:", - value: "topweek", + label: 'Сортировка:', + value: 'topweek', options: [ - { label: "Топ недели", value: "topweek" }, - { label: "По алфавиту", value: "alphabet" }, - { label: "По дате обновления", value: "upd" }, - { label: "По дате создания", value: "new" }, - { label: "По просмотрам", value: "count" }, - { label: "Топ года", value: "topyear" }, - { label: "Топ дня", value: "topday" }, - { label: "Топ за все время", value: "alltime" }, - { label: "Топ месяца", value: "topmonth" }, + { label: 'Топ недели', value: 'topweek' }, + { label: 'По алфавиту', value: 'alphabet' }, + { label: 'По дате обновления', value: 'upd' }, + { label: 'По дате создания', value: 'new' }, + { label: 'По просмотрам', value: 'count' }, + { label: 'Топ года', value: 'topyear' }, + { label: 'Топ дня', value: 'topday' }, + { label: 'Топ за все время', value: 'alltime' }, + { label: 'Топ месяца', value: 'topmonth' }, ], type: FilterTypes.Picker, }, sortdaycreate: { - label: "Дата добавления:", - value: "1", + label: 'Дата добавления:', + value: '1', options: [ - { label: "Любое", value: "1" }, - { label: "От 120 до 180 дней", value: "1218" }, - { label: "От 180 до 365 дней", value: "1836" }, - { label: "От 30 до 60 дней", value: "3060" }, - { label: "От 365 дней", value: "365" }, - { label: "От 60 до 90 дней", value: "6090" }, - { label: "От 90 до 120 дней", value: "9012" }, - { label: "Послед. 30 дней", value: "30" }, + { label: 'Любое', value: '1' }, + { label: 'От 120 до 180 дней', value: '1218' }, + { label: 'От 180 до 365 дней', value: '1836' }, + { label: 'От 30 до 60 дней', value: '3060' }, + { label: 'От 365 дней', value: '365' }, + { label: 'От 60 до 90 дней', value: '6090' }, + { label: 'От 90 до 120 дней', value: '9012' }, + { label: 'Послед. 30 дней', value: '30' }, ], type: FilterTypes.Picker, }, sortcountchapt: { - label: "Количество глав:", - value: "1", + label: 'Количество глав:', + value: '1', options: [ - { label: "Любое кол-во глав", value: "1" }, - { label: "До 500", value: "500" }, - { label: "От 1000 до 2000", value: "1020" }, - { label: "От 2000 до 3000", value: "2030" }, - { label: "От 3000 до 4000", value: "3040" }, - { label: "От 4000", value: "400" }, - { label: "От 500 до 1000", value: "510" }, + { label: 'Любое кол-во глав', value: '1' }, + { label: 'До 500', value: '500' }, + { label: 'От 1000 до 2000', value: '1020' }, + { label: 'От 2000 до 3000', value: '2030' }, + { label: 'От 3000 до 4000', value: '3040' }, + { label: 'От 4000', value: '400' }, + { label: 'От 500 до 1000', value: '510' }, ], type: FilterTypes.Picker, }, genre: { - label: "Жанры:", + label: 'Жанры:', value: { include: [], exclude: [] }, options: [ - { label: "Боевые Искусства", value: "Боевые Искусства" }, - { label: "Виртуальный Мир", value: "Виртуальный Мир" }, - { label: "Гарем", value: "Гарем" }, - { label: "Детектив", value: "Детектив" }, - { label: "Драма", value: "Драма" }, - { label: "Игра", value: "Игра" }, - { label: "Истории из жизни", value: "Истории из жизни" }, - { label: "Исторический", value: "Исторический" }, - { label: "История", value: "История" }, - { label: "Исэкай", value: "Исэкай" }, - { label: "Комедия", value: "Комедия" }, - { label: "Меха", value: "Меха" }, - { label: "Мистика", value: "Мистика" }, - { label: "Научная Фантастика", value: "Научная Фантастика" }, - { label: "Повседневность", value: "Повседневность" }, - { label: "Постапокалипсис", value: "Постапокалипсис" }, - { label: "Приключения", value: "Приключения" }, - { label: "Психология", value: "Психология" }, - { label: "Романтика", value: "Романтика" }, - { label: "Сверхъестественное", value: "Сверхъестественное" }, - { label: "Сёнэн", value: "Сёнэн" }, - { label: "Сёнэн-ай", value: "Сёнэн-ай" }, - { label: "Спорт", value: "Спорт" }, - { label: "Сэйнэн", value: "Сэйнэн" }, - { label: "Сюаньхуа", value: "Сюаньхуа" }, - { label: "Трагедия", value: "Трагедия" }, - { label: "Триллер", value: "Триллер" }, - { label: "Фантастика", value: "Фантастика" }, - { label: "Фэнтези", value: "Фэнтези" }, - { label: "Хоррор", value: "Хоррор" }, - { label: "Школьная жизнь", value: "Школьная жизнь" }, - { label: "Шоунен", value: "Шоунен" }, - { label: "Экшн", value: "Экшн" }, - { label: "Этти", value: "Этти" }, - { label: "Юри", value: "Юри" }, - { label: "Adult", value: "Adult" }, - { label: "Ecchi", value: "Ecchi" }, - { label: "Josei", value: "Josei" }, - { label: "Lolicon", value: "Lolicon" }, - { label: "Mature", value: "Mature" }, - { label: "Shoujo", value: "Shoujo" }, - { label: "Wuxia", value: "Wuxia" }, - { label: "Xianxia", value: "Xianxia" }, - { label: "Xuanhuan", value: "Xuanhuan" }, - { label: "Yaoi", value: "Yaoi" }, + { label: 'Боевые Искусства', value: 'Боевые Искусства' }, + { label: 'Виртуальный Мир', value: 'Виртуальный Мир' }, + { label: 'Гарем', value: 'Гарем' }, + { label: 'Детектив', value: 'Детектив' }, + { label: 'Драма', value: 'Драма' }, + { label: 'Игра', value: 'Игра' }, + { label: 'Истории из жизни', value: 'Истории из жизни' }, + { label: 'Исторический', value: 'Исторический' }, + { label: 'История', value: 'История' }, + { label: 'Исэкай', value: 'Исэкай' }, + { label: 'Комедия', value: 'Комедия' }, + { label: 'Меха', value: 'Меха' }, + { label: 'Мистика', value: 'Мистика' }, + { label: 'Научная Фантастика', value: 'Научная Фантастика' }, + { label: 'Повседневность', value: 'Повседневность' }, + { label: 'Постапокалипсис', value: 'Постапокалипсис' }, + { label: 'Приключения', value: 'Приключения' }, + { label: 'Психология', value: 'Психология' }, + { label: 'Романтика', value: 'Романтика' }, + { label: 'Сверхъестественное', value: 'Сверхъестественное' }, + { label: 'Сёнэн', value: 'Сёнэн' }, + { label: 'Сёнэн-ай', value: 'Сёнэн-ай' }, + { label: 'Спорт', value: 'Спорт' }, + { label: 'Сэйнэн', value: 'Сэйнэн' }, + { label: 'Сюаньхуа', value: 'Сюаньхуа' }, + { label: 'Трагедия', value: 'Трагедия' }, + { label: 'Триллер', value: 'Триллер' }, + { label: 'Фантастика', value: 'Фантастика' }, + { label: 'Фэнтези', value: 'Фэнтези' }, + { label: 'Хоррор', value: 'Хоррор' }, + { label: 'Школьная жизнь', value: 'Школьная жизнь' }, + { label: 'Шоунен', value: 'Шоунен' }, + { label: 'Экшн', value: 'Экшн' }, + { label: 'Этти', value: 'Этти' }, + { label: 'Юри', value: 'Юри' }, + { label: 'Adult', value: 'Adult' }, + { label: 'Ecchi', value: 'Ecchi' }, + { label: 'Josei', value: 'Josei' }, + { label: 'Lolicon', value: 'Lolicon' }, + { label: 'Mature', value: 'Mature' }, + { label: 'Shoujo', value: 'Shoujo' }, + { label: 'Wuxia', value: 'Wuxia' }, + { label: 'Xianxia', value: 'Xianxia' }, + { label: 'Xuanhuan', value: 'Xuanhuan' }, + { label: 'Yaoi', value: 'Yaoi' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, lang: { - label: "Выбрать языки:", + label: 'Выбрать языки:', value: [], options: [ - { label: "Английский", value: "Английский" }, - { label: "Китайский", value: "Китайский" }, - { label: "Корейский", value: "Корейский" }, - { label: "Японский", value: "Японский" }, + { label: 'Английский', value: 'Английский' }, + { label: 'Китайский', value: 'Китайский' }, + { label: 'Корейский', value: 'Корейский' }, + { label: 'Японский', value: 'Японский' }, ], type: FilterTypes.CheckboxGroup, }, diff --git a/plugins/russian/neobook.ts b/plugins/russian/neobook.ts index ea7556e..e8211f7 100644 --- a/plugins/russian/neobook.ts +++ b/plugins/russian/neobook.ts @@ -1,61 +1,64 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { defaultCover } from "@libs/defaultCover"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; const statusKey: { [key: string]: string } = { - "0": NovelStatus.Unknown, - "1": NovelStatus.Ongoing, - "2": NovelStatus.Completed, - "3": NovelStatus.OnHiatus, - "4": NovelStatus.Cancelled, + '0': NovelStatus.Unknown, + '1': NovelStatus.Ongoing, + '2': NovelStatus.Completed, + '3': NovelStatus.OnHiatus, + '4': NovelStatus.Cancelled, }; class Neobook implements Plugin.PluginBase { - id = "neobook"; - name = "Neobook"; - site = "https://neobook.org"; - version = "1.0.0"; - icon = "src/ru/neobook/icon.png"; + id = 'neobook'; + name = 'Neobook'; + site = 'https://neobook.org'; + version = '1.0.0'; + icon = 'src/ru/neobook/icon.png'; async fetchNovels( page: number, - { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, searchTerm?: string, ): Promise { const formData = new FormData(); - formData.append("version", "4.0"); - formData.append("uid", "0"); - formData.append("utoken", ""); - formData.append("resource", "general"); - formData.append("action", "get_bundle"); - formData.append("bundle", "bundle_books"); - formData.append("target", "feed"); - formData.append("page", page.toString()); - formData.append("filter_category_id", filters?.category?.value || "0"); - formData.append("filter_completed", "-1"); - formData.append("filter_search", searchTerm || ""); - formData.append("filter_tags", filters?.tags?.value || ""); + formData.append('version', '4.0'); + formData.append('uid', '0'); + formData.append('utoken', ''); + formData.append('resource', 'general'); + formData.append('action', 'get_bundle'); + formData.append('bundle', 'bundle_books'); + formData.append('target', 'feed'); + formData.append('page', page.toString()); + formData.append('filter_category_id', filters?.category?.value || '0'); + formData.append('filter_completed', '-1'); + formData.append('filter_search', searchTerm || ''); + formData.append('filter_tags', filters?.tags?.value || ''); formData.append( - "filter_sort", - showLatestNovels ? "new" : filters?.sort?.value || "popular", + 'filter_sort', + showLatestNovels ? 'new' : filters?.sort?.value || 'popular', ); - formData.append("filter_timeread", filters?.timeread?.value || "0-999999"); + formData.append('filter_timeread', filters?.timeread?.value || '0-999999'); - const result = await fetchApi("https://api.neobook.org/", { - method: "post", + const result = await fetchApi('https://api.neobook.org/', { + method: 'post', body: formData, }); const json = (await result.json()) as { bundle_books: BundleBooks }; const novels: Plugin.NovelItem[] = []; if (json.bundle_books.feed?.length) { - json.bundle_books.feed?.forEach((novel) => + json.bundle_books.feed?.forEach(novel => novels.push({ name: novel.title, cover: novel?.attachment?.image?.m || defaultCover, - path: novel.token + "/", + path: novel.token + '/', }), ); } @@ -77,12 +80,13 @@ class Neobook implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.resolveUrl(novelPath, true)) - .then((res) => res.text()); + const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); const novel: Plugin.SourceNovel = { path: novelPath, - name: "", + name: '', cover: defaultCover, }; @@ -91,22 +95,22 @@ class Neobook implements Plugin.PluginBase { const book = JSON.parse(bookRaw[1]) as Novels; novel.name = book.title; - novel.summary = book.text?.replace?.(/
/g, "\n") || book.text_fix; + novel.summary = book.text?.replace?.(/
/g, '\n') || book.text_fix; novel.author = book.user && book.user.firstname && book.user.lastname - ? book.user.firstname + " " + book.user.lastname - : book.user?.initials || ""; - novel.status = statusKey[book.status || "0"] || NovelStatus.Unknown; + ? book.user.firstname + ' ' + book.user.lastname + : book.user?.initials || ''; + novel.status = statusKey[book.status || '0'] || NovelStatus.Unknown; if (book.attachment?.image?.m) novel.cover = book.attachment.image.m; - if (book.tags?.length) novel.genres = book.tags.join(","); + if (book.tags?.length) novel.genres = book.tags.join(','); const chapters: Plugin.ChapterItem[] = []; book.chapters?.forEach((chapter, chapterIndex) => { - if (chapter.access == "1" && chapter.status == "1") { + if (chapter.access == '1' && chapter.status == '1') { chapters.push({ - name: chapter.title || "Глава " + (chapterIndex + 1), + name: chapter.title || 'Глава ' + (chapterIndex + 1), path: `?book=${book.token}&chapter=${chapter.token}`, releaseTime: null, chapterNumber: Number(chapter.sort) || chapterIndex + 1, @@ -120,19 +124,18 @@ class Neobook implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.resolveUrl(chapterPath)) - .then((res) => res.text()); + const body = await fetchApi(this.resolveUrl(chapterPath)).then(res => + res.text(), + ); const bookRaw = body.match(/var data = ({.*?});/); - let chapterText = ""; + let chapterText = ''; if (bookRaw instanceof Array && bookRaw.length >= 2) { const book = JSON.parse(bookRaw[1]) as Novels; - const token = chapterPath.split("=")[2]; - const chapter = book.chapters?.find?.( - (chapter) => chapter.token == token, - ); - chapterText = (chapter?.data?.html || "").replace(/
/g, ""); + const token = chapterPath.split('=')[2]; + const chapter = book.chapters?.find?.(chapter => chapter.token == token); + chapterText = (chapter?.data?.html || '').replace(/
/g, ''); } return chapterText; @@ -140,67 +143,67 @@ class Neobook implements Plugin.PluginBase { fetchImage = fetchFile; resolveUrl = (path: string, isNovel?: boolean) => - this.site + (isNovel ? "/book/" : "/reader/") + path; + this.site + (isNovel ? '/book/' : '/reader/') + path; filters = { sort: { - label: "Сортировка:", - value: "popular", + label: 'Сортировка:', + value: 'popular', options: [ - { label: "Сначала популярные", value: "popular" }, - { label: "Сначала новые", value: "new" }, - { label: "В случайном порядке", value: "rand" }, + { label: 'Сначала популярные', value: 'popular' }, + { label: 'Сначала новые', value: 'new' }, + { label: 'В случайном порядке', value: 'rand' }, ], type: FilterTypes.Picker, }, timeread: { - label: "Время прочтения:", - value: "0-999999", + label: 'Время прочтения:', + value: '0-999999', options: [ - { label: "Все", value: "0-999999" }, - { label: "Менее 15 минут", value: "0-900" }, - { label: "15-30 минут", value: "900-1800" }, - { label: "30-60 минут", value: "1800-3600" }, - { label: "1-2 часа", value: "3600-7200" }, - { label: "Более 2 часов", value: "7200-999999" }, + { label: 'Все', value: '0-999999' }, + { label: 'Менее 15 минут', value: '0-900' }, + { label: '15-30 минут', value: '900-1800' }, + { label: '30-60 минут', value: '1800-3600' }, + { label: '1-2 часа', value: '3600-7200' }, + { label: 'Более 2 часов', value: '7200-999999' }, ], type: FilterTypes.Picker, }, category: { - label: "Жанр:", - value: "", + label: 'Жанр:', + value: '', options: [ - { label: "Все", value: "" }, - { label: "Антиутопия", value: "10" }, - { label: "Детектив", value: "13" }, - { label: "Детские книги", value: "14" }, - { label: "Драма", value: "15" }, - { label: "Другое", value: "16" }, - { label: "История", value: "18" }, - { label: "Мелодрама", value: "21" }, - { label: "Мистика", value: "22" }, - { label: "Научная фантастика", value: "23" }, - { label: "Нон-фикшн", value: "35" }, - { label: "Подростки и молодежь", value: "26" }, - { label: "Постапокалипсис", value: "27" }, - { label: "Поэзия", value: "28" }, - { label: "Приключения", value: "29" }, - { label: "Рассказ", value: "34" }, - { label: "Роман", value: "36" }, - { label: "Творчество", value: "40" }, - { label: "Триллер", value: "91" }, - { label: "Ужасы", value: "90" }, - { label: "Фантастика", value: "41" }, - { label: "Фанфик", value: "42" }, - { label: "Фэнтези", value: "44" }, - { label: "Эротика", value: "46" }, - { label: "Юмор", value: "47" }, + { label: 'Все', value: '' }, + { label: 'Антиутопия', value: '10' }, + { label: 'Детектив', value: '13' }, + { label: 'Детские книги', value: '14' }, + { label: 'Драма', value: '15' }, + { label: 'Другое', value: '16' }, + { label: 'История', value: '18' }, + { label: 'Мелодрама', value: '21' }, + { label: 'Мистика', value: '22' }, + { label: 'Научная фантастика', value: '23' }, + { label: 'Нон-фикшн', value: '35' }, + { label: 'Подростки и молодежь', value: '26' }, + { label: 'Постапокалипсис', value: '27' }, + { label: 'Поэзия', value: '28' }, + { label: 'Приключения', value: '29' }, + { label: 'Рассказ', value: '34' }, + { label: 'Роман', value: '36' }, + { label: 'Творчество', value: '40' }, + { label: 'Триллер', value: '91' }, + { label: 'Ужасы', value: '90' }, + { label: 'Фантастика', value: '41' }, + { label: 'Фанфик', value: '42' }, + { label: 'Фэнтези', value: '44' }, + { label: 'Эротика', value: '46' }, + { label: 'Юмор', value: '47' }, ], type: FilterTypes.Picker, }, tags: { - label: "Тэги:", - value: "", + label: 'Тэги:', + value: '', type: FilterTypes.TextInput, }, } satisfies Filters; @@ -401,7 +404,7 @@ interface Price { deadline: string; } -type Currency = "NEO"; +type Currency = 'NEO'; interface Rating { value: string; diff --git a/plugins/russian/novelOvh.ts b/plugins/russian/novelOvh.ts index dc73269..d3944e7 100644 --- a/plugins/russian/novelOvh.ts +++ b/plugins/russian/novelOvh.ts @@ -1,29 +1,33 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; class novelOvh implements Plugin.PluginBase { - id = "novelovh"; - name = "НовелОВХ"; - site = "https://novel.ovh"; - version = "1.0.0"; - icon = "src/ru/novelovh/icon.png"; + id = 'novelovh'; + name = 'НовелОВХ'; + site = 'https://novel.ovh'; + version = '1.0.0'; + icon = 'src/ru/novelovh/icon.png'; async popularNovels( pageNo: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/novel?page=" + (pageNo - 1); - url += "&sort=" + - (showLatestNovels ? "updatedAt" : filters?.sort?.value || "averageRating") + ",desc"; + let url = this.site + '/novel?page=' + (pageNo - 1); + url += + '&sort=' + + (showLatestNovels + ? 'updatedAt' + : filters?.sort?.value || 'averageRating') + + ',desc'; - const result = await fetchApi(url + "&_data=routes/reader/book/index"); + const result = await fetchApi(url + '&_data=routes/reader/book/index'); const body = (await result.json()) as { books: BooksEntity[] }; const novels: Plugin.NovelItem[] = []; - body.books.forEach((novel) => + body.books.forEach(novel => novels.push({ name: novel.name.ru, cover: novel.poster, @@ -36,7 +40,8 @@ class novelOvh implements Plugin.PluginBase { async parseNovel(novelPath: string): Promise { const result = await fetchApi( - this.resolveUrl(novelPath, true) + "?_data=routes/reader/book/$slug/index", + this.resolveUrl(novelPath, true) + + '?_data=routes/reader/book/$slug/index', ); const json = (await result.json()) as responseNovel; @@ -44,19 +49,19 @@ class novelOvh implements Plugin.PluginBase { path: novelPath, name: json.book.name.ru, cover: json.book.poster, - genres: json.book.labels?.map?.((label) => label.name).join(","), + genres: json.book.labels?.map?.(label => label.name).join(','), summary: json.book.description, status: - json.book.status == "ONGOING" + json.book.status == 'ONGOING' ? NovelStatus.Ongoing : NovelStatus.Completed, }; - json.book.relations?.forEach((person) => { + json.book.relations?.forEach(person => { switch (person.type) { - case "AUTHOR": + case 'AUTHOR': novel.author = person.publisher.name; - case "ARTIST": + case 'ARTIST': novel.artist = person.publisher.name; } }); @@ -67,11 +72,14 @@ class novelOvh implements Plugin.PluginBase { chapters.push({ name: chapter.title || - "Том " + (chapter.volume || 0) + " " + - (chapter.name || "Глава " + - (chapter.number || json.chapters.length - chapterIndex)), - path: novelPath + "/" + chapter.id + "/0", - releaseTime: dayjs(chapter.createdAt).format("LLL"), + 'Том ' + + (chapter.volume || 0) + + ' ' + + (chapter.name || + 'Глава ' + + (chapter.number || json.chapters.length - chapterIndex)), + path: novelPath + '/' + chapter.id + '/0', + releaseTime: dayjs(chapter.createdAt).format('LLL'), chapterNumber: json.chapters.length - chapterIndex, }), ); @@ -82,7 +90,7 @@ class novelOvh implements Plugin.PluginBase { async parseChapter(chapterPath: string): Promise { const result = await fetchApi( - "https://api.novel.ovh/v2/chapters/" + chapterPath.split("/")[1], + 'https://api.novel.ovh/v2/chapters/' + chapterPath.split('/')[1], ); const book = (await result.json()) as responseChapter; const image = Object.fromEntries( @@ -98,11 +106,11 @@ class novelOvh implements Plugin.PluginBase { page: number | undefined = 1, ): Promise { const url = `${this.site}/novel?search=${searchTerm}&page=${page - 1}`; - const result = await fetchApi(url + "&_data=routes/reader/book/index"); + const result = await fetchApi(url + '&_data=routes/reader/book/index'); const body = (await result.json()) as { books: BooksEntity[] }; const novels: Plugin.NovelItem[] = []; - body.books.forEach((novel) => + body.books.forEach(novel => novels.push({ name: novel.name.ru, cover: novel.poster, @@ -116,37 +124,39 @@ class novelOvh implements Plugin.PluginBase { jsonToHtml = ( json: ContentEntity[], image: { [key: string]: string }, - html: string = "", + html: string = '', ) => { - json.forEach((element) => { + json.forEach(element => { switch (element.type) { - case "image": + case 'image': if (element.attrs?.pages?.[0]) { html += ``; } break; - case "hardBreak": - html += "
"; + case 'hardBreak': + html += '
'; break; - case "horizontalRule": - case "delimiter": + case 'horizontalRule': + case 'delimiter': html += '

***

'; break; - case "paragraph": + case 'paragraph': html += - "" + - (element.content ? this.jsonToHtml(element.content, image) : "
") + - "

"; + : '') + + '>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '
') + + '

'; break; - case "text": + case 'text': html += element.text; break; default: - html += JSON.stringify(element, null, "\t"); //maybe I missed something. + html += JSON.stringify(element, null, '\t'); //maybe I missed something. break; } }); @@ -154,20 +164,21 @@ class novelOvh implements Plugin.PluginBase { }; fetchImage = fetchFile; - resolveUrl = (path: string, isNovel?: boolean) => this.site + "/novel/" + path; + resolveUrl = (path: string, isNovel?: boolean) => + this.site + '/novel/' + path; filters = { sort: { - label: "Сортировка", - value: "averageRating", + label: 'Сортировка', + value: 'averageRating', options: [ - { label: "Кол-во просмотров", value: "viewsCount" }, - { label: "Кол-во лайков", value: "likesCount" }, - { label: "Кол-во глав", value: "chaptersCount" }, - { label: "Кол-во закладок", value: "bookmarksCount" }, - { label: "Рейтингу", value: "averageRating" }, - { label: "Дате создания", value: "createdAt" }, - { label: "Дате обновления", value: "updatedAt" }, + { label: 'Кол-во просмотров', value: 'viewsCount' }, + { label: 'Кол-во лайков', value: 'likesCount' }, + { label: 'Кол-во глав', value: 'chaptersCount' }, + { label: 'Кол-во закладок', value: 'bookmarksCount' }, + { label: 'Рейтингу', value: 'averageRating' }, + { label: 'Дате создания', value: 'createdAt' }, + { label: 'Дате обновления', value: 'updatedAt' }, ], type: FilterTypes.Picker, }, diff --git a/plugins/russian/novelTL.ts b/plugins/russian/novelTL.ts index 36af083..d22cec8 100644 --- a/plugins/russian/novelTL.ts +++ b/plugins/russian/novelTL.ts @@ -1,46 +1,46 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { defaultCover } from "@libs/defaultCover"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; const statusKey: { [key: string]: string } = { - "active": NovelStatus.Ongoing, - "completed": NovelStatus.Completed, - "freezed": NovelStatus.OnHiatus, - "unknown": NovelStatus.Unknown, + 'active': NovelStatus.Ongoing, + 'completed': NovelStatus.Completed, + 'freezed': NovelStatus.OnHiatus, + 'unknown': NovelStatus.Unknown, }; class TL implements Plugin.PluginBase { - id = "TL"; - name = "NovelTL"; - site = "https://novel.tl"; - version = "1.0.0"; - icon = "src/ru/noveltl/icon.png"; + id = 'TL'; + name = 'NovelTL'; + site = 'https://novel.tl'; + version = '1.0.0'; + icon = 'src/ru/noveltl/icon.png'; async popularNovels( page: number, { filters }: Plugin.PopularNovelsOptions, ): Promise { - const result = await fetchApi(this.site + "/api/site/v2/graphql", { - method: "post", + const result = await fetchApi(this.site + '/api/site/v2/graphql', { + method: 'post', headers: { - Accept: "application/json, text/plain, */*", - "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", - "Content-Type": "application/json", + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json', }, Referer: this.site, body: JSON.stringify({ query: - "query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}", + 'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}', variables: { filter: { tags: filters?.tags?.value || [], genres: filters?.genres?.value || [], }, - hostname: "novel.tl", + hostname: 'novel.tl', limit: 40, page, }, @@ -50,7 +50,7 @@ class TL implements Plugin.PluginBase { const json = (await result.json()) as response; const novels: Plugin.NovelItem[] = []; - json.data?.projects?.content?.forEach((novel) => + json.data?.projects?.content?.forEach(novel => novels.push({ name: novel.title, path: novel.fullUrl, @@ -64,12 +64,12 @@ class TL implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + "/api/site/v2/graphql", { - method: "post", + const result = await fetchApi(this.site + '/api/site/v2/graphql', { + method: 'post', headers: { - Accept: "application/json, text/plain, */*", - "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", - "Content-Type": "application/json", + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json', }, Referer: this.site, body: JSON.stringify({ @@ -83,49 +83,51 @@ class TL implements Plugin.PluginBase { const json = (await result.json()) as response; const novel: Plugin.SourceNovel = { path: novelPath, - name: json.data.project?.title || "", + name: json.data.project?.title || '', cover: json.data.project?.covers?.[0]?.url ? this.site + json.data.project.covers[0].url : defaultCover, summary: json.data.project?.annotation?.text, status: - statusKey[json.data.project?.translationStatus || "unknown"] || NovelStatus.Unknown, + statusKey[json.data.project?.translationStatus || 'unknown'] || + NovelStatus.Unknown, }; const genres = [json.data.project?.tags, json.data.project?.genres] .flat() - .map((item) => item?.nameRu || item?.nameEng) - .filter((item) => item); + .map(item => item?.nameRu || item?.nameEng) + .filter(item => item); if (genres?.length) { - novel.genres = genres.join(", "); + novel.genres = genres.join(', '); } - json.data.project?.persons?.forEach((person) => { - if (person.role == "author" && person.name.firstName) { + json.data.project?.persons?.forEach(person => { + if (person.role == 'author' && person.name.firstName) { novel.author = - person.name.firstName + " " + (person.name?.lastName || ""); + person.name.firstName + ' ' + (person.name?.lastName || ''); } - if (person.role == "illustrator" && person.name.firstName) { + if (person.role == 'illustrator' && person.name.firstName) { novel.artist = - person.name.firstName + " " + (person.name?.lastName || ""); + person.name.firstName + ' ' + (person.name?.lastName || ''); } }); const chapters: Plugin.ChapterItem[] = []; - json.data.project?.subprojects?.content?.forEach((work) => + json.data.project?.subprojects?.content?.forEach(work => work.volumes.content.forEach((volume, volumeIndex) => volume.chapters.forEach((chapter, chapterIndex) => { if (chapter.published) { chapters.push({ name: - (volume.shortName || "Том " + (volumeIndex + 1)) + " " + - (chapter.title || "Глава " + (chapterIndex + 1)), + (volume.shortName || 'Том ' + (volumeIndex + 1)) + + ' ' + + (chapter.title || 'Глава ' + (chapterIndex + 1)), path: chapter.fullUrl, - releaseTime: dayjs(chapter.publishDate).format("LLL"), + releaseTime: dayjs(chapter.publishDate).format('LLL'), chapterNumber: chapters.length + 1, - page: work.title || "Основная серия", + page: work.title || 'Основная серия', }); } }), @@ -137,17 +139,17 @@ class TL implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + "/api/site/v2/graphql", { - method: "post", + const result = await fetchApi(this.site + '/api/site/v2/graphql', { + method: 'post', headers: { - Accept: "application/json, text/plain, */*", - "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", - "Content-Type": "application/json", + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json', }, Referer: this.site, body: JSON.stringify({ query: - "query($url:String){chapter(chapter:{fullUrl:$url}){text{text}}}", + 'query($url:String){chapter(chapter:{fullUrl:$url}){text{text}}}', variables: { url: decodeURI(chapterPath), }, @@ -155,14 +157,14 @@ class TL implements Plugin.PluginBase { }); const json = (await result.json()) as response; - const loadedCheerio = parseHTML(json.data.chapter?.text?.text || ""); - loadedCheerio("p > a[href]").each((index, element) => { - let src = loadedCheerio(element).attr("href") || ""; - if (!src.startsWith("http")) { + const loadedCheerio = parseHTML(json.data.chapter?.text?.text || ''); + loadedCheerio('p > a[href]').each((index, element) => { + let src = loadedCheerio(element).attr('href') || ''; + if (!src.startsWith('http')) { src = this.site + src; } - loadedCheerio(element).find("picture").remove(); - loadedCheerio(element).removeAttr("href"); + loadedCheerio(element).find('picture').remove(); + loadedCheerio(element).removeAttr('href'); loadedCheerio(``).appendTo(element); }); @@ -174,22 +176,22 @@ class TL implements Plugin.PluginBase { searchTerm: string, page: number | undefined = 1, ): Promise { - const result = await fetchApi(this.site + "/api/site/v2/graphql", { - method: "post", + const result = await fetchApi(this.site + '/api/site/v2/graphql', { + method: 'post', headers: { - Accept: "application/json, text/plain, */*", - "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", - "Content-Type": "application/json", + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json', }, Referer: this.site, body: JSON.stringify({ query: - "query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}", + 'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}', variables: { filter: { query: searchTerm, }, - hostname: "novel.tl", + hostname: 'novel.tl', limit: 40, page, }, @@ -198,7 +200,7 @@ class TL implements Plugin.PluginBase { const json = (await result.json()) as response; const novels: Plugin.NovelItem[] = []; - json?.data?.projects?.content?.forEach((novel) => + json?.data?.projects?.content?.forEach(novel => novels.push({ name: novel.title, path: novel.fullUrl, @@ -212,668 +214,668 @@ class TL implements Plugin.PluginBase { } fetchImage = fetchFile; - resolveUrl = (path: string, isNovel?: boolean) => "https://" + path; + resolveUrl = (path: string, isNovel?: boolean) => 'https://' + path; filters = { tags: { - label: "Тэги", + label: 'Тэги', value: [], options: [ - { label: "Автоматы", value: "64" }, - { label: "Агрессивные персонажи", value: "26" }, - { label: "Ад", value: "338" }, - { label: "Адаптация манги", value: "8" }, - { label: "Академия", value: "5" }, - { label: "Актёрское искусство", value: "7" }, - { label: "Актеры озвучки", value: "746" }, - { label: "Алхимия", value: "27" }, - { label: "Альтернативная реальность", value: "30" }, - { label: "Амнезия", value: "31" }, - { label: "Анабиоз", value: "170" }, - { label: "Анал", value: "33" }, - { label: "Ангелы", value: "38" }, - { label: "Андроиды", value: "37" }, - { label: "Анти-рост персонажа", value: "25" }, - { label: "Антигерой", value: "43" }, - { label: "Антикварный магазин", value: "44" }, - { label: "Антимагия", value: "41" }, - { label: "Антиутопия", value: "225" }, - { label: "Апатичный главный герой", value: "46" }, - { label: "Апокалипсис", value: "47" }, - { label: "Аристократия", value: "51" }, - { label: "Армия", value: "53" }, - { label: "Артефакты", value: "58" }, - { label: "Асоциальный главный герой", value: "42" }, - { label: "Ассасины", value: "61" }, - { label: "Атмосфера Европы", value: "245" }, - { label: "Банды", value: "302" }, - { label: "БДСМ", value: "77" }, - { label: "Бедный главный герой", value: "541" }, - { label: "Бездушный главный герой", value: "143" }, - { label: "Беззаботный герой", value: "115" }, - { label: "Безответная любовь", value: "213" }, - { label: "Безответная любовь", value: "740" }, - { label: "Безработные", value: "377" }, - { label: "Беременность", value: "551" }, - { label: "Бескорыстная любовь", value: "733" }, - { label: "Бессмертные", value: "359" }, - { label: "Бесстрашный главный герой", value: "274" }, - { label: "Библиотека", value: "393" }, - { label: "Биография", value: "86" }, - { label: "Близнецы", value: "729" }, - { label: "Богачи", value: "753" }, - { label: "Боги", value: "318" }, - { label: "Богини", value: "316" }, - { label: "Богоподобный протагонист", value: "314" }, - { label: "Боевая академия", value: "75" }, - { label: "Боевая парочка", value: "547" }, - { label: "Божественная защита", value: "208" }, - { label: "Божественная сила", value: "317" }, - { label: "Болеющие персонажи", value: "648" }, - { label: "Борьба за власть", value: "548" }, - { label: "Брак по расчету", value: "55" }, - { label: "Братство", value: "106" }, - { label: "Братья или сёстры", value: "646" }, - { label: "Брошенные дети", value: "1" }, - { label: "Бывший герой", value: "290" }, - { label: "Вампиры", value: "742" }, - { label: "Ваншот", value: "496" }, - { label: "Вдали от родителей", value: "3" }, - { label: "Ведьмы", value: "756" }, - { label: "Везучий главный герой", value: "411" }, - { label: "Верные подданные", value: "410" }, - { label: "Вероломные герои/плетение интриг", value: "424" }, - { label: "Взросление персонажа", value: "24" }, - { label: "Видит то, чего остальной люд не в силах", value: "617" }, - { label: "Виртуальная реальность", value: "744" }, - { label: "Владелец магазина", value: "677" }, - { label: "Владелец уникального оружия", value: "736" }, - { label: "Властные персонажи", value: "545" }, - { label: "Влияние прошлого", value: "517" }, - { label: "Внезапное обогащение", value: "691" }, - { label: "Внезапное обретение силы", value: "690" }, - { label: "Военные хроники", value: "749" }, - { label: "Возвращение в родной мир", value: "587" }, - { label: "Война", value: "750" }, - { label: "Вокалоид", value: "745" }, - { label: "Волшебные твари", value: "413" }, - { label: "Воскрешение", value: "586" }, - { label: "Воспитанный главный герой", value: "537" }, - { label: "Воспоминания прошлого", value: "283" }, - { label: "Враги становятся союзниками", value: "238" }, - { label: "Врата в иной мир", value: "303" }, - { label: "Временной парадокс", value: "711" }, - { label: "Вторжение на Землю", value: "228" }, - { label: "Второй шанс", value: "608" }, - { label: "Выживание", value: "695" }, - { label: "Вынужденные условия проживания", value: "287" }, - { label: "Высокомерный герой", value: "56" }, - { label: "Гадание", value: "207" }, - { label: "Галлюцинации", value: "189" }, - { label: "Гарем рабов", value: "656" }, - { label: "Геймеры", value: "301" }, - { label: "Генетические модификации", value: "306" }, - { label: "Герои", value: "341" }, - { label: "Герои непонятного пола", value: "36" }, - { label: "Героиня девушка-сорванец", value: "715" }, - { label: "Героиня красавица", value: "81" }, - { label: "Герой влюбляется первым", value: "561" }, - { label: "Герой использует щит", value: "636" }, - { label: "Герой красавец", value: "329" }, - { label: "Герой со множеством тел", value: "564" }, - { label: "Герой-бесстыдник", value: "632" }, - { label: "Герой-извращенец", value: "521" }, - { label: "Герой-трудяга", value: "330" }, - { label: "Герой-яндере", value: "421" }, - { label: "Гетерохромия", value: "342" }, - { label: "Гильдии", value: "324" }, - { label: "Гиперопека", value: "509" }, - { label: "Главная героиня — женщина", value: "277" }, - { label: "Главный герой - гений", value: "308" }, - { label: "Главный герой - Моб", value: "446" }, - { label: "Главный герой — знаменитость", value: "263" }, - { label: "Главный герой — мужчина", value: "419" }, - { label: "Главный герой в поисках гарема", value: "331" }, - { label: "Главный герой заранее готовится", value: "557" }, - { label: "Главный герой не человек", value: "488" }, - { label: "Главный герой приёмный", value: "20" }, - { label: "Главный герой сразу силён", value: "563" }, - { label: "Гладиаторы", value: "310" }, - { label: "Глухой к любви герой", value: "194" }, - { label: "Гоблины", value: "313" }, - { label: "Големы", value: "319" }, - { label: "Гомункул", value: "347" }, - { label: "Горничные", value: "418" }, - { label: "Государственные интриги", value: "212" }, - { label: "Гринд", value: "322" }, - { label: "Давняя травма", value: "518" }, - { label: "Двойник", value: "97" }, - { label: "Дворецкие", value: "111" }, - { label: "Дворфы", value: "224" }, - { label: "Дворяне", value: "487" }, - { label: "Двоюродные братья или сёстры", value: "162" }, - { label: "Девочки-волшебницы", value: "415" }, - { label: "Демоны", value: "193" }, - { label: "Депрессия", value: "196" }, - { label: "Детектив", value: "198" }, - { label: "Детективное расследование", value: "473" }, - { label: "Детское насилие", value: "125" }, - { label: "Детское обещание", value: "130" }, - { label: "Дискриминация", value: "203" }, - { label: "Длительный разрыв отношений", value: "400" }, - { label: "Домашняя жизнь", value: "45" }, - { label: "Драконы", value: "218" }, - { label: "Древний Китай", value: "34" }, - { label: "Дружба", value: "293" }, - { label: "Друзья детства", value: "128" }, - { label: "Друзья становятся врагами", value: "292" }, - { label: "Духи", value: "673" }, - { label: "Духовная сила", value: "665" }, - { label: "Духовный наставник", value: "671" }, - { label: "Душещипательная история", value: "335" }, - { label: "Души", value: "666" }, - { label: "Дьявольский путь культивации", value: "192" }, - { label: "Ёкай", value: "763" }, - { label: "Есть аниме-адаптация", value: "10" }, - { label: "Есть видеоигра", value: "13" }, - { label: "Есть дорама-адаптация", value: "11" }, - { label: "Есть манга-адаптация", value: "14" }, - { label: "Есть манхва-адаптация", value: "16" }, - { label: "Есть маньхуа-адаптация", value: "15" }, - { label: "Есть фильм", value: "17" }, - { label: "Есть CD дорама-адаптация", value: "12" }, - { label: "Жёсткий главный герой", value: "597" }, - { label: "Жестокие персонажи", value: "4" }, - { label: "Жёстокие персонажи", value: "169" }, - { label: "Животное-спутник", value: "78" }, - { label: "Жизнь в одиночку", value: "396" }, - { label: "Жрецы", value: "555" }, - { label: "Жрицы", value: "554" }, - { label: "Забота о детях", value: "127" }, - { label: "Заботливый главный герой", value: "116" }, - { label: "Загадочное заболевание", value: "471" }, - { label: "Загадочное прошлое родителей", value: "470" }, - { label: "Заговоры", value: "154" }, - { label: "Закалка тела", value: "96" }, - { label: "Закон джунглей", value: "682" }, - { label: "Закулисная борба", value: "603" }, - { label: "Замкнутый главный герой", value: "371" }, - { label: "Запечатанная сила", value: "607" }, - { label: "Затворник", value: "346" }, - { label: "Звери", value: "80" }, - { label: "Зверолюди", value: "39" }, - { label: "Звероподобные", value: "79" }, - { label: "Земледелие", value: "268" }, - { label: "Злобные благородные девы", value: "743" }, - { label: "Зловещие организации", value: "247" }, - { label: "Злой главный герой", value: "248" }, - { label: "Злые боги", value: "246" }, - { label: "Злые религии", value: "249" }, - { label: "Знаменитости", value: "118" }, - { label: "Знания из прошлой жизни", value: "553" }, - { label: "Зомби", value: "767" }, - { label: "Игра на выживание", value: "696" }, - { label: "Игровая рейтинговая система", value: "300" }, - { label: "Из грязи в князи", value: "542" }, - { label: "Известные родители", value: "262" }, - { label: "Изменение внешнего вида", value: "48" }, - { label: "Изнасилование", value: "574" }, - { label: "Инвалидность", value: "202" }, - { label: "Индустриализация", value: "364" }, - { label: "Инженер", value: "241" }, - { label: "Инцест", value: "361" }, - { label: "Искажённая личность", value: "730" }, - { label: "Искусственный интеллект", value: "59" }, - { label: "Исполнение желаний", value: "755" }, - { label: "Каннибализм", value: "113" }, - { label: "Карточные игры", value: "114" }, - { label: "Кладбищенский смотритель", value: "321" }, - { label: "Классика", value: "134" }, - { label: "Книги", value: "99" }, - { label: "Книги навыков", value: "654" }, - { label: "Книжный червь", value: "100" }, - { label: "Коллеги", value: "140" }, - { label: "Колледж/Университет", value: "145" }, - { label: "Командная работа", value: "701" }, - { label: "Комедийный подтекст", value: "147" }, - { label: "Комплекс брата", value: "105" }, - { label: "Комплекс неполноценности", value: "365" }, - { label: "Контракты", value: "155" }, - { label: "Контроль разума", value: "441" }, - { label: "Конфликт в семье", value: "261" }, - { label: "Конфликт верности", value: "153" }, - { label: "Копейщик", value: "668" }, - { label: "Королевская особа", value: "596" }, - { label: "Королевства", value: "382" }, - { label: "Короткий рассказ", value: "638" }, - { label: "Коррупция", value: "157" }, - { label: "Космические Войны", value: "158" }, - { label: "Космос", value: "507" }, - { label: "Косплей", value: "159" }, - { label: "Кража способностей", value: "2" }, - { label: "Крафтинг", value: "164" }, - { label: "Кризис личности", value: "357" }, - { label: "Кровь", value: "320" }, - { label: "Кружки, клубы", value: "138" }, - { label: "Кудэрэ", value: "384" }, - { label: "Кузнец", value: "90" }, - { label: "Кукловоды", value: "567" }, - { label: "Куклы/Марионетки", value: "211" }, - { label: "Кулинария", value: "156" }, - { label: "Культивация", value: "171" }, - { label: "Куннилингус", value: "172" }, - { label: "Легенды", value: "391" }, - { label: "Легкая жизнь", value: "229" }, - { label: "Лекари", value: "334" }, - { label: "Ленивый главный герой", value: "389" }, - { label: "Лидерство", value: "390" }, - { label: "Любит персонажа младше себя", value: "765" }, - { label: "Любит персонажа старше себя", value: "494" }, - { label: "Любовное соперничество", value: "406" }, - { label: "Любовные треугольники", value: "407" }, - { label: "Любовь детства", value: "129" }, - { label: "Любовь с первого взгляда", value: "404" }, - { label: "Любящие родители", value: "215" }, - { label: "Маги", value: "757" }, - { label: "Магические технологии", value: "417" }, - { label: "Магия", value: "412" }, - { label: "Магия призыва", value: "694" }, - { label: "Маленькие девочки", value: "397" }, - { label: "Мангака", value: "423" }, - { label: "Маниакальная любовь", value: "492" }, - { label: "Марти Сью", value: "508" }, - { label: "Мастер подземелий", value: "222" }, - { label: "Мастурбация", value: "432" }, - { label: "Матриархат", value: "433" }, - { label: "Межпространственные путешествия", value: "370" }, - { label: "Менеджмент", value: "422" }, - { label: "Месть", value: "588" }, - { label: "Мечи и магия", value: "697" }, - { label: "Мечники", value: "698" }, - { label: "Миленькие дети", value: "176" }, - { label: "Милитаризм", value: "439" }, - { label: "Миловидная история", value: "178" }, - { label: "Милый главный герой", value: "177" }, - { label: "Минет", value: "275" }, - { label: "Мировое древо", value: "760" }, - { label: "Миролюбивый главный герой", value: "510" }, - { label: "Мифология", value: "475" }, - { label: "Младшие сёстры", value: "766" }, - { label: "ММОРПГ", value: "445" }, - { label: "Множество временных линий", value: "463" }, - { label: "Модели", value: "447" }, - { label: "Молод не по годам", value: "49" }, - { label: "Монстры", value: "454" }, - { label: "Мрачность", value: "183" }, - { label: "Музыка", value: "466" }, - { label: "Музыкальные группы", value: "68" }, - { label: "Мутации", value: "468" }, - { label: "На волосок от смерти", value: "479" }, - { label: "На все руки мастер", value: "374" }, - { label: "Навыки имеют стоимость", value: "150" }, - { label: "Нагота", value: "490" }, - { label: "Наездники на драконах", value: "216" }, - { label: "Наёмники", value: "437" }, - { label: "Наивный главный герой", value: "476" }, - { label: "Наследие", value: "366" }, - { label: "Настоящая любовь", value: "200" }, - { label: "Настоящий гарем", value: "540" }, - { label: "Небесное испытание", value: "337" }, - { label: "Невезучий главный герой", value: "738" }, - { label: "Недоверчивый протагонист", value: "206" }, - { label: "Недооцененный главный герой", value: "734" }, - { label: "Недопонимание", value: "444" }, - { label: "Некромант", value: "480" }, - { label: "Нелинейное повествование", value: "489" }, - { label: "Неловкий главный герой", value: "67" }, - { label: "Нелюбимый протагонист", value: "333" }, - { label: "Немой персонаж", value: "469" }, - { label: "Ненадежный рассказчик", value: "739" }, - { label: "Непримечательный герой", value: "65" }, - { label: "Нерешительный протагонист", value: "363" }, - { label: "Несгибаемый главный герой", value: "676" }, - { label: "Несколько главных героев", value: "460" }, - { label: "Несколько миров", value: "461" }, - { label: "Несколько перерожденцев", value: "462" }, - { label: "Несколько попаданцев", value: "464" }, - { label: "Несравненная скорость культивации", value: "269" }, - { label: "Нехватка/отсутствие здравого смысла", value: "385" }, - { label: "Нечестный протагонист", value: "205" }, - { label: "НИИТ", value: "481" }, - { label: "Ниндзя", value: "486" }, - { label: "Обмен телами", value: "95" }, - { label: "Оборотни", value: "754" }, - { label: "Объект любви влюбился первым", value: "405" }, - { label: "Объект любви популярен", value: "543" }, - { label: "Огнестрельное оружие", value: "280" }, - { label: "Ограничение на время жизни", value: "394" }, - { label: "Одержимость", value: "544" }, - { label: "Один родитель", value: "651" }, - { label: "Одинокий главный герой", value: "399" }, - { label: "Одиночество", value: "398" }, - { label: "Одно тело на двоих", value: "634" }, - { label: "Око силы", value: "253" }, - { label: "Оммёдзи", value: "498" }, - { label: "Организованная преступность", value: "500" }, - { label: "Оргия", value: "501" }, - { label: "Орки", value: "499" }, - { label: "Основано на аниме", value: "74" }, - { label: "Основано на видеоигре", value: "72" }, - { label: "Основано на визуальной новелле", value: "73" }, - { label: "Основано на песне", value: "70" }, - { label: "Основано на фильме", value: "69" }, - { label: "Осторожный главный герой", value: "117" }, - { label: "От ненависти до любви один шаг", value: "239" }, - { label: "От сильного с сильнейшему", value: "684" }, - { label: "От слабого к сильному", value: "752" }, - { label: "Отаку", value: "503" }, - { label: "Отзывчивая любовь", value: "380" }, - { label: "Отзывчивый главный герой", value: "339" }, - { label: "Отношение учитель-ученик", value: "430" }, - { label: "Отношения бог-человек", value: "315" }, - { label: "Отношения между учителем и учеником", value: "687" }, - { label: "Отношения начальник-подчинённый", value: "101" }, - { label: "Отношения против воли", value: "286" }, - { label: "Отношения с нечеловеком", value: "353" }, - { label: "Отношения сэмпай-кохай", value: "621" }, - { label: "Отношения хозяин-слуга", value: "431" }, - { label: "Отомэ-игры", value: "504" }, - { label: "ОТП", value: "562" }, - { label: "Отшельники", value: "505" }, - { label: "Офисный работник", value: "600" }, - { label: "Официанты", value: "748" }, - { label: "Охотники", value: "355" }, - { label: "Падшие ангелы", value: "255" }, - { label: "Падшие дворяне", value: "256" }, - { label: "Пайзури", value: "511" }, - { label: "Паразиты", value: "513" }, - { label: "Параллельные миры", value: "512" }, - { label: "Парк развлечений", value: "32" }, - { label: "Пародия", value: "515" }, - { label: "Перемещение между мирами", value: "758" }, - { label: "Переодевание", value: "167" }, - { label: "Переплавка пилюль", value: "529" }, - { label: "Перерождение в девушку", value: "420" }, - { label: "Перерождение в другом мире", value: "580" }, - { label: "Перерождение в другом мире", value: "720" }, - { label: "Перерождение в игровом мире", value: "579" }, - { label: "Перерождение в монстра", value: "577" }, - { label: "Персонажи с мазохистскими наклонностями", value: "429" }, - { label: "Персонажи с садистскими наклонностями", value: "598" }, - { label: "Петля времени", value: "709" }, - { label: "Петтинг", value: "328" }, - { label: "Писатели", value: "761" }, - { label: "Питомцы", value: "522" }, - { label: "Племенное общество", value: "726" }, - { label: "Повелитель демонов", value: "191" }, - { label: "Повествование от нескольких лиц", value: "459" }, - { label: "Поглощение навыков", value: "653" }, - { label: "Подземелья", value: "223" }, - { label: "Поздно начинающаяся романтическая линия", value: "387" }, - { label: "Политика", value: "538" }, - { label: "Полулюди", value: "190" }, - { label: "Помолвка", value: "240" }, - { label: "Попаданец в другой мир", value: "723" }, - { label: "Попаданец в игровой мир", value: "722" }, - { label: "Постапокалипсис", value: "546" }, - { label: "Постижение Дао", value: "181" }, - { label: "Потеря девственности", value: "282" }, - { label: "Потустороннее пространство", value: "416" }, - { label: "Похищения", value: "379" }, - { label: "Прагматичный главный герой", value: "549" }, - { label: "Предательство", value: "83" }, - { label: "Предвидение", value: "550" }, - { label: "Преследователи", value: "674" }, - { label: "Преступление", value: "165" }, - { label: "Преступность", value: "166" }, - { label: "Приёмные дети", value: "19" }, - { label: "Призванный герой", value: "693" }, - { label: "Призраки", value: "309" }, - { label: "Приключения", value: "22" }, - { label: "Принуждение", value: "108" }, - { label: "Приручатель", value: "453" }, - { label: "Прислуга", value: "625" }, - { label: "Приставучий любовник", value: "136" }, - { label: "Притворная любовь", value: "552" }, - { label: "Пришельцы", value: "28" }, - { label: "Программист", value: "558" }, - { label: "Произведение - обладатель наград", value: "66" }, - { label: "Проклятия", value: "175" }, - { label: "Промывка мозгов", value: "102" }, - { label: "Пропуск времени", value: "712" }, - { label: "Пророчества", value: "559" }, - { label: "Протагонист в очках", value: "312" }, - { label: "Протагонист раб", value: "657" }, - { label: "Психокинез", value: "565" }, - { label: "Психопаты", value: "566" }, - { label: "Путешествия", value: "759" }, - { label: "Путешествия во времени", value: "713" }, - { label: "Пытки", value: "716" }, - { label: "Рабы", value: "658" }, - { label: "Развитие отношений", value: "160" }, - { label: "Развитие персонажа", value: "119" }, - { label: "Раздвоение личности", value: "458" }, - { label: "Размеренная романтика", value: "661" }, - { label: "Разница в статусе", value: "201" }, - { label: "Разорванная помолвка", value: "104" }, - { label: "Разумные предметы", value: "622" }, - { label: "Рай", value: "336" }, - { label: "Раняя романтика", value: "227" }, - { label: "Расизм", value: "573" }, - { label: "Расследования", value: "372" }, - { label: "Ребенок — главный герой", value: "126" }, - { label: "Реверс-гарем", value: "589" }, - { label: "Реверс-изнасилование", value: "590" }, - { label: "Резкая смена характера", value: "520" }, - { label: "Реинкарнация", value: "581" }, - { label: "Религии", value: "582" }, - { label: "Ресторан", value: "585" }, - { label: "Решительный главный герой", value: "199" }, - { label: "Робкий протагонист", value: "714" }, - { label: "Родительсткий комплекс", value: "514" }, - { label: "Родные не связанные кровью", value: "647" }, - { label: "Родословные", value: "94" }, - { label: "РПГ-система", value: "392" }, - { label: "Рыцари", value: "383" }, - { label: "Самовлюбленный главный герой", value: "477" }, - { label: "Самоубийства", value: "692" }, - { label: "Самураи", value: "601" }, - { label: "Сборник коротких историй", value: "144" }, - { label: "Свадьба", value: "426" }, - { label: "Связанные сюжетные линии", value: "369" }, - { label: "Святые", value: "599" }, - { label: "Секреты", value: "614" }, - { label: "Секс втроём", value: "707" }, - { label: "Секс-рабыня", value: "629" }, - { label: "Семейная любовь", value: "257" }, - { label: "Семь добродетелей", value: "627" }, - { label: "Семь смертных грехов", value: "626" }, - { label: "Серийные убийцы", value: "624" }, - { label: "Сикигами", value: "637" }, - { label: "Сильная любовь от старших", value: "214" }, - { label: "Синдром восьмиклассника", value: "132" }, - { label: "Синдром сестры", value: "652" }, - { label: "Сироты", value: "502" }, - { label: "Системный администратор", value: "699" }, - { label: "Скромный главный герой", value: "409" }, - { label: "Скрытые романтические отношения", value: "23" }, - { label: "Скрытые способности", value: "343" }, - { label: "Скряга", value: "450" }, - { label: "Скульпторы", value: "606" }, - { label: "Слабая романтика", value: "688" }, - { label: "Слабый главный герой", value: "751" }, - { label: "Сложные семейные отношения", value: "149" }, - { label: "Смена расы", value: "572" }, - { label: "Смертельное заболевание", value: "704" }, - { label: "Смерть", value: "185" }, - { label: "Смерть любимых", value: "186" }, - { label: "Современность", value: "448" }, - { label: "Современные знания в слаборазвитых мирах", value: "449" }, - { label: "Сожительство", value: "141" }, - { label: "Создание артефактов", value: "57" }, - { label: "Создание клана", value: "133" }, - { label: "Создание королевства", value: "381" }, - { label: "Создание навыков", value: "655" }, - { label: "Сокрытие истинных способностей", value: "344" }, - { label: "Сокрытие личности", value: "345" }, - { label: "Солдаты", value: "664" }, - { label: "Сон", value: "659" }, - { label: "Соседи по комнате", value: "595" }, - { label: "Социальные изгои", value: "663" }, - { label: "Спасение мира", value: "602" }, - { label: "Спорящая пара", value: "84" }, - { label: "Способность перевоплощения", value: "719" }, - { label: "Средневековье", value: "436" }, - { label: "Стеснительные персонажи", value: "643" }, - { label: "Стокгольмский синдром", value: "575" }, - { label: "Стокгольмский синдром", value: "675" }, - { label: "Стратег", value: "681" }, - { label: "Стратегические битвы", value: "680" }, - { label: "Стрелки", value: "325" }, - { label: "Стрельба из лука", value: "50" }, - { label: "Студенческий совет", value: "686" }, - { label: "Судьба", value: "197" }, - { label: "Суккубы", value: "689" }, - { label: "Суперспособности", value: "669" }, - { label: "Суровая тренировка", value: "332" }, - { label: "Сцены насилия", value: "195" }, - { label: "Таинственное прошлое", value: "472" }, - { label: "Тайная личность", value: "610" }, - { label: "Тайные организации", value: "611" }, - { label: "Телохранители", value: "98" }, - { label: "Тентакли", value: "703" }, - { label: "Террористы", value: "705" }, - { label: "Технологический разрыв", value: "702" }, - { label: "Тихие герои", value: "568" }, - { label: "Толстый протагонист", value: "271" }, - { label: "Торговцы", value: "438" }, - { label: "Травник", value: "340" }, - { label: "Трагичное прошлое", value: "718" }, - { label: "Трап", value: "725" }, - { label: "Триллер", value: "708" }, - { label: "Турнир", value: "76" }, - { label: "Тюрьма", value: "556" }, - { label: "Убийства", value: "465" }, - { label: "Уверенный в себе главный герой", value: "151" }, - { label: "Умная пара", value: "662" }, - { label: "Умный главный герой", value: "135" }, - { label: "Уникальная техника культивации", value: "735" }, - { label: "Уникальные оружия", value: "737" }, - { label: "Управление бизнесом", value: "109" }, - { label: "Управление временем", value: "710" }, - { label: "Управление пространством", value: "667" }, - { label: "Ускоренный рост", value: "6" }, - { label: "Учёные", value: "605" }, - { label: "Учителя", value: "700" }, - { label: "Фамильяры", value: "258" }, - { label: "Фанатизм", value: "264" }, - { label: "Фармацевт", value: "523" }, - { label: "Феи", value: "254" }, - { label: "Фениксы", value: "526" }, - { label: "Фетиш на грудь", value: "103" }, - { label: "Фетиш на очки", value: "311" }, - { label: "Философия", value: "524" }, - { label: "Фобии/Страхи", value: "525" }, - { label: "Фольклор", value: "285" }, - { label: "Формирование армии", value: "54" }, - { label: "Футанари", value: "295" }, - { label: "Футуристический мир", value: "296" }, - { label: "Фэнтези", value: "267" }, - { label: "Фэнтезийные существа", value: "266" }, - { label: "Хакеры", value: "326" }, - { label: "Харизматичный протагонист", value: "120" }, - { label: "Хитрый главный герой", value: "173" }, - { label: "Хладнокровный главный герой", value: "112" }, - { label: "Художники", value: "60" }, - { label: "Хулиганы", value: "188" }, - { label: "Цундере", value: "728" }, - { label: "Чат", value: "122" }, - { label: "Человек-оружие", value: "352" }, - { label: "Человекоподобный главный герой", value: "354" }, - { label: "Честный главный герой", value: "348" }, - { label: "Четкие любовные убеждения", value: "683" }, - { label: "Читы", value: "123" }, - { label: "Чудаковатые герои", value: "569" }, - { label: "Чужие воспоминания", value: "721" }, - { label: "Шантаж", value: "89" }, - { label: "Шеф-повар", value: "124" }, - { label: "Школа для девочек", value: "29" }, - { label: "Шоу-бизнес", value: "642" }, - { label: "Шпионы", value: "670" }, - { label: "Эволюция", value: "250" }, - { label: "Эгоистичный главный герой", value: "618" }, - { label: "Экзорцизм", value: "252" }, - { label: "Экономика", value: "230" }, - { label: "Эксгибиционизм", value: "251" }, - { label: "Эксперименты на людях", value: "351" }, - { label: "Элементальная магия", value: "234" }, - { label: "Элементы игрового мира", value: "299" }, - { label: "Элементы романтики", value: "594" }, - { label: "Элементы юри", value: "640" }, - { label: "Элементы яоя", value: "641" }, - { label: "Эльфы", value: "235" }, - { label: "Эпизодичность", value: "243" }, - { label: "Язвительные персонажи", value: "635" }, - { label: "Языковой барьер", value: "386" }, - { label: "Яндере", value: "762" }, - { label: "Яойщица", value: "294" }, - { label: "Японские силы самообороны", value: "378" }, - { label: "R-15", value: "570" }, - { label: "R-18", value: "571" }, + { label: 'Автоматы', value: '64' }, + { label: 'Агрессивные персонажи', value: '26' }, + { label: 'Ад', value: '338' }, + { label: 'Адаптация манги', value: '8' }, + { label: 'Академия', value: '5' }, + { label: 'Актёрское искусство', value: '7' }, + { label: 'Актеры озвучки', value: '746' }, + { label: 'Алхимия', value: '27' }, + { label: 'Альтернативная реальность', value: '30' }, + { label: 'Амнезия', value: '31' }, + { label: 'Анабиоз', value: '170' }, + { label: 'Анал', value: '33' }, + { label: 'Ангелы', value: '38' }, + { label: 'Андроиды', value: '37' }, + { label: 'Анти-рост персонажа', value: '25' }, + { label: 'Антигерой', value: '43' }, + { label: 'Антикварный магазин', value: '44' }, + { label: 'Антимагия', value: '41' }, + { label: 'Антиутопия', value: '225' }, + { label: 'Апатичный главный герой', value: '46' }, + { label: 'Апокалипсис', value: '47' }, + { label: 'Аристократия', value: '51' }, + { label: 'Армия', value: '53' }, + { label: 'Артефакты', value: '58' }, + { label: 'Асоциальный главный герой', value: '42' }, + { label: 'Ассасины', value: '61' }, + { label: 'Атмосфера Европы', value: '245' }, + { label: 'Банды', value: '302' }, + { label: 'БДСМ', value: '77' }, + { label: 'Бедный главный герой', value: '541' }, + { label: 'Бездушный главный герой', value: '143' }, + { label: 'Беззаботный герой', value: '115' }, + { label: 'Безответная любовь', value: '213' }, + { label: 'Безответная любовь', value: '740' }, + { label: 'Безработные', value: '377' }, + { label: 'Беременность', value: '551' }, + { label: 'Бескорыстная любовь', value: '733' }, + { label: 'Бессмертные', value: '359' }, + { label: 'Бесстрашный главный герой', value: '274' }, + { label: 'Библиотека', value: '393' }, + { label: 'Биография', value: '86' }, + { label: 'Близнецы', value: '729' }, + { label: 'Богачи', value: '753' }, + { label: 'Боги', value: '318' }, + { label: 'Богини', value: '316' }, + { label: 'Богоподобный протагонист', value: '314' }, + { label: 'Боевая академия', value: '75' }, + { label: 'Боевая парочка', value: '547' }, + { label: 'Божественная защита', value: '208' }, + { label: 'Божественная сила', value: '317' }, + { label: 'Болеющие персонажи', value: '648' }, + { label: 'Борьба за власть', value: '548' }, + { label: 'Брак по расчету', value: '55' }, + { label: 'Братство', value: '106' }, + { label: 'Братья или сёстры', value: '646' }, + { label: 'Брошенные дети', value: '1' }, + { label: 'Бывший герой', value: '290' }, + { label: 'Вампиры', value: '742' }, + { label: 'Ваншот', value: '496' }, + { label: 'Вдали от родителей', value: '3' }, + { label: 'Ведьмы', value: '756' }, + { label: 'Везучий главный герой', value: '411' }, + { label: 'Верные подданные', value: '410' }, + { label: 'Вероломные герои/плетение интриг', value: '424' }, + { label: 'Взросление персонажа', value: '24' }, + { label: 'Видит то, чего остальной люд не в силах', value: '617' }, + { label: 'Виртуальная реальность', value: '744' }, + { label: 'Владелец магазина', value: '677' }, + { label: 'Владелец уникального оружия', value: '736' }, + { label: 'Властные персонажи', value: '545' }, + { label: 'Влияние прошлого', value: '517' }, + { label: 'Внезапное обогащение', value: '691' }, + { label: 'Внезапное обретение силы', value: '690' }, + { label: 'Военные хроники', value: '749' }, + { label: 'Возвращение в родной мир', value: '587' }, + { label: 'Война', value: '750' }, + { label: 'Вокалоид', value: '745' }, + { label: 'Волшебные твари', value: '413' }, + { label: 'Воскрешение', value: '586' }, + { label: 'Воспитанный главный герой', value: '537' }, + { label: 'Воспоминания прошлого', value: '283' }, + { label: 'Враги становятся союзниками', value: '238' }, + { label: 'Врата в иной мир', value: '303' }, + { label: 'Временной парадокс', value: '711' }, + { label: 'Вторжение на Землю', value: '228' }, + { label: 'Второй шанс', value: '608' }, + { label: 'Выживание', value: '695' }, + { label: 'Вынужденные условия проживания', value: '287' }, + { label: 'Высокомерный герой', value: '56' }, + { label: 'Гадание', value: '207' }, + { label: 'Галлюцинации', value: '189' }, + { label: 'Гарем рабов', value: '656' }, + { label: 'Геймеры', value: '301' }, + { label: 'Генетические модификации', value: '306' }, + { label: 'Герои', value: '341' }, + { label: 'Герои непонятного пола', value: '36' }, + { label: 'Героиня девушка-сорванец', value: '715' }, + { label: 'Героиня красавица', value: '81' }, + { label: 'Герой влюбляется первым', value: '561' }, + { label: 'Герой использует щит', value: '636' }, + { label: 'Герой красавец', value: '329' }, + { label: 'Герой со множеством тел', value: '564' }, + { label: 'Герой-бесстыдник', value: '632' }, + { label: 'Герой-извращенец', value: '521' }, + { label: 'Герой-трудяга', value: '330' }, + { label: 'Герой-яндере', value: '421' }, + { label: 'Гетерохромия', value: '342' }, + { label: 'Гильдии', value: '324' }, + { label: 'Гиперопека', value: '509' }, + { label: 'Главная героиня — женщина', value: '277' }, + { label: 'Главный герой - гений', value: '308' }, + { label: 'Главный герой - Моб', value: '446' }, + { label: 'Главный герой — знаменитость', value: '263' }, + { label: 'Главный герой — мужчина', value: '419' }, + { label: 'Главный герой в поисках гарема', value: '331' }, + { label: 'Главный герой заранее готовится', value: '557' }, + { label: 'Главный герой не человек', value: '488' }, + { label: 'Главный герой приёмный', value: '20' }, + { label: 'Главный герой сразу силён', value: '563' }, + { label: 'Гладиаторы', value: '310' }, + { label: 'Глухой к любви герой', value: '194' }, + { label: 'Гоблины', value: '313' }, + { label: 'Големы', value: '319' }, + { label: 'Гомункул', value: '347' }, + { label: 'Горничные', value: '418' }, + { label: 'Государственные интриги', value: '212' }, + { label: 'Гринд', value: '322' }, + { label: 'Давняя травма', value: '518' }, + { label: 'Двойник', value: '97' }, + { label: 'Дворецкие', value: '111' }, + { label: 'Дворфы', value: '224' }, + { label: 'Дворяне', value: '487' }, + { label: 'Двоюродные братья или сёстры', value: '162' }, + { label: 'Девочки-волшебницы', value: '415' }, + { label: 'Демоны', value: '193' }, + { label: 'Депрессия', value: '196' }, + { label: 'Детектив', value: '198' }, + { label: 'Детективное расследование', value: '473' }, + { label: 'Детское насилие', value: '125' }, + { label: 'Детское обещание', value: '130' }, + { label: 'Дискриминация', value: '203' }, + { label: 'Длительный разрыв отношений', value: '400' }, + { label: 'Домашняя жизнь', value: '45' }, + { label: 'Драконы', value: '218' }, + { label: 'Древний Китай', value: '34' }, + { label: 'Дружба', value: '293' }, + { label: 'Друзья детства', value: '128' }, + { label: 'Друзья становятся врагами', value: '292' }, + { label: 'Духи', value: '673' }, + { label: 'Духовная сила', value: '665' }, + { label: 'Духовный наставник', value: '671' }, + { label: 'Душещипательная история', value: '335' }, + { label: 'Души', value: '666' }, + { label: 'Дьявольский путь культивации', value: '192' }, + { label: 'Ёкай', value: '763' }, + { label: 'Есть аниме-адаптация', value: '10' }, + { label: 'Есть видеоигра', value: '13' }, + { label: 'Есть дорама-адаптация', value: '11' }, + { label: 'Есть манга-адаптация', value: '14' }, + { label: 'Есть манхва-адаптация', value: '16' }, + { label: 'Есть маньхуа-адаптация', value: '15' }, + { label: 'Есть фильм', value: '17' }, + { label: 'Есть CD дорама-адаптация', value: '12' }, + { label: 'Жёсткий главный герой', value: '597' }, + { label: 'Жестокие персонажи', value: '4' }, + { label: 'Жёстокие персонажи', value: '169' }, + { label: 'Животное-спутник', value: '78' }, + { label: 'Жизнь в одиночку', value: '396' }, + { label: 'Жрецы', value: '555' }, + { label: 'Жрицы', value: '554' }, + { label: 'Забота о детях', value: '127' }, + { label: 'Заботливый главный герой', value: '116' }, + { label: 'Загадочное заболевание', value: '471' }, + { label: 'Загадочное прошлое родителей', value: '470' }, + { label: 'Заговоры', value: '154' }, + { label: 'Закалка тела', value: '96' }, + { label: 'Закон джунглей', value: '682' }, + { label: 'Закулисная борба', value: '603' }, + { label: 'Замкнутый главный герой', value: '371' }, + { label: 'Запечатанная сила', value: '607' }, + { label: 'Затворник', value: '346' }, + { label: 'Звери', value: '80' }, + { label: 'Зверолюди', value: '39' }, + { label: 'Звероподобные', value: '79' }, + { label: 'Земледелие', value: '268' }, + { label: 'Злобные благородные девы', value: '743' }, + { label: 'Зловещие организации', value: '247' }, + { label: 'Злой главный герой', value: '248' }, + { label: 'Злые боги', value: '246' }, + { label: 'Злые религии', value: '249' }, + { label: 'Знаменитости', value: '118' }, + { label: 'Знания из прошлой жизни', value: '553' }, + { label: 'Зомби', value: '767' }, + { label: 'Игра на выживание', value: '696' }, + { label: 'Игровая рейтинговая система', value: '300' }, + { label: 'Из грязи в князи', value: '542' }, + { label: 'Известные родители', value: '262' }, + { label: 'Изменение внешнего вида', value: '48' }, + { label: 'Изнасилование', value: '574' }, + { label: 'Инвалидность', value: '202' }, + { label: 'Индустриализация', value: '364' }, + { label: 'Инженер', value: '241' }, + { label: 'Инцест', value: '361' }, + { label: 'Искажённая личность', value: '730' }, + { label: 'Искусственный интеллект', value: '59' }, + { label: 'Исполнение желаний', value: '755' }, + { label: 'Каннибализм', value: '113' }, + { label: 'Карточные игры', value: '114' }, + { label: 'Кладбищенский смотритель', value: '321' }, + { label: 'Классика', value: '134' }, + { label: 'Книги', value: '99' }, + { label: 'Книги навыков', value: '654' }, + { label: 'Книжный червь', value: '100' }, + { label: 'Коллеги', value: '140' }, + { label: 'Колледж/Университет', value: '145' }, + { label: 'Командная работа', value: '701' }, + { label: 'Комедийный подтекст', value: '147' }, + { label: 'Комплекс брата', value: '105' }, + { label: 'Комплекс неполноценности', value: '365' }, + { label: 'Контракты', value: '155' }, + { label: 'Контроль разума', value: '441' }, + { label: 'Конфликт в семье', value: '261' }, + { label: 'Конфликт верности', value: '153' }, + { label: 'Копейщик', value: '668' }, + { label: 'Королевская особа', value: '596' }, + { label: 'Королевства', value: '382' }, + { label: 'Короткий рассказ', value: '638' }, + { label: 'Коррупция', value: '157' }, + { label: 'Космические Войны', value: '158' }, + { label: 'Космос', value: '507' }, + { label: 'Косплей', value: '159' }, + { label: 'Кража способностей', value: '2' }, + { label: 'Крафтинг', value: '164' }, + { label: 'Кризис личности', value: '357' }, + { label: 'Кровь', value: '320' }, + { label: 'Кружки, клубы', value: '138' }, + { label: 'Кудэрэ', value: '384' }, + { label: 'Кузнец', value: '90' }, + { label: 'Кукловоды', value: '567' }, + { label: 'Куклы/Марионетки', value: '211' }, + { label: 'Кулинария', value: '156' }, + { label: 'Культивация', value: '171' }, + { label: 'Куннилингус', value: '172' }, + { label: 'Легенды', value: '391' }, + { label: 'Легкая жизнь', value: '229' }, + { label: 'Лекари', value: '334' }, + { label: 'Ленивый главный герой', value: '389' }, + { label: 'Лидерство', value: '390' }, + { label: 'Любит персонажа младше себя', value: '765' }, + { label: 'Любит персонажа старше себя', value: '494' }, + { label: 'Любовное соперничество', value: '406' }, + { label: 'Любовные треугольники', value: '407' }, + { label: 'Любовь детства', value: '129' }, + { label: 'Любовь с первого взгляда', value: '404' }, + { label: 'Любящие родители', value: '215' }, + { label: 'Маги', value: '757' }, + { label: 'Магические технологии', value: '417' }, + { label: 'Магия', value: '412' }, + { label: 'Магия призыва', value: '694' }, + { label: 'Маленькие девочки', value: '397' }, + { label: 'Мангака', value: '423' }, + { label: 'Маниакальная любовь', value: '492' }, + { label: 'Марти Сью', value: '508' }, + { label: 'Мастер подземелий', value: '222' }, + { label: 'Мастурбация', value: '432' }, + { label: 'Матриархат', value: '433' }, + { label: 'Межпространственные путешествия', value: '370' }, + { label: 'Менеджмент', value: '422' }, + { label: 'Месть', value: '588' }, + { label: 'Мечи и магия', value: '697' }, + { label: 'Мечники', value: '698' }, + { label: 'Миленькие дети', value: '176' }, + { label: 'Милитаризм', value: '439' }, + { label: 'Миловидная история', value: '178' }, + { label: 'Милый главный герой', value: '177' }, + { label: 'Минет', value: '275' }, + { label: 'Мировое древо', value: '760' }, + { label: 'Миролюбивый главный герой', value: '510' }, + { label: 'Мифология', value: '475' }, + { label: 'Младшие сёстры', value: '766' }, + { label: 'ММОРПГ', value: '445' }, + { label: 'Множество временных линий', value: '463' }, + { label: 'Модели', value: '447' }, + { label: 'Молод не по годам', value: '49' }, + { label: 'Монстры', value: '454' }, + { label: 'Мрачность', value: '183' }, + { label: 'Музыка', value: '466' }, + { label: 'Музыкальные группы', value: '68' }, + { label: 'Мутации', value: '468' }, + { label: 'На волосок от смерти', value: '479' }, + { label: 'На все руки мастер', value: '374' }, + { label: 'Навыки имеют стоимость', value: '150' }, + { label: 'Нагота', value: '490' }, + { label: 'Наездники на драконах', value: '216' }, + { label: 'Наёмники', value: '437' }, + { label: 'Наивный главный герой', value: '476' }, + { label: 'Наследие', value: '366' }, + { label: 'Настоящая любовь', value: '200' }, + { label: 'Настоящий гарем', value: '540' }, + { label: 'Небесное испытание', value: '337' }, + { label: 'Невезучий главный герой', value: '738' }, + { label: 'Недоверчивый протагонист', value: '206' }, + { label: 'Недооцененный главный герой', value: '734' }, + { label: 'Недопонимание', value: '444' }, + { label: 'Некромант', value: '480' }, + { label: 'Нелинейное повествование', value: '489' }, + { label: 'Неловкий главный герой', value: '67' }, + { label: 'Нелюбимый протагонист', value: '333' }, + { label: 'Немой персонаж', value: '469' }, + { label: 'Ненадежный рассказчик', value: '739' }, + { label: 'Непримечательный герой', value: '65' }, + { label: 'Нерешительный протагонист', value: '363' }, + { label: 'Несгибаемый главный герой', value: '676' }, + { label: 'Несколько главных героев', value: '460' }, + { label: 'Несколько миров', value: '461' }, + { label: 'Несколько перерожденцев', value: '462' }, + { label: 'Несколько попаданцев', value: '464' }, + { label: 'Несравненная скорость культивации', value: '269' }, + { label: 'Нехватка/отсутствие здравого смысла', value: '385' }, + { label: 'Нечестный протагонист', value: '205' }, + { label: 'НИИТ', value: '481' }, + { label: 'Ниндзя', value: '486' }, + { label: 'Обмен телами', value: '95' }, + { label: 'Оборотни', value: '754' }, + { label: 'Объект любви влюбился первым', value: '405' }, + { label: 'Объект любви популярен', value: '543' }, + { label: 'Огнестрельное оружие', value: '280' }, + { label: 'Ограничение на время жизни', value: '394' }, + { label: 'Одержимость', value: '544' }, + { label: 'Один родитель', value: '651' }, + { label: 'Одинокий главный герой', value: '399' }, + { label: 'Одиночество', value: '398' }, + { label: 'Одно тело на двоих', value: '634' }, + { label: 'Око силы', value: '253' }, + { label: 'Оммёдзи', value: '498' }, + { label: 'Организованная преступность', value: '500' }, + { label: 'Оргия', value: '501' }, + { label: 'Орки', value: '499' }, + { label: 'Основано на аниме', value: '74' }, + { label: 'Основано на видеоигре', value: '72' }, + { label: 'Основано на визуальной новелле', value: '73' }, + { label: 'Основано на песне', value: '70' }, + { label: 'Основано на фильме', value: '69' }, + { label: 'Осторожный главный герой', value: '117' }, + { label: 'От ненависти до любви один шаг', value: '239' }, + { label: 'От сильного с сильнейшему', value: '684' }, + { label: 'От слабого к сильному', value: '752' }, + { label: 'Отаку', value: '503' }, + { label: 'Отзывчивая любовь', value: '380' }, + { label: 'Отзывчивый главный герой', value: '339' }, + { label: 'Отношение учитель-ученик', value: '430' }, + { label: 'Отношения бог-человек', value: '315' }, + { label: 'Отношения между учителем и учеником', value: '687' }, + { label: 'Отношения начальник-подчинённый', value: '101' }, + { label: 'Отношения против воли', value: '286' }, + { label: 'Отношения с нечеловеком', value: '353' }, + { label: 'Отношения сэмпай-кохай', value: '621' }, + { label: 'Отношения хозяин-слуга', value: '431' }, + { label: 'Отомэ-игры', value: '504' }, + { label: 'ОТП', value: '562' }, + { label: 'Отшельники', value: '505' }, + { label: 'Офисный работник', value: '600' }, + { label: 'Официанты', value: '748' }, + { label: 'Охотники', value: '355' }, + { label: 'Падшие ангелы', value: '255' }, + { label: 'Падшие дворяне', value: '256' }, + { label: 'Пайзури', value: '511' }, + { label: 'Паразиты', value: '513' }, + { label: 'Параллельные миры', value: '512' }, + { label: 'Парк развлечений', value: '32' }, + { label: 'Пародия', value: '515' }, + { label: 'Перемещение между мирами', value: '758' }, + { label: 'Переодевание', value: '167' }, + { label: 'Переплавка пилюль', value: '529' }, + { label: 'Перерождение в девушку', value: '420' }, + { label: 'Перерождение в другом мире', value: '580' }, + { label: 'Перерождение в другом мире', value: '720' }, + { label: 'Перерождение в игровом мире', value: '579' }, + { label: 'Перерождение в монстра', value: '577' }, + { label: 'Персонажи с мазохистскими наклонностями', value: '429' }, + { label: 'Персонажи с садистскими наклонностями', value: '598' }, + { label: 'Петля времени', value: '709' }, + { label: 'Петтинг', value: '328' }, + { label: 'Писатели', value: '761' }, + { label: 'Питомцы', value: '522' }, + { label: 'Племенное общество', value: '726' }, + { label: 'Повелитель демонов', value: '191' }, + { label: 'Повествование от нескольких лиц', value: '459' }, + { label: 'Поглощение навыков', value: '653' }, + { label: 'Подземелья', value: '223' }, + { label: 'Поздно начинающаяся романтическая линия', value: '387' }, + { label: 'Политика', value: '538' }, + { label: 'Полулюди', value: '190' }, + { label: 'Помолвка', value: '240' }, + { label: 'Попаданец в другой мир', value: '723' }, + { label: 'Попаданец в игровой мир', value: '722' }, + { label: 'Постапокалипсис', value: '546' }, + { label: 'Постижение Дао', value: '181' }, + { label: 'Потеря девственности', value: '282' }, + { label: 'Потустороннее пространство', value: '416' }, + { label: 'Похищения', value: '379' }, + { label: 'Прагматичный главный герой', value: '549' }, + { label: 'Предательство', value: '83' }, + { label: 'Предвидение', value: '550' }, + { label: 'Преследователи', value: '674' }, + { label: 'Преступление', value: '165' }, + { label: 'Преступность', value: '166' }, + { label: 'Приёмные дети', value: '19' }, + { label: 'Призванный герой', value: '693' }, + { label: 'Призраки', value: '309' }, + { label: 'Приключения', value: '22' }, + { label: 'Принуждение', value: '108' }, + { label: 'Приручатель', value: '453' }, + { label: 'Прислуга', value: '625' }, + { label: 'Приставучий любовник', value: '136' }, + { label: 'Притворная любовь', value: '552' }, + { label: 'Пришельцы', value: '28' }, + { label: 'Программист', value: '558' }, + { label: 'Произведение - обладатель наград', value: '66' }, + { label: 'Проклятия', value: '175' }, + { label: 'Промывка мозгов', value: '102' }, + { label: 'Пропуск времени', value: '712' }, + { label: 'Пророчества', value: '559' }, + { label: 'Протагонист в очках', value: '312' }, + { label: 'Протагонист раб', value: '657' }, + { label: 'Психокинез', value: '565' }, + { label: 'Психопаты', value: '566' }, + { label: 'Путешествия', value: '759' }, + { label: 'Путешествия во времени', value: '713' }, + { label: 'Пытки', value: '716' }, + { label: 'Рабы', value: '658' }, + { label: 'Развитие отношений', value: '160' }, + { label: 'Развитие персонажа', value: '119' }, + { label: 'Раздвоение личности', value: '458' }, + { label: 'Размеренная романтика', value: '661' }, + { label: 'Разница в статусе', value: '201' }, + { label: 'Разорванная помолвка', value: '104' }, + { label: 'Разумные предметы', value: '622' }, + { label: 'Рай', value: '336' }, + { label: 'Раняя романтика', value: '227' }, + { label: 'Расизм', value: '573' }, + { label: 'Расследования', value: '372' }, + { label: 'Ребенок — главный герой', value: '126' }, + { label: 'Реверс-гарем', value: '589' }, + { label: 'Реверс-изнасилование', value: '590' }, + { label: 'Резкая смена характера', value: '520' }, + { label: 'Реинкарнация', value: '581' }, + { label: 'Религии', value: '582' }, + { label: 'Ресторан', value: '585' }, + { label: 'Решительный главный герой', value: '199' }, + { label: 'Робкий протагонист', value: '714' }, + { label: 'Родительсткий комплекс', value: '514' }, + { label: 'Родные не связанные кровью', value: '647' }, + { label: 'Родословные', value: '94' }, + { label: 'РПГ-система', value: '392' }, + { label: 'Рыцари', value: '383' }, + { label: 'Самовлюбленный главный герой', value: '477' }, + { label: 'Самоубийства', value: '692' }, + { label: 'Самураи', value: '601' }, + { label: 'Сборник коротких историй', value: '144' }, + { label: 'Свадьба', value: '426' }, + { label: 'Связанные сюжетные линии', value: '369' }, + { label: 'Святые', value: '599' }, + { label: 'Секреты', value: '614' }, + { label: 'Секс втроём', value: '707' }, + { label: 'Секс-рабыня', value: '629' }, + { label: 'Семейная любовь', value: '257' }, + { label: 'Семь добродетелей', value: '627' }, + { label: 'Семь смертных грехов', value: '626' }, + { label: 'Серийные убийцы', value: '624' }, + { label: 'Сикигами', value: '637' }, + { label: 'Сильная любовь от старших', value: '214' }, + { label: 'Синдром восьмиклассника', value: '132' }, + { label: 'Синдром сестры', value: '652' }, + { label: 'Сироты', value: '502' }, + { label: 'Системный администратор', value: '699' }, + { label: 'Скромный главный герой', value: '409' }, + { label: 'Скрытые романтические отношения', value: '23' }, + { label: 'Скрытые способности', value: '343' }, + { label: 'Скряга', value: '450' }, + { label: 'Скульпторы', value: '606' }, + { label: 'Слабая романтика', value: '688' }, + { label: 'Слабый главный герой', value: '751' }, + { label: 'Сложные семейные отношения', value: '149' }, + { label: 'Смена расы', value: '572' }, + { label: 'Смертельное заболевание', value: '704' }, + { label: 'Смерть', value: '185' }, + { label: 'Смерть любимых', value: '186' }, + { label: 'Современность', value: '448' }, + { label: 'Современные знания в слаборазвитых мирах', value: '449' }, + { label: 'Сожительство', value: '141' }, + { label: 'Создание артефактов', value: '57' }, + { label: 'Создание клана', value: '133' }, + { label: 'Создание королевства', value: '381' }, + { label: 'Создание навыков', value: '655' }, + { label: 'Сокрытие истинных способностей', value: '344' }, + { label: 'Сокрытие личности', value: '345' }, + { label: 'Солдаты', value: '664' }, + { label: 'Сон', value: '659' }, + { label: 'Соседи по комнате', value: '595' }, + { label: 'Социальные изгои', value: '663' }, + { label: 'Спасение мира', value: '602' }, + { label: 'Спорящая пара', value: '84' }, + { label: 'Способность перевоплощения', value: '719' }, + { label: 'Средневековье', value: '436' }, + { label: 'Стеснительные персонажи', value: '643' }, + { label: 'Стокгольмский синдром', value: '575' }, + { label: 'Стокгольмский синдром', value: '675' }, + { label: 'Стратег', value: '681' }, + { label: 'Стратегические битвы', value: '680' }, + { label: 'Стрелки', value: '325' }, + { label: 'Стрельба из лука', value: '50' }, + { label: 'Студенческий совет', value: '686' }, + { label: 'Судьба', value: '197' }, + { label: 'Суккубы', value: '689' }, + { label: 'Суперспособности', value: '669' }, + { label: 'Суровая тренировка', value: '332' }, + { label: 'Сцены насилия', value: '195' }, + { label: 'Таинственное прошлое', value: '472' }, + { label: 'Тайная личность', value: '610' }, + { label: 'Тайные организации', value: '611' }, + { label: 'Телохранители', value: '98' }, + { label: 'Тентакли', value: '703' }, + { label: 'Террористы', value: '705' }, + { label: 'Технологический разрыв', value: '702' }, + { label: 'Тихие герои', value: '568' }, + { label: 'Толстый протагонист', value: '271' }, + { label: 'Торговцы', value: '438' }, + { label: 'Травник', value: '340' }, + { label: 'Трагичное прошлое', value: '718' }, + { label: 'Трап', value: '725' }, + { label: 'Триллер', value: '708' }, + { label: 'Турнир', value: '76' }, + { label: 'Тюрьма', value: '556' }, + { label: 'Убийства', value: '465' }, + { label: 'Уверенный в себе главный герой', value: '151' }, + { label: 'Умная пара', value: '662' }, + { label: 'Умный главный герой', value: '135' }, + { label: 'Уникальная техника культивации', value: '735' }, + { label: 'Уникальные оружия', value: '737' }, + { label: 'Управление бизнесом', value: '109' }, + { label: 'Управление временем', value: '710' }, + { label: 'Управление пространством', value: '667' }, + { label: 'Ускоренный рост', value: '6' }, + { label: 'Учёные', value: '605' }, + { label: 'Учителя', value: '700' }, + { label: 'Фамильяры', value: '258' }, + { label: 'Фанатизм', value: '264' }, + { label: 'Фармацевт', value: '523' }, + { label: 'Феи', value: '254' }, + { label: 'Фениксы', value: '526' }, + { label: 'Фетиш на грудь', value: '103' }, + { label: 'Фетиш на очки', value: '311' }, + { label: 'Философия', value: '524' }, + { label: 'Фобии/Страхи', value: '525' }, + { label: 'Фольклор', value: '285' }, + { label: 'Формирование армии', value: '54' }, + { label: 'Футанари', value: '295' }, + { label: 'Футуристический мир', value: '296' }, + { label: 'Фэнтези', value: '267' }, + { label: 'Фэнтезийные существа', value: '266' }, + { label: 'Хакеры', value: '326' }, + { label: 'Харизматичный протагонист', value: '120' }, + { label: 'Хитрый главный герой', value: '173' }, + { label: 'Хладнокровный главный герой', value: '112' }, + { label: 'Художники', value: '60' }, + { label: 'Хулиганы', value: '188' }, + { label: 'Цундере', value: '728' }, + { label: 'Чат', value: '122' }, + { label: 'Человек-оружие', value: '352' }, + { label: 'Человекоподобный главный герой', value: '354' }, + { label: 'Честный главный герой', value: '348' }, + { label: 'Четкие любовные убеждения', value: '683' }, + { label: 'Читы', value: '123' }, + { label: 'Чудаковатые герои', value: '569' }, + { label: 'Чужие воспоминания', value: '721' }, + { label: 'Шантаж', value: '89' }, + { label: 'Шеф-повар', value: '124' }, + { label: 'Школа для девочек', value: '29' }, + { label: 'Шоу-бизнес', value: '642' }, + { label: 'Шпионы', value: '670' }, + { label: 'Эволюция', value: '250' }, + { label: 'Эгоистичный главный герой', value: '618' }, + { label: 'Экзорцизм', value: '252' }, + { label: 'Экономика', value: '230' }, + { label: 'Эксгибиционизм', value: '251' }, + { label: 'Эксперименты на людях', value: '351' }, + { label: 'Элементальная магия', value: '234' }, + { label: 'Элементы игрового мира', value: '299' }, + { label: 'Элементы романтики', value: '594' }, + { label: 'Элементы юри', value: '640' }, + { label: 'Элементы яоя', value: '641' }, + { label: 'Эльфы', value: '235' }, + { label: 'Эпизодичность', value: '243' }, + { label: 'Язвительные персонажи', value: '635' }, + { label: 'Языковой барьер', value: '386' }, + { label: 'Яндере', value: '762' }, + { label: 'Яойщица', value: '294' }, + { label: 'Японские силы самообороны', value: '378' }, + { label: 'R-15', value: '570' }, + { label: 'R-18', value: '571' }, ], type: FilterTypes.CheckboxGroup, }, genres: { - label: "Жанры", + label: 'Жанры', value: [], options: [ - { label: "16+", value: "1" }, - { label: "18+", value: "2" }, - { label: "Боевик", value: "3" }, - { label: "Боевые искусства", value: "4" }, - { label: "Вампиры", value: "47" }, - { label: "Военное", value: "44" }, - { label: "Гарем", value: "5" }, - { label: "Демоны", value: "48" }, - { label: "Детектив", value: "6" }, - { label: "Дзёсей", value: "50" }, - { label: "Драма", value: "7" }, - { label: "Игры", value: "45" }, - { label: "Историческое", value: "8" }, - { label: "Киберпанк", value: "9" }, - { label: "Комедия", value: "10" }, - { label: "Космос", value: "46" }, - { label: "Магия", value: "42" }, - { label: "Махо-сёдзё", value: "12" }, - { label: "Меха", value: "13" }, - { label: "Мистика", value: "14" }, - { label: "Музыка", value: "49" }, - { label: "Научная фантастика", value: "15" }, - { label: "Пародия", value: "16" }, - { label: "Повседневность", value: "17" }, - { label: "Приключения", value: "18" }, - { label: "Психологическое", value: "19" }, - { label: "Романтика", value: "20" }, - { label: "Сверхъестественное", value: "21" }, - { label: "Сёдзё", value: "22" }, - { label: "Сёдзё-ай", value: "23" }, - { label: "Сёнэн", value: "24" }, - { label: "Сёнэн-ай", value: "25" }, - { label: "Смена пола", value: "26" }, - { label: "Современность", value: "27" }, - { label: "Спорт", value: "28" }, - { label: "Супер сила", value: "43" }, - { label: "Сэйнэн", value: "29" }, - { label: "Трагедия", value: "32" }, - { label: "Триллер", value: "33" }, - { label: "Ужасы", value: "34" }, - { label: "Уся", value: "35" }, - { label: "Фэнтези", value: "36" }, - { label: "Школьная жизнь", value: "37" }, - { label: "Этти", value: "38" }, - { label: "Юри", value: "39" }, - { label: "Яой", value: "40" }, - { label: "LitRPG", value: "41" }, + { label: '16+', value: '1' }, + { label: '18+', value: '2' }, + { label: 'Боевик', value: '3' }, + { label: 'Боевые искусства', value: '4' }, + { label: 'Вампиры', value: '47' }, + { label: 'Военное', value: '44' }, + { label: 'Гарем', value: '5' }, + { label: 'Демоны', value: '48' }, + { label: 'Детектив', value: '6' }, + { label: 'Дзёсей', value: '50' }, + { label: 'Драма', value: '7' }, + { label: 'Игры', value: '45' }, + { label: 'Историческое', value: '8' }, + { label: 'Киберпанк', value: '9' }, + { label: 'Комедия', value: '10' }, + { label: 'Космос', value: '46' }, + { label: 'Магия', value: '42' }, + { label: 'Махо-сёдзё', value: '12' }, + { label: 'Меха', value: '13' }, + { label: 'Мистика', value: '14' }, + { label: 'Музыка', value: '49' }, + { label: 'Научная фантастика', value: '15' }, + { label: 'Пародия', value: '16' }, + { label: 'Повседневность', value: '17' }, + { label: 'Приключения', value: '18' }, + { label: 'Психологическое', value: '19' }, + { label: 'Романтика', value: '20' }, + { label: 'Сверхъестественное', value: '21' }, + { label: 'Сёдзё', value: '22' }, + { label: 'Сёдзё-ай', value: '23' }, + { label: 'Сёнэн', value: '24' }, + { label: 'Сёнэн-ай', value: '25' }, + { label: 'Смена пола', value: '26' }, + { label: 'Современность', value: '27' }, + { label: 'Спорт', value: '28' }, + { label: 'Супер сила', value: '43' }, + { label: 'Сэйнэн', value: '29' }, + { label: 'Трагедия', value: '32' }, + { label: 'Триллер', value: '33' }, + { label: 'Ужасы', value: '34' }, + { label: 'Уся', value: '35' }, + { label: 'Фэнтези', value: '36' }, + { label: 'Школьная жизнь', value: '37' }, + { label: 'Этти', value: '38' }, + { label: 'Юри', value: '39' }, + { label: 'Яой', value: '40' }, + { label: 'LitRPG', value: '41' }, ], type: FilterTypes.CheckboxGroup, }, diff --git a/plugins/russian/ranobehub.ts b/plugins/russian/ranobehub.ts index c153211..36c4927 100644 --- a/plugins/russian/ranobehub.ts +++ b/plugins/russian/ranobehub.ts @@ -1,9 +1,9 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; const statusKey: { [key: number]: string } = { 1: NovelStatus.Ongoing, @@ -12,11 +12,11 @@ const statusKey: { [key: number]: string } = { }; class RNBH implements Plugin.PluginBase { - id = "RNBH.org"; - name = "RanobeHub"; - version = "1.0.0"; - site = "https://ranobehub.org"; - icon = "src/ru/ranobehub/icon.png"; + id = 'RNBH.org'; + name = 'RanobeHub'; + version = '1.0.0'; + site = 'https://ranobehub.org'; + icon = 'src/ru/ranobehub/icon.png'; async popularNovels( pageNo: number, @@ -25,15 +25,15 @@ class RNBH implements Plugin.PluginBase { filters, }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/api/search?page=" + pageNo + "&sort="; + let url = this.site + '/api/search?page=' + pageNo + '&sort='; url += showLatestNovels - ? "last_chapter_at" - : filters?.sort?.value || "computed_rating"; - url += "&status=" + (filters?.status?.value ? filters?.status?.value : "0"); + ? 'last_chapter_at' + : filters?.sort?.value || 'computed_rating'; + url += '&status=' + (filters?.status?.value ? filters?.status?.value : '0'); if (filters) { if (filters.country?.value?.length) { - url += "&country=" + filters.country.value.join(","); + url += '&country=' + filters.country.value.join(','); } const includeTags = [ @@ -41,10 +41,10 @@ class RNBH implements Plugin.PluginBase { filters.events?.value?.include, ] .flat() - .filter((t) => t); + .filter(t => t); if (includeTags.length) { - url += "&tags:positive=" + includeTags.join(","); + url += '&tags:positive=' + includeTags.join(','); } const excludeTags = [ @@ -52,18 +52,18 @@ class RNBH implements Plugin.PluginBase { filters.events?.value?.exclude, ] .flat() - .filter((t) => t); + .filter(t => t); if (excludeTags.length) { - url += "&tags:negative=" + excludeTags.join(","); + url += '&tags:negative=' + excludeTags.join(','); } } - url += "&take=40"; + url += '&take=40'; const result = await fetchApi(url); const body = (await result.json()) as { resource: responseNovels[] }; const novels: Plugin.NovelItem[] = []; - body.resource.forEach((novel) => + body.resource.forEach(novel => novels.push({ name: novel.names.rus || novel.names.eng || novel.names.original, cover: novel.poster.medium, @@ -75,47 +75,49 @@ class RNBH implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.site + "/api/ranobe/" + novelPath); + const result = await fetchApi(this.site + '/api/ranobe/' + novelPath); const json = (await result.json()) as { data: responseNovel }; const novel: Plugin.SourceNovel = { path: novelPath, - name: json.data.names.rus || json.data.names.eng || "", + name: json.data.names.rus || json.data.names.eng || '', cover: json.data.posters.medium, summary: json.data.description.trim(), - author: json.data?.authors?.[0]?.name_eng || "", + author: json.data?.authors?.[0]?.name_eng || '', status: statusKey[json.data.status.id] || NovelStatus.Unknown, }; const tags = [json.data.tags.events, json.data.tags.genres] .flat() - .map((tags) => tags?.names?.rus || tags?.names?.eng || tags?.title) - .filter((tags) => tags); + .map(tags => tags?.names?.rus || tags?.names?.eng || tags?.title) + .filter(tags => tags); if (tags.length) { - novel.genres = tags.join(", "); + novel.genres = tags.join(', '); } const chapters: Plugin.ChapterItem[] = []; const chaptersRaw = await fetchApi( - this.site + "/api/ranobe/" + novelPath + "/contents", + this.site + '/api/ranobe/' + novelPath + '/contents', ); const chaptersJSON = (await chaptersRaw.json()) as { volumes: VolumesEntity[]; }; - chaptersJSON.volumes.forEach((volume) => - volume.chapters?.forEach((chapter) => + chaptersJSON.volumes.forEach(volume => + volume.chapters?.forEach(chapter => chapters.push({ name: chapter.name, path: volume.num && chapter.num - ? novelPath + "/" + volume.num + "/" + chapter.num - : chapter.url.replace(this.site, ""), - releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format("LLL"), + ? novelPath + '/' + volume.num + '/' + chapter.num + : chapter.url.replace(this.site, ''), + releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format( + 'LLL', + ), chapterNumber: chapters.length + 1, - }) - ) + }), + ), ); novel.chapters = chapters; @@ -127,20 +129,23 @@ class RNBH implements Plugin.PluginBase { const body = await result.text(); const loadedCheerio = parseHTML(body); - loadedCheerio(".chapter-hoticons").remove(); - loadedCheerio("div.text:nth-child(1) img").each((index, element) => { - if (!loadedCheerio(element).attr("src")?.startsWith("http")) { - const dataMediaId = loadedCheerio(element).attr("data-media-id"); - loadedCheerio(element).attr("src", this.site + "/api/media/" + dataMediaId); + loadedCheerio('.chapter-hoticons').remove(); + loadedCheerio('div.text:nth-child(1) img').each((index, element) => { + if (!loadedCheerio(element).attr('src')?.startsWith('http')) { + const dataMediaId = loadedCheerio(element).attr('data-media-id'); + loadedCheerio(element).attr( + 'src', + this.site + '/api/media/' + dataMediaId, + ); } }); - let chapterText = loadedCheerio("div.text:nth-child(1)").html() || ""; + let chapterText = loadedCheerio('div.text:nth-child(1)').html() || ''; // Remove script tags chapterText = chapterText?.replace( /<\s*script[^>]*>[\s\S]*?<\/script>/gim, - "", + '', ); return chapterText; } @@ -152,17 +157,17 @@ class RNBH implements Plugin.PluginBase { const novels: Plugin.NovelItem[] = []; data - ?.find((item) => item?.meta?.key === "ranobe") - ?.data?.forEach((novel) => + ?.find(item => item?.meta?.key === 'ranobe') + ?.data?.forEach(novel => novels.push({ name: novel?.names?.rus || novel?.names?.eng || novel.name || novel?.names?.original || - "", + '', path: novel.id.toString(), - cover: novel?.image?.replace("/small", "/medium"), + cover: novel?.image?.replace('/small', '/medium'), }), ); @@ -170,963 +175,985 @@ class RNBH implements Plugin.PluginBase { } fetchImage = fetchFile; - resolveUrl = (path: string, isNovel?: boolean) => this.site + "/ranobe/" + path; + resolveUrl = (path: string, isNovel?: boolean) => + this.site + '/ranobe/' + path; filters = { sort: { - label: "Сортировка", - value: "computed_rating", + label: 'Сортировка', + value: 'computed_rating', options: [ - { label: "по рейтингу", value: "computed_rating" }, - { label: "по дате обновления", value: "last_chapter_at" }, - { label: "по дате добавления", value: "created_at" }, - { label: "по названию", value: "name_rus" }, - { label: "по просмотрам", value: "views" }, - { label: "по количеству глав", value: "count_chapters" }, - { label: "по объему перевода", value: "count_of_symbols" }, + { label: 'по рейтингу', value: 'computed_rating' }, + { label: 'по дате обновления', value: 'last_chapter_at' }, + { label: 'по дате добавления', value: 'created_at' }, + { label: 'по названию', value: 'name_rus' }, + { label: 'по просмотрам', value: 'views' }, + { label: 'по количеству глав', value: 'count_chapters' }, + { label: 'по объему перевода', value: 'count_of_symbols' }, ], type: FilterTypes.Picker, }, status: { - label: "Статус перевода", - value: "", + label: 'Статус перевода', + value: '', options: [ - { label: "Любой", value: "" }, - { label: "В процессе", value: "1" }, - { label: "Завершено", value: "2" }, - { label: "Заморожено", value: "3" }, - { label: "Неизвестно", value: "4" }, + { label: 'Любой', value: '' }, + { label: 'В процессе', value: '1' }, + { label: 'Завершено', value: '2' }, + { label: 'Заморожено', value: '3' }, + { label: 'Неизвестно', value: '4' }, ], type: FilterTypes.Picker, }, country: { - label: "Тип", + label: 'Тип', value: [], options: [ - { label: "Китай", value: "2" }, - { label: "Корея", value: "3" }, - { label: "США", value: "4" }, - { label: "Япония", value: "1" }, + { label: 'Китай', value: '2' }, + { label: 'Корея', value: '3' }, + { label: 'США', value: '4' }, + { label: 'Япония', value: '1' }, ], type: FilterTypes.CheckboxGroup, }, events: { - label: "События", + label: 'События', value: { include: [], exclude: [] }, options: [ - { label: "[Награжденная работа]", value: "611" }, - { label: "18+", value: "338" }, - { label: "Авантюристы", value: "353" }, - { label: "Автоматоны", value: "538" }, - { label: "Агрессивные персонажи", value: "434" }, - { label: "Ад", value: "509" }, - { label: "Адаптация в радиопостановку", value: "522" }, - { label: "Академия", value: "25" }, - { label: "Актеры озвучки", value: "578" }, - { label: "Активный главный герой", value: "132" }, - { label: "Алхимия", value: "116" }, - { label: "Альтернативный мир", value: "28" }, - { label: "Амнезия/Потеря памяти", value: "247" }, - { label: "Анабиоз", value: "657" }, - { label: "Ангелы", value: "218" }, - { label: "Андрогинные персонажи", value: "217" }, - { label: "Андроиды", value: "82" }, - { label: "Анти-магия", value: "471" }, - { label: "Антигерой", value: "346" }, - { label: "Антикварный магазин", value: "572" }, - { label: "Антисоциальный главный герой", value: "562" }, - { label: "Антиутопия", value: "663" }, - { label: "Апатичный протагонист", value: "29" }, - { label: "Апокалипсис", value: "314" }, - { label: "Аранжированный брак", value: "285" }, - { label: "Армия", value: "598" }, - { label: "Артефакты", value: "117" }, - { label: "Артисты", value: "460" }, - { label: "Банды", value: "581" }, - { label: "БДСМ", value: "676" }, - { label: "Бедный главный герой", value: "309" }, - { label: "Безжалостный главный герой", value: "144" }, - { label: "Беззаботный главный герой", value: "355" }, - { label: "Безусловная любовь", value: "650" }, - { label: "Беременность", value: "131" }, - { label: "Бесполый главный герой", value: "222" }, - { label: "Бессмертные", value: "275" }, - { label: "Бесстрашный протагонист", value: "619" }, - { label: "Бесстыдный главный герой", value: "256" }, - { label: "Бесчестный главный герой", value: "699" }, - { label: "Библиотека", value: "342" }, - { label: "Бизнесмен ", value: "813" }, - { label: "Биочип", value: "120" }, - { label: "Бисексуальный главный герой", value: "822" }, - { label: "Близнецы", value: "148" }, - { label: "Боги", value: "211" }, - { label: "Богини", value: "356" }, - { label: "Боевая академия", value: "369" }, - { label: "Боевые духи", value: "347" }, - { label: "Боевые соревнования", value: "422" }, - { label: "Божественная защита", value: "336" }, - { label: "Божественные силы", value: "224" }, - { label: "Большая разница в возрасте между героем и его любовным интересом", value: "348" }, - { label: "Борьба за власть", value: "544" }, - { label: "Брак", value: "363" }, - { label: "Брак по расчету", value: "65" }, - { label: "Братский комплекс", value: "31" }, - { label: "Братство", value: "413" }, - { label: "Братья и сестры", value: "518" }, - { label: "Буддизм", value: "742" }, - { label: "Быстрая культивация", value: "273" }, - { label: "Быстрообучаемый", value: "221" }, - { label: "Валькирии", value: "667" }, - { label: "Вампиры", value: "266" }, - { label: "Ваншот", value: "679" }, - { label: "Ведьмы", value: "169" }, - { label: "Вежливый главный герой", value: "289" }, - { label: "Верные подчиненные", value: "225" }, - { label: "Взрослый главный герой", value: "183" }, - { label: "Видит то, чего не видят другие", value: "636" }, - { label: "Виртуальная реальность", value: "313" }, - { label: "Владелец магазина", value: "653" }, - { label: "Внезапная сила", value: "376" }, - { label: "Внезапное богатство", value: "802" }, - { label: "Внешний вид отличается от фактического возраста", value: "334" }, - { label: "Военные Летописи", value: "740" }, - { label: "Возвращение из другого мира", value: "673" }, - { label: "Войны", value: "58" }, - { label: "Вокалоид", value: "678" }, - { label: "Волшебники/Волшебницы", value: "477" }, - { label: "Волшебные звери", value: "201" }, - { label: "Воображаемый друг", value: "614" }, - { label: "Воры", value: "326" }, - { label: "Воскрешение", value: "78" }, - { label: "Враги становятся возлюбленными", value: "428" }, - { label: "Враги становятся союзниками", value: "502" }, - { label: "Врата в другой мир", value: "558" }, - { label: "Врачи", value: "286" }, - { label: "Временной парадокс", value: "163" }, - { label: "Всемогущий главный герой", value: "42" }, - { label: "Вторжение на землю", value: "77" }, - { label: "Второй шанс", value: "112" }, - { label: "Вуайеризм", value: "851" }, - { label: "Выживание", value: "290" }, - { label: "Высокомерные персонажи", value: "268" }, - { label: "Гадание", value: "540" }, - { label: "Гарем рабов", value: "828" }, - { label: "Геймеры", value: "302" }, - { label: "Генералы", value: "223" }, - { label: "Генетические модификации", value: "620" }, - { label: "Гениальный главный герой", value: "566" }, - { label: "Герои", value: "173" }, - { label: "Героиня — сорванец", value: "525" }, - { label: "Герой влюбляется первым", value: "64" }, - { label: "Гетерохромия", value: "510" }, - { label: "Гильдии", value: "323" }, - { label: "Гипнотизм", value: "768" }, - { label: "Главный герой — бог", value: "486" }, - { label: "Главный герой — гуманоид", value: "595" }, - { label: "Главный герой — женщина", value: "63" }, - { label: "Главный герой — мужчина", value: "39" }, - { label: "Главный герой — наполовину человек", value: "362" }, - { label: "Главный герой — отец", value: "859" }, - { label: "Главный герой — раб", value: "832" }, - { label: "Главный герой — ребенок", value: "415" }, - { label: "Главный герой — рубака", value: "400" }, - { label: "Главный герой — собиратель гарема", value: "439" }, - { label: "Главный герой влюбляется первым", value: "655" }, - { label: "Главный герой играет роль", value: "396" }, - { label: "Главный герой носит очки", value: "637" }, - { label: "Главный герой пацифист", value: "675" }, - { label: "Главный герой с несколькими телами", value: "628" }, - { label: "Главный герой силен с самого начала", value: "45" }, - { label: "Гладиаторы", value: "549" }, - { label: "Глуповатый главный герой", value: "295" }, - { label: "Гоблины", value: "529" }, - { label: "Големы", value: "569" }, - { label: "Гомункул", value: "850" }, - { label: "Горничные", value: "380" }, - { label: "Госпиталь", value: "1043" }, - { label: "Готовка", value: "193" }, - { label: "Гриндинг", value: "303" }, - { label: "Дао Компаньон", value: "384" }, - { label: "Даосизм", value: "792" }, - { label: "Дарк", value: "151" }, - { label: "Дварфы", value: "220" }, - { label: "Двойная личность", value: "601" }, - { label: "Двойник", value: "547" }, - { label: "Дворецкий", value: "623" }, - { label: "Дворяне", value: "41" }, - { label: "Дворянство/Аристократия", value: "354" }, - { label: "Девушки-монстры", value: "634" }, - { label: "Демоническая техника культивации", value: "706" }, - { label: "Демоны", value: "6" }, - { label: "Денежный долг", value: "841" }, - { label: "Депрессия", value: "494" }, - { label: "Детективы", value: "561" }, - { label: "Дискриминация", value: "34" }, - { label: "Добыча денег одно из основных стремлений главного героя", value: "307" }, - { label: "Долгая разлука", value: "200" }, - { label: "Домашние дела", value: "178" }, - { label: "Домогательство", value: "394" }, - { label: "Драконы", value: "195" }, - { label: "Драконьи всадники", value: "555" }, - { label: "Древние времена", value: "102" }, - { label: "Древний Китай", value: "284" }, - { label: "Дружба", value: "97" }, - { label: "Друзья детства", value: "170" }, - { label: "Друзья становятся врагами", value: "507" }, - { label: "Друиды", value: "427" }, - { label: "Дух лисы", value: "842" }, - { label: "Духи/Призраки", value: "46" }, - { label: "Духовный советник", value: "351" }, - { label: "Душевность", value: "587" }, - { label: "Души", value: "136" }, - { label: "Европейская атмосфера", value: "457" }, - { label: "Ёкаи", value: "516" }, - { label: "Есть аниме-адаптация", value: "26" }, - { label: "Есть видеоигра по мотивам", value: "491" }, - { label: "Есть манга-адаптация", value: "27" }, - { label: "Есть манхва-адаптация", value: "453" }, - { label: "Есть маньхуа-адаптация", value: "298" }, - { label: "Есть сериал-адаптация", value: "421" }, - { label: "Есть фильм по мотивам", value: "47" }, - { label: "Женища-наставник", value: "438" }, - { label: "Жертва изнасилования влюбляется в насильника", value: "414" }, - { label: "Жесткая, двуличная личность", value: "249" }, - { label: "Жестокие персонажи", value: "436" }, - { label: "Жестокое обращение с ребенком", value: "617" }, - { label: "Жестокость", value: "127" }, - { label: "Животноводство", value: "765" }, - { label: "Животные черты", value: "466" }, - { label: "Жизнь в квартире", value: "554" }, - { label: "Жрицы", value: "564" }, - { label: "Заботливый главный герой", value: "176" }, - { label: "Забывчивый главный герой", value: "579" }, - { label: "Заговоры", value: "177" }, - { label: "Закалка тела", value: "269" }, - { label: "Законники", value: "769" }, - { label: "Замкнутый главный герой", value: "533" }, - { label: "Запечатанная сила", value: "344" }, - { label: "Застенчивые персонажи", value: "443" }, - { label: "Звери", value: "119" }, - { label: "Звери-компаньоны", value: "192" }, - { label: "Злой протагонист", value: "125" }, - { label: "Злые боги", value: "437" }, - { label: "Злые организации", value: "503" }, - { label: "Злые религии", value: "725" }, - { label: "Знаменитости", value: "397" }, - { label: "Знаменитый главный герой", value: "469" }, - { label: "Знания современного мира", value: "185" }, - { label: "Зомби", value: "321" }, - { label: "Игра на выживание", value: "162" }, - { label: "Игривый протагонист", value: "700" }, - { label: "Игровая система рейтинга", value: "301" }, - { label: "Игровые элементы", value: "152" }, - { label: "Игрушки (18+)", value: "644" }, - { label: "Из грязи в князи", value: "330" }, - { label: "Из женщины в мужчину ", value: "688" }, - { label: "Из мужчины в женщину", value: "698" }, - { label: "Из полного в худого", value: "809" }, - { label: "Из слабого в сильного", value: "81" }, - { label: "Из страшно(го/й) в красиво(го/ю)", value: "810" }, - { label: "Извращенный главный герой", value: "349" }, - { label: "Изгои", value: "472" }, - { label: "Изменение расы", value: "627" }, - { label: "Изменения внешнего вида", value: "191" }, - { label: "Изменения личности", value: "99" }, - { label: "Изнасилование", value: "110" }, - { label: "Изображения жестокости", value: "260" }, - { label: "Империи", value: "749" }, - { label: "Инвалидность", value: "656" }, - { label: "Индустриализация", value: "180" }, - { label: "Инженер", value: "35" }, - { label: "Инцест", value: "463" }, - { label: "Искусственный интеллект", value: "118" }, - { label: "Исследования", value: "635" }, - { label: "Каннибализм", value: "377" }, - { label: "Карточные игры", value: "654" }, - { label: "Киберспорт", value: "658" }, - { label: "Кланы", value: "950" }, - { label: "Класс безработного [Игровой класс в игре]", value: "727" }, - { label: "Клоны", value: "365" }, - { label: "Клубы", value: "96" }, - { label: "Книги", value: "341" }, - { label: "Книги навыков", value: "312" }, - { label: "Книжный червь", value: "455" }, - { label: "Коварство", value: "111" }, - { label: "Коллеги", value: "683" }, - { label: "Колледж/Университет", value: "712" }, - { label: "Кома", value: "826" }, - { label: "Командная работа", value: "426" }, - { label: "Комедийный оттенок", value: "523" }, - { label: "Комплекс неполноценности", value: "489" }, - { label: "Комплекс семейных отношений", value: "104" }, - { label: "Конкуренция", value: "746" }, - { label: "Контракты", value: "483" }, - { label: "Контроль разума/сознания", value: "262" }, - { label: "Копейщик", value: "339" }, - { label: "Королевская битва", value: "1025" }, - { label: "Королевская власть", value: "53" }, - { label: "Королевства", value: "141" }, - { label: "Коррупция", value: "378" }, - { label: "Космические войны", value: "674" }, - { label: "Красивый герой", value: "107" }, - { label: "Крафт", value: "287" }, - { label: "Кризис личности", value: "599" }, - { label: "Кругосветное путешествие", value: "257" }, - { label: "Кудере", value: "440" }, - { label: "Кузены", value: "701" }, - { label: "Кузнец", value: "454" }, - { label: "Кукловоды", value: "431" }, - { label: "Куклы/марионетки", value: "573" }, - { label: "Культивация", value: "123" }, - { label: "Куннилингус", value: "632" }, - { label: "Легенды", value: "430" }, - { label: "Легкая жизнь", value: "604" }, - { label: "Ленивый главный герой", value: "570" }, - { label: "Лидерство", value: "424" }, - { label: "Лоли", value: "92" }, - { label: "Лотерея", value: "401" }, - { label: "Любовный интерес влюбляется первым", value: "647" }, - { label: "Любовный интерес главного героя носит очки", value: "576" }, - { label: "Любовный треугольник", value: "98" }, - { label: "Любовь детства", value: "500" }, - { label: "Любовь с первого взгляда", value: "730" }, - { label: "Магические надписи", value: "373" }, - { label: "Магические печати", value: "277" }, - { label: "Магические технологии", value: "357" }, - { label: "Магическое пространство/измерение, доступное не всем персонажам", value: "244" }, - { label: "Магия", value: "38" }, - { label: "Магия призыва", value: "333" }, - { label: "Мазохистские персонажи", value: "660" }, - { label: "Манипулятивные персонажи", value: "130" }, - { label: "Мания", value: "91" }, - { label: "Мастер на все руки", value: "390" }, - { label: "Мастурбация", value: "846" }, - { label: "Махо-сёдзё", value: "496" }, - { label: "Медицинские знания", value: "441" }, - { label: "Медленная романтическая линия", value: "113" }, - { label: "Медленное развитие на старте ", value: "670" }, - { label: "Межпространственные путешествия", value: "316" }, - { label: "Менеджмент", value: "182" }, - { label: "Мертвый главный герой", value: "794" }, - { label: "Месть", value: "88" }, - { label: "Метаморфы", value: "234" }, - { label: "Меч и магия", value: "55" }, - { label: "Мечник", value: "607" }, - { label: "Мечты", value: "733" }, - { label: "Милая история", value: "709" }, - { label: "Милое дитя", value: "596" }, - { label: "Милый главный герой", value: "697" }, - { label: "Мировое дерево", value: "385" }, - { label: "Мистический ореол вокруг семьи", value: "409" }, - { label: "Мифические звери", value: "418" }, - { label: "Мифология", value: "468" }, - { label: "Младшие братья", value: "843" }, - { label: "Младшие сестры", value: "465" }, - { label: "ММОРПГ (ЛитРПГ)", value: "306" }, - { label: "Множество перемещенных людей", value: "488" }, - { label: "Множество реальностей", value: "278" }, - { label: "Множество реинкарнированных людей", value: "227" }, - { label: "Модели", value: "649" }, - { label: "Молчаливый персонаж", value: "705" }, - { label: "Монстры", value: "69" }, - { label: "Мужская гей-пара", value: "685" }, - { label: "Мужчина-яндере", value: "155" }, - { label: "Музыка", value: "589" }, - { label: "Музыкальные группы", value: "588" }, - { label: "Мутации", value: "668" }, - { label: "Мутированные существа", value: "317" }, - { label: "Навык кражи", value: "626" }, - { label: "Навязчивая любовь", value: "85" }, - { label: "Наемники", value: "324" }, - { label: "Назойливый возлюбленный", value: "501" }, - { label: "Наивный главный герой", value: "66" }, - { label: "Наркотики", value: "661" }, - { label: "Нарциссический главный герой", value: "470" }, - { label: "Насилие сексуального характера", value: "824" }, - { label: "Наследование", value: "372" }, - { label: "Национализм", value: "318" }, - { label: "Не блещущий внешне главный герой", value: "328" }, - { label: "Не родные братья и сестры", value: "464" }, - { label: "Небеса", value: "508" }, - { label: "Небесное испытание", value: "274" }, - { label: "Негуманоидный главный герой", value: "622" }, - { label: "Недоверчивый главный герой", value: "485" }, - { label: "Недооцененный главный герой", value: "140" }, - { label: "Недоразумения", value: "202" }, - { label: "Неизлечимая болезнь", value: "75" }, - { label: "Некромант", value: "308" }, - { label: "Нелинейная история", value: "157" }, - { label: "Ненавистный главный герой", value: "487" }, - { label: "Ненадежный рассказчик", value: "165" }, - { label: "Нерезиденты", value: "821" }, - { label: "Нерешительный главный герой", value: "741" }, - { label: "Несерьезный главный герой", value: "294" }, - { label: "Несколько временных линий", value: "517" }, - { label: "Несколько главных героев", value: "475" }, - { label: "Несколько идентичностей", value: "615" }, - { label: "Несколько личностей", value: "474" }, - { label: "Нетораре", value: "721" }, - { label: "Нетори", value: "450" }, - { label: "Неудачливый главный герой", value: "567" }, - { label: "Ниндзя", value: "358" }, - { label: "Обещание из детства", value: "738" }, - { label: "Обманщик", value: "411" }, - { label: "Обмен телами", value: "94" }, - { label: "Обнаженка", value: "417" }, - { label: "Обольщение", value: "718" }, - { label: "Оборотни", value: "624" }, - { label: "Обратный гарем", value: "255" }, - { label: "Общество монстров", value: "226" }, - { label: "Обязательство", value: "548" }, - { label: "Огнестрельное оружие", value: "179" }, - { label: "Ограниченная продолжительность жизни", value: "73" }, - { label: "Одержимость", value: "447" }, - { label: "Одинокий главный герой", value: "304" }, - { label: "Одиночество", value: "199" }, - { label: "Одиночное проживание", value: "207" }, - { label: "Околосмертные переживания", value: "773" }, - { label: "Оммёдзи", value: "542" }, - { label: "Омоложение", value: "237" }, - { label: "Организованная преступность", value: "478" }, - { label: "Оргия", value: "825" }, - { label: "Орки", value: "228" }, - { label: "Освоение навыков", value: "235" }, - { label: "Основано на аниме", value: "553" }, - { label: "Основано на видео игре", value: "704" }, - { label: "Основано на визуальной новелле ", value: "861" }, - { label: "Основано на песне", value: "677" }, - { label: "Основано на фильме", value: "552" }, - { label: "Осторожный главный герой", value: "122" }, - { label: "Отаку", value: "512" }, - { label: "Открытый космос", value: "150" }, - { label: "Отношения в сети", value: "713" }, - { label: "Отношения между богом и человеком", value: "603" }, - { label: "Отношения между людьми и нелюдьми", value: "205" }, - { label: "Отношения на расстоянии", value: "852" }, - { label: "Отношения начальник-подчиненный", value: "590" }, - { label: "Отношения Сенпай-Коухай", value: "513" }, - { label: "Отношения ученика и учителя", value: "451" }, - { label: "Отношения учитель-ученик", value: "343" }, - { label: "Отношения хозяин-слуга", value: "206" }, - { label: "Отомэ игра", value: "723" }, - { label: "Отсутствие здравого смысла", value: "605" }, - { label: "Отсутствие родителей", value: "575" }, - { label: "Офисный роман", value: "686" }, - { label: "Официанты", value: "681" }, - { label: "Охотники", value: "288" }, - { label: "Очаровательный главный герой", value: "659" }, - { label: "Падшее дворянство", value: "618" }, - { label: "Падшие ангелы", value: "505" }, - { label: "Пайзури", value: "630" }, - { label: "Паразиты", value: "535" }, - { label: "Параллельные миры", value: "86" }, - { label: "Парк развлечений", value: "586" }, - { label: "Пародия", value: "319" }, - { label: "Певцы/Певицы", value: "734" }, - { label: "Первая любовь", value: "716" }, - { label: "Первоисточник новеллы — манга", value: "560" }, - { label: "Первый раз", value: "845" }, - { label: "Перемещение в другой мир, имея при себе современные достижения", value: "732" }, - { label: "Перемещение в игровой мир", value: "532" }, - { label: "Перемещение в иной мир", value: "754" }, - { label: "Перерождение в ином мире", value: "755" }, - { label: "Переселение души/Трансмиграция", value: "139" }, - { label: "Персонаж использует щит", value: "631" }, - { label: "Петля времени", value: "79" }, - { label: "Пираты", value: "817" }, - { label: "Писатели", value: "408" }, - { label: "Питомцы", value: "253" }, - { label: "Племенное общество", value: "291" }, - { label: "Повелитель демонов", value: "171" }, - { label: "Повествование от нескольких лиц/Несколько точек зрения", value: "156" }, - { label: "Подземелья", value: "219" }, - { label: "Пожелания", value: "166" }, - { label: "Познание Дао", value: "360" }, - { label: "Покинутое дитя", value: "534" }, - { label: "Полигамия", value: "296" }, - { label: "Политика", value: "43" }, - { label: "Полиция", value: "801" }, - { label: "Полулюди", value: "335" }, - { label: "Пользователь уникального оружия", value: "432" }, - { label: "Популярный любовный интерес", value: "241" }, - { label: "Постапокалиптика", value: "60" }, - { label: "Потерянные цивилизации", value: "751" }, - { label: "Похищения людей", value: "707" }, - { label: "Поэзия", value: "404" }, - { label: "Правонарушители", value: "694" }, - { label: "Прагматичный главный герой", value: "536" }, - { label: "Преданный любовный интерес", value: "106" }, - { label: "Предательство", value: "103" }, - { label: "Предвидение", value: "52" }, - { label: "Прекрасная героиня", value: "30" }, - { label: "Преступники", value: "728" }, - { label: "Преступность", value: "83" }, - { label: "Призванный герой", value: "147" }, - { label: "Призраки", value: "51" }, - { label: "Принуждение к отношениям", value: "602" }, - { label: "Принцессы", value: "933" }, - { label: "Притворная пара", value: "600" }, - { label: "Причудливые персонажи", value: "297" }, - { label: "Пришельцы/Инопланетяне", value: "76" }, - { label: "Программист", value: "666" }, - { label: "Проклятия", value: "48" }, - { label: "Промывание мозгов", value: "519" }, - { label: "Пропуск времени", value: "138" }, - { label: "Пророчества", value: "429" }, - { label: "Проститутки", value: "844" }, - { label: "Пространственное манипулирование", value: "375" }, - { label: "Прошлое играет большую роль", value: "215" }, - { label: "Прыжки между мирами", value: "737" }, - { label: "Психические силы", value: "265" }, - { label: "Психопаты", value: "158" }, - { label: "Путешествие во времени", value: "80" }, - { label: "Пытка", value: "168" }, - { label: "Рабы", value: "829" }, - { label: "Развод", value: "771" }, - { label: "Разумные предметы", value: "174" }, - { label: "Расизм", value: "320" }, - { label: "Рассказ", value: "61" }, - { label: "Расторжения помолвки", value: "243" }, - { label: "Расы зооморфов", value: "209" }, - { label: "Ревность", value: "717" }, - { label: "Редакторы", value: "684" }, - { label: "Реинкарнация", value: "281" }, - { label: "Реинкарнация в монстра", value: "204" }, - { label: "Реинкарнация в объект", value: "692" }, - { label: "Религии", value: "565" }, - { label: "Репортеры", value: "837" }, - { label: "Ресторан", value: "652" }, - { label: "Решительный главный герой", value: "124" }, - { label: "Робкий главный герой", value: "800" }, - { label: "Родитель одиночка", value: "687" }, - { label: "Родительский комплекс", value: "531" }, - { label: "Родословная", value: "121" }, - { label: "Романтический подсюжет ", value: "159" }, - { label: "Рост персонажа", value: "95" }, - { label: "Рыцари", value: "142" }, - { label: "Садистские персонажи", value: "642" }, - { label: "Самоотверженный главный герой", value: "748" }, - { label: "Самоубийства", value: "616" }, - { label: "Самурай", value: "646" }, - { label: "Сборник коротких историй", value: "456" }, - { label: "Связанные сюжетные линии", value: "473" }, - { label: "Святые", value: "761" }, - { label: "Священники", value: "325" }, - { label: "Сдержанный главный герой", value: "799" }, - { label: "Секретные организации", value: "331" }, - { label: "Секреты", value: "577" }, - { label: "Секс рабы", value: "830" }, - { label: "Семейный конфликт", value: "770" }, - { label: "Семь добродетелей", value: "233" }, - { label: "Семь смертных грехов", value: "134" }, - { label: "Семья", value: "251" }, - { label: "Сёнэн-ай подсюжет ", value: "664" }, - { label: "Серийные убийцы", value: "497" }, - { label: "Сестринский комплекс", value: "498" }, - { label: "Сила духа", value: "282" }, - { label: "Сила, требующая платы за пользование", value: "520" }, - { label: "Сильная пара", value: "109" }, - { label: "Сильный в сильнейшего", value: "359" }, - { label: "Сильный любовный интерес", value: "161" }, - { label: "Синдром восьмиклассника", value: "90" }, - { label: "Сироты", value: "214" }, - { label: "Система уровней", value: "198" }, - { label: "Системный администратор", value: "476" }, - { label: "Скрытие истинной личности", value: "371" }, - { label: "Скрытие истинных способностей", value: "252" }, - { label: "Скрытный главный герой", value: "133" }, - { label: "Скрытые способности", value: "128" }, - { label: "Скульпторы", value: "366" }, - { label: "Слабо выраженная романтическая линия", value: "213" }, - { label: "Слабый главный герой", value: "188" }, - { label: "Слепой главный герой", value: "864" }, - { label: "Слуги", value: "232" }, - { label: "Смерть", value: "49" }, - { label: "Смерть близких", value: "361" }, - { label: "Собственнические персонажи", value: "108" }, - { label: "Современность", value: "40" }, - { label: "Сожительство", value: "482" }, - { label: "Создание армии", value: "175" }, - { label: "Создание артефактов", value: "292" }, - { label: "Создание клана", value: "448" }, - { label: "Создание королевства", value: "181" }, - { label: "Создание навыков", value: "236" }, - { label: "Создание секты", value: "393" }, - { label: "Солдаты/Военные", value: "71" }, - { label: "Сон", value: "571" }, - { label: "Состоятельные персонажи", value: "67" }, - { label: "Социальная иерархия на основе силы", value: "137" }, - { label: "Социальные изгои", value: "480" }, - { label: "Спасение мира", value: "597" }, - { label: "Специальные способности", value: "54" }, - { label: "Спокойный главный герой", value: "32" }, - { label: "Справедливый главный герой", value: "702" }, - { label: "Средневековье", value: "184" }, - { label: "Ссорящаяся пара", value: "293" }, - { label: "Сталкеры", value: "689" }, - { label: "Старение", value: "190" }, - { label: "Стоические персонажи", value: "444" }, - { label: "Стокгольмский синдром", value: "643" }, - { label: "Стратег", value: "425" }, - { label: "Стратегические битвы", value: "160" }, - { label: "Стратегия", value: "1038" }, - { label: "Стрелки", value: "458" }, - { label: "Стрельба из лука", value: "383" }, - { label: "Студенческий совет", value: "490" }, - { label: "Судьба", value: "271" }, - { label: "Суккубы", value: "484" }, - { label: "Супер герои", value: "1039" }, - { label: "Суровая подготовка", value: "261" }, - { label: "Таинственная болезнь", value: "74" }, - { label: "Таинственное прошлое", value: "263" }, - { label: "Тайная личность", value: "310" }, - { label: "Тайные отношения", value: "812" }, - { label: "Танцоры", value: "840" }, - { label: "Телохранители", value: "452" }, - { label: "Тентакли", value: "693" }, - { label: "Террористы", value: "515" }, - { label: "Технологический разрыв", value: "621" }, - { label: "Тихие персонажи", value: "546" }, - { label: "Толстый главный герой", value: "299" }, - { label: "Торговцы", value: "416" }, - { label: "Травля/Буллинг", value: "89" }, - { label: "Травник", value: "708" }, - { label: "Трагическое прошлое", value: "164" }, - { label: "Трансплантация воспоминаний", value: "367" }, - { label: "Трап (Путаница с гендером персонажа)", value: "582" }, - { label: "Трудолюбивый главный герой", value: "37" }, - { label: "Тюрьма", value: "479" }, - { label: "Убийства", value: "84" }, - { label: "Убийцы", value: "248" }, - { label: "Убийцы драконов", value: "606" }, - { label: "Уверенный главный герой", value: "270" }, - { label: "Удачливый главный герой", value: "402" }, - { label: "Укротитель монстров", value: "337" }, - { label: "Умения из прошлой жизни", value: "280" }, - { label: "Умная пара", value: "493" }, - { label: "Умный главный герой", value: "33" }, - { label: "Уникальная техника Культивации", value: "254" }, - { label: "Уникальное оружие", value: "340" }, - { label: "Управление бизнесом", value: "315" }, - { label: "Управление временем", value: "167" }, - { label: "Управление кровью", value: "764" }, - { label: "Упрямый главный герой", value: "672" }, - { label: "Уродливый главный герой", value: "803" }, - { label: "Ускоренный рост", value: "300" }, - { label: "Усыновленные дети", value: "481" }, - { label: "Усыновленный главный герой", value: "412" }, - { label: "Уход за детьми", value: "398" }, - { label: "Учителя", value: "345" }, - { label: "Фамильяры", value: "541" }, - { label: "Фанатизм", value: "613" }, - { label: "Фантастические существа", value: "322" }, - { label: "Фанфикшн", value: "388" }, - { label: "Фармацевт", value: "715" }, - { label: "Фарминг", value: "379" }, - { label: "Феи", value: "210" }, - { label: "Фелляция", value: "584" }, - { label: "Фениксы", value: "374" }, - { label: "Фетиш груди", value: "499" }, - { label: "Философия", value: "87" }, - { label: "Фильмы", value: "403" }, - { label: "Флэшбэки", value: "528" }, - { label: "Фобии", value: "100" }, - { label: "Фольклор", value: "467" }, - { label: "Футанари", value: "568" }, - { label: "Футуристический сеттинг", value: "382" }, - { label: "Фэнтези мир", value: "126" }, - { label: "Хакеры", value: "399" }, - { label: "Харизматический герой", value: "391" }, - { label: "Хикикомори/Затворники", value: "462" }, - { label: "Хитроумный главный герой", value: "105" }, - { label: "Хозяин подземелий", value: "557" }, - { label: "Холодный главный герой", value: "259" }, - { label: "Хорошие отношения с семьей", value: "506" }, - { label: "Хранители могил", value: "68" }, - { label: "Целители", value: "389" }, - { label: "Цзянши", value: "735" }, - { label: "Цундэрэ", value: "445" }, - { label: "Чаты", value: "580" }, - { label: "Человеческое оружие", value: "521" }, - { label: "Честный главный герой", value: "240" }, - { label: "Читы", value: "238" }, - { label: "Шантаж", value: "526" }, - { label: "Шеф-повар", value: "239" }, - { label: "Шикигами", value: "543" }, - { label: "Школа только для девочек", value: "633" }, - { label: "Шота", value: "514" }, - { label: "Шоу-бизнес", value: "407" }, - { label: "Шпионы", value: "563" }, - { label: "Эволюция", value: "196" }, - { label: "Эгоистичный главный герой", value: "539" }, - { label: "Эйдетическая память", value: "392" }, - { label: "Экзорсизм", value: "504" }, - { label: "Экономика", value: "492" }, - { label: "Эксгибиционизм", value: "639" }, - { label: "Эксперименты с людьми", value: "129" }, - { label: "Элементальная магия", value: "395" }, - { label: "Эльфы", value: "172" }, - { label: "Эмоционально слабый главный герой", value: "816" }, - { label: "Эпизодический", value: "612" }, - { label: "Юный любовный интерес", value: "446" }, - { label: "Яды", value: "410" }, - { label: "Языкастые персонажи", value: "406" }, - { label: "Языковой барьер", value: "59" }, - { label: "Яндере", value: "208" }, - { label: "Японские силы самообороны", value: "559" }, - { label: "Ярко выраженная романтическая линия", value: "272" }, - { label: "Abusive Characters", value: "625" }, - { label: "Adapted to Visual Novel", value: "1046" }, - { label: "Adopted-lead", value: "886" }, - { label: "Adultery", value: "866" }, - { label: "Affair", value: "645" }, - { label: "Age-gap", value: "923" }, - { label: "Almost-human-lead", value: "975" }, - { label: "An*l", value: "780" }, - { label: "Androgynous-male-lead", value: "1002" }, - { label: "Anti-hero-lead", value: "976" }, - { label: "Apathetic-lead", value: "967" }, - { label: "Arms Dealers", value: "839" }, - { label: "Army-commander", value: "1003" }, - { label: "Artifact-refining", value: "924" }, - { label: "Autism", value: "855" }, - { label: "Awkward Protagonist", value: "669" }, - { label: "Awkward-lead", value: "892" }, - { label: "Bestiality", value: "796" }, - { label: "Big-breasts", value: "908" }, - { label: "Birth-of-a-nation", value: "1004" }, - { label: "Bisexual-lead", value: "1027" }, - { label: "Body-refining", value: "947" }, - { label: "Body-swap/s", value: "1036" }, - { label: "Boss-subordinate-relationship", value: "876" }, - { label: "Bride-kidnapping", value: "948" }, - { label: "Caring-lead", value: "879" }, - { label: "Cautious-lead", value: "1032" }, - { label: "Changed-man", value: "925" }, - { label: "Charismatic-lead", value: "1005" }, - { label: "Child-lead", value: "936" }, - { label: "Clever-lead", value: "880" }, - { label: "Clumsy Love Interests", value: "815" }, - { label: "Cold Love Interests", value: "651" }, - { label: "Coming of Age", value: "860" }, - { label: "Confident-lead", value: "894" }, - { label: "Confident-male-lead", value: "951" }, - { label: "Confinement", value: "767" }, - { label: "Conflicting Loyalties", value: "793" }, - { label: "Couple Growth", value: "690" }, - { label: "Court Official", value: "863" }, - { label: "Cowardly Protagonist", value: "703" }, - { label: "Cowardly-lead", value: "1000" }, - { label: "Cross-dressing", value: "250" }, - { label: "Cunnilingus", value: "867" }, - { label: "Cunning-lead", value: "952" }, - { label: "Cunning-male-lead", value: "953" }, - { label: "Curious Protagonist", value: "849" }, - { label: "Cute-lead", value: "887" }, - { label: "Dense-lead", value: "868" }, - { label: "Determined-lead", value: "972" }, - { label: "Developing-technology", value: "1006" }, - { label: "Devil/s", value: "1007" }, - { label: "Different Social Status", value: "758" }, - { label: "Disfigurement", value: "838" }, - { label: "Doting Love Interests", value: "665" }, - { label: "Doting Older Siblings", value: "609" }, - { label: "Doting Parents", value: "530" }, - { label: "Dungeon/s-exploring", value: "990" }, - { label: "Elderly Protagonist", value: "774" }, - { label: "Enlightenment", value: "729" }, - { label: "Eunuch", value: "1047" }, - { label: "Eye Powers", value: "386" }, - { label: "Family Business", value: "785" }, - { label: "Famous Parents", value: "790" }, - { label: "Famous-lead", value: "897" }, - { label: "Fanfic", value: "942" }, - { label: "Fated Lovers", value: "797" }, - { label: "Fellatio", value: "870" }, - { label: "Female-lead", value: "888" }, - { label: "First-time-intercourse", value: "871" }, - { label: "Fleet Battles", value: "836" }, - { label: "Forced Living Arrangements", value: "808" }, - { label: "Forced Marriage", value: "835" }, - { label: "Former Hero", value: "752" }, - { label: "Fujoshi", value: "1045" }, - { label: "Galge", value: "848" }, - { label: "Gambling", value: "1031" }, - { label: "Gamelit", value: "994" }, - { label: "Genderless-lead", value: "1008" }, - { label: "Glasses-wearing-lead", value: "995" }, - { label: "Guardian Relationship", value: "197" }, - { label: "H*ndjob", value: "629" }, - { label: "Half-human-lead", value: "956" }, - { label: "Hard-working-lead", value: "957" }, - { label: "Hard-working-male-lead", value: "958" }, - { label: "Hard-working-protagonist/s", value: "1030" }, - { label: "Harem-seeking-lead", value: "929" }, - { label: "Harem-subtext", value: "909" }, - { label: "Helpful Protagonist", value: "760" }, - { label: "Helpful-lead", value: "882" }, - { label: "Hidden-gem", value: "998" }, - { label: "High-fantasy", value: "991" }, - { label: "Human-becomes-demon/monster", value: "1009" }, - { label: "Human-nonhuman-relationship", value: "914" }, - { label: "Humanoid-lead", value: "915" }, - { label: "Imperial Harem", value: "805" }, - { label: "Incubus", value: "1049" }, - { label: "Insects", value: "831" }, - { label: "Interspatial-storage", value: "959" }, - { label: "Kind Love Interests", value: "757" }, - { label: "Large-number-of-skills", value: "1010" }, - { label: "Legendary-hero", value: "1011" }, - { label: "Litrpg", value: "977" }, - { label: "Love Rivals", value: "788" }, - { label: "Lovers Reunited", value: "853" }, - { label: "Low-fantasy", value: "1037" }, - { label: "Master-disciple-relationship", value: "899" }, - { label: "Matriarchy", value: "807" }, - { label: "Mature-lead", value: "900" }, - { label: "Mind Break", value: "862" }, - { label: "Mismatched Couple", value: "695" }, - { label: "Mob Protagonist", value: "648" }, - { label: "Mob-lead", value: "902" }, - { label: "Modern", value: "988" }, - { label: "Monster-pov", value: "1012" }, - { label: "Mpreg", value: "856" }, - { label: "Multiple-povs", value: "1013" }, - { label: "Mystery Solving", value: "710" }, - { label: "Naive-lead", value: "1014" }, - { label: "Near-death-experience", value: "996" }, - { label: "Neet", value: "495" }, - { label: "Nightmares", value: "833" }, - { label: "Nobility", value: "1015" }, - { label: "Non-human-lead", value: "978" }, - { label: "Non-humanoid-lead", value: "1016" }, - { label: "Not-so-secret-identity", value: "979" }, - { label: "Omegaverse", value: "857" }, - { label: "Online-game", value: "910" }, - { label: "Online-gaming", value: "911" }, - { label: "Outdoor Interc**rse", value: "847" }, - { label: "Outdoor-intercourse", value: "903" }, - { label: "Overprotective Siblings", value: "610" }, - { label: "Part-Time Job", value: "865" }, - { label: "Past Trauma", value: "726" }, - { label: "Past-memories", value: "1017" }, - { label: "Persistent Love Interests", value: "714" }, - { label: "Perverted-lead", value: "932" }, - { label: "Photography", value: "798" }, - { label: "Pill Based Cultivation", value: "279" }, - { label: "Pill Concocting", value: "245" }, - { label: "Pilots", value: "820" }, - { label: "Playboys", value: "795" }, - { label: "Playful-lead", value: "989" }, - { label: "Polyandry", value: "811" }, - { label: "Portal-fantasy-/-isekai", value: "992" }, - { label: "Pragmatic-lead", value: "884" }, - { label: "Profanity", value: "980" }, - { label: "Protagonist-loyal-to-love-interest", value: "905" }, - { label: "R-15 (Японское возрастное ограничение)", value: "230" }, - { label: "Rebellion", value: "834" }, - { label: "Reincarnated in a Game World", value: "804" }, - { label: "Reluctant Protagonist", value: "696" }, - { label: "Reverse R*pe", value: "776" }, - { label: "Reversible Couple", value: "854" }, - { label: "Ruling-class", value: "943" }, - { label: "S*x Friends", value: "750" }, - { label: "S*xual Cultivation Technique", value: "823" }, - { label: "Salaryman", value: "1035" }, - { label: "Satire", value: "981" }, - { label: "Schemes And Conspiracies", value: "745" }, - { label: "Scientist/s", value: "997" }, - { label: "Scientists", value: "1029" }, - { label: "Secret Crush", value: "827" }, - { label: "Seme Protagonist", value: "1034" }, - { label: "Seme-lead", value: "1028" }, - { label: "Sentimental Protagonist", value: "1048" }, - { label: "Seven-heavenly-virtues", value: "1018" }, - { label: "Sexual Cultivation Technique", value: "350" }, - { label: "Sexual-content", value: "982" }, - { label: "Sexuality", value: "983" }, - { label: "Sharing A Body", value: "814" }, - { label: "Shotacon", value: "858" }, - { label: "Shoujo-Ai Subplot", value: "777" }, - { label: "Sibling Rivalry", value: "766" }, - { label: "Sibling's Care", value: "1042" }, - { label: "Sickly Characters", value: "787" }, - { label: "Skills", value: "1019" }, - { label: "Slave-harem", value: "1001" }, - { label: "Slime", value: "1020" }, - { label: "Smart-male-lead", value: "963" }, - { label: "Spirit Users", value: "739" }, - { label: "Straight Seme", value: "819" }, - { label: "Straight Uke", value: "818" }, - { label: "Strength-based-social-hierarchy", value: "974" }, - { label: "Strong-lead", value: "944" }, - { label: "Strong-male-lead", value: "964" }, - { label: "Student-teacher-relationship", value: "1026" }, - { label: "Suspense", value: "987" }, - { label: "Sword-and-sorcery", value: "875" }, - { label: "Threesome", value: "574" }, - { label: "Transformation Ability", value: "736" }, - { label: "Traumatising-content", value: "1040" }, - { label: "Underestimated-lead", value: "919" }, - { label: "Underestimated-male-lead", value: "966" }, - { label: "Unique-abilities", value: "1021" }, - { label: "Unique-skill", value: "1022" }, - { label: "Unique-skills", value: "1023" }, - { label: "Unlimited Flow", value: "1044" }, - { label: "Unrequited Love", value: "806" }, - { label: "Urban-fantasy", value: "1041" }, - { label: "Villainess Noble Girls", value: "719" }, - { label: "Weak-lead", value: "885" }, - { label: "Web-novel", value: "1024" }, + { label: '[Награжденная работа]', value: '611' }, + { label: '18+', value: '338' }, + { label: 'Авантюристы', value: '353' }, + { label: 'Автоматоны', value: '538' }, + { label: 'Агрессивные персонажи', value: '434' }, + { label: 'Ад', value: '509' }, + { label: 'Адаптация в радиопостановку', value: '522' }, + { label: 'Академия', value: '25' }, + { label: 'Актеры озвучки', value: '578' }, + { label: 'Активный главный герой', value: '132' }, + { label: 'Алхимия', value: '116' }, + { label: 'Альтернативный мир', value: '28' }, + { label: 'Амнезия/Потеря памяти', value: '247' }, + { label: 'Анабиоз', value: '657' }, + { label: 'Ангелы', value: '218' }, + { label: 'Андрогинные персонажи', value: '217' }, + { label: 'Андроиды', value: '82' }, + { label: 'Анти-магия', value: '471' }, + { label: 'Антигерой', value: '346' }, + { label: 'Антикварный магазин', value: '572' }, + { label: 'Антисоциальный главный герой', value: '562' }, + { label: 'Антиутопия', value: '663' }, + { label: 'Апатичный протагонист', value: '29' }, + { label: 'Апокалипсис', value: '314' }, + { label: 'Аранжированный брак', value: '285' }, + { label: 'Армия', value: '598' }, + { label: 'Артефакты', value: '117' }, + { label: 'Артисты', value: '460' }, + { label: 'Банды', value: '581' }, + { label: 'БДСМ', value: '676' }, + { label: 'Бедный главный герой', value: '309' }, + { label: 'Безжалостный главный герой', value: '144' }, + { label: 'Беззаботный главный герой', value: '355' }, + { label: 'Безусловная любовь', value: '650' }, + { label: 'Беременность', value: '131' }, + { label: 'Бесполый главный герой', value: '222' }, + { label: 'Бессмертные', value: '275' }, + { label: 'Бесстрашный протагонист', value: '619' }, + { label: 'Бесстыдный главный герой', value: '256' }, + { label: 'Бесчестный главный герой', value: '699' }, + { label: 'Библиотека', value: '342' }, + { label: 'Бизнесмен ', value: '813' }, + { label: 'Биочип', value: '120' }, + { label: 'Бисексуальный главный герой', value: '822' }, + { label: 'Близнецы', value: '148' }, + { label: 'Боги', value: '211' }, + { label: 'Богини', value: '356' }, + { label: 'Боевая академия', value: '369' }, + { label: 'Боевые духи', value: '347' }, + { label: 'Боевые соревнования', value: '422' }, + { label: 'Божественная защита', value: '336' }, + { label: 'Божественные силы', value: '224' }, + { + label: + 'Большая разница в возрасте между героем и его любовным интересом', + value: '348', + }, + { label: 'Борьба за власть', value: '544' }, + { label: 'Брак', value: '363' }, + { label: 'Брак по расчету', value: '65' }, + { label: 'Братский комплекс', value: '31' }, + { label: 'Братство', value: '413' }, + { label: 'Братья и сестры', value: '518' }, + { label: 'Буддизм', value: '742' }, + { label: 'Быстрая культивация', value: '273' }, + { label: 'Быстрообучаемый', value: '221' }, + { label: 'Валькирии', value: '667' }, + { label: 'Вампиры', value: '266' }, + { label: 'Ваншот', value: '679' }, + { label: 'Ведьмы', value: '169' }, + { label: 'Вежливый главный герой', value: '289' }, + { label: 'Верные подчиненные', value: '225' }, + { label: 'Взрослый главный герой', value: '183' }, + { label: 'Видит то, чего не видят другие', value: '636' }, + { label: 'Виртуальная реальность', value: '313' }, + { label: 'Владелец магазина', value: '653' }, + { label: 'Внезапная сила', value: '376' }, + { label: 'Внезапное богатство', value: '802' }, + { + label: 'Внешний вид отличается от фактического возраста', + value: '334', + }, + { label: 'Военные Летописи', value: '740' }, + { label: 'Возвращение из другого мира', value: '673' }, + { label: 'Войны', value: '58' }, + { label: 'Вокалоид', value: '678' }, + { label: 'Волшебники/Волшебницы', value: '477' }, + { label: 'Волшебные звери', value: '201' }, + { label: 'Воображаемый друг', value: '614' }, + { label: 'Воры', value: '326' }, + { label: 'Воскрешение', value: '78' }, + { label: 'Враги становятся возлюбленными', value: '428' }, + { label: 'Враги становятся союзниками', value: '502' }, + { label: 'Врата в другой мир', value: '558' }, + { label: 'Врачи', value: '286' }, + { label: 'Временной парадокс', value: '163' }, + { label: 'Всемогущий главный герой', value: '42' }, + { label: 'Вторжение на землю', value: '77' }, + { label: 'Второй шанс', value: '112' }, + { label: 'Вуайеризм', value: '851' }, + { label: 'Выживание', value: '290' }, + { label: 'Высокомерные персонажи', value: '268' }, + { label: 'Гадание', value: '540' }, + { label: 'Гарем рабов', value: '828' }, + { label: 'Геймеры', value: '302' }, + { label: 'Генералы', value: '223' }, + { label: 'Генетические модификации', value: '620' }, + { label: 'Гениальный главный герой', value: '566' }, + { label: 'Герои', value: '173' }, + { label: 'Героиня — сорванец', value: '525' }, + { label: 'Герой влюбляется первым', value: '64' }, + { label: 'Гетерохромия', value: '510' }, + { label: 'Гильдии', value: '323' }, + { label: 'Гипнотизм', value: '768' }, + { label: 'Главный герой — бог', value: '486' }, + { label: 'Главный герой — гуманоид', value: '595' }, + { label: 'Главный герой — женщина', value: '63' }, + { label: 'Главный герой — мужчина', value: '39' }, + { label: 'Главный герой — наполовину человек', value: '362' }, + { label: 'Главный герой — отец', value: '859' }, + { label: 'Главный герой — раб', value: '832' }, + { label: 'Главный герой — ребенок', value: '415' }, + { label: 'Главный герой — рубака', value: '400' }, + { label: 'Главный герой — собиратель гарема', value: '439' }, + { label: 'Главный герой влюбляется первым', value: '655' }, + { label: 'Главный герой играет роль', value: '396' }, + { label: 'Главный герой носит очки', value: '637' }, + { label: 'Главный герой пацифист', value: '675' }, + { label: 'Главный герой с несколькими телами', value: '628' }, + { label: 'Главный герой силен с самого начала', value: '45' }, + { label: 'Гладиаторы', value: '549' }, + { label: 'Глуповатый главный герой', value: '295' }, + { label: 'Гоблины', value: '529' }, + { label: 'Големы', value: '569' }, + { label: 'Гомункул', value: '850' }, + { label: 'Горничные', value: '380' }, + { label: 'Госпиталь', value: '1043' }, + { label: 'Готовка', value: '193' }, + { label: 'Гриндинг', value: '303' }, + { label: 'Дао Компаньон', value: '384' }, + { label: 'Даосизм', value: '792' }, + { label: 'Дарк', value: '151' }, + { label: 'Дварфы', value: '220' }, + { label: 'Двойная личность', value: '601' }, + { label: 'Двойник', value: '547' }, + { label: 'Дворецкий', value: '623' }, + { label: 'Дворяне', value: '41' }, + { label: 'Дворянство/Аристократия', value: '354' }, + { label: 'Девушки-монстры', value: '634' }, + { label: 'Демоническая техника культивации', value: '706' }, + { label: 'Демоны', value: '6' }, + { label: 'Денежный долг', value: '841' }, + { label: 'Депрессия', value: '494' }, + { label: 'Детективы', value: '561' }, + { label: 'Дискриминация', value: '34' }, + { + label: 'Добыча денег одно из основных стремлений главного героя', + value: '307', + }, + { label: 'Долгая разлука', value: '200' }, + { label: 'Домашние дела', value: '178' }, + { label: 'Домогательство', value: '394' }, + { label: 'Драконы', value: '195' }, + { label: 'Драконьи всадники', value: '555' }, + { label: 'Древние времена', value: '102' }, + { label: 'Древний Китай', value: '284' }, + { label: 'Дружба', value: '97' }, + { label: 'Друзья детства', value: '170' }, + { label: 'Друзья становятся врагами', value: '507' }, + { label: 'Друиды', value: '427' }, + { label: 'Дух лисы', value: '842' }, + { label: 'Духи/Призраки', value: '46' }, + { label: 'Духовный советник', value: '351' }, + { label: 'Душевность', value: '587' }, + { label: 'Души', value: '136' }, + { label: 'Европейская атмосфера', value: '457' }, + { label: 'Ёкаи', value: '516' }, + { label: 'Есть аниме-адаптация', value: '26' }, + { label: 'Есть видеоигра по мотивам', value: '491' }, + { label: 'Есть манга-адаптация', value: '27' }, + { label: 'Есть манхва-адаптация', value: '453' }, + { label: 'Есть маньхуа-адаптация', value: '298' }, + { label: 'Есть сериал-адаптация', value: '421' }, + { label: 'Есть фильм по мотивам', value: '47' }, + { label: 'Женища-наставник', value: '438' }, + { label: 'Жертва изнасилования влюбляется в насильника', value: '414' }, + { label: 'Жесткая, двуличная личность', value: '249' }, + { label: 'Жестокие персонажи', value: '436' }, + { label: 'Жестокое обращение с ребенком', value: '617' }, + { label: 'Жестокость', value: '127' }, + { label: 'Животноводство', value: '765' }, + { label: 'Животные черты', value: '466' }, + { label: 'Жизнь в квартире', value: '554' }, + { label: 'Жрицы', value: '564' }, + { label: 'Заботливый главный герой', value: '176' }, + { label: 'Забывчивый главный герой', value: '579' }, + { label: 'Заговоры', value: '177' }, + { label: 'Закалка тела', value: '269' }, + { label: 'Законники', value: '769' }, + { label: 'Замкнутый главный герой', value: '533' }, + { label: 'Запечатанная сила', value: '344' }, + { label: 'Застенчивые персонажи', value: '443' }, + { label: 'Звери', value: '119' }, + { label: 'Звери-компаньоны', value: '192' }, + { label: 'Злой протагонист', value: '125' }, + { label: 'Злые боги', value: '437' }, + { label: 'Злые организации', value: '503' }, + { label: 'Злые религии', value: '725' }, + { label: 'Знаменитости', value: '397' }, + { label: 'Знаменитый главный герой', value: '469' }, + { label: 'Знания современного мира', value: '185' }, + { label: 'Зомби', value: '321' }, + { label: 'Игра на выживание', value: '162' }, + { label: 'Игривый протагонист', value: '700' }, + { label: 'Игровая система рейтинга', value: '301' }, + { label: 'Игровые элементы', value: '152' }, + { label: 'Игрушки (18+)', value: '644' }, + { label: 'Из грязи в князи', value: '330' }, + { label: 'Из женщины в мужчину ', value: '688' }, + { label: 'Из мужчины в женщину', value: '698' }, + { label: 'Из полного в худого', value: '809' }, + { label: 'Из слабого в сильного', value: '81' }, + { label: 'Из страшно(го/й) в красиво(го/ю)', value: '810' }, + { label: 'Извращенный главный герой', value: '349' }, + { label: 'Изгои', value: '472' }, + { label: 'Изменение расы', value: '627' }, + { label: 'Изменения внешнего вида', value: '191' }, + { label: 'Изменения личности', value: '99' }, + { label: 'Изнасилование', value: '110' }, + { label: 'Изображения жестокости', value: '260' }, + { label: 'Империи', value: '749' }, + { label: 'Инвалидность', value: '656' }, + { label: 'Индустриализация', value: '180' }, + { label: 'Инженер', value: '35' }, + { label: 'Инцест', value: '463' }, + { label: 'Искусственный интеллект', value: '118' }, + { label: 'Исследования', value: '635' }, + { label: 'Каннибализм', value: '377' }, + { label: 'Карточные игры', value: '654' }, + { label: 'Киберспорт', value: '658' }, + { label: 'Кланы', value: '950' }, + { label: 'Класс безработного [Игровой класс в игре]', value: '727' }, + { label: 'Клоны', value: '365' }, + { label: 'Клубы', value: '96' }, + { label: 'Книги', value: '341' }, + { label: 'Книги навыков', value: '312' }, + { label: 'Книжный червь', value: '455' }, + { label: 'Коварство', value: '111' }, + { label: 'Коллеги', value: '683' }, + { label: 'Колледж/Университет', value: '712' }, + { label: 'Кома', value: '826' }, + { label: 'Командная работа', value: '426' }, + { label: 'Комедийный оттенок', value: '523' }, + { label: 'Комплекс неполноценности', value: '489' }, + { label: 'Комплекс семейных отношений', value: '104' }, + { label: 'Конкуренция', value: '746' }, + { label: 'Контракты', value: '483' }, + { label: 'Контроль разума/сознания', value: '262' }, + { label: 'Копейщик', value: '339' }, + { label: 'Королевская битва', value: '1025' }, + { label: 'Королевская власть', value: '53' }, + { label: 'Королевства', value: '141' }, + { label: 'Коррупция', value: '378' }, + { label: 'Космические войны', value: '674' }, + { label: 'Красивый герой', value: '107' }, + { label: 'Крафт', value: '287' }, + { label: 'Кризис личности', value: '599' }, + { label: 'Кругосветное путешествие', value: '257' }, + { label: 'Кудере', value: '440' }, + { label: 'Кузены', value: '701' }, + { label: 'Кузнец', value: '454' }, + { label: 'Кукловоды', value: '431' }, + { label: 'Куклы/марионетки', value: '573' }, + { label: 'Культивация', value: '123' }, + { label: 'Куннилингус', value: '632' }, + { label: 'Легенды', value: '430' }, + { label: 'Легкая жизнь', value: '604' }, + { label: 'Ленивый главный герой', value: '570' }, + { label: 'Лидерство', value: '424' }, + { label: 'Лоли', value: '92' }, + { label: 'Лотерея', value: '401' }, + { label: 'Любовный интерес влюбляется первым', value: '647' }, + { label: 'Любовный интерес главного героя носит очки', value: '576' }, + { label: 'Любовный треугольник', value: '98' }, + { label: 'Любовь детства', value: '500' }, + { label: 'Любовь с первого взгляда', value: '730' }, + { label: 'Магические надписи', value: '373' }, + { label: 'Магические печати', value: '277' }, + { label: 'Магические технологии', value: '357' }, + { + label: + 'Магическое пространство/измерение, доступное не всем персонажам', + value: '244', + }, + { label: 'Магия', value: '38' }, + { label: 'Магия призыва', value: '333' }, + { label: 'Мазохистские персонажи', value: '660' }, + { label: 'Манипулятивные персонажи', value: '130' }, + { label: 'Мания', value: '91' }, + { label: 'Мастер на все руки', value: '390' }, + { label: 'Мастурбация', value: '846' }, + { label: 'Махо-сёдзё', value: '496' }, + { label: 'Медицинские знания', value: '441' }, + { label: 'Медленная романтическая линия', value: '113' }, + { label: 'Медленное развитие на старте ', value: '670' }, + { label: 'Межпространственные путешествия', value: '316' }, + { label: 'Менеджмент', value: '182' }, + { label: 'Мертвый главный герой', value: '794' }, + { label: 'Месть', value: '88' }, + { label: 'Метаморфы', value: '234' }, + { label: 'Меч и магия', value: '55' }, + { label: 'Мечник', value: '607' }, + { label: 'Мечты', value: '733' }, + { label: 'Милая история', value: '709' }, + { label: 'Милое дитя', value: '596' }, + { label: 'Милый главный герой', value: '697' }, + { label: 'Мировое дерево', value: '385' }, + { label: 'Мистический ореол вокруг семьи', value: '409' }, + { label: 'Мифические звери', value: '418' }, + { label: 'Мифология', value: '468' }, + { label: 'Младшие братья', value: '843' }, + { label: 'Младшие сестры', value: '465' }, + { label: 'ММОРПГ (ЛитРПГ)', value: '306' }, + { label: 'Множество перемещенных людей', value: '488' }, + { label: 'Множество реальностей', value: '278' }, + { label: 'Множество реинкарнированных людей', value: '227' }, + { label: 'Модели', value: '649' }, + { label: 'Молчаливый персонаж', value: '705' }, + { label: 'Монстры', value: '69' }, + { label: 'Мужская гей-пара', value: '685' }, + { label: 'Мужчина-яндере', value: '155' }, + { label: 'Музыка', value: '589' }, + { label: 'Музыкальные группы', value: '588' }, + { label: 'Мутации', value: '668' }, + { label: 'Мутированные существа', value: '317' }, + { label: 'Навык кражи', value: '626' }, + { label: 'Навязчивая любовь', value: '85' }, + { label: 'Наемники', value: '324' }, + { label: 'Назойливый возлюбленный', value: '501' }, + { label: 'Наивный главный герой', value: '66' }, + { label: 'Наркотики', value: '661' }, + { label: 'Нарциссический главный герой', value: '470' }, + { label: 'Насилие сексуального характера', value: '824' }, + { label: 'Наследование', value: '372' }, + { label: 'Национализм', value: '318' }, + { label: 'Не блещущий внешне главный герой', value: '328' }, + { label: 'Не родные братья и сестры', value: '464' }, + { label: 'Небеса', value: '508' }, + { label: 'Небесное испытание', value: '274' }, + { label: 'Негуманоидный главный герой', value: '622' }, + { label: 'Недоверчивый главный герой', value: '485' }, + { label: 'Недооцененный главный герой', value: '140' }, + { label: 'Недоразумения', value: '202' }, + { label: 'Неизлечимая болезнь', value: '75' }, + { label: 'Некромант', value: '308' }, + { label: 'Нелинейная история', value: '157' }, + { label: 'Ненавистный главный герой', value: '487' }, + { label: 'Ненадежный рассказчик', value: '165' }, + { label: 'Нерезиденты', value: '821' }, + { label: 'Нерешительный главный герой', value: '741' }, + { label: 'Несерьезный главный герой', value: '294' }, + { label: 'Несколько временных линий', value: '517' }, + { label: 'Несколько главных героев', value: '475' }, + { label: 'Несколько идентичностей', value: '615' }, + { label: 'Несколько личностей', value: '474' }, + { label: 'Нетораре', value: '721' }, + { label: 'Нетори', value: '450' }, + { label: 'Неудачливый главный герой', value: '567' }, + { label: 'Ниндзя', value: '358' }, + { label: 'Обещание из детства', value: '738' }, + { label: 'Обманщик', value: '411' }, + { label: 'Обмен телами', value: '94' }, + { label: 'Обнаженка', value: '417' }, + { label: 'Обольщение', value: '718' }, + { label: 'Оборотни', value: '624' }, + { label: 'Обратный гарем', value: '255' }, + { label: 'Общество монстров', value: '226' }, + { label: 'Обязательство', value: '548' }, + { label: 'Огнестрельное оружие', value: '179' }, + { label: 'Ограниченная продолжительность жизни', value: '73' }, + { label: 'Одержимость', value: '447' }, + { label: 'Одинокий главный герой', value: '304' }, + { label: 'Одиночество', value: '199' }, + { label: 'Одиночное проживание', value: '207' }, + { label: 'Околосмертные переживания', value: '773' }, + { label: 'Оммёдзи', value: '542' }, + { label: 'Омоложение', value: '237' }, + { label: 'Организованная преступность', value: '478' }, + { label: 'Оргия', value: '825' }, + { label: 'Орки', value: '228' }, + { label: 'Освоение навыков', value: '235' }, + { label: 'Основано на аниме', value: '553' }, + { label: 'Основано на видео игре', value: '704' }, + { label: 'Основано на визуальной новелле ', value: '861' }, + { label: 'Основано на песне', value: '677' }, + { label: 'Основано на фильме', value: '552' }, + { label: 'Осторожный главный герой', value: '122' }, + { label: 'Отаку', value: '512' }, + { label: 'Открытый космос', value: '150' }, + { label: 'Отношения в сети', value: '713' }, + { label: 'Отношения между богом и человеком', value: '603' }, + { label: 'Отношения между людьми и нелюдьми', value: '205' }, + { label: 'Отношения на расстоянии', value: '852' }, + { label: 'Отношения начальник-подчиненный', value: '590' }, + { label: 'Отношения Сенпай-Коухай', value: '513' }, + { label: 'Отношения ученика и учителя', value: '451' }, + { label: 'Отношения учитель-ученик', value: '343' }, + { label: 'Отношения хозяин-слуга', value: '206' }, + { label: 'Отомэ игра', value: '723' }, + { label: 'Отсутствие здравого смысла', value: '605' }, + { label: 'Отсутствие родителей', value: '575' }, + { label: 'Офисный роман', value: '686' }, + { label: 'Официанты', value: '681' }, + { label: 'Охотники', value: '288' }, + { label: 'Очаровательный главный герой', value: '659' }, + { label: 'Падшее дворянство', value: '618' }, + { label: 'Падшие ангелы', value: '505' }, + { label: 'Пайзури', value: '630' }, + { label: 'Паразиты', value: '535' }, + { label: 'Параллельные миры', value: '86' }, + { label: 'Парк развлечений', value: '586' }, + { label: 'Пародия', value: '319' }, + { label: 'Певцы/Певицы', value: '734' }, + { label: 'Первая любовь', value: '716' }, + { label: 'Первоисточник новеллы — манга', value: '560' }, + { label: 'Первый раз', value: '845' }, + { + label: + 'Перемещение в другой мир, имея при себе современные достижения', + value: '732', + }, + { label: 'Перемещение в игровой мир', value: '532' }, + { label: 'Перемещение в иной мир', value: '754' }, + { label: 'Перерождение в ином мире', value: '755' }, + { label: 'Переселение души/Трансмиграция', value: '139' }, + { label: 'Персонаж использует щит', value: '631' }, + { label: 'Петля времени', value: '79' }, + { label: 'Пираты', value: '817' }, + { label: 'Писатели', value: '408' }, + { label: 'Питомцы', value: '253' }, + { label: 'Племенное общество', value: '291' }, + { label: 'Повелитель демонов', value: '171' }, + { + label: 'Повествование от нескольких лиц/Несколько точек зрения', + value: '156', + }, + { label: 'Подземелья', value: '219' }, + { label: 'Пожелания', value: '166' }, + { label: 'Познание Дао', value: '360' }, + { label: 'Покинутое дитя', value: '534' }, + { label: 'Полигамия', value: '296' }, + { label: 'Политика', value: '43' }, + { label: 'Полиция', value: '801' }, + { label: 'Полулюди', value: '335' }, + { label: 'Пользователь уникального оружия', value: '432' }, + { label: 'Популярный любовный интерес', value: '241' }, + { label: 'Постапокалиптика', value: '60' }, + { label: 'Потерянные цивилизации', value: '751' }, + { label: 'Похищения людей', value: '707' }, + { label: 'Поэзия', value: '404' }, + { label: 'Правонарушители', value: '694' }, + { label: 'Прагматичный главный герой', value: '536' }, + { label: 'Преданный любовный интерес', value: '106' }, + { label: 'Предательство', value: '103' }, + { label: 'Предвидение', value: '52' }, + { label: 'Прекрасная героиня', value: '30' }, + { label: 'Преступники', value: '728' }, + { label: 'Преступность', value: '83' }, + { label: 'Призванный герой', value: '147' }, + { label: 'Призраки', value: '51' }, + { label: 'Принуждение к отношениям', value: '602' }, + { label: 'Принцессы', value: '933' }, + { label: 'Притворная пара', value: '600' }, + { label: 'Причудливые персонажи', value: '297' }, + { label: 'Пришельцы/Инопланетяне', value: '76' }, + { label: 'Программист', value: '666' }, + { label: 'Проклятия', value: '48' }, + { label: 'Промывание мозгов', value: '519' }, + { label: 'Пропуск времени', value: '138' }, + { label: 'Пророчества', value: '429' }, + { label: 'Проститутки', value: '844' }, + { label: 'Пространственное манипулирование', value: '375' }, + { label: 'Прошлое играет большую роль', value: '215' }, + { label: 'Прыжки между мирами', value: '737' }, + { label: 'Психические силы', value: '265' }, + { label: 'Психопаты', value: '158' }, + { label: 'Путешествие во времени', value: '80' }, + { label: 'Пытка', value: '168' }, + { label: 'Рабы', value: '829' }, + { label: 'Развод', value: '771' }, + { label: 'Разумные предметы', value: '174' }, + { label: 'Расизм', value: '320' }, + { label: 'Рассказ', value: '61' }, + { label: 'Расторжения помолвки', value: '243' }, + { label: 'Расы зооморфов', value: '209' }, + { label: 'Ревность', value: '717' }, + { label: 'Редакторы', value: '684' }, + { label: 'Реинкарнация', value: '281' }, + { label: 'Реинкарнация в монстра', value: '204' }, + { label: 'Реинкарнация в объект', value: '692' }, + { label: 'Религии', value: '565' }, + { label: 'Репортеры', value: '837' }, + { label: 'Ресторан', value: '652' }, + { label: 'Решительный главный герой', value: '124' }, + { label: 'Робкий главный герой', value: '800' }, + { label: 'Родитель одиночка', value: '687' }, + { label: 'Родительский комплекс', value: '531' }, + { label: 'Родословная', value: '121' }, + { label: 'Романтический подсюжет ', value: '159' }, + { label: 'Рост персонажа', value: '95' }, + { label: 'Рыцари', value: '142' }, + { label: 'Садистские персонажи', value: '642' }, + { label: 'Самоотверженный главный герой', value: '748' }, + { label: 'Самоубийства', value: '616' }, + { label: 'Самурай', value: '646' }, + { label: 'Сборник коротких историй', value: '456' }, + { label: 'Связанные сюжетные линии', value: '473' }, + { label: 'Святые', value: '761' }, + { label: 'Священники', value: '325' }, + { label: 'Сдержанный главный герой', value: '799' }, + { label: 'Секретные организации', value: '331' }, + { label: 'Секреты', value: '577' }, + { label: 'Секс рабы', value: '830' }, + { label: 'Семейный конфликт', value: '770' }, + { label: 'Семь добродетелей', value: '233' }, + { label: 'Семь смертных грехов', value: '134' }, + { label: 'Семья', value: '251' }, + { label: 'Сёнэн-ай подсюжет ', value: '664' }, + { label: 'Серийные убийцы', value: '497' }, + { label: 'Сестринский комплекс', value: '498' }, + { label: 'Сила духа', value: '282' }, + { label: 'Сила, требующая платы за пользование', value: '520' }, + { label: 'Сильная пара', value: '109' }, + { label: 'Сильный в сильнейшего', value: '359' }, + { label: 'Сильный любовный интерес', value: '161' }, + { label: 'Синдром восьмиклассника', value: '90' }, + { label: 'Сироты', value: '214' }, + { label: 'Система уровней', value: '198' }, + { label: 'Системный администратор', value: '476' }, + { label: 'Скрытие истинной личности', value: '371' }, + { label: 'Скрытие истинных способностей', value: '252' }, + { label: 'Скрытный главный герой', value: '133' }, + { label: 'Скрытые способности', value: '128' }, + { label: 'Скульпторы', value: '366' }, + { label: 'Слабо выраженная романтическая линия', value: '213' }, + { label: 'Слабый главный герой', value: '188' }, + { label: 'Слепой главный герой', value: '864' }, + { label: 'Слуги', value: '232' }, + { label: 'Смерть', value: '49' }, + { label: 'Смерть близких', value: '361' }, + { label: 'Собственнические персонажи', value: '108' }, + { label: 'Современность', value: '40' }, + { label: 'Сожительство', value: '482' }, + { label: 'Создание армии', value: '175' }, + { label: 'Создание артефактов', value: '292' }, + { label: 'Создание клана', value: '448' }, + { label: 'Создание королевства', value: '181' }, + { label: 'Создание навыков', value: '236' }, + { label: 'Создание секты', value: '393' }, + { label: 'Солдаты/Военные', value: '71' }, + { label: 'Сон', value: '571' }, + { label: 'Состоятельные персонажи', value: '67' }, + { label: 'Социальная иерархия на основе силы', value: '137' }, + { label: 'Социальные изгои', value: '480' }, + { label: 'Спасение мира', value: '597' }, + { label: 'Специальные способности', value: '54' }, + { label: 'Спокойный главный герой', value: '32' }, + { label: 'Справедливый главный герой', value: '702' }, + { label: 'Средневековье', value: '184' }, + { label: 'Ссорящаяся пара', value: '293' }, + { label: 'Сталкеры', value: '689' }, + { label: 'Старение', value: '190' }, + { label: 'Стоические персонажи', value: '444' }, + { label: 'Стокгольмский синдром', value: '643' }, + { label: 'Стратег', value: '425' }, + { label: 'Стратегические битвы', value: '160' }, + { label: 'Стратегия', value: '1038' }, + { label: 'Стрелки', value: '458' }, + { label: 'Стрельба из лука', value: '383' }, + { label: 'Студенческий совет', value: '490' }, + { label: 'Судьба', value: '271' }, + { label: 'Суккубы', value: '484' }, + { label: 'Супер герои', value: '1039' }, + { label: 'Суровая подготовка', value: '261' }, + { label: 'Таинственная болезнь', value: '74' }, + { label: 'Таинственное прошлое', value: '263' }, + { label: 'Тайная личность', value: '310' }, + { label: 'Тайные отношения', value: '812' }, + { label: 'Танцоры', value: '840' }, + { label: 'Телохранители', value: '452' }, + { label: 'Тентакли', value: '693' }, + { label: 'Террористы', value: '515' }, + { label: 'Технологический разрыв', value: '621' }, + { label: 'Тихие персонажи', value: '546' }, + { label: 'Толстый главный герой', value: '299' }, + { label: 'Торговцы', value: '416' }, + { label: 'Травля/Буллинг', value: '89' }, + { label: 'Травник', value: '708' }, + { label: 'Трагическое прошлое', value: '164' }, + { label: 'Трансплантация воспоминаний', value: '367' }, + { label: 'Трап (Путаница с гендером персонажа)', value: '582' }, + { label: 'Трудолюбивый главный герой', value: '37' }, + { label: 'Тюрьма', value: '479' }, + { label: 'Убийства', value: '84' }, + { label: 'Убийцы', value: '248' }, + { label: 'Убийцы драконов', value: '606' }, + { label: 'Уверенный главный герой', value: '270' }, + { label: 'Удачливый главный герой', value: '402' }, + { label: 'Укротитель монстров', value: '337' }, + { label: 'Умения из прошлой жизни', value: '280' }, + { label: 'Умная пара', value: '493' }, + { label: 'Умный главный герой', value: '33' }, + { label: 'Уникальная техника Культивации', value: '254' }, + { label: 'Уникальное оружие', value: '340' }, + { label: 'Управление бизнесом', value: '315' }, + { label: 'Управление временем', value: '167' }, + { label: 'Управление кровью', value: '764' }, + { label: 'Упрямый главный герой', value: '672' }, + { label: 'Уродливый главный герой', value: '803' }, + { label: 'Ускоренный рост', value: '300' }, + { label: 'Усыновленные дети', value: '481' }, + { label: 'Усыновленный главный герой', value: '412' }, + { label: 'Уход за детьми', value: '398' }, + { label: 'Учителя', value: '345' }, + { label: 'Фамильяры', value: '541' }, + { label: 'Фанатизм', value: '613' }, + { label: 'Фантастические существа', value: '322' }, + { label: 'Фанфикшн', value: '388' }, + { label: 'Фармацевт', value: '715' }, + { label: 'Фарминг', value: '379' }, + { label: 'Феи', value: '210' }, + { label: 'Фелляция', value: '584' }, + { label: 'Фениксы', value: '374' }, + { label: 'Фетиш груди', value: '499' }, + { label: 'Философия', value: '87' }, + { label: 'Фильмы', value: '403' }, + { label: 'Флэшбэки', value: '528' }, + { label: 'Фобии', value: '100' }, + { label: 'Фольклор', value: '467' }, + { label: 'Футанари', value: '568' }, + { label: 'Футуристический сеттинг', value: '382' }, + { label: 'Фэнтези мир', value: '126' }, + { label: 'Хакеры', value: '399' }, + { label: 'Харизматический герой', value: '391' }, + { label: 'Хикикомори/Затворники', value: '462' }, + { label: 'Хитроумный главный герой', value: '105' }, + { label: 'Хозяин подземелий', value: '557' }, + { label: 'Холодный главный герой', value: '259' }, + { label: 'Хорошие отношения с семьей', value: '506' }, + { label: 'Хранители могил', value: '68' }, + { label: 'Целители', value: '389' }, + { label: 'Цзянши', value: '735' }, + { label: 'Цундэрэ', value: '445' }, + { label: 'Чаты', value: '580' }, + { label: 'Человеческое оружие', value: '521' }, + { label: 'Честный главный герой', value: '240' }, + { label: 'Читы', value: '238' }, + { label: 'Шантаж', value: '526' }, + { label: 'Шеф-повар', value: '239' }, + { label: 'Шикигами', value: '543' }, + { label: 'Школа только для девочек', value: '633' }, + { label: 'Шота', value: '514' }, + { label: 'Шоу-бизнес', value: '407' }, + { label: 'Шпионы', value: '563' }, + { label: 'Эволюция', value: '196' }, + { label: 'Эгоистичный главный герой', value: '539' }, + { label: 'Эйдетическая память', value: '392' }, + { label: 'Экзорсизм', value: '504' }, + { label: 'Экономика', value: '492' }, + { label: 'Эксгибиционизм', value: '639' }, + { label: 'Эксперименты с людьми', value: '129' }, + { label: 'Элементальная магия', value: '395' }, + { label: 'Эльфы', value: '172' }, + { label: 'Эмоционально слабый главный герой', value: '816' }, + { label: 'Эпизодический', value: '612' }, + { label: 'Юный любовный интерес', value: '446' }, + { label: 'Яды', value: '410' }, + { label: 'Языкастые персонажи', value: '406' }, + { label: 'Языковой барьер', value: '59' }, + { label: 'Яндере', value: '208' }, + { label: 'Японские силы самообороны', value: '559' }, + { label: 'Ярко выраженная романтическая линия', value: '272' }, + { label: 'Abusive Characters', value: '625' }, + { label: 'Adapted to Visual Novel', value: '1046' }, + { label: 'Adopted-lead', value: '886' }, + { label: 'Adultery', value: '866' }, + { label: 'Affair', value: '645' }, + { label: 'Age-gap', value: '923' }, + { label: 'Almost-human-lead', value: '975' }, + { label: 'An*l', value: '780' }, + { label: 'Androgynous-male-lead', value: '1002' }, + { label: 'Anti-hero-lead', value: '976' }, + { label: 'Apathetic-lead', value: '967' }, + { label: 'Arms Dealers', value: '839' }, + { label: 'Army-commander', value: '1003' }, + { label: 'Artifact-refining', value: '924' }, + { label: 'Autism', value: '855' }, + { label: 'Awkward Protagonist', value: '669' }, + { label: 'Awkward-lead', value: '892' }, + { label: 'Bestiality', value: '796' }, + { label: 'Big-breasts', value: '908' }, + { label: 'Birth-of-a-nation', value: '1004' }, + { label: 'Bisexual-lead', value: '1027' }, + { label: 'Body-refining', value: '947' }, + { label: 'Body-swap/s', value: '1036' }, + { label: 'Boss-subordinate-relationship', value: '876' }, + { label: 'Bride-kidnapping', value: '948' }, + { label: 'Caring-lead', value: '879' }, + { label: 'Cautious-lead', value: '1032' }, + { label: 'Changed-man', value: '925' }, + { label: 'Charismatic-lead', value: '1005' }, + { label: 'Child-lead', value: '936' }, + { label: 'Clever-lead', value: '880' }, + { label: 'Clumsy Love Interests', value: '815' }, + { label: 'Cold Love Interests', value: '651' }, + { label: 'Coming of Age', value: '860' }, + { label: 'Confident-lead', value: '894' }, + { label: 'Confident-male-lead', value: '951' }, + { label: 'Confinement', value: '767' }, + { label: 'Conflicting Loyalties', value: '793' }, + { label: 'Couple Growth', value: '690' }, + { label: 'Court Official', value: '863' }, + { label: 'Cowardly Protagonist', value: '703' }, + { label: 'Cowardly-lead', value: '1000' }, + { label: 'Cross-dressing', value: '250' }, + { label: 'Cunnilingus', value: '867' }, + { label: 'Cunning-lead', value: '952' }, + { label: 'Cunning-male-lead', value: '953' }, + { label: 'Curious Protagonist', value: '849' }, + { label: 'Cute-lead', value: '887' }, + { label: 'Dense-lead', value: '868' }, + { label: 'Determined-lead', value: '972' }, + { label: 'Developing-technology', value: '1006' }, + { label: 'Devil/s', value: '1007' }, + { label: 'Different Social Status', value: '758' }, + { label: 'Disfigurement', value: '838' }, + { label: 'Doting Love Interests', value: '665' }, + { label: 'Doting Older Siblings', value: '609' }, + { label: 'Doting Parents', value: '530' }, + { label: 'Dungeon/s-exploring', value: '990' }, + { label: 'Elderly Protagonist', value: '774' }, + { label: 'Enlightenment', value: '729' }, + { label: 'Eunuch', value: '1047' }, + { label: 'Eye Powers', value: '386' }, + { label: 'Family Business', value: '785' }, + { label: 'Famous Parents', value: '790' }, + { label: 'Famous-lead', value: '897' }, + { label: 'Fanfic', value: '942' }, + { label: 'Fated Lovers', value: '797' }, + { label: 'Fellatio', value: '870' }, + { label: 'Female-lead', value: '888' }, + { label: 'First-time-intercourse', value: '871' }, + { label: 'Fleet Battles', value: '836' }, + { label: 'Forced Living Arrangements', value: '808' }, + { label: 'Forced Marriage', value: '835' }, + { label: 'Former Hero', value: '752' }, + { label: 'Fujoshi', value: '1045' }, + { label: 'Galge', value: '848' }, + { label: 'Gambling', value: '1031' }, + { label: 'Gamelit', value: '994' }, + { label: 'Genderless-lead', value: '1008' }, + { label: 'Glasses-wearing-lead', value: '995' }, + { label: 'Guardian Relationship', value: '197' }, + { label: 'H*ndjob', value: '629' }, + { label: 'Half-human-lead', value: '956' }, + { label: 'Hard-working-lead', value: '957' }, + { label: 'Hard-working-male-lead', value: '958' }, + { label: 'Hard-working-protagonist/s', value: '1030' }, + { label: 'Harem-seeking-lead', value: '929' }, + { label: 'Harem-subtext', value: '909' }, + { label: 'Helpful Protagonist', value: '760' }, + { label: 'Helpful-lead', value: '882' }, + { label: 'Hidden-gem', value: '998' }, + { label: 'High-fantasy', value: '991' }, + { label: 'Human-becomes-demon/monster', value: '1009' }, + { label: 'Human-nonhuman-relationship', value: '914' }, + { label: 'Humanoid-lead', value: '915' }, + { label: 'Imperial Harem', value: '805' }, + { label: 'Incubus', value: '1049' }, + { label: 'Insects', value: '831' }, + { label: 'Interspatial-storage', value: '959' }, + { label: 'Kind Love Interests', value: '757' }, + { label: 'Large-number-of-skills', value: '1010' }, + { label: 'Legendary-hero', value: '1011' }, + { label: 'Litrpg', value: '977' }, + { label: 'Love Rivals', value: '788' }, + { label: 'Lovers Reunited', value: '853' }, + { label: 'Low-fantasy', value: '1037' }, + { label: 'Master-disciple-relationship', value: '899' }, + { label: 'Matriarchy', value: '807' }, + { label: 'Mature-lead', value: '900' }, + { label: 'Mind Break', value: '862' }, + { label: 'Mismatched Couple', value: '695' }, + { label: 'Mob Protagonist', value: '648' }, + { label: 'Mob-lead', value: '902' }, + { label: 'Modern', value: '988' }, + { label: 'Monster-pov', value: '1012' }, + { label: 'Mpreg', value: '856' }, + { label: 'Multiple-povs', value: '1013' }, + { label: 'Mystery Solving', value: '710' }, + { label: 'Naive-lead', value: '1014' }, + { label: 'Near-death-experience', value: '996' }, + { label: 'Neet', value: '495' }, + { label: 'Nightmares', value: '833' }, + { label: 'Nobility', value: '1015' }, + { label: 'Non-human-lead', value: '978' }, + { label: 'Non-humanoid-lead', value: '1016' }, + { label: 'Not-so-secret-identity', value: '979' }, + { label: 'Omegaverse', value: '857' }, + { label: 'Online-game', value: '910' }, + { label: 'Online-gaming', value: '911' }, + { label: 'Outdoor Interc**rse', value: '847' }, + { label: 'Outdoor-intercourse', value: '903' }, + { label: 'Overprotective Siblings', value: '610' }, + { label: 'Part-Time Job', value: '865' }, + { label: 'Past Trauma', value: '726' }, + { label: 'Past-memories', value: '1017' }, + { label: 'Persistent Love Interests', value: '714' }, + { label: 'Perverted-lead', value: '932' }, + { label: 'Photography', value: '798' }, + { label: 'Pill Based Cultivation', value: '279' }, + { label: 'Pill Concocting', value: '245' }, + { label: 'Pilots', value: '820' }, + { label: 'Playboys', value: '795' }, + { label: 'Playful-lead', value: '989' }, + { label: 'Polyandry', value: '811' }, + { label: 'Portal-fantasy-/-isekai', value: '992' }, + { label: 'Pragmatic-lead', value: '884' }, + { label: 'Profanity', value: '980' }, + { label: 'Protagonist-loyal-to-love-interest', value: '905' }, + { label: 'R-15 (Японское возрастное ограничение)', value: '230' }, + { label: 'Rebellion', value: '834' }, + { label: 'Reincarnated in a Game World', value: '804' }, + { label: 'Reluctant Protagonist', value: '696' }, + { label: 'Reverse R*pe', value: '776' }, + { label: 'Reversible Couple', value: '854' }, + { label: 'Ruling-class', value: '943' }, + { label: 'S*x Friends', value: '750' }, + { label: 'S*xual Cultivation Technique', value: '823' }, + { label: 'Salaryman', value: '1035' }, + { label: 'Satire', value: '981' }, + { label: 'Schemes And Conspiracies', value: '745' }, + { label: 'Scientist/s', value: '997' }, + { label: 'Scientists', value: '1029' }, + { label: 'Secret Crush', value: '827' }, + { label: 'Seme Protagonist', value: '1034' }, + { label: 'Seme-lead', value: '1028' }, + { label: 'Sentimental Protagonist', value: '1048' }, + { label: 'Seven-heavenly-virtues', value: '1018' }, + { label: 'Sexual Cultivation Technique', value: '350' }, + { label: 'Sexual-content', value: '982' }, + { label: 'Sexuality', value: '983' }, + { label: 'Sharing A Body', value: '814' }, + { label: 'Shotacon', value: '858' }, + { label: 'Shoujo-Ai Subplot', value: '777' }, + { label: 'Sibling Rivalry', value: '766' }, + { label: "Sibling's Care", value: '1042' }, + { label: 'Sickly Characters', value: '787' }, + { label: 'Skills', value: '1019' }, + { label: 'Slave-harem', value: '1001' }, + { label: 'Slime', value: '1020' }, + { label: 'Smart-male-lead', value: '963' }, + { label: 'Spirit Users', value: '739' }, + { label: 'Straight Seme', value: '819' }, + { label: 'Straight Uke', value: '818' }, + { label: 'Strength-based-social-hierarchy', value: '974' }, + { label: 'Strong-lead', value: '944' }, + { label: 'Strong-male-lead', value: '964' }, + { label: 'Student-teacher-relationship', value: '1026' }, + { label: 'Suspense', value: '987' }, + { label: 'Sword-and-sorcery', value: '875' }, + { label: 'Threesome', value: '574' }, + { label: 'Transformation Ability', value: '736' }, + { label: 'Traumatising-content', value: '1040' }, + { label: 'Underestimated-lead', value: '919' }, + { label: 'Underestimated-male-lead', value: '966' }, + { label: 'Unique-abilities', value: '1021' }, + { label: 'Unique-skill', value: '1022' }, + { label: 'Unique-skills', value: '1023' }, + { label: 'Unlimited Flow', value: '1044' }, + { label: 'Unrequited Love', value: '806' }, + { label: 'Urban-fantasy', value: '1041' }, + { label: 'Villainess Noble Girls', value: '719' }, + { label: 'Weak-lead', value: '885' }, + { label: 'Web-novel', value: '1024' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, tags: { - label: "Жанры", + label: 'Жанры', value: { include: [], exclude: [] }, options: [ - { label: "Боевые искусства", value: "22" }, - { label: "Гарем", value: "114" }, - { label: "Гендер бендер", value: "246" }, - { label: "Дзёсэй", value: "216" }, - { label: "Для взрослых", value: "115" }, - { label: "Для взрослых", value: "258" }, - { label: "Драма", value: "7" }, - { label: "Исторический", value: "101" }, - { label: "Комедия", value: "17" }, - { label: "Лоликон", value: "638" }, - { label: "Магический реализм", value: "922" }, - { label: "Меха", value: "24" }, - { label: "Милитари", value: "12" }, - { label: "Мистика", value: "2" }, - { label: "Научная фантастика", value: "13" }, - { label: "Непристойность", value: "747" }, - { label: "Повседневность", value: "93" }, - { label: "Приключение", value: "11" }, - { label: "Психология", value: "18" }, - { label: "Романтика", value: "9" }, - { label: "Сверхъестественное", value: "20" }, - { label: "Сёдзё", value: "15" }, - { label: "Сёдзё-ай", value: "23" }, - { label: "Сёнэн", value: "189" }, - { label: "Сёнэн-ай", value: "680" }, - { label: "Спорт", value: "420" }, - { label: "Сэйнэн", value: "5" }, - { label: "Сюаньхуа", value: "242" }, - { label: "Сянься", value: "364" }, - { label: "Трагедия", value: "19" }, - { label: "Триллер", value: "3" }, - { label: "Ужасы", value: "1" }, - { label: "Уся", value: "720" }, - { label: "Фэнтези", value: "8" }, - { label: "Школьная жизнь", value: "21" }, - { label: "Экшн", value: "14" }, - { label: "Эччи", value: "327" }, - { label: "Юри", value: "691" }, - { label: "Яой", value: "682" }, - { label: "Eastern fantasy", value: "907" }, - { label: "Isekai", value: "999" }, - { label: "Video games", value: "993" }, + { label: 'Боевые искусства', value: '22' }, + { label: 'Гарем', value: '114' }, + { label: 'Гендер бендер', value: '246' }, + { label: 'Дзёсэй', value: '216' }, + { label: 'Для взрослых', value: '115' }, + { label: 'Для взрослых', value: '258' }, + { label: 'Драма', value: '7' }, + { label: 'Исторический', value: '101' }, + { label: 'Комедия', value: '17' }, + { label: 'Лоликон', value: '638' }, + { label: 'Магический реализм', value: '922' }, + { label: 'Меха', value: '24' }, + { label: 'Милитари', value: '12' }, + { label: 'Мистика', value: '2' }, + { label: 'Научная фантастика', value: '13' }, + { label: 'Непристойность', value: '747' }, + { label: 'Повседневность', value: '93' }, + { label: 'Приключение', value: '11' }, + { label: 'Психология', value: '18' }, + { label: 'Романтика', value: '9' }, + { label: 'Сверхъестественное', value: '20' }, + { label: 'Сёдзё', value: '15' }, + { label: 'Сёдзё-ай', value: '23' }, + { label: 'Сёнэн', value: '189' }, + { label: 'Сёнэн-ай', value: '680' }, + { label: 'Спорт', value: '420' }, + { label: 'Сэйнэн', value: '5' }, + { label: 'Сюаньхуа', value: '242' }, + { label: 'Сянься', value: '364' }, + { label: 'Трагедия', value: '19' }, + { label: 'Триллер', value: '3' }, + { label: 'Ужасы', value: '1' }, + { label: 'Уся', value: '720' }, + { label: 'Фэнтези', value: '8' }, + { label: 'Школьная жизнь', value: '21' }, + { label: 'Экшн', value: '14' }, + { label: 'Эччи', value: '327' }, + { label: 'Юри', value: '691' }, + { label: 'Яой', value: '682' }, + { label: 'Eastern fantasy', value: '907' }, + { label: 'Isekai', value: '999' }, + { label: 'Video games', value: '993' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts index b7bbcff..56e47b8 100644 --- a/plugins/russian/ranobelib.ts +++ b/plugins/russian/ranobelib.ts @@ -1,9 +1,9 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; const statusKey: { [key: number]: string } = { 1: NovelStatus.Ongoing, @@ -13,11 +13,11 @@ const statusKey: { [key: number]: string } = { }; class RLIB implements Plugin.PluginBase { - id = "RLIB"; - name = "RanobeLib"; - site = "https://ranobelib.me"; - version = "1.0.0"; - icon = "src/ru/ranobelib/icon.png"; + id = 'RLIB'; + name = 'RanobeLib'; + site = 'https://ranobelib.me'; + version = '1.0.0'; + icon = 'src/ru/ranobelib/icon.png'; ui: string | undefined = undefined; async popularNovels( @@ -27,63 +27,74 @@ class RLIB implements Plugin.PluginBase { filters, }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/manga-list?sort="; - url += showLatestNovels ? "last_chapter_at" : filters?.sort?.value || "rate"; - url += "&dir=" + (filters?.order?.value || "desc"); - url += "&chapters[min]=" + (filters?.require_chapters?.value ? "1" : "0"); + let url = this.site + '/manga-list?sort='; + url += showLatestNovels + ? 'last_chapter_at' + : filters?.sort?.value || 'rate'; + url += '&dir=' + (filters?.order?.value || 'desc'); + url += '&chapters[min]=' + (filters?.require_chapters?.value ? '1' : '0'); Object.entries(filters || {}).forEach(([type, { value }]: any) => { if (value instanceof Array && value.length) { - url += "&" + type + "[]=" + value.join("&" + type + "[]="); + url += '&' + type + '[]=' + value.join('&' + type + '[]='); } if (value?.include instanceof Array && value.include.length) { - url += "&" + type + "[include][]=" + - value.include.join("&" + type + "[include][]="); + url += + '&' + + type + + '[include][]=' + + value.include.join('&' + type + '[include][]='); } if (value?.exclude instanceof Array && value.exclude.length) { - url += "&" + type + "[exclude][]=" + - value.exclude.join("&" + type + "[exclude][]="); + url += + '&' + + type + + '[exclude][]=' + + value.exclude.join('&' + type + '[exclude][]='); } }); - url += "&page=" + pageNo; + url += '&page=' + pageNo; - const result = await fetchApi(url).then((res) => res.text()); + const result = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(result); - this.ui = loadedCheerio("a.header-right-menu__item") - .attr("href") - ?.replace?.(/[^0-9]/g, ""); + this.ui = loadedCheerio('a.header-right-menu__item') + .attr('href') + ?.replace?.(/[^0-9]/g, ''); const novels: Plugin.NovelItem[] = []; - loadedCheerio(".media-card-wrap").each((index, element) => { - const name = loadedCheerio(element).find(".media-card__title").text(); - const cover = loadedCheerio(element).find("a.media-card").attr("data-src"); - const url = loadedCheerio(element).find("a.media-card").attr("href"); + loadedCheerio('.media-card-wrap').each((index, element) => { + const name = loadedCheerio(element).find('.media-card__title').text(); + const cover = loadedCheerio(element) + .find('a.media-card') + .attr('data-src'); + const url = loadedCheerio(element).find('a.media-card').attr('href'); if (!url) return; - novels.push({ name, cover, path: url.replace(this.site, "") }); + novels.push({ name, cover, path: url.replace(this.site, '') }); }); return novels; } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.resolveUrl(novelPath, true)) - .then((res) => res.text()); + const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, - name: loadedCheerio(".media-name__main").text()?.trim?.() || "", + name: loadedCheerio('.media-name__main').text()?.trim?.() || '', }; - novel.cover = loadedCheerio(".container_responsive img").attr("src"); + novel.cover = loadedCheerio('.container_responsive img').attr('src'); - novel.summary = loadedCheerio(".media-description__text").text().trim(); + novel.summary = loadedCheerio('.media-description__text').text().trim(); novel.genres = loadedCheerio('div[class="media-tags"]') .text() .trim() - .replace(/[\n\r]+/g, ", ") - .replace(/ /g, ""); + .replace(/[\n\r]+/g, ', ') + .replace(/ /g, ''); loadedCheerio( 'div[class="media-info-list paper"] > [class="media-info-list__item"]', @@ -92,12 +103,12 @@ class RLIB implements Plugin.PluginBase { .find('div[class="media-info-list__title"]') .text(); - if (name === "Автор") { + if (name === 'Автор') { novel.author = loadedCheerio(this) .find('div[class="media-info-list__value"]') .text() .trim(); - } else if (name === "Художник") { + } else if (name === 'Художник') { novel.artist = loadedCheerio(this) .find('div[class="media-info-list__value"]') .text() @@ -105,9 +116,9 @@ class RLIB implements Plugin.PluginBase { } }); - this.ui = loadedCheerio("a.header-right-menu__item") - .attr("href") - ?.replace?.(/[^0-9]/g, ""); + this.ui = loadedCheerio('a.header-right-menu__item') + .attr('href') + ?.replace?.(/[^0-9]/g, ''); const chaptersRaw = body.match(/window\.__DATA__ = ({.*?});/); if (chaptersRaw instanceof Array && chaptersRaw.length >= 2) { @@ -119,7 +130,8 @@ class RLIB implements Plugin.PluginBase { chaptersJson.manga.engName || chaptersJson.manga.name; } - novel.status = statusKey[chaptersJson.manga.status] || NovelStatus.Unknown; + novel.status = + statusKey[chaptersJson.manga.status] || NovelStatus.Unknown; this.ui = chaptersJson?.user?.id; if (!chaptersJson.chapters.list?.length) return novel; @@ -127,16 +139,19 @@ class RLIB implements Plugin.PluginBase { const customPage: { [key: number]: string } = {}; const customOrder: { [key: number]: number } = {}; - if (chaptersJson.chapters.branches?.length && chaptersJson.chapters.branches.length > 1) { + if ( + chaptersJson.chapters.branches?.length && + chaptersJson.chapters.branches.length > 1 + ) { //if the novel is being translated by more than one team chaptersJson.chapters.branches.forEach(({ teams, id }) => { if (teams?.length) { customPage[id || 0] = - teams.find((team) => team.is_active)?.name || teams[0].name; + teams.find(team => team.is_active)?.name || teams[0].name; } }); //fixes the chapter's position. - chaptersJson.chapters.list.forEach((chapter) => { + chaptersJson.chapters.list.forEach(chapter => { chapter.index = customOrder[chapter.branch_id || 0] || 1; customOrder[chapter.branch_id || 0] = (customOrder[chapter.branch_id || 0] || 1) + 1; @@ -147,19 +162,24 @@ class RLIB implements Plugin.PluginBase { chaptersJson.chapters.list.forEach((chapter, chapterIndex) => chapters.push({ name: - "Том " + chapter.chapter_volume + - " Глава " + chapter.chapter_number + - (chapter.chapter_name ? " " + chapter.chapter_name.trim() : ""), + 'Том ' + + chapter.chapter_volume + + ' Глава ' + + chapter.chapter_number + + (chapter.chapter_name ? ' ' + chapter.chapter_name.trim() : ''), path: novelPath + - "/v" + chapter.chapter_volume + - "/c" + chapter.chapter_number + - "?bid=" + (chapter.branch_id || ""), - releaseTime: dayjs(chapter.chapter_created_at).format("LLL"), + '/v' + + chapter.chapter_volume + + '/c' + + chapter.chapter_number + + '?bid=' + + (chapter.branch_id || ''), + releaseTime: dayjs(chapter.chapter_created_at).format('LLL'), chapterNumber: customOrder[chapter.branch_id || 0] - (chapter.index || 0) || totalChapters - chapterIndex, - page: customPage[chapter.branch_id || 0] || "Основной перевод", + page: customPage[chapter.branch_id || 0] || 'Основной перевод', }), ); novel.chapters = @@ -176,38 +196,39 @@ class RLIB implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.resolveUrl(chapterPath)) - .then((res) => res.text()); + const result = await fetchApi(this.resolveUrl(chapterPath)).then(res => + res.text(), + ); const loadedCheerio = parseHTML(result); - loadedCheerio(".reader-container img").each((index, element) => { + loadedCheerio('.reader-container img').each((index, element) => { const src = - loadedCheerio(element).attr("data-src") || - loadedCheerio(element).attr("src"); - if (!src?.startsWith("http")) { - loadedCheerio(element).attr("src", this.site + src); + loadedCheerio(element).attr('data-src') || + loadedCheerio(element).attr('src'); + if (!src?.startsWith('http')) { + loadedCheerio(element).attr('src', this.site + src); } else { - loadedCheerio(element).attr("src", src); + loadedCheerio(element).attr('src', src); } - loadedCheerio(element).removeAttr("data-src"); + loadedCheerio(element).removeAttr('data-src'); }); - const chapterText = loadedCheerio(".reader-container").html(); - return chapterText || ""; + const chapterText = loadedCheerio('.reader-container').html(); + return chapterText || ''; } async searchNovels(searchTerm: string): Promise { const result = await fetchApi( - this.site + "/search?q=" + searchTerm + "&type=manga", + this.site + '/search?q=' + searchTerm + '&type=manga', ); const body = (await result.json()) as Manga[]; const novels: Plugin.NovelItem[] = []; - body.forEach((novel) => + body.forEach(novel => novels.push({ name: novel.rus_name || novel.name, cover: novel.coverImage, - path: "/" + novel.slug, + path: '/' + novel.slug, }), ); @@ -216,223 +237,223 @@ class RLIB implements Plugin.PluginBase { fetchImage = fetchFile; resolveUrl = (path: string, isNovel?: boolean) => - this.site + path + (this.ui ? (isNovel ? "?" : "&") + "ui=" + this.ui : ""); + this.site + path + (this.ui ? (isNovel ? '?' : '&') + 'ui=' + this.ui : ''); filters = { sort: { - label: "Сортировка", - value: "rate", + label: 'Сортировка', + value: 'rate', options: [ - { label: "Рейтинг", value: "rate" }, - { label: "Имя", value: "name" }, - { label: "Просмотры", value: "views" }, - { label: "Дате добавления", value: "created_at" }, - { label: "Дате обновления", value: "last_chapter_at" }, - { label: "Количество глав", value: "chap_count" }, + { label: 'Рейтинг', value: 'rate' }, + { label: 'Имя', value: 'name' }, + { label: 'Просмотры', value: 'views' }, + { label: 'Дате добавления', value: 'created_at' }, + { label: 'Дате обновления', value: 'last_chapter_at' }, + { label: 'Количество глав', value: 'chap_count' }, ], type: FilterTypes.Picker, }, order: { - label: "Порядок", - value: "desc", + label: 'Порядок', + value: 'desc', options: [ - { label: "По убыванию", value: "desc" }, - { label: "По возрастанию", value: "asc" }, + { label: 'По убыванию', value: 'desc' }, + { label: 'По возрастанию', value: 'asc' }, ], type: FilterTypes.Picker, }, type: { - label: "Тип", + label: 'Тип', value: [], options: [ - { label: "Авторский", value: "14" }, - { label: "Английский", value: "13" }, - { label: "Китай", value: "12" }, - { label: "Корея", value: "11" }, - { label: "Фанфик", value: "15" }, - { label: "Япония", value: "10" }, + { label: 'Авторский', value: '14' }, + { label: 'Английский', value: '13' }, + { label: 'Китай', value: '12' }, + { label: 'Корея', value: '11' }, + { label: 'Фанфик', value: '15' }, + { label: 'Япония', value: '10' }, ], type: FilterTypes.CheckboxGroup, }, format: { - label: "Формат выпуска", + label: 'Формат выпуска', value: { include: [], exclude: [] }, options: [ - { label: "4-кома (Ёнкома)", value: "1" }, - { label: "В цвете", value: "4" }, - { label: "Веб", value: "6" }, - { label: "Вебтун", value: "7" }, - { label: "Додзинси", value: "3" }, - { label: "Сборник", value: "2" }, - { label: "Сингл", value: "5" }, + { label: '4-кома (Ёнкома)', value: '1' }, + { label: 'В цвете', value: '4' }, + { label: 'Веб', value: '6' }, + { label: 'Вебтун', value: '7' }, + { label: 'Додзинси', value: '3' }, + { label: 'Сборник', value: '2' }, + { label: 'Сингл', value: '5' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, status: { - label: "Статус перевода", + label: 'Статус перевода', value: [], options: [ - { label: "Продолжается", value: "1" }, - { label: "Завершен", value: "2" }, - { label: "Заморожен", value: "3" }, - { label: "Заброшен", value: "4" }, + { label: 'Продолжается', value: '1' }, + { label: 'Завершен', value: '2' }, + { label: 'Заморожен', value: '3' }, + { label: 'Заброшен', value: '4' }, ], type: FilterTypes.CheckboxGroup, }, manga_status: { - label: "Статус тайтла", + label: 'Статус тайтла', value: [], options: [ - { label: "Онгоинг", value: "1" }, - { label: "Завершён", value: "2" }, - { label: "Анонс", value: "3" }, - { label: "Приостановлен", value: "4" }, - { label: "Выпуск прекращён", value: "5" }, + { label: 'Онгоинг', value: '1' }, + { label: 'Завершён', value: '2' }, + { label: 'Анонс', value: '3' }, + { label: 'Приостановлен', value: '4' }, + { label: 'Выпуск прекращён', value: '5' }, ], type: FilterTypes.CheckboxGroup, }, genres: { - label: "Жанры", + label: 'Жанры', value: { include: [], exclude: [] }, options: [ - { label: "Арт", value: "32" }, - { label: "Безумие", value: "91" }, - { label: "Боевик", value: "34" }, - { label: "Боевые искусства", value: "35" }, - { label: "Вампиры", value: "36" }, - { label: "Военное", value: "89" }, - { label: "Гарем", value: "37" }, - { label: "Гендерная интрига", value: "38" }, - { label: "Героическое фэнтези", value: "39" }, - { label: "Демоны", value: "81" }, - { label: "Детектив", value: "40" }, - { label: "Детское", value: "88" }, - { label: "Дзёсэй", value: "41" }, - { label: "Драма", value: "43" }, - { label: "Игра", value: "44" }, - { label: "Исекай", value: "79" }, - { label: "История", value: "45" }, - { label: "Киберпанк", value: "46" }, - { label: "Кодомо", value: "76" }, - { label: "Комедия", value: "47" }, - { label: "Космос", value: "83" }, - { label: "Магия", value: "85" }, - { label: "Махо-сёдзё", value: "48" }, - { label: "Машины", value: "90" }, - { label: "Меха", value: "49" }, - { label: "Мистика", value: "50" }, - { label: "Музыка", value: "80" }, - { label: "Научная фантастика", value: "51" }, - { label: "Омегаверс", value: "77" }, - { label: "Пародия", value: "86" }, - { label: "Повседневность", value: "52" }, - { label: "Полиция", value: "82" }, - { label: "Постапокалиптика", value: "53" }, - { label: "Приключения", value: "54" }, - { label: "Психология", value: "55" }, - { label: "Романтика", value: "56" }, - { label: "Самурайский боевик", value: "57" }, - { label: "Сверхъестественное", value: "58" }, - { label: "Сёдзё", value: "59" }, - { label: "Сёдзё-ай", value: "60" }, - { label: "Сёнэн", value: "61" }, - { label: "Сёнэн-ай", value: "62" }, - { label: "Спорт", value: "63" }, - { label: "Супер сила", value: "87" }, - { label: "Сэйнэн", value: "64" }, - { label: "Трагедия", value: "65" }, - { label: "Триллер", value: "66" }, - { label: "Ужасы", value: "67" }, - { label: "Фантастика", value: "68" }, - { label: "Фэнтези", value: "69" }, - { label: "Школа", value: "70" }, - { label: "Эротика", value: "71" }, - { label: "Этти", value: "72" }, - { label: "Юри", value: "73" }, - { label: "Яой", value: "74" }, + { label: 'Арт', value: '32' }, + { label: 'Безумие', value: '91' }, + { label: 'Боевик', value: '34' }, + { label: 'Боевые искусства', value: '35' }, + { label: 'Вампиры', value: '36' }, + { label: 'Военное', value: '89' }, + { label: 'Гарем', value: '37' }, + { label: 'Гендерная интрига', value: '38' }, + { label: 'Героическое фэнтези', value: '39' }, + { label: 'Демоны', value: '81' }, + { label: 'Детектив', value: '40' }, + { label: 'Детское', value: '88' }, + { label: 'Дзёсэй', value: '41' }, + { label: 'Драма', value: '43' }, + { label: 'Игра', value: '44' }, + { label: 'Исекай', value: '79' }, + { label: 'История', value: '45' }, + { label: 'Киберпанк', value: '46' }, + { label: 'Кодомо', value: '76' }, + { label: 'Комедия', value: '47' }, + { label: 'Космос', value: '83' }, + { label: 'Магия', value: '85' }, + { label: 'Махо-сёдзё', value: '48' }, + { label: 'Машины', value: '90' }, + { label: 'Меха', value: '49' }, + { label: 'Мистика', value: '50' }, + { label: 'Музыка', value: '80' }, + { label: 'Научная фантастика', value: '51' }, + { label: 'Омегаверс', value: '77' }, + { label: 'Пародия', value: '86' }, + { label: 'Повседневность', value: '52' }, + { label: 'Полиция', value: '82' }, + { label: 'Постапокалиптика', value: '53' }, + { label: 'Приключения', value: '54' }, + { label: 'Психология', value: '55' }, + { label: 'Романтика', value: '56' }, + { label: 'Самурайский боевик', value: '57' }, + { label: 'Сверхъестественное', value: '58' }, + { label: 'Сёдзё', value: '59' }, + { label: 'Сёдзё-ай', value: '60' }, + { label: 'Сёнэн', value: '61' }, + { label: 'Сёнэн-ай', value: '62' }, + { label: 'Спорт', value: '63' }, + { label: 'Супер сила', value: '87' }, + { label: 'Сэйнэн', value: '64' }, + { label: 'Трагедия', value: '65' }, + { label: 'Триллер', value: '66' }, + { label: 'Ужасы', value: '67' }, + { label: 'Фантастика', value: '68' }, + { label: 'Фэнтези', value: '69' }, + { label: 'Школа', value: '70' }, + { label: 'Эротика', value: '71' }, + { label: 'Этти', value: '72' }, + { label: 'Юри', value: '73' }, + { label: 'Яой', value: '74' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, tags: { - label: "Теги", + label: 'Теги', value: { include: [], exclude: [] }, options: [ - { label: "Авантюристы", value: "328" }, - { label: "Антигерой", value: "176" }, - { label: "Бессмертные", value: "333" }, - { label: "Боги", value: "218" }, - { label: "Борьба за власть", value: "309" }, - { label: "Брат и сестра", value: "360" }, - { label: "Ведьма", value: "339" }, - { label: "Видеоигры", value: "204" }, - { label: "Виртуальная реальность", value: "214" }, - { label: "Владыка демонов", value: "349" }, - { label: "Военные", value: "198" }, - { label: "Воспоминания из другого мира", value: "310" }, - { label: "Выживание", value: "212" }, - { label: "ГГ женщина", value: "294" }, - { label: "ГГ имба", value: "292" }, - { label: "ГГ мужчина", value: "295" }, - { label: "ГГ не ояш", value: "325" }, - { label: "ГГ не человек", value: "331" }, - { label: "ГГ ояш", value: "326" }, - { label: "Главный герой бог", value: "324" }, - { label: "Глупый ГГ", value: "298" }, - { label: "Горничные", value: "171" }, - { label: "Гуро", value: "306" }, - { label: "Гяру", value: "197" }, - { label: "Демоны", value: "157" }, - { label: "Драконы", value: "313" }, - { label: "Древний мир", value: "317" }, - { label: "Зверолюди", value: "163" }, - { label: "Зомби", value: "155" }, - { label: "Исторические фигуры", value: "323" }, - { label: "Кулинария", value: "158" }, - { label: "Культивация", value: "161" }, - { label: "ЛГБТ", value: "344" }, - { label: "ЛитРПГ", value: "319" }, - { label: "Лоли", value: "206" }, - { label: "Магия", value: "170" }, - { label: "Машинный перевод", value: "345" }, - { label: "Медицина", value: "159" }, - { label: "Межгалактическая война", value: "330" }, - { label: "Монстр Девушки", value: "207" }, - { label: "Монстры", value: "208" }, - { label: "Мрачный мир", value: "316" }, - { label: "Музыка", value: "358" }, - { label: "Музыка", value: "209" }, - { label: "Ниндзя", value: "199" }, - { label: "Обратный Гарем", value: "210" }, - { label: "Офисные Работники", value: "200" }, - { label: "Пираты", value: "341" }, - { label: "Подземелья", value: "314" }, - { label: "Политика", value: "311" }, - { label: "Полиция", value: "201" }, - { label: "Преступники / Криминал", value: "205" }, - { label: "Призраки / Духи", value: "196" }, - { label: "Призыватели", value: "329" }, - { label: "Прыжки между мирами", value: "321" }, - { label: "Путешествие в другой мир", value: "318" }, - { label: "Путешествие во времени", value: "213" }, - { label: "Рабы", value: "355" }, - { label: "Ранги силы", value: "312" }, - { label: "Реинкарнация", value: "154" }, - { label: "Самураи", value: "202" }, - { label: "Скрытие личности", value: "315" }, - { label: "Средневековье", value: "174" }, - { label: "Традиционные игры", value: "203" }, - { label: "Умный ГГ", value: "303" }, - { label: "Характерный рост", value: "332" }, - { label: "Хикикомори", value: "167" }, - { label: "Эволюция", value: "322" }, - { label: "Элементы РПГ", value: "327" }, - { label: "Эльфы", value: "217" }, - { label: "Якудза", value: "165" }, + { label: 'Авантюристы', value: '328' }, + { label: 'Антигерой', value: '176' }, + { label: 'Бессмертные', value: '333' }, + { label: 'Боги', value: '218' }, + { label: 'Борьба за власть', value: '309' }, + { label: 'Брат и сестра', value: '360' }, + { label: 'Ведьма', value: '339' }, + { label: 'Видеоигры', value: '204' }, + { label: 'Виртуальная реальность', value: '214' }, + { label: 'Владыка демонов', value: '349' }, + { label: 'Военные', value: '198' }, + { label: 'Воспоминания из другого мира', value: '310' }, + { label: 'Выживание', value: '212' }, + { label: 'ГГ женщина', value: '294' }, + { label: 'ГГ имба', value: '292' }, + { label: 'ГГ мужчина', value: '295' }, + { label: 'ГГ не ояш', value: '325' }, + { label: 'ГГ не человек', value: '331' }, + { label: 'ГГ ояш', value: '326' }, + { label: 'Главный герой бог', value: '324' }, + { label: 'Глупый ГГ', value: '298' }, + { label: 'Горничные', value: '171' }, + { label: 'Гуро', value: '306' }, + { label: 'Гяру', value: '197' }, + { label: 'Демоны', value: '157' }, + { label: 'Драконы', value: '313' }, + { label: 'Древний мир', value: '317' }, + { label: 'Зверолюди', value: '163' }, + { label: 'Зомби', value: '155' }, + { label: 'Исторические фигуры', value: '323' }, + { label: 'Кулинария', value: '158' }, + { label: 'Культивация', value: '161' }, + { label: 'ЛГБТ', value: '344' }, + { label: 'ЛитРПГ', value: '319' }, + { label: 'Лоли', value: '206' }, + { label: 'Магия', value: '170' }, + { label: 'Машинный перевод', value: '345' }, + { label: 'Медицина', value: '159' }, + { label: 'Межгалактическая война', value: '330' }, + { label: 'Монстр Девушки', value: '207' }, + { label: 'Монстры', value: '208' }, + { label: 'Мрачный мир', value: '316' }, + { label: 'Музыка', value: '358' }, + { label: 'Музыка', value: '209' }, + { label: 'Ниндзя', value: '199' }, + { label: 'Обратный Гарем', value: '210' }, + { label: 'Офисные Работники', value: '200' }, + { label: 'Пираты', value: '341' }, + { label: 'Подземелья', value: '314' }, + { label: 'Политика', value: '311' }, + { label: 'Полиция', value: '201' }, + { label: 'Преступники / Криминал', value: '205' }, + { label: 'Призраки / Духи', value: '196' }, + { label: 'Призыватели', value: '329' }, + { label: 'Прыжки между мирами', value: '321' }, + { label: 'Путешествие в другой мир', value: '318' }, + { label: 'Путешествие во времени', value: '213' }, + { label: 'Рабы', value: '355' }, + { label: 'Ранги силы', value: '312' }, + { label: 'Реинкарнация', value: '154' }, + { label: 'Самураи', value: '202' }, + { label: 'Скрытие личности', value: '315' }, + { label: 'Средневековье', value: '174' }, + { label: 'Традиционные игры', value: '203' }, + { label: 'Умный ГГ', value: '303' }, + { label: 'Характерный рост', value: '332' }, + { label: 'Хикикомори', value: '167' }, + { label: 'Эволюция', value: '322' }, + { label: 'Элементы РПГ', value: '327' }, + { label: 'Эльфы', value: '217' }, + { label: 'Якудза', value: '165' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, require_chapters: { - label: "Только проекты с главами", + label: 'Только проекты с главами', value: true, type: FilterTypes.Switch, }, diff --git a/plugins/russian/ranoberf.ts b/plugins/russian/ranoberf.ts index 1b6d980..810bf4c 100644 --- a/plugins/russian/ranoberf.ts +++ b/plugins/russian/ranoberf.ts @@ -1,43 +1,43 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { defaultCover } from "@libs/defaultCover"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; class RNRF implements Plugin.PluginBase { - id = "RNRF"; - name = "РанобэРФ"; - site = "https://ранобэ.рф"; - version = "1.0.0"; - icon = "src/ru/ranoberf/icon.png"; + id = 'RNRF'; + name = 'РанобэРФ'; + site = 'https://ранобэ.рф'; + version = '1.0.0'; + icon = 'src/ru/ranoberf/icon.png'; async popularNovels( pageNo: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/books?order="; + let url = this.site + '/books?order='; url += showLatestNovels - ? "lastPublishedChapter" - : filters?.sort?.value || "popular"; - url += "&page=" + pageNo; + ? 'lastPublishedChapter' + : filters?.sort?.value || 'popular'; + url += '&page=' + pageNo; const result = await fetchApi(url); const body = await result.text(); const loadedCheerio = parseHTML(body); - const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); - const json: response = JSON.parse(jsonRaw || "{}"); + const jsonRaw = loadedCheerio('#__NEXT_DATA__').html(); + const json: response = JSON.parse(jsonRaw || '{}'); const novels: Plugin.NovelItem[] = []; - json.props.pageProps?.totalData?.items?.forEach((novel) => + json.props.pageProps?.totalData?.items?.forEach(novel => novels.push({ name: novel.title, cover: novel?.verticalImage?.url ? this.site + novel.verticalImage.url : defaultCover, - path: "/" + novel.slug, + path: '/' + novel.slug, }), ); @@ -49,19 +49,19 @@ class RNRF implements Plugin.PluginBase { const body = await result.text(); const loadedCheerio = parseHTML(body); - const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); - const book = (JSON.parse(jsonRaw || "{}") as response).props.pageProps.book; + const jsonRaw = loadedCheerio('#__NEXT_DATA__').html(); + const book = (JSON.parse(jsonRaw || '{}') as response).props.pageProps.book; const novel: Plugin.SourceNovel = { path: novelPath, - name: book?.title || "", + name: book?.title || '', cover: book?.verticalImage?.url ? this.site + book.verticalImage.url : defaultCover, summary: book?.description, - author: book?.author || "", - genres: book?.genres.map((item) => item.title).join(", "), - status: book?.additionalInfo.includes("Активен") + author: book?.author || '', + genres: book?.genres.map(item => item.title).join(', '), + status: book?.additionalInfo.includes('Активен') ? NovelStatus.Ongoing : NovelStatus.Completed, }; @@ -73,7 +73,7 @@ class RNRF implements Plugin.PluginBase { chapters.push({ name: chapter.title, path: chapter.url, - releaseTime: dayjs(chapter.publishedAt).format("LLL"), + releaseTime: dayjs(chapter.publishedAt).format('LLL'), chapterNumber: book.chapters.length - chapterIndex, }); } @@ -84,22 +84,22 @@ class RNRF implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const result = await fetchApi(this.site + chapterPath).then((res) => + const result = await fetchApi(this.site + chapterPath).then(res => res.text(), ); let loadedCheerio = parseHTML(result); - const jsonRaw = loadedCheerio("#__NEXT_DATA__").html(); - const json: response = JSON.parse(jsonRaw || "{}"); + const jsonRaw = loadedCheerio('#__NEXT_DATA__').html(); + const json: response = JSON.parse(jsonRaw || '{}'); loadedCheerio = parseHTML( - json.props.pageProps?.chapter?.content?.text || "", + json.props.pageProps?.chapter?.content?.text || '', ); - loadedCheerio("img").each((index, element) => { - if (!loadedCheerio(element).attr("src")?.startsWith("http")) { - const src = loadedCheerio(element).attr("src"); - loadedCheerio(element).attr("src", this.site + src); + loadedCheerio('img').each((index, element) => { + if (!loadedCheerio(element).attr('src')?.startsWith('http')) { + const src = loadedCheerio(element).attr('src'); + loadedCheerio(element).attr('src', this.site + src); } }); @@ -113,13 +113,13 @@ class RNRF implements Plugin.PluginBase { const body = (await result.json()) as { items: Item[] }; const novels: Plugin.NovelItem[] = []; - body.items.forEach((novel) => + body.items.forEach(novel => novels.push({ name: novel.title, cover: novel?.verticalImage?.url ? this.site + novel.verticalImage.url : defaultCover, - path: "/" + novel.slug, + path: '/' + novel.slug, }), ); @@ -130,13 +130,13 @@ class RNRF implements Plugin.PluginBase { filters = { sort: { - label: "Сортировка", - value: "popular", + label: 'Сортировка', + value: 'popular', options: [ - { label: "Рейтинг", value: "popular" }, - { label: "Дате добавления", value: "new" }, - { label: "Дате обновления", value: "lastPublishedChapter" }, - { label: "Законченные", value: "completed" }, + { label: 'Рейтинг', value: 'popular' }, + { label: 'Дате добавления', value: 'new' }, + { label: 'Дате обновления', value: 'lastPublishedChapter' }, + { label: 'Законченные', value: 'completed' }, ], type: FilterTypes.Picker, }, diff --git a/plugins/russian/renovels.ts b/plugins/russian/renovels.ts index 4ce55b4..efd0893 100644 --- a/plugins/russian/renovels.ts +++ b/plugins/russian/renovels.ts @@ -1,17 +1,17 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; -const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); class ReN implements Plugin.PluginBase { - id = "ReN"; - name = "Renovels"; - icon = "src/ru/renovels/icon.png"; - site = "https://renovels.org"; - version = "1.0.0"; + id = 'ReN'; + name = 'Renovels'; + icon = 'src/ru/renovels/icon.png'; + site = 'https://renovels.org'; + version = '1.0.0'; async popularNovels( pageNo: number, @@ -20,28 +20,31 @@ class ReN implements Plugin.PluginBase { filters, }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/api/search/catalog/?count=30&ordering=-"; - url += showLatestNovels ? "chapter_date" : filters?.sort?.value || "rating"; + let url = this.site + '/api/search/catalog/?count=30&ordering=-'; + url += showLatestNovels ? 'chapter_date' : filters?.sort?.value || 'rating'; Object.entries(filters || {}).forEach(([type, { value }]: any) => { if (value instanceof Array && value.length) { - url += "&" + type + "=" + value.join("&" + type + "="); + url += '&' + type + '=' + value.join('&' + type + '='); } if (value?.include instanceof Array && value.include.length) { - url += "&" + type + "=" + value.include.join("&" + type + "="); + url += '&' + type + '=' + value.include.join('&' + type + '='); } if (value?.exclude instanceof Array && value.exclude.length) { - url += "&exclude_" + type + "=" + - value.exclude.join("&exclude_" + type + "="); + url += + '&exclude_' + + type + + '=' + + value.exclude.join('&exclude_' + type + '='); } }); - url += "&page=" + pageNo; + url += '&page=' + pageNo; const result = await fetchApi(url); const body = (await result.json()) as { content: responseNovels[] }; - const novels: Plugin.NovelItem[] = body.content.map((novel) => ({ + const novels: Plugin.NovelItem[] = body.content.map(novel => ({ name: novel.main_name || novel.secondary_name, cover: this.site + (novel.img.high || novel.img.mid || novel.img.low), path: novel.dir, @@ -51,7 +54,7 @@ class ReN implements Plugin.PluginBase { } async parseNovel(novelID: string): Promise { - const result = await fetchApi(this.site + "/api/titles/" + novelID); + const result = await fetchApi(this.site + '/api/titles/' + novelID); const body = (await result.json()) as { content: responseNovel }; const novel: Plugin.SourceNovel = { @@ -65,18 +68,18 @@ class ReN implements Plugin.PluginBase { this.site + (body.content.img.high || body.content.img.mid || body.content.img.low), status: - body.content.status.name === "Продолжается" + body.content.status.name === 'Продолжается' ? NovelStatus.Ongoing : NovelStatus.Completed, }; const tags = [body.content?.genres, body.content?.categories] .flat() - .map((tags) => tags?.name) - .filter((tags) => tags); + .map(tags => tags?.name) + .filter(tags => tags); if (tags.length) { - novel.genres = tags.join(","); + novel.genres = tags.join(','); } const totalPages = @@ -89,22 +92,26 @@ class ReN implements Plugin.PluginBase { await delay(1000); const chapterResult = await fetchApi( this.site + - "/api/titles/chapters/?branch_id=" + branch_id + - "&count=100&page=" + (page + 1), + '/api/titles/chapters/?branch_id=' + + branch_id + + '&count=100&page=' + + (page + 1), ); const volumes = (await chapterResult.json()) as { content: responseСhapters[]; }; - volumes.content.forEach((chapter) => { + volumes.content.forEach(chapter => { if (!chapter.is_paid || chapter.is_bought) { chapters.push({ name: - "Том " + chapter.tome + - " Глава " + chapter.chapter + - (chapter.name ? " " + chapter.name.trim() : ""), - path: novelID + "/" + chapter.id, - releaseTime: dayjs(chapter.upload_date).format("LLL"), + 'Том ' + + chapter.tome + + ' Глава ' + + chapter.chapter + + (chapter.name ? ' ' + chapter.name.trim() : ''), + path: novelID + '/' + chapter.id, + releaseTime: dayjs(chapter.upload_date).format('LLL'), chapterNumber: chapter.index, }); } @@ -116,7 +123,7 @@ class ReN implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const url = this.site + "/api/titles/chapters/" + chapterPath.split("/")[1]; + const url = this.site + '/api/titles/chapters/' + chapterPath.split('/')[1]; const result = await fetchApi(url); const body = (await result.json()) as { content: responseСhapter }; @@ -132,7 +139,7 @@ class ReN implements Plugin.PluginBase { const body = (await result.json()) as { content: responseNovels[] }; const novels: Plugin.NovelItem[] = []; - body.content.forEach((novel) => + body.content.forEach(novel => novels.push({ name: novel.main_name || novel.secondary_name, cover: this.site + (novel.img.high || novel.img.mid || novel.img.low), @@ -144,797 +151,798 @@ class ReN implements Plugin.PluginBase { } fetchImage = fetchFile; - resolveUrl = (path: string, isNovel?: boolean) => this.site + "/novel/" + path; + resolveUrl = (path: string, isNovel?: boolean) => + this.site + '/novel/' + path; filters = { sort: { - label: "Сортировка", - value: "rating", + label: 'Сортировка', + value: 'rating', options: [ - { label: "Рейтинг", value: "rating" }, - { label: "Просмотры", value: "views" }, - { label: "Лайкам", value: "votes" }, - { label: "Дате добавления", value: "id" }, - { label: "Дате обновления", value: "chapter_date" }, - { label: "Количество глав", value: "count_chapters" }, + { label: 'Рейтинг', value: 'rating' }, + { label: 'Просмотры', value: 'views' }, + { label: 'Лайкам', value: 'votes' }, + { label: 'Дате добавления', value: 'id' }, + { label: 'Дате обновления', value: 'chapter_date' }, + { label: 'Количество глав', value: 'count_chapters' }, ], type: FilterTypes.Picker, }, genres: { - label: "Жанры:", + label: 'Жанры:', value: { include: [], exclude: [] }, options: [ - { label: "Боевик", value: "112" }, - { label: "Война", value: "123" }, - { label: "Детектив", value: "114" }, - { label: "Драма", value: "125" }, - { label: "Историческая проза", value: "115" }, - { label: "ЛитРПГ", value: "109" }, - { label: "Любовные романы", value: "116" }, - { label: "Мистика", value: "103" }, - { label: "Научная фантастика", value: "121" }, - { label: "Повседневность", value: "128" }, - { label: "Подростковая проза", value: "113" }, - { label: "Политический роман", value: "119" }, - { label: "Попаданцы", value: "108" }, - { label: "Поэзия", value: "105" }, - { label: "Разное", value: "111" }, - { label: "РеалРПГ", value: "117" }, - { label: "Романтика", value: "126" }, - { label: "Современная проза", value: "102" }, - { label: "Современная фантастика", value: "127" }, - { label: "Спорт", value: "122" }, - { label: "Трагедия", value: "129" }, - { label: "Триллер", value: "110" }, - { label: "Триллер и саспенс", value: "124" }, - { label: "Ужасы", value: "106" }, - { label: "Фантастика", value: "101" }, - { label: "Фанфик", value: "107" }, - { label: "Фьюжн роман", value: "120" }, - { label: "Фэнтези", value: "100" }, - { label: "Эротика", value: "118" }, - { label: "Юмор", value: "104" }, + { label: 'Боевик', value: '112' }, + { label: 'Война', value: '123' }, + { label: 'Детектив', value: '114' }, + { label: 'Драма', value: '125' }, + { label: 'Историческая проза', value: '115' }, + { label: 'ЛитРПГ', value: '109' }, + { label: 'Любовные романы', value: '116' }, + { label: 'Мистика', value: '103' }, + { label: 'Научная фантастика', value: '121' }, + { label: 'Повседневность', value: '128' }, + { label: 'Подростковая проза', value: '113' }, + { label: 'Политический роман', value: '119' }, + { label: 'Попаданцы', value: '108' }, + { label: 'Поэзия', value: '105' }, + { label: 'Разное', value: '111' }, + { label: 'РеалРПГ', value: '117' }, + { label: 'Романтика', value: '126' }, + { label: 'Современная проза', value: '102' }, + { label: 'Современная фантастика', value: '127' }, + { label: 'Спорт', value: '122' }, + { label: 'Трагедия', value: '129' }, + { label: 'Триллер', value: '110' }, + { label: 'Триллер и саспенс', value: '124' }, + { label: 'Ужасы', value: '106' }, + { label: 'Фантастика', value: '101' }, + { label: 'Фанфик', value: '107' }, + { label: 'Фьюжн роман', value: '120' }, + { label: 'Фэнтези', value: '100' }, + { label: 'Эротика', value: '118' }, + { label: 'Юмор', value: '104' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, categories: { - label: "Тэги:", + label: 'Тэги:', value: { include: [], exclude: [] }, options: [ - { label: "[Награжденная работа]", value: "648" }, - { label: "18+", value: "423" }, - { label: "Абьюзеры", value: "663" }, - { label: "Авантюристы", value: "436" }, - { label: "Автоматоны", value: "593" }, - { label: "Агрессивные персонажи", value: "502" }, - { label: "Ад", value: "569" }, - { label: "Адаптация в радиопостановку", value: "581" }, - { label: "Академия", value: "153" }, - { label: "Актеры озвучки", value: "627" }, - { label: "Активный главный герой", value: "251" }, - { label: "Алхимия", value: "233" }, - { label: "Альтернативная история", value: "147" }, - { label: "Альтернативный мир", value: "156" }, - { label: "Амнезия/Потеря памяти", value: "344" }, - { label: "Анабиоз", value: "685" }, - { label: "Ангелы", value: "183" }, - { label: "Андрогинные персонажи", value: "321" }, - { label: "Андроиды", value: "203" }, - { label: "Анти-магия", value: "533" }, - { label: "Антигерой", value: "432" }, - { label: "Антикварный магазин", value: "623" }, - { label: "Антисоциальный главный герой", value: "613" }, - { label: "Антиутопия", value: "142" }, - { label: "Апатичный протагонист", value: "157" }, - { label: "Апокалипсис", value: "402" }, - { label: "Аранжированный брак", value: "375" }, - { label: "Армия", value: "639" }, - { label: "Артефакты", value: "234" }, - { label: "Артисты", value: "523" }, - { label: "Банды", value: "630" }, - { label: "БДСМ", value: "699" }, - { label: "Бедный главный герой", value: "398" }, - { label: "Безжалостный главный герой", value: "261" }, - { label: "Беззаботный главный герой", value: "438" }, - { label: "Безусловная любовь", value: "679" }, - { label: "Беременность", value: "250" }, - { label: "Бесполый главный герой", value: "325" }, - { label: "Бессмертные", value: "368" }, - { label: "Бесстрашный протагонист", value: "656" }, - { label: "Бесстыдный главный герой", value: "352" }, - { label: "Бесчестный главный герой", value: "716" }, - { label: "Библиотека", value: "428" }, - { label: "Бизнес-литература", value: "121" }, - { label: "Бизнесмен", value: "769" }, - { label: "Биочип", value: "238" }, - { label: "Бисексуальный главный герой", value: "773" }, - { label: "Близнецы", value: "263" }, - { label: "Боги", value: "317" }, - { label: "Богини", value: "439" }, - { label: "Боевая академия", value: "450" }, - { label: "Боевая фантастика", value: "149" }, - { label: "Боевое фэнтези", value: "104" }, - { label: "Боевые духи", value: "433" }, - { label: "Боевые соревнования", value: "493" }, - { label: "Божественная защита", value: "421" }, - { label: "Божественные силы", value: "327" }, - { label: "Борьба за власть", value: "599" }, - { label: "Брак", value: "446" }, - { label: "Брак по расчету", value: "188" }, - { label: "Братский комплекс", value: "159" }, - { label: "Братство", value: "487" }, - { label: "Братья и сестры", value: "577" }, - { label: "Буддизм", value: "745" }, - { label: "Быстрая культивация", value: "366" }, - { label: "Быстрообучаемый", value: "324" }, - { label: "Валькирии", value: "692" }, - { label: "Вампиры", value: "360" }, - { label: "Ваншот", value: "702" }, - { label: "Ведьмы", value: "281" }, - { label: "Вежливый главный герой", value: "379" }, - { label: "Верные подчиненные", value: "328" }, - { label: "Взрослый главный герой", value: "295" }, - { label: "Видит то, чего не видят другие", value: "671" }, - { label: "Виртуальная реальность", value: "401" }, - { label: "Владелец магазина", value: "681" }, - { label: "Внезапная сила", value: "455" }, - { label: "Внезапное богатство", value: "765" }, - { label: "Внешний вид отличается от факт", value: "419" }, - { label: "Военные Летописи", value: "743" }, - { label: "Возвращение из другого мира", value: "696" }, - { label: "Войны", value: "181" }, - { label: "Вокалоид", value: "701" }, - { label: "Волшебники/Волшебницы", value: "539" }, - { label: "Волшебные звери", value: "308" }, - { label: "Воображаемый друг", value: "651" }, - { label: "Воры", value: "414" }, - { label: "Воскрешение", value: "199" }, - { label: "Враги становятся возлюбленными", value: "498" }, - { label: "Враги становятся союзниками", value: "562" }, - { label: "Врата в другой мир", value: "609" }, - { label: "Врачи", value: "376" }, - { label: "Временной парадокс", value: "275" }, - { label: "Всемогущий главный герой", value: "169" }, - { label: "Вторжение на землю", value: "198" }, - { label: "Второй шанс", value: "231" }, - { label: "Вуайеризм", value: "790" }, - { label: "Выживание", value: "380" }, - { label: "Высокомерные персонажи", value: "361" }, - { label: "Гадание", value: "595" }, - { label: "Гарем рабов", value: "777" }, - { label: "Гг - женщина", value: "186" }, - { label: "Гг - мужчина", value: "166" }, - { label: "Гг силен с самого начала", value: "171" }, - { label: "Геймеры", value: "392" }, - { label: "Генералы", value: "326" }, - { label: "Генетические модификации", value: "657" }, - { label: "Гениальный главный герой", value: "617" }, - { label: "Герои", value: "285" }, - { label: "Героиня — сорванец", value: "583" }, - { label: "Героическая фантастика", value: "140" }, - { label: "Героическое фэнтези", value: "107" }, - { label: "Герой влюбляется первым", value: "187" }, - { label: "Гетерохромия", value: "570" }, - { label: "Гильдии", value: "411" }, - { label: "Гипнотизм", value: "755" }, - { label: "Главный герой — бог", value: "547" }, - { label: "Главный герой — гуманоид", value: "636" }, - { label: "Главный герой — наполовину чел", value: "445" }, - { label: "Главный герой — отец", value: "792" }, - { label: "Главный герой — раб", value: "780" }, - { label: "Главный герой — ребенок", value: "488" }, - { label: "Главный герой — рубака", value: "476" }, - { label: "Главный герой влюбляется первы", value: "683" }, - { label: "Главный герой играет роль", value: "472" }, - { label: "Главный герой носит очки", value: "672" }, - { label: "Главный герой пацифист", value: "698" }, - { label: "Гладиаторы", value: "603" }, - { label: "Глуповатый главный герой", value: "385" }, - { label: "Гоблины", value: "586" }, - { label: "Големы", value: "620" }, - { label: "Гомункул", value: "789" }, - { label: "Горничные", value: "459" }, - { label: "Городское фэнтези", value: "105" }, - { label: "Госпиталь", value: "800" }, - { label: "Готовка", value: "302" }, - { label: "Гриндинг", value: "393" }, - { label: "Дао Компаньон", value: "462" }, - { label: "Даосизм", value: "760" }, - { label: "Дварфы", value: "323" }, - { label: "Двойная личность", value: "642" }, - { label: "Двойник", value: "601" }, - { label: "Дворецкий", value: "660" }, - { label: "Дворяне", value: "168" }, - { label: "Дворянство/Аристократия", value: "437" }, - { label: "Девушки-монстры", value: "669" }, - { label: "Демоническая техника культивац", value: "722" }, - { label: "Демоны", value: "152" }, - { label: "Денежный долг", value: "783" }, - { label: "Депрессия", value: "555" }, - { label: "Детектив", value: "116" }, - { label: "Детективы", value: "612" }, - { label: "Детская литература", value: "123" }, - { label: "Дискриминация", value: "162" }, - { label: "Добыча денег в приоритете", value: "396" }, - { label: "Документальная проза", value: "122" }, - { label: "Долгая разлука", value: "307" }, - { label: "Домашние дела", value: "290" }, - { label: "Домогательство", value: "470" }, - { label: "Драконы", value: "303" }, - { label: "Драконьи всадники", value: "607" }, - { label: "Древние времена", value: "221" }, - { label: "Древний Китай", value: "374" }, - { label: "Дружба", value: "217" }, - { label: "Друзья детства", value: "282" }, - { label: "Друзья становятся врагами", value: "567" }, - { label: "Друиды", value: "497" }, - { label: "Дух лисы", value: "784" }, - { label: "Духи/призраки", value: "172" }, - { label: "Духовный советник", value: "435" }, - { label: "Душевность", value: "633" }, - { label: "Души", value: "254" }, - { label: "Европейская атмосфера", value: "521" }, - { label: "Ёкаи", value: "575" }, - { label: "Есть аниме-адаптация", value: "154" }, - { label: "Есть видеоигра по мотивам", value: "552" }, - { label: "Есть манга", value: "155" }, - { label: "Есть манхва-адаптация", value: "517" }, - { label: "Есть маньхуа-адаптация", value: "388" }, - { label: "Есть сериал-адаптация", value: "492" }, - { label: "Есть фильм", value: "173" }, - { label: "Женища-наставник", value: "505" }, - { label: "Жесткая, двуличная личность", value: "346" }, - { label: "Жестокие персонажи", value: "503" }, - { label: "Жестокое обращение с ребенком", value: "654" }, - { label: "Жестокость", value: "245" }, - { label: "Животноводство", value: "754" }, - { label: "Животные черты", value: "528" }, - { label: "Жизнь в квартире", value: "606" }, - { label: "Жрицы", value: "615" }, - { label: "Заботливый главный герой", value: "288" }, - { label: "Забывчивый главный герой", value: "628" }, - { label: "Заговоры", value: "289" }, - { label: "Закалка тела", value: "362" }, - { label: "Законники", value: "756" }, - { label: "Замкнутый главный герой", value: "589" }, - { label: "Запечатанная сила", value: "430" }, - { label: "Застенчивые персонажи", value: "508" }, - { label: "Звери", value: "237" }, - { label: "Звери-компаньоны", value: "301" }, - { label: "Злой протагонист", value: "243" }, - { label: "Злые боги", value: "504" }, - { label: "Злые организации", value: "563" }, - { label: "Злые религии", value: "734" }, - { label: "Знаменитости", value: "473" }, - { label: "Знаменитый главный герой", value: "531" }, - { label: "Знания современного мира", value: "297" }, - { label: "Зомби", value: "409" }, - { label: "Игра на выживание", value: "274" }, - { label: "Игривый протагонист", value: "717" }, - { label: "Игровая система рейтинга", value: "391" }, - { label: "Игровые элементы", value: "266" }, - { label: "Игрушки (18+)", value: "676" }, - { label: "Из грязи в князи", value: "416" }, - { label: "Из женщины в мужчину", value: "709" }, - { label: "Из мужчины в женщину", value: "715" }, - { label: "Из полного в худого", value: "767" }, - { label: "Из слабого в сильного", value: "202" }, - { label: "Извращенный главный герой", value: "434" }, - { label: "Изгои", value: "534" }, - { label: "Изменение расы", value: "664" }, - { label: "Изменения внешнего вида", value: "300" }, - { label: "Изменения личности", value: "219" }, - { label: "Изнасилование", value: "229" }, - { label: "Изображения жестокости", value: "355" }, - { label: "ИИ", value: "235" }, - { label: "Империи", value: "748" }, - { label: "Инвалидность", value: "684" }, - { label: "Индустриализация", value: "292" }, - { label: "Инженер", value: "163" }, - { label: "Инцест", value: "525" }, - { label: "Искусственный интеллект", value: "236" }, - { label: "Исследования", value: "670" }, - { label: "Историческая проза", value: "119" }, - { label: "Исторические приключения", value: "120" }, - { label: "Исторический детектив", value: "117" }, - { label: "Исторический любовный роман", value: "138" }, - { label: "Историческое фэнтези", value: "110" }, - { label: "Каннибализм", value: "456" }, - { label: "Карточные игры", value: "682" }, - { label: "Киберпанк", value: "141" }, - { label: "Киберспорт", value: "686" }, - { label: "Кланы", value: "796" }, - { label: "Класс безработного", value: "735" }, - { label: "Клоны", value: "447" }, - { label: "Клубы", value: "216" }, - { label: "Книги", value: "427" }, - { label: "Книги навыков", value: "400" }, - { label: "Книжный червь", value: "519" }, - { label: "Коварство", value: "230" }, - { label: "Коллеги", value: "704" }, - { label: "Колледж/Университет", value: "726" }, - { label: "Кома", value: "776" }, - { label: "Командная работа", value: "496" }, - { label: "Комедийный оттенок", value: "582" }, - { label: "Комплекс неполноценности", value: "550" }, - { label: "Комплекс семейных отношений", value: "223" }, - { label: "Конкуренция", value: "746" }, - { label: "Контроль разума/сознания", value: "357" }, - { label: "Копейщик", value: "425" }, - { label: "Королевская битва", value: "797" }, - { label: "Королевская власть", value: "178" }, - { label: "Королевства", value: "259" }, - { label: "Короткий любовный роман", value: "136" }, - { label: "Коррупция", value: "457" }, - { label: "Космическая фантастика", value: "146" }, - { label: "Космические войны", value: "697" }, - { label: "Красивый герой", value: "226" }, - { label: "Крафт", value: "377" }, - { label: "Кризис личности", value: "640" }, - { label: "Кругосветное путешествие", value: "353" }, - { label: "Кудере", value: "506" }, - { label: "Кузены", value: "718" }, - { label: "Кузнец", value: "518" }, - { label: "Кукловоды", value: "501" }, - { label: "Куклы/марионетки", value: "624" }, - { label: "Культивация", value: "241" }, - { label: "Куннилингус", value: "667" }, - { label: "Легенды", value: "500" }, - { label: "Легкая жизнь", value: "644" }, - { label: "Ленивый главный герой", value: "621" }, - { label: "Лидерство", value: "494" }, - { label: "Лоли", value: "213" }, - { label: "Лотерея", value: "477" }, - { label: "Любовное фэнтези", value: "135" }, - { label: "Любовный треугольник", value: "218" }, - { label: "Любовь детства", value: "560" }, - { label: "Любовь с первого взгляда", value: "737" }, - { label: "Магические надписи", value: "453" }, - { label: "Магические печати", value: "369" }, - { label: "Магические технологии", value: "440" }, - { label: "Магия", value: "165" }, - { label: "Магия призыва", value: "418" }, - { label: "Мазохистские персонажи", value: "688" }, - { label: "Манипулятивные персонажи", value: "249" }, - { label: "Мания", value: "212" }, - { label: "Мастер на все руки", value: "466" }, - { label: "Мастурбация", value: "788" }, - { label: "Махо-сёдзё", value: "556" }, - { label: "Медицинские знания", value: "507" }, - { label: "Медленная романтическая линия", value: "232" }, - { label: "Медленное развитие на старте", value: "694" }, - { label: "Межпространственные путешестви", value: "404" }, - { label: "Менеджмент", value: "294" }, - { label: "Мертвый главный герой", value: "761" }, - { label: "Месть", value: "209" }, - { label: "Метаморфы", value: "335" }, - { label: "Меч и магия", value: "180" }, - { label: "Мечник", value: "647" }, - { label: "Мечты", value: "738" }, - { label: "Милая история", value: "725" }, - { label: "Милое дитя", value: "637" }, - { label: "Милый главный герой", value: "714" }, - { label: "Мировое дерево", value: "463" }, - { label: "Мистика", value: "151" }, - { label: "Мистический ореол вокруг семьи", value: "483" }, - { label: "Мифические звери", value: "491" }, - { label: "Мифология", value: "530" }, - { label: "Младшие братья", value: "785" }, - { label: "Младшие сестры", value: "527" }, - { label: "ММОРПГ (ЛитРПГ)", value: "395" }, - { label: "Множество перемещенных людей", value: "549" }, - { label: "Множество реальностей", value: "370" }, - { label: "Множество реинкарнированных лю", value: "330" }, - { label: "Модели", value: "678" }, - { label: "Молчаливый персонаж", value: "721" }, - { label: "Монстры", value: "192" }, - { label: "Мужская гей-пара", value: "706" }, - { label: "Мужчина-яндере", value: "267" }, - { label: "Музыка", value: "635" }, - { label: "Музыкальные группы", value: "634" }, - { label: "Мутации", value: "693" }, - { label: "Мутированные существа", value: "405" }, - { label: "Навык кражи", value: "662" }, - { label: "Навязчивая любовь", value: "206" }, - { label: "Наемники", value: "412" }, - { label: "Назойливый возлюбленный", value: "561" }, - { label: "Наивный главный герой", value: "189" }, - { label: "Наркотики", value: "689" }, - { label: "Нарциссический главный герой", value: "532" }, - { label: "Насилие сексуального характера", value: "774" }, - { label: "Наследование", value: "452" }, - { label: "Научная фантастика", value: "144" }, - { label: "Национализм", value: "406" }, - { label: "Не блещущий внешне главный гер", value: "415" }, - { label: "Не родные братья и сестры", value: "526" }, - { label: "Небеса", value: "568" }, - { label: "Небесное испытание", value: "367" }, - { label: "Негуманоидный главный герой", value: "659" }, - { label: "Недоверчивый главный герой", value: "546" }, - { label: "Недооцененный главный герой", value: "258" }, - { label: "Недоразумения", value: "309" }, - { label: "Неизлечимая болезнь", value: "196" }, - { label: "Некромант", value: "397" }, - { label: "Нелинейная история", value: "269" }, - { label: "Ненавистный главный герой", value: "548" }, - { label: "Ненадежный рассказчик", value: "277" }, - { label: "Нерезиденты", value: "772" }, - { label: "Нерешительный главный герой", value: "744" }, - { label: "Несерьезный главный герой", value: "384" }, - { label: "Несколько временных линий", value: "576" }, - { label: "Несколько гг", value: "268" }, - { label: "Несколько главных героев", value: "537" }, - { label: "Несколько идентичностей", value: "652" }, - { label: "Несколько личностей", value: "536" }, - { label: "Нетораре", value: "732" }, - { label: "Нетори", value: "514" }, - { label: "Неудачливый главный герой", value: "618" }, - { label: "Ниндзя", value: "441" }, - { label: "Новелла", value: "103" }, - { label: "Обещание из детства", value: "742" }, - { label: "Обманщик", value: "485" }, - { label: "Обмен телами", value: "214" }, - { label: "Обнаженка", value: "490" }, - { label: "Обольщение", value: "731" }, - { label: "Оборотни", value: "661" }, - { label: "Обратный гарем", value: "351" }, - { label: "Общество монстров", value: "329" }, - { label: "Обязательство", value: "602" }, - { label: "Огнестрельное оружие", value: "291" }, - { label: "Ограниченное время жизни", value: "194" }, - { label: "Одержимость", value: "512" }, - { label: "Одинокий главный герой", value: "394" }, - { label: "Одиночество", value: "306" }, - { label: "Одиночное проживание", value: "313" }, - { label: "Околосмертные переживания", value: "759" }, - { label: "Оммёдзи", value: "597" }, - { label: "Омоложение", value: "338" }, - { label: "Организованная преступность", value: "540" }, - { label: "Оргия", value: "775" }, - { label: "Орки", value: "331" }, - { label: "Освоение навыков", value: "336" }, - { label: "Основано на аниме", value: "605" }, - { label: "Основано на видео игре", value: "720" }, - { label: "Основано на визуальной новелле", value: "793" }, - { label: "Основано на песне", value: "700" }, - { label: "Основано на фильме", value: "604" }, - { label: "Осторожный главный герой", value: "240" }, - { label: "Отаку", value: "571" }, - { label: "Открытый космос", value: "264" }, - { label: "Отношения в сети", value: "727" }, - { label: "Отношения между людьми и нелюд", value: "311" }, - { label: "Отношения на расстоянии", value: "791" }, - { label: "Отношения Сенпай-Коухай", value: "572" }, - { label: "Отношения ученика и учителя", value: "515" }, - { label: "Отношения учитель-ученик", value: "429" }, - { label: "Отношения хозяин-слуга", value: "312" }, - { label: "Отомэ игра", value: "733" }, - { label: "Отсутствие здравого смысла", value: "645" }, - { label: "Отсутствие родителей", value: "625" }, - { label: "Офисный роман", value: "707" }, - { label: "Официанты", value: "703" }, - { label: "Охотники", value: "378" }, - { label: "Очаровательный главный герой", value: "687" }, - { label: "Падшее дворянство", value: "655" }, - { label: "Падшие ангелы", value: "565" }, - { label: "Пайзури", value: "665" }, - { label: "Паразиты", value: "591" }, - { label: "Параллельные миры", value: "207" }, - { label: "Парк", value: "265" }, - { label: "Парк развлечений", value: "632" }, - { label: "Пародия", value: "407" }, - { label: "Певцы/Певицы", value: "739" }, - { label: "Первая любовь", value: "729" }, - { label: "Первоисточник новеллы — манга", value: "611" }, - { label: "Первый раз", value: "787" }, - { label: "Перемещение в игровой мир", value: "588" }, - { label: "Перемещение в иной мир", value: "750" }, - { label: "Перерождение в ином мире", value: "751" }, - { label: "Переселение души/трансмиграция", value: "257" }, - { label: "Персонаж использует щит", value: "666" }, - { label: "Петля времени", value: "200" }, - { label: "Пираты", value: "771" }, - { label: "Писатели", value: "482" }, - { label: "Питомцы", value: "349" }, - { label: "Племенное общество", value: "381" }, - { label: "Повелитель демонов", value: "283" }, - { label: "Подземелья", value: "322" }, - { label: "Подростковая проза", value: "114" }, - { label: "Пожелания", value: "278" }, - { label: "Познание Дао", value: "443" }, - { label: "Покинутое дитя", value: "590" }, - { label: "Полигамия", value: "386" }, - { label: "Политика", value: "170" }, - { label: "Политический роман", value: "133" }, - { label: "Полиция", value: "764" }, - { label: "Полулюди", value: "420" }, - { label: "Популярный любовный интерес", value: "342" }, - { label: "Постапокалипсис", value: "145" }, - { label: "Постапокалиптика", value: "184" }, - { label: "Потерянные цивилизации", value: "749" }, - { label: "Похищения людей", value: "723" }, - { label: "Поэзия", value: "111" }, - { label: "Правонарушители", value: "713" }, - { label: "Прагматичный главный герой", value: "592" }, - { label: "Преданный любовный интерес", value: "225" }, - { label: "Предательство", value: "222" }, - { label: "Предвидение", value: "177" }, - { label: "Прекрасная героиня", value: "158" }, - { label: "Преступники", value: "736" }, - { label: "Преступность", value: "204" }, - { label: "Призванный герой", value: "262" }, - { label: "Призраки", value: "176" }, - { label: "Принуждение к отношениям", value: "643" }, - { label: "Принцессы", value: "795" }, - { label: "Притворная пара", value: "641" }, - { label: "Причудливые персонажи", value: "387" }, - { label: "Пришельцы/инопланетяне", value: "197" }, - { label: "Программист", value: "691" }, - { label: "Проклятия", value: "174" }, - { label: "Промывание мозгов", value: "578" }, - { label: "Пропуск времени", value: "256" }, - { label: "Пророчества", value: "499" }, - { label: "Проститутки", value: "786" }, - { label: "Прошлое играет большую роль", value: "320" }, - { label: "Прыжки между мирами", value: "741" }, - { label: "Психические силы", value: "359" }, - { label: "Психопаты", value: "270" }, - { label: "Публицистика", value: "124" }, - { label: "Путешествие во времени", value: "201" }, - { label: "Пытка", value: "280" }, - { label: "Рабы", value: "778" }, - { label: "Развитие личности", value: "125" }, - { label: "Развод", value: "758" }, - { label: "Разумные предметы", value: "286" }, - { label: "Расизм", value: "408" }, - { label: "Рассказ", value: "185" }, - { label: "Расторжения помолвки", value: "343" }, - { label: "Расы зооморфов", value: "315" }, - { label: "Реализм", value: "246" }, - { label: "Ревность", value: "730" }, - { label: "Редакторы", value: "705" }, - { label: "Реинкарнация", value: "372" }, - { label: "Реинкарнация в монстра", value: "310" }, - { label: "Реинкарнация в объект", value: "711" }, - { label: "Религии", value: "616" }, - { label: "Репортеры", value: "781" }, - { label: "Ресторан", value: "680" }, - { label: "Решительный главный герой", value: "242" }, - { label: "Робкий главный герой", value: "763" }, - { label: "Родитель одиночка", value: "708" }, - { label: "Родительский комплекс", value: "587" }, - { label: "Родословная", value: "239" }, - { label: "Романтическая эротика", value: "132" }, - { label: "Романтический подсюжет", value: "271" }, - { label: "Рост персонажа", value: "215" }, - { label: "Рыцари", value: "260" }, - { label: "Садистские персонажи", value: "674" }, - { label: "Самоотверженный главный герой", value: "747" }, - { label: "Самурай", value: "677" }, - { label: "Сборник коротких историй", value: "520" }, - { label: "Связанные сюжетные линии", value: "535" }, - { label: "Святые", value: "752" }, - { label: "Священники", value: "413" }, - { label: "Сдержанный главный герой", value: "762" }, - { label: "Секретные организации", value: "417" }, - { label: "Секреты", value: "626" }, - { label: "Секс рабы", value: "779" }, - { label: "Семейный конфликт", value: "757" }, - { label: "Семь добродетелей", value: "334" }, - { label: "Семь смертных грехов", value: "253" }, - { label: "Семья", value: "347" }, - { label: "Сёнэн-ай подсюжет", value: "690" }, - { label: "Серийные убийцы", value: "557" }, - { label: "Сестринский комплекс", value: "558" }, - { label: "Сила духа", value: "373" }, - { label: "Сила, требующая платы за польз", value: "579" }, - { label: "Сильная пара", value: "228" }, - { label: "Сильный в сильнейшего", value: "442" }, - { label: "Сильный любовный интерес", value: "273" }, - { label: "Синдром восьмиклассника", value: "211" }, - { label: "Сироты", value: "319" }, - { label: "Система уровней", value: "305" }, - { label: "Системный администратор", value: "538" }, - { label: "Сказка", value: "126" }, - { label: "Скрытие истинной личности", value: "451" }, - { label: "Скрытие истинных способностей", value: "348" }, - { label: "Скрытный главный герой", value: "252" }, - { label: "Скрытые способности", value: "247" }, - { label: "Скульпторы", value: "448" }, - { label: "Слабо выраженная романтическая", value: "318" }, - { label: "Слабый главный герой", value: "298" }, - { label: "Слепой главный герой", value: "794" }, - { label: "Слуги", value: "333" }, - { label: "Слэш", value: "131" }, - { label: "Смерть", value: "175" }, - { label: "Смерть близких", value: "444" }, - { label: "Собственнические персонажи", value: "227" }, - { label: "Современная проза", value: "150" }, - { label: "Современность", value: "167" }, - { label: "Современный любовный роман", value: "137" }, - { label: "Сожительство", value: "544" }, - { label: "Создание армии", value: "287" }, - { label: "Создание артефактов", value: "382" }, - { label: "Создание клана", value: "513" }, - { label: "Создание королевства", value: "293" }, - { label: "Создание навыков", value: "337" }, - { label: "Создание секты", value: "469" }, - { label: "Солдаты/военные", value: "193" }, - { label: "Сон", value: "622" }, - { label: "Состоятельные персонажи", value: "190" }, - { label: "Социальная иерархия по силе", value: "255" }, - { label: "Социальная фантастика", value: "148" }, - { label: "Социальные изгои", value: "542" }, - { label: "Спасение мира", value: "638" }, - { label: "Специальные способности", value: "179" }, - { label: "Спокойный главный герой", value: "160" }, - { label: "Справедливый главный герой", value: "719" }, - { label: "Средневековье", value: "296" }, - { label: "Ссорящаяся пара", value: "383" }, - { label: "Сталкеры", value: "710" }, - { label: "Старение", value: "299" }, - { label: "Стимпанк", value: "139" }, - { label: "Стоические персонажи", value: "509" }, - { label: "Стокгольмский синдром", value: "675" }, - { label: "Стратег", value: "495" }, - { label: "Стратегические битвы", value: "272" }, - { label: "Стратегия", value: "798" }, - { label: "Стрелки", value: "522" }, - { label: "Стрельба из лука", value: "461" }, - { label: "Студенческий совет", value: "551" }, - { label: "Судьба", value: "364" }, - { label: "Суккубы", value: "545" }, - { label: "Супер герои", value: "799" }, - { label: "Суровая подготовка", value: "356" }, - { label: "Таинственная болезнь", value: "195" }, - { label: "Таинственное прошлое", value: "358" }, - { label: "Тайная личность", value: "399" }, - { label: "Тайные отношения", value: "768" }, - { label: "Танцоры", value: "782" }, - { label: "Телохранители", value: "516" }, - { label: "Темное фэнтези", value: "106" }, - { label: "Тентакли", value: "712" }, - { label: "Террористы", value: "574" }, - { label: "Технологический разрыв", value: "658" }, - { label: "Тихие персонажи", value: "600" }, - { label: "Толстый главный герой", value: "389" }, - { label: "Торговцы", value: "489" }, - { label: "Травля/буллинг", value: "210" }, - { label: "Травник", value: "724" }, - { label: "Трагическое прошлое", value: "276" }, - { label: "Трансплантация воспоминаний", value: "449" }, - { label: "Триллер", value: "134" }, - { label: "Трудолюбивый главный герой", value: "164" }, - { label: "Тюрьма", value: "541" }, - { label: "Убийства", value: "205" }, - { label: "Убийцы", value: "345" }, - { label: "Убийцы драконов", value: "646" }, - { label: "Уверенный главный герой", value: "363" }, - { label: "Удачливый главный герой", value: "478" }, - { label: "Ужасы", value: "112" }, - { label: "Укротитель монстров", value: "422" }, - { label: "Умения из прошлой жизни", value: "371" }, - { label: "Умная пара", value: "554" }, - { label: "Умный главный герой", value: "161" }, - { label: "Уникальная техника Культивации", value: "350" }, - { label: "Уникальное оружие", value: "426" }, - { label: "Управление бизнесом", value: "403" }, - { label: "Управление временем", value: "279" }, - { label: "Управление кровью", value: "753" }, - { label: "Упрямый главный герой", value: "695" }, - { label: "Уродливый главный герой", value: "766" }, - { label: "Ускоренный рост", value: "390" }, - { label: "Усыновленные дети", value: "543" }, - { label: "Усыновленный главный герой", value: "486" }, - { label: "Уход за детьми", value: "474" }, - { label: "Учителя", value: "431" }, - { label: "Фамильяры", value: "596" }, - { label: "Фанатизм", value: "650" }, - { label: "Фантастические существа", value: "410" }, - { label: "Фантастический детектив", value: "115" }, - { label: "Фанфик", value: "113" }, - { label: "Фанфикшн", value: "464" }, - { label: "Фармацевт", value: "728" }, - { label: "Фарминг", value: "458" }, - { label: "Феи", value: "316" }, - { label: "Фелляция", value: "631" }, - { label: "Фемслэш", value: "127" }, - { label: "Фениксы", value: "454" }, - { label: "Фетиш груди", value: "559" }, - { label: "Философия", value: "208" }, - { label: "Фильмы", value: "479" }, - { label: "Флэшбэки", value: "585" }, - { label: "Фобии", value: "220" }, - { label: "Фольклор", value: "529" }, - { label: "Футанари", value: "619" }, - { label: "Футуристический сеттинг", value: "460" }, - { label: "Фэнтези мир", value: "244" }, - { label: "Хакеры", value: "475" }, - { label: "Харизматический герой", value: "467" }, - { label: "Хикикомори/Затворники", value: "524" }, - { label: "Хитроумный главный герой", value: "224" }, - { label: "Хозяин подземелий", value: "608" }, - { label: "Холодный главный герой", value: "354" }, - { label: "Хорошие отношения с семьей", value: "566" }, - { label: "Хранители могил", value: "191" }, - { label: "Целители", value: "465" }, - { label: "Цзянши", value: "740" }, - { label: "Цундэрэ", value: "510" }, - { label: "Чаты", value: "629" }, - { label: "Человеческое оружие", value: "580" }, - { label: "Честный главный герой", value: "341" }, - { label: "Читы", value: "339" }, - { label: "Шантаж", value: "584" }, - { label: "Шеф-повар", value: "340" }, - { label: "Шикигами", value: "598" }, - { label: "Школа только для девочек", value: "668" }, - { label: "Шота", value: "573" }, - { label: "Шоу-бизнес", value: "481" }, - { label: "Шпионский детектив", value: "118" }, - { label: "Шпионы", value: "614" }, - { label: "Эволюция", value: "304" }, - { label: "Эгоистичный главный герой", value: "594" }, - { label: "Эйдетическая память", value: "468" }, - { label: "Экзорсизм", value: "564" }, - { label: "Экономика", value: "553" }, - { label: "Эксгибиционизм", value: "673" }, - { label: "Эксперименты с людьми", value: "248" }, - { label: "Элементальная магия", value: "471" }, - { label: "Эльфы", value: "284" }, - { label: "Эмоционально слабый гг", value: "770" }, - { label: "Эпизодический", value: "649" }, - { label: "Эпическое фэнтези", value: "109" }, - { label: "Эротическая фантастика", value: "129" }, - { label: "Эротический фанфик", value: "128" }, - { label: "Эротическое фэнтези", value: "130" }, - { label: "Юмористическая фантастика", value: "143" }, - { label: "Юмористическое фэнтези", value: "108" }, - { label: "Юный любовный интерес", value: "511" }, - { label: "Яды", value: "484" }, - { label: "Языкастые персонажи", value: "480" }, - { label: "Языковой барьер", value: "182" }, - { label: "Яндере", value: "314" }, - { label: "Японские силы самообороны", value: "610" }, - { label: "Ярко выраженная романтика", value: "365" }, - { label: "R-15 Японское возрастное огр.", value: "332" }, - { label: "R-18", value: "424" }, + { label: '[Награжденная работа]', value: '648' }, + { label: '18+', value: '423' }, + { label: 'Абьюзеры', value: '663' }, + { label: 'Авантюристы', value: '436' }, + { label: 'Автоматоны', value: '593' }, + { label: 'Агрессивные персонажи', value: '502' }, + { label: 'Ад', value: '569' }, + { label: 'Адаптация в радиопостановку', value: '581' }, + { label: 'Академия', value: '153' }, + { label: 'Актеры озвучки', value: '627' }, + { label: 'Активный главный герой', value: '251' }, + { label: 'Алхимия', value: '233' }, + { label: 'Альтернативная история', value: '147' }, + { label: 'Альтернативный мир', value: '156' }, + { label: 'Амнезия/Потеря памяти', value: '344' }, + { label: 'Анабиоз', value: '685' }, + { label: 'Ангелы', value: '183' }, + { label: 'Андрогинные персонажи', value: '321' }, + { label: 'Андроиды', value: '203' }, + { label: 'Анти-магия', value: '533' }, + { label: 'Антигерой', value: '432' }, + { label: 'Антикварный магазин', value: '623' }, + { label: 'Антисоциальный главный герой', value: '613' }, + { label: 'Антиутопия', value: '142' }, + { label: 'Апатичный протагонист', value: '157' }, + { label: 'Апокалипсис', value: '402' }, + { label: 'Аранжированный брак', value: '375' }, + { label: 'Армия', value: '639' }, + { label: 'Артефакты', value: '234' }, + { label: 'Артисты', value: '523' }, + { label: 'Банды', value: '630' }, + { label: 'БДСМ', value: '699' }, + { label: 'Бедный главный герой', value: '398' }, + { label: 'Безжалостный главный герой', value: '261' }, + { label: 'Беззаботный главный герой', value: '438' }, + { label: 'Безусловная любовь', value: '679' }, + { label: 'Беременность', value: '250' }, + { label: 'Бесполый главный герой', value: '325' }, + { label: 'Бессмертные', value: '368' }, + { label: 'Бесстрашный протагонист', value: '656' }, + { label: 'Бесстыдный главный герой', value: '352' }, + { label: 'Бесчестный главный герой', value: '716' }, + { label: 'Библиотека', value: '428' }, + { label: 'Бизнес-литература', value: '121' }, + { label: 'Бизнесмен', value: '769' }, + { label: 'Биочип', value: '238' }, + { label: 'Бисексуальный главный герой', value: '773' }, + { label: 'Близнецы', value: '263' }, + { label: 'Боги', value: '317' }, + { label: 'Богини', value: '439' }, + { label: 'Боевая академия', value: '450' }, + { label: 'Боевая фантастика', value: '149' }, + { label: 'Боевое фэнтези', value: '104' }, + { label: 'Боевые духи', value: '433' }, + { label: 'Боевые соревнования', value: '493' }, + { label: 'Божественная защита', value: '421' }, + { label: 'Божественные силы', value: '327' }, + { label: 'Борьба за власть', value: '599' }, + { label: 'Брак', value: '446' }, + { label: 'Брак по расчету', value: '188' }, + { label: 'Братский комплекс', value: '159' }, + { label: 'Братство', value: '487' }, + { label: 'Братья и сестры', value: '577' }, + { label: 'Буддизм', value: '745' }, + { label: 'Быстрая культивация', value: '366' }, + { label: 'Быстрообучаемый', value: '324' }, + { label: 'Валькирии', value: '692' }, + { label: 'Вампиры', value: '360' }, + { label: 'Ваншот', value: '702' }, + { label: 'Ведьмы', value: '281' }, + { label: 'Вежливый главный герой', value: '379' }, + { label: 'Верные подчиненные', value: '328' }, + { label: 'Взрослый главный герой', value: '295' }, + { label: 'Видит то, чего не видят другие', value: '671' }, + { label: 'Виртуальная реальность', value: '401' }, + { label: 'Владелец магазина', value: '681' }, + { label: 'Внезапная сила', value: '455' }, + { label: 'Внезапное богатство', value: '765' }, + { label: 'Внешний вид отличается от факт', value: '419' }, + { label: 'Военные Летописи', value: '743' }, + { label: 'Возвращение из другого мира', value: '696' }, + { label: 'Войны', value: '181' }, + { label: 'Вокалоид', value: '701' }, + { label: 'Волшебники/Волшебницы', value: '539' }, + { label: 'Волшебные звери', value: '308' }, + { label: 'Воображаемый друг', value: '651' }, + { label: 'Воры', value: '414' }, + { label: 'Воскрешение', value: '199' }, + { label: 'Враги становятся возлюбленными', value: '498' }, + { label: 'Враги становятся союзниками', value: '562' }, + { label: 'Врата в другой мир', value: '609' }, + { label: 'Врачи', value: '376' }, + { label: 'Временной парадокс', value: '275' }, + { label: 'Всемогущий главный герой', value: '169' }, + { label: 'Вторжение на землю', value: '198' }, + { label: 'Второй шанс', value: '231' }, + { label: 'Вуайеризм', value: '790' }, + { label: 'Выживание', value: '380' }, + { label: 'Высокомерные персонажи', value: '361' }, + { label: 'Гадание', value: '595' }, + { label: 'Гарем рабов', value: '777' }, + { label: 'Гг - женщина', value: '186' }, + { label: 'Гг - мужчина', value: '166' }, + { label: 'Гг силен с самого начала', value: '171' }, + { label: 'Геймеры', value: '392' }, + { label: 'Генералы', value: '326' }, + { label: 'Генетические модификации', value: '657' }, + { label: 'Гениальный главный герой', value: '617' }, + { label: 'Герои', value: '285' }, + { label: 'Героиня — сорванец', value: '583' }, + { label: 'Героическая фантастика', value: '140' }, + { label: 'Героическое фэнтези', value: '107' }, + { label: 'Герой влюбляется первым', value: '187' }, + { label: 'Гетерохромия', value: '570' }, + { label: 'Гильдии', value: '411' }, + { label: 'Гипнотизм', value: '755' }, + { label: 'Главный герой — бог', value: '547' }, + { label: 'Главный герой — гуманоид', value: '636' }, + { label: 'Главный герой — наполовину чел', value: '445' }, + { label: 'Главный герой — отец', value: '792' }, + { label: 'Главный герой — раб', value: '780' }, + { label: 'Главный герой — ребенок', value: '488' }, + { label: 'Главный герой — рубака', value: '476' }, + { label: 'Главный герой влюбляется первы', value: '683' }, + { label: 'Главный герой играет роль', value: '472' }, + { label: 'Главный герой носит очки', value: '672' }, + { label: 'Главный герой пацифист', value: '698' }, + { label: 'Гладиаторы', value: '603' }, + { label: 'Глуповатый главный герой', value: '385' }, + { label: 'Гоблины', value: '586' }, + { label: 'Големы', value: '620' }, + { label: 'Гомункул', value: '789' }, + { label: 'Горничные', value: '459' }, + { label: 'Городское фэнтези', value: '105' }, + { label: 'Госпиталь', value: '800' }, + { label: 'Готовка', value: '302' }, + { label: 'Гриндинг', value: '393' }, + { label: 'Дао Компаньон', value: '462' }, + { label: 'Даосизм', value: '760' }, + { label: 'Дварфы', value: '323' }, + { label: 'Двойная личность', value: '642' }, + { label: 'Двойник', value: '601' }, + { label: 'Дворецкий', value: '660' }, + { label: 'Дворяне', value: '168' }, + { label: 'Дворянство/Аристократия', value: '437' }, + { label: 'Девушки-монстры', value: '669' }, + { label: 'Демоническая техника культивац', value: '722' }, + { label: 'Демоны', value: '152' }, + { label: 'Денежный долг', value: '783' }, + { label: 'Депрессия', value: '555' }, + { label: 'Детектив', value: '116' }, + { label: 'Детективы', value: '612' }, + { label: 'Детская литература', value: '123' }, + { label: 'Дискриминация', value: '162' }, + { label: 'Добыча денег в приоритете', value: '396' }, + { label: 'Документальная проза', value: '122' }, + { label: 'Долгая разлука', value: '307' }, + { label: 'Домашние дела', value: '290' }, + { label: 'Домогательство', value: '470' }, + { label: 'Драконы', value: '303' }, + { label: 'Драконьи всадники', value: '607' }, + { label: 'Древние времена', value: '221' }, + { label: 'Древний Китай', value: '374' }, + { label: 'Дружба', value: '217' }, + { label: 'Друзья детства', value: '282' }, + { label: 'Друзья становятся врагами', value: '567' }, + { label: 'Друиды', value: '497' }, + { label: 'Дух лисы', value: '784' }, + { label: 'Духи/призраки', value: '172' }, + { label: 'Духовный советник', value: '435' }, + { label: 'Душевность', value: '633' }, + { label: 'Души', value: '254' }, + { label: 'Европейская атмосфера', value: '521' }, + { label: 'Ёкаи', value: '575' }, + { label: 'Есть аниме-адаптация', value: '154' }, + { label: 'Есть видеоигра по мотивам', value: '552' }, + { label: 'Есть манга', value: '155' }, + { label: 'Есть манхва-адаптация', value: '517' }, + { label: 'Есть маньхуа-адаптация', value: '388' }, + { label: 'Есть сериал-адаптация', value: '492' }, + { label: 'Есть фильм', value: '173' }, + { label: 'Женища-наставник', value: '505' }, + { label: 'Жесткая, двуличная личность', value: '346' }, + { label: 'Жестокие персонажи', value: '503' }, + { label: 'Жестокое обращение с ребенком', value: '654' }, + { label: 'Жестокость', value: '245' }, + { label: 'Животноводство', value: '754' }, + { label: 'Животные черты', value: '528' }, + { label: 'Жизнь в квартире', value: '606' }, + { label: 'Жрицы', value: '615' }, + { label: 'Заботливый главный герой', value: '288' }, + { label: 'Забывчивый главный герой', value: '628' }, + { label: 'Заговоры', value: '289' }, + { label: 'Закалка тела', value: '362' }, + { label: 'Законники', value: '756' }, + { label: 'Замкнутый главный герой', value: '589' }, + { label: 'Запечатанная сила', value: '430' }, + { label: 'Застенчивые персонажи', value: '508' }, + { label: 'Звери', value: '237' }, + { label: 'Звери-компаньоны', value: '301' }, + { label: 'Злой протагонист', value: '243' }, + { label: 'Злые боги', value: '504' }, + { label: 'Злые организации', value: '563' }, + { label: 'Злые религии', value: '734' }, + { label: 'Знаменитости', value: '473' }, + { label: 'Знаменитый главный герой', value: '531' }, + { label: 'Знания современного мира', value: '297' }, + { label: 'Зомби', value: '409' }, + { label: 'Игра на выживание', value: '274' }, + { label: 'Игривый протагонист', value: '717' }, + { label: 'Игровая система рейтинга', value: '391' }, + { label: 'Игровые элементы', value: '266' }, + { label: 'Игрушки (18+)', value: '676' }, + { label: 'Из грязи в князи', value: '416' }, + { label: 'Из женщины в мужчину', value: '709' }, + { label: 'Из мужчины в женщину', value: '715' }, + { label: 'Из полного в худого', value: '767' }, + { label: 'Из слабого в сильного', value: '202' }, + { label: 'Извращенный главный герой', value: '434' }, + { label: 'Изгои', value: '534' }, + { label: 'Изменение расы', value: '664' }, + { label: 'Изменения внешнего вида', value: '300' }, + { label: 'Изменения личности', value: '219' }, + { label: 'Изнасилование', value: '229' }, + { label: 'Изображения жестокости', value: '355' }, + { label: 'ИИ', value: '235' }, + { label: 'Империи', value: '748' }, + { label: 'Инвалидность', value: '684' }, + { label: 'Индустриализация', value: '292' }, + { label: 'Инженер', value: '163' }, + { label: 'Инцест', value: '525' }, + { label: 'Искусственный интеллект', value: '236' }, + { label: 'Исследования', value: '670' }, + { label: 'Историческая проза', value: '119' }, + { label: 'Исторические приключения', value: '120' }, + { label: 'Исторический детектив', value: '117' }, + { label: 'Исторический любовный роман', value: '138' }, + { label: 'Историческое фэнтези', value: '110' }, + { label: 'Каннибализм', value: '456' }, + { label: 'Карточные игры', value: '682' }, + { label: 'Киберпанк', value: '141' }, + { label: 'Киберспорт', value: '686' }, + { label: 'Кланы', value: '796' }, + { label: 'Класс безработного', value: '735' }, + { label: 'Клоны', value: '447' }, + { label: 'Клубы', value: '216' }, + { label: 'Книги', value: '427' }, + { label: 'Книги навыков', value: '400' }, + { label: 'Книжный червь', value: '519' }, + { label: 'Коварство', value: '230' }, + { label: 'Коллеги', value: '704' }, + { label: 'Колледж/Университет', value: '726' }, + { label: 'Кома', value: '776' }, + { label: 'Командная работа', value: '496' }, + { label: 'Комедийный оттенок', value: '582' }, + { label: 'Комплекс неполноценности', value: '550' }, + { label: 'Комплекс семейных отношений', value: '223' }, + { label: 'Конкуренция', value: '746' }, + { label: 'Контроль разума/сознания', value: '357' }, + { label: 'Копейщик', value: '425' }, + { label: 'Королевская битва', value: '797' }, + { label: 'Королевская власть', value: '178' }, + { label: 'Королевства', value: '259' }, + { label: 'Короткий любовный роман', value: '136' }, + { label: 'Коррупция', value: '457' }, + { label: 'Космическая фантастика', value: '146' }, + { label: 'Космические войны', value: '697' }, + { label: 'Красивый герой', value: '226' }, + { label: 'Крафт', value: '377' }, + { label: 'Кризис личности', value: '640' }, + { label: 'Кругосветное путешествие', value: '353' }, + { label: 'Кудере', value: '506' }, + { label: 'Кузены', value: '718' }, + { label: 'Кузнец', value: '518' }, + { label: 'Кукловоды', value: '501' }, + { label: 'Куклы/марионетки', value: '624' }, + { label: 'Культивация', value: '241' }, + { label: 'Куннилингус', value: '667' }, + { label: 'Легенды', value: '500' }, + { label: 'Легкая жизнь', value: '644' }, + { label: 'Ленивый главный герой', value: '621' }, + { label: 'Лидерство', value: '494' }, + { label: 'Лоли', value: '213' }, + { label: 'Лотерея', value: '477' }, + { label: 'Любовное фэнтези', value: '135' }, + { label: 'Любовный треугольник', value: '218' }, + { label: 'Любовь детства', value: '560' }, + { label: 'Любовь с первого взгляда', value: '737' }, + { label: 'Магические надписи', value: '453' }, + { label: 'Магические печати', value: '369' }, + { label: 'Магические технологии', value: '440' }, + { label: 'Магия', value: '165' }, + { label: 'Магия призыва', value: '418' }, + { label: 'Мазохистские персонажи', value: '688' }, + { label: 'Манипулятивные персонажи', value: '249' }, + { label: 'Мания', value: '212' }, + { label: 'Мастер на все руки', value: '466' }, + { label: 'Мастурбация', value: '788' }, + { label: 'Махо-сёдзё', value: '556' }, + { label: 'Медицинские знания', value: '507' }, + { label: 'Медленная романтическая линия', value: '232' }, + { label: 'Медленное развитие на старте', value: '694' }, + { label: 'Межпространственные путешестви', value: '404' }, + { label: 'Менеджмент', value: '294' }, + { label: 'Мертвый главный герой', value: '761' }, + { label: 'Месть', value: '209' }, + { label: 'Метаморфы', value: '335' }, + { label: 'Меч и магия', value: '180' }, + { label: 'Мечник', value: '647' }, + { label: 'Мечты', value: '738' }, + { label: 'Милая история', value: '725' }, + { label: 'Милое дитя', value: '637' }, + { label: 'Милый главный герой', value: '714' }, + { label: 'Мировое дерево', value: '463' }, + { label: 'Мистика', value: '151' }, + { label: 'Мистический ореол вокруг семьи', value: '483' }, + { label: 'Мифические звери', value: '491' }, + { label: 'Мифология', value: '530' }, + { label: 'Младшие братья', value: '785' }, + { label: 'Младшие сестры', value: '527' }, + { label: 'ММОРПГ (ЛитРПГ)', value: '395' }, + { label: 'Множество перемещенных людей', value: '549' }, + { label: 'Множество реальностей', value: '370' }, + { label: 'Множество реинкарнированных лю', value: '330' }, + { label: 'Модели', value: '678' }, + { label: 'Молчаливый персонаж', value: '721' }, + { label: 'Монстры', value: '192' }, + { label: 'Мужская гей-пара', value: '706' }, + { label: 'Мужчина-яндере', value: '267' }, + { label: 'Музыка', value: '635' }, + { label: 'Музыкальные группы', value: '634' }, + { label: 'Мутации', value: '693' }, + { label: 'Мутированные существа', value: '405' }, + { label: 'Навык кражи', value: '662' }, + { label: 'Навязчивая любовь', value: '206' }, + { label: 'Наемники', value: '412' }, + { label: 'Назойливый возлюбленный', value: '561' }, + { label: 'Наивный главный герой', value: '189' }, + { label: 'Наркотики', value: '689' }, + { label: 'Нарциссический главный герой', value: '532' }, + { label: 'Насилие сексуального характера', value: '774' }, + { label: 'Наследование', value: '452' }, + { label: 'Научная фантастика', value: '144' }, + { label: 'Национализм', value: '406' }, + { label: 'Не блещущий внешне главный гер', value: '415' }, + { label: 'Не родные братья и сестры', value: '526' }, + { label: 'Небеса', value: '568' }, + { label: 'Небесное испытание', value: '367' }, + { label: 'Негуманоидный главный герой', value: '659' }, + { label: 'Недоверчивый главный герой', value: '546' }, + { label: 'Недооцененный главный герой', value: '258' }, + { label: 'Недоразумения', value: '309' }, + { label: 'Неизлечимая болезнь', value: '196' }, + { label: 'Некромант', value: '397' }, + { label: 'Нелинейная история', value: '269' }, + { label: 'Ненавистный главный герой', value: '548' }, + { label: 'Ненадежный рассказчик', value: '277' }, + { label: 'Нерезиденты', value: '772' }, + { label: 'Нерешительный главный герой', value: '744' }, + { label: 'Несерьезный главный герой', value: '384' }, + { label: 'Несколько временных линий', value: '576' }, + { label: 'Несколько гг', value: '268' }, + { label: 'Несколько главных героев', value: '537' }, + { label: 'Несколько идентичностей', value: '652' }, + { label: 'Несколько личностей', value: '536' }, + { label: 'Нетораре', value: '732' }, + { label: 'Нетори', value: '514' }, + { label: 'Неудачливый главный герой', value: '618' }, + { label: 'Ниндзя', value: '441' }, + { label: 'Новелла', value: '103' }, + { label: 'Обещание из детства', value: '742' }, + { label: 'Обманщик', value: '485' }, + { label: 'Обмен телами', value: '214' }, + { label: 'Обнаженка', value: '490' }, + { label: 'Обольщение', value: '731' }, + { label: 'Оборотни', value: '661' }, + { label: 'Обратный гарем', value: '351' }, + { label: 'Общество монстров', value: '329' }, + { label: 'Обязательство', value: '602' }, + { label: 'Огнестрельное оружие', value: '291' }, + { label: 'Ограниченное время жизни', value: '194' }, + { label: 'Одержимость', value: '512' }, + { label: 'Одинокий главный герой', value: '394' }, + { label: 'Одиночество', value: '306' }, + { label: 'Одиночное проживание', value: '313' }, + { label: 'Околосмертные переживания', value: '759' }, + { label: 'Оммёдзи', value: '597' }, + { label: 'Омоложение', value: '338' }, + { label: 'Организованная преступность', value: '540' }, + { label: 'Оргия', value: '775' }, + { label: 'Орки', value: '331' }, + { label: 'Освоение навыков', value: '336' }, + { label: 'Основано на аниме', value: '605' }, + { label: 'Основано на видео игре', value: '720' }, + { label: 'Основано на визуальной новелле', value: '793' }, + { label: 'Основано на песне', value: '700' }, + { label: 'Основано на фильме', value: '604' }, + { label: 'Осторожный главный герой', value: '240' }, + { label: 'Отаку', value: '571' }, + { label: 'Открытый космос', value: '264' }, + { label: 'Отношения в сети', value: '727' }, + { label: 'Отношения между людьми и нелюд', value: '311' }, + { label: 'Отношения на расстоянии', value: '791' }, + { label: 'Отношения Сенпай-Коухай', value: '572' }, + { label: 'Отношения ученика и учителя', value: '515' }, + { label: 'Отношения учитель-ученик', value: '429' }, + { label: 'Отношения хозяин-слуга', value: '312' }, + { label: 'Отомэ игра', value: '733' }, + { label: 'Отсутствие здравого смысла', value: '645' }, + { label: 'Отсутствие родителей', value: '625' }, + { label: 'Офисный роман', value: '707' }, + { label: 'Официанты', value: '703' }, + { label: 'Охотники', value: '378' }, + { label: 'Очаровательный главный герой', value: '687' }, + { label: 'Падшее дворянство', value: '655' }, + { label: 'Падшие ангелы', value: '565' }, + { label: 'Пайзури', value: '665' }, + { label: 'Паразиты', value: '591' }, + { label: 'Параллельные миры', value: '207' }, + { label: 'Парк', value: '265' }, + { label: 'Парк развлечений', value: '632' }, + { label: 'Пародия', value: '407' }, + { label: 'Певцы/Певицы', value: '739' }, + { label: 'Первая любовь', value: '729' }, + { label: 'Первоисточник новеллы — манга', value: '611' }, + { label: 'Первый раз', value: '787' }, + { label: 'Перемещение в игровой мир', value: '588' }, + { label: 'Перемещение в иной мир', value: '750' }, + { label: 'Перерождение в ином мире', value: '751' }, + { label: 'Переселение души/трансмиграция', value: '257' }, + { label: 'Персонаж использует щит', value: '666' }, + { label: 'Петля времени', value: '200' }, + { label: 'Пираты', value: '771' }, + { label: 'Писатели', value: '482' }, + { label: 'Питомцы', value: '349' }, + { label: 'Племенное общество', value: '381' }, + { label: 'Повелитель демонов', value: '283' }, + { label: 'Подземелья', value: '322' }, + { label: 'Подростковая проза', value: '114' }, + { label: 'Пожелания', value: '278' }, + { label: 'Познание Дао', value: '443' }, + { label: 'Покинутое дитя', value: '590' }, + { label: 'Полигамия', value: '386' }, + { label: 'Политика', value: '170' }, + { label: 'Политический роман', value: '133' }, + { label: 'Полиция', value: '764' }, + { label: 'Полулюди', value: '420' }, + { label: 'Популярный любовный интерес', value: '342' }, + { label: 'Постапокалипсис', value: '145' }, + { label: 'Постапокалиптика', value: '184' }, + { label: 'Потерянные цивилизации', value: '749' }, + { label: 'Похищения людей', value: '723' }, + { label: 'Поэзия', value: '111' }, + { label: 'Правонарушители', value: '713' }, + { label: 'Прагматичный главный герой', value: '592' }, + { label: 'Преданный любовный интерес', value: '225' }, + { label: 'Предательство', value: '222' }, + { label: 'Предвидение', value: '177' }, + { label: 'Прекрасная героиня', value: '158' }, + { label: 'Преступники', value: '736' }, + { label: 'Преступность', value: '204' }, + { label: 'Призванный герой', value: '262' }, + { label: 'Призраки', value: '176' }, + { label: 'Принуждение к отношениям', value: '643' }, + { label: 'Принцессы', value: '795' }, + { label: 'Притворная пара', value: '641' }, + { label: 'Причудливые персонажи', value: '387' }, + { label: 'Пришельцы/инопланетяне', value: '197' }, + { label: 'Программист', value: '691' }, + { label: 'Проклятия', value: '174' }, + { label: 'Промывание мозгов', value: '578' }, + { label: 'Пропуск времени', value: '256' }, + { label: 'Пророчества', value: '499' }, + { label: 'Проститутки', value: '786' }, + { label: 'Прошлое играет большую роль', value: '320' }, + { label: 'Прыжки между мирами', value: '741' }, + { label: 'Психические силы', value: '359' }, + { label: 'Психопаты', value: '270' }, + { label: 'Публицистика', value: '124' }, + { label: 'Путешествие во времени', value: '201' }, + { label: 'Пытка', value: '280' }, + { label: 'Рабы', value: '778' }, + { label: 'Развитие личности', value: '125' }, + { label: 'Развод', value: '758' }, + { label: 'Разумные предметы', value: '286' }, + { label: 'Расизм', value: '408' }, + { label: 'Рассказ', value: '185' }, + { label: 'Расторжения помолвки', value: '343' }, + { label: 'Расы зооморфов', value: '315' }, + { label: 'Реализм', value: '246' }, + { label: 'Ревность', value: '730' }, + { label: 'Редакторы', value: '705' }, + { label: 'Реинкарнация', value: '372' }, + { label: 'Реинкарнация в монстра', value: '310' }, + { label: 'Реинкарнация в объект', value: '711' }, + { label: 'Религии', value: '616' }, + { label: 'Репортеры', value: '781' }, + { label: 'Ресторан', value: '680' }, + { label: 'Решительный главный герой', value: '242' }, + { label: 'Робкий главный герой', value: '763' }, + { label: 'Родитель одиночка', value: '708' }, + { label: 'Родительский комплекс', value: '587' }, + { label: 'Родословная', value: '239' }, + { label: 'Романтическая эротика', value: '132' }, + { label: 'Романтический подсюжет', value: '271' }, + { label: 'Рост персонажа', value: '215' }, + { label: 'Рыцари', value: '260' }, + { label: 'Садистские персонажи', value: '674' }, + { label: 'Самоотверженный главный герой', value: '747' }, + { label: 'Самурай', value: '677' }, + { label: 'Сборник коротких историй', value: '520' }, + { label: 'Связанные сюжетные линии', value: '535' }, + { label: 'Святые', value: '752' }, + { label: 'Священники', value: '413' }, + { label: 'Сдержанный главный герой', value: '762' }, + { label: 'Секретные организации', value: '417' }, + { label: 'Секреты', value: '626' }, + { label: 'Секс рабы', value: '779' }, + { label: 'Семейный конфликт', value: '757' }, + { label: 'Семь добродетелей', value: '334' }, + { label: 'Семь смертных грехов', value: '253' }, + { label: 'Семья', value: '347' }, + { label: 'Сёнэн-ай подсюжет', value: '690' }, + { label: 'Серийные убийцы', value: '557' }, + { label: 'Сестринский комплекс', value: '558' }, + { label: 'Сила духа', value: '373' }, + { label: 'Сила, требующая платы за польз', value: '579' }, + { label: 'Сильная пара', value: '228' }, + { label: 'Сильный в сильнейшего', value: '442' }, + { label: 'Сильный любовный интерес', value: '273' }, + { label: 'Синдром восьмиклассника', value: '211' }, + { label: 'Сироты', value: '319' }, + { label: 'Система уровней', value: '305' }, + { label: 'Системный администратор', value: '538' }, + { label: 'Сказка', value: '126' }, + { label: 'Скрытие истинной личности', value: '451' }, + { label: 'Скрытие истинных способностей', value: '348' }, + { label: 'Скрытный главный герой', value: '252' }, + { label: 'Скрытые способности', value: '247' }, + { label: 'Скульпторы', value: '448' }, + { label: 'Слабо выраженная романтическая', value: '318' }, + { label: 'Слабый главный герой', value: '298' }, + { label: 'Слепой главный герой', value: '794' }, + { label: 'Слуги', value: '333' }, + { label: 'Слэш', value: '131' }, + { label: 'Смерть', value: '175' }, + { label: 'Смерть близких', value: '444' }, + { label: 'Собственнические персонажи', value: '227' }, + { label: 'Современная проза', value: '150' }, + { label: 'Современность', value: '167' }, + { label: 'Современный любовный роман', value: '137' }, + { label: 'Сожительство', value: '544' }, + { label: 'Создание армии', value: '287' }, + { label: 'Создание артефактов', value: '382' }, + { label: 'Создание клана', value: '513' }, + { label: 'Создание королевства', value: '293' }, + { label: 'Создание навыков', value: '337' }, + { label: 'Создание секты', value: '469' }, + { label: 'Солдаты/военные', value: '193' }, + { label: 'Сон', value: '622' }, + { label: 'Состоятельные персонажи', value: '190' }, + { label: 'Социальная иерархия по силе', value: '255' }, + { label: 'Социальная фантастика', value: '148' }, + { label: 'Социальные изгои', value: '542' }, + { label: 'Спасение мира', value: '638' }, + { label: 'Специальные способности', value: '179' }, + { label: 'Спокойный главный герой', value: '160' }, + { label: 'Справедливый главный герой', value: '719' }, + { label: 'Средневековье', value: '296' }, + { label: 'Ссорящаяся пара', value: '383' }, + { label: 'Сталкеры', value: '710' }, + { label: 'Старение', value: '299' }, + { label: 'Стимпанк', value: '139' }, + { label: 'Стоические персонажи', value: '509' }, + { label: 'Стокгольмский синдром', value: '675' }, + { label: 'Стратег', value: '495' }, + { label: 'Стратегические битвы', value: '272' }, + { label: 'Стратегия', value: '798' }, + { label: 'Стрелки', value: '522' }, + { label: 'Стрельба из лука', value: '461' }, + { label: 'Студенческий совет', value: '551' }, + { label: 'Судьба', value: '364' }, + { label: 'Суккубы', value: '545' }, + { label: 'Супер герои', value: '799' }, + { label: 'Суровая подготовка', value: '356' }, + { label: 'Таинственная болезнь', value: '195' }, + { label: 'Таинственное прошлое', value: '358' }, + { label: 'Тайная личность', value: '399' }, + { label: 'Тайные отношения', value: '768' }, + { label: 'Танцоры', value: '782' }, + { label: 'Телохранители', value: '516' }, + { label: 'Темное фэнтези', value: '106' }, + { label: 'Тентакли', value: '712' }, + { label: 'Террористы', value: '574' }, + { label: 'Технологический разрыв', value: '658' }, + { label: 'Тихие персонажи', value: '600' }, + { label: 'Толстый главный герой', value: '389' }, + { label: 'Торговцы', value: '489' }, + { label: 'Травля/буллинг', value: '210' }, + { label: 'Травник', value: '724' }, + { label: 'Трагическое прошлое', value: '276' }, + { label: 'Трансплантация воспоминаний', value: '449' }, + { label: 'Триллер', value: '134' }, + { label: 'Трудолюбивый главный герой', value: '164' }, + { label: 'Тюрьма', value: '541' }, + { label: 'Убийства', value: '205' }, + { label: 'Убийцы', value: '345' }, + { label: 'Убийцы драконов', value: '646' }, + { label: 'Уверенный главный герой', value: '363' }, + { label: 'Удачливый главный герой', value: '478' }, + { label: 'Ужасы', value: '112' }, + { label: 'Укротитель монстров', value: '422' }, + { label: 'Умения из прошлой жизни', value: '371' }, + { label: 'Умная пара', value: '554' }, + { label: 'Умный главный герой', value: '161' }, + { label: 'Уникальная техника Культивации', value: '350' }, + { label: 'Уникальное оружие', value: '426' }, + { label: 'Управление бизнесом', value: '403' }, + { label: 'Управление временем', value: '279' }, + { label: 'Управление кровью', value: '753' }, + { label: 'Упрямый главный герой', value: '695' }, + { label: 'Уродливый главный герой', value: '766' }, + { label: 'Ускоренный рост', value: '390' }, + { label: 'Усыновленные дети', value: '543' }, + { label: 'Усыновленный главный герой', value: '486' }, + { label: 'Уход за детьми', value: '474' }, + { label: 'Учителя', value: '431' }, + { label: 'Фамильяры', value: '596' }, + { label: 'Фанатизм', value: '650' }, + { label: 'Фантастические существа', value: '410' }, + { label: 'Фантастический детектив', value: '115' }, + { label: 'Фанфик', value: '113' }, + { label: 'Фанфикшн', value: '464' }, + { label: 'Фармацевт', value: '728' }, + { label: 'Фарминг', value: '458' }, + { label: 'Феи', value: '316' }, + { label: 'Фелляция', value: '631' }, + { label: 'Фемслэш', value: '127' }, + { label: 'Фениксы', value: '454' }, + { label: 'Фетиш груди', value: '559' }, + { label: 'Философия', value: '208' }, + { label: 'Фильмы', value: '479' }, + { label: 'Флэшбэки', value: '585' }, + { label: 'Фобии', value: '220' }, + { label: 'Фольклор', value: '529' }, + { label: 'Футанари', value: '619' }, + { label: 'Футуристический сеттинг', value: '460' }, + { label: 'Фэнтези мир', value: '244' }, + { label: 'Хакеры', value: '475' }, + { label: 'Харизматический герой', value: '467' }, + { label: 'Хикикомори/Затворники', value: '524' }, + { label: 'Хитроумный главный герой', value: '224' }, + { label: 'Хозяин подземелий', value: '608' }, + { label: 'Холодный главный герой', value: '354' }, + { label: 'Хорошие отношения с семьей', value: '566' }, + { label: 'Хранители могил', value: '191' }, + { label: 'Целители', value: '465' }, + { label: 'Цзянши', value: '740' }, + { label: 'Цундэрэ', value: '510' }, + { label: 'Чаты', value: '629' }, + { label: 'Человеческое оружие', value: '580' }, + { label: 'Честный главный герой', value: '341' }, + { label: 'Читы', value: '339' }, + { label: 'Шантаж', value: '584' }, + { label: 'Шеф-повар', value: '340' }, + { label: 'Шикигами', value: '598' }, + { label: 'Школа только для девочек', value: '668' }, + { label: 'Шота', value: '573' }, + { label: 'Шоу-бизнес', value: '481' }, + { label: 'Шпионский детектив', value: '118' }, + { label: 'Шпионы', value: '614' }, + { label: 'Эволюция', value: '304' }, + { label: 'Эгоистичный главный герой', value: '594' }, + { label: 'Эйдетическая память', value: '468' }, + { label: 'Экзорсизм', value: '564' }, + { label: 'Экономика', value: '553' }, + { label: 'Эксгибиционизм', value: '673' }, + { label: 'Эксперименты с людьми', value: '248' }, + { label: 'Элементальная магия', value: '471' }, + { label: 'Эльфы', value: '284' }, + { label: 'Эмоционально слабый гг', value: '770' }, + { label: 'Эпизодический', value: '649' }, + { label: 'Эпическое фэнтези', value: '109' }, + { label: 'Эротическая фантастика', value: '129' }, + { label: 'Эротический фанфик', value: '128' }, + { label: 'Эротическое фэнтези', value: '130' }, + { label: 'Юмористическая фантастика', value: '143' }, + { label: 'Юмористическое фэнтези', value: '108' }, + { label: 'Юный любовный интерес', value: '511' }, + { label: 'Яды', value: '484' }, + { label: 'Языкастые персонажи', value: '480' }, + { label: 'Языковой барьер', value: '182' }, + { label: 'Яндере', value: '314' }, + { label: 'Японские силы самообороны', value: '610' }, + { label: 'Ярко выраженная романтика', value: '365' }, + { label: 'R-15 Японское возрастное огр.', value: '332' }, + { label: 'R-18', value: '424' }, ], type: FilterTypes.ExcludableCheckboxGroup, }, types: { - label: "Типы:", + label: 'Типы:', value: [], options: [ - { label: "Авторское", value: "1" }, - { label: "Другое", value: "7" }, - { label: "Запад", value: "5" }, - { label: "Китай", value: "4" }, - { label: "Корея", value: "3" }, - { label: "Фанфики", value: "6" }, - { label: "Япония", value: "2" }, + { label: 'Авторское', value: '1' }, + { label: 'Другое', value: '7' }, + { label: 'Запад', value: '5' }, + { label: 'Китай', value: '4' }, + { label: 'Корея', value: '3' }, + { label: 'Фанфики', value: '6' }, + { label: 'Япония', value: '2' }, ], type: FilterTypes.CheckboxGroup, }, status: { - label: "Статус проекта:", + label: 'Статус проекта:', value: [], options: [ - { label: "Анонс", value: "4" }, - { label: "Закончен", value: "0" }, - { label: "Заморожен", value: "2" }, - { label: "Лицензировано", value: "5" }, - { label: "Нет переводчика", value: "3" }, - { label: "Продолжается", value: "1" }, + { label: 'Анонс', value: '4' }, + { label: 'Закончен', value: '0' }, + { label: 'Заморожен', value: '2' }, + { label: 'Лицензировано', value: '5' }, + { label: 'Нет переводчика', value: '3' }, + { label: 'Продолжается', value: '1' }, ], type: FilterTypes.CheckboxGroup, }, age_limit: { - label: "Возрастной рейтинг:", + label: 'Возрастной рейтинг:', value: [], options: [ - { label: "16+", value: "1" }, - { label: "18+", value: "2" }, - { label: "Для всех", value: "0" }, + { label: '16+', value: '1' }, + { label: '18+', value: '2' }, + { label: 'Для всех', value: '0' }, ], type: FilterTypes.CheckboxGroup, }, diff --git a/plugins/russian/zelluloza.ts b/plugins/russian/zelluloza.ts index c8afc62..d8d3e93 100644 --- a/plugins/russian/zelluloza.ts +++ b/plugins/russian/zelluloza.ts @@ -1,17 +1,17 @@ -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; -import qs from "qs"; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; +import qs from 'qs'; class Zelluloza implements Plugin.PluginBase { - id = "zelluloza"; - name = "Целлюлоза"; - site = "https://zelluloza.ru"; - version = "1.0.0"; - icon = "src/ru/zelluloza/icon.png"; + id = 'zelluloza'; + name = 'Целлюлоза'; + site = 'https://zelluloza.ru'; + version = '1.0.0'; + icon = 'src/ru/zelluloza/icon.png'; async popularNovels( pageNo: number, @@ -20,36 +20,36 @@ class Zelluloza implements Plugin.PluginBase { showLatestNovels, }: Plugin.PopularNovelsOptions, ): Promise { - const sort = showLatestNovels ? "0.0" : filters?.sort?.value || "6.0"; - const genres = filters?.genres?.value || ""; + const sort = showLatestNovels ? '0.0' : filters?.sort?.value || '6.0'; + const genres = filters?.genres?.value || ''; - const body = await fetchApi(this.site + "/ajaxcall/", { + const body = await fetchApi(this.site + '/ajaxcall/', { headers: { - "Content-Type": "application/x-www-form-urlencoded", - Referer: this.site + "/top/freebooks/", + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.site + '/top/freebooks/', Origin: this.site, }, - method: "POST", + method: 'POST', body: qs.stringify({ - op: "morebooks", - par1: "", + op: 'morebooks', + par1: '', par2: `125:0:${genres}:0.${sort}.0.0.0.0.0.0.0.0.0.1.s.1..:${pageNo}`, - par4: "", + par4: '', }), - }).then((res) => res.text()); + }).then(res => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; loadedCheerio('div[style="display: flex"]').each((index, element) => { novels.push({ - name: loadedCheerio(element).find('span[itemprop="name"]').text() || "", + name: loadedCheerio(element).find('span[itemprop="name"]').text() || '', cover: this.site + - loadedCheerio(element).find('img[class="shadow"]').attr("src"), + loadedCheerio(element).find('img[class="shadow"]').attr('src'), path: ( - loadedCheerio(element).find('img[class="shadow"]').attr("onclick") || - "" - ).replace(/\D/g, ""), + loadedCheerio(element).find('img[class="shadow"]').attr('onclick') || + '' + ).replace(/\D/g, ''), }); }); @@ -57,20 +57,21 @@ class Zelluloza implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.resolveUrl(novelPath, true)) - .then((res) => res.text()); + const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, name: loadedCheerio('span[itemprop="name"]').text(), - cover: this.site + loadedCheerio('img[class="shadow"]').attr("src"), - genres: loadedCheerio(".gnres").text()?.split?.(": ")?.[1], + cover: this.site + loadedCheerio('img[class="shadow"]').attr('src'), + genres: loadedCheerio('.gnres').text()?.split?.(': ')?.[1], summary: - loadedCheerio("#bann_full").text() || - loadedCheerio("#bann_short").text(), - author: loadedCheerio(".author_link").text(), - status: loadedCheerio(".tech_decription").text().includes("Пишется") + loadedCheerio('#bann_full').text() || + loadedCheerio('#bann_short').text(), + author: loadedCheerio('.author_link').text(), + status: loadedCheerio('.tech_decription').text().includes('Пишется') ? NovelStatus.Ongoing : NovelStatus.Completed, }; @@ -78,7 +79,9 @@ class Zelluloza implements Plugin.PluginBase { const chapters: Plugin.ChapterItem[] = []; loadedCheerio('ul[class="g0"] div[class="w800_m"]').each( (chapterIndex, element) => { - const isFree = loadedCheerio(element).find('div[class="chaptfree"]').length; + const isFree = loadedCheerio(element).find( + 'div[class="chaptfree"]', + ).length; if (isFree) { const chapter = loadedCheerio(element).find('a[class="chptitle"]'); const releaseDate = loadedCheerio(element) @@ -86,7 +89,7 @@ class Zelluloza implements Plugin.PluginBase { .text(); chapters.push({ name: chapter.text().trim(), - path: (chapter.attr("href") || "").split("/").slice(2, 4).join("/"), + path: (chapter.attr('href') || '').split('/').slice(2, 4).join('/'), releaseTime: this.parseDate(releaseDate), chapterNumber: chapterIndex + 1, }); @@ -99,33 +102,33 @@ class Zelluloza implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const [bookID, chapterID] = chapterPath.split("/"); - const body = await fetchApi(this.site + "/ajaxcall/", { + const [bookID, chapterID] = chapterPath.split('/'); + const body = await fetchApi(this.site + '/ajaxcall/', { headers: { - "Content-Type": "application/x-www-form-urlencoded", + 'Content-Type': 'application/x-www-form-urlencoded', Referer: this.resolveUrl(chapterPath), Origin: this.site, }, - method: "POST", + method: 'POST', body: qs.stringify({ - op: "getbook", + op: 'getbook', par1: bookID, par2: chapterID, }), - }).then((res) => res.text()); + }).then(res => res.text()); - const encrypted = body.split("")[0].split("\n"); + const encrypted = body.split('')[0].split('\n'); const decrypted = encrypted - .map((str) => decrypt(str)) - .join("") - .replace(/\r/g, "") + .map(str => decrypt(str)) + .join('') + .replace(/\r/g, '') .trim(); const chapterText = decrypted //cosmetic filters - .replace(/\[\*]([\s\S]*?)\[\/]/g, "$1") - .replace(/\[_]([\s\S]*?)\[\/]/g, "$1") - .replace(/\[-]([\s\S]*?)\[\/]/g, "$1") - .replace(/\[~]([\s\S]*?)\[\/]/g, "$1"); /* + .replace(/\[\*]([\s\S]*?)\[\/]/g, '$1') + .replace(/\[_]([\s\S]*?)\[\/]/g, '$1') + .replace(/\[-]([\s\S]*?)\[\/]/g, '$1') + .replace(/\[~]([\s\S]*?)\[\/]/g, '$1'); /* .replace(/\[!]([\s\S]*?)\[\/]/g, '$1') .replace(/\[ctr]([\s\S]*?)\[\/]/g, "
$1
") .replace(/\[rht]([\s\S]*?)\[\/]/g, '$1') @@ -143,47 +146,47 @@ class Zelluloza implements Plugin.PluginBase { searchTerm: string, pageNo: number, ): Promise { - const body = await fetchApi(this.site + "/ajaxcall/", { + const body = await fetchApi(this.site + '/ajaxcall/', { headers: { - "Content-Type": "application/x-www-form-urlencoded", - Referer: this.site + "/search/done/#result", + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.site + '/search/done/#result', Origin: this.site, }, - method: "POST", + method: 'POST', body: qs.stringify({ - op: "morebooks", + op: 'morebooks', par1: searchTerm, - par2: "206:0:0:0.0.0.0.0.0.0.10.0.0.0.0.0..0..:" + pageNo, - par4: "", + par2: '206:0:0:0.0.0.0.0.0.0.10.0.0.0.0.0..0..:' + pageNo, + par4: '', }), - }).then((res) => res.text()); + }).then(res => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; loadedCheerio('div[style="display: flex"]').each((index, element) => { novels.push({ - name: loadedCheerio(element).find('span[itemprop="name"]').text() || "", + name: loadedCheerio(element).find('span[itemprop="name"]').text() || '', cover: this.site + - loadedCheerio(element).find('img[class="shadow"]').attr("src"), + loadedCheerio(element).find('img[class="shadow"]').attr('src'), path: ( - loadedCheerio(element).find('img[class="shadow"]').attr("onclick") || - "" - ).replace(/\D/g, ""), + loadedCheerio(element).find('img[class="shadow"]').attr('onclick') || + '' + ).replace(/\D/g, ''), }); }); return novels; } - parseDate = (dateString: string | undefined = "") => { + parseDate = (dateString: string | undefined = '') => { const [, day, month, year, hour, minute] = dateString.match(/(\d{2})\.(\d{2})\.(\d{4})г (\d{2}):(\d{2})/) || []; if (day && month && year && hour && minute) { return dayjs( - year + "-" + month + "-" + day + " " + hour + ":" + minute, - ).format("LLL"); + year + '-' + month + '-' + day + ' ' + hour + ':' + minute, + ).format('LLL'); } return null; @@ -191,167 +194,167 @@ class Zelluloza implements Plugin.PluginBase { fetchImage = fetchFile; resolveUrl = (path: string, isNovel?: boolean) => - this.site + "/books/" + path; + this.site + '/books/' + path; filters = { sort: { - label: "Сортировка", - value: "6.0", + label: 'Сортировка', + value: '6.0', options: [ - { label: "Топ-недели", value: "6.0" }, - { label: "Бестселеры", value: "5.0" }, - { label: "Популярные у читателей", value: "2.0" }, - { label: "Книжные новинки", value: "0.0" }, - { label: "Законченные книги", value: "0.1" }, + { label: 'Топ-недели', value: '6.0' }, + { label: 'Бестселеры', value: '5.0' }, + { label: 'Популярные у читателей', value: '2.0' }, + { label: 'Книжные новинки', value: '0.0' }, + { label: 'Законченные книги', value: '0.1' }, ], type: FilterTypes.Picker, }, genres: { - label: "Жанры", - value: "", + label: 'Жанры', + value: '', options: [ - { label: "Все", value: "" }, - { label: "16+", value: "204" }, - { label: "18+", value: "100" }, - { label: "Авантюра", value: "347" }, - { label: "Альтернативная история", value: "55" }, - { label: "Ангст", value: "463" }, - { label: "Аниме", value: "405" }, - { label: "Антиутопия", value: "21" }, - { label: "Апокалипсис", value: "368" }, - { label: "Аудиокнига", value: "391" }, - { label: "Байки", value: "435" }, - { label: "Бизнес-книги", value: "522" }, - { label: "Биография", value: "52" }, - { label: "Биопанк", value: "379" }, - { label: "Боевая фантастика", value: "512" }, - { label: "Боевик", value: "61" }, - { label: "Боевые искусства", value: "142" }, - { label: "Бояръ-аниме", value: "433" }, - { label: "Вбоквелл", value: "372" }, - { label: "Вестерн", value: "96" }, - { label: "Война миров", value: "137" }, - { label: "Восточное фэнтези", value: "505" }, - { label: "Городское фентези", value: "63" }, - { label: "Городское фэнтези", value: "520" }, - { label: "Готика", value: "83" }, - { label: "Детективы", value: "4" }, - { label: "Детские книги", value: "8" }, - { label: "Документальные книги", value: "351" }, - { label: "Дом и дача", value: "534" }, - { label: "Дорожные истории", value: "41" }, - { label: "Драма", value: "47" }, - { label: "Древняя литература", value: "529" }, - { label: "Женский психологический роман", value: "186" }, - { label: "Женский роман", value: "141" }, - { label: "Зарубежная классика", value: "524" }, - { label: "Здоровье и красота", value: "533" }, - { label: "Изучение языков", value: "537" }, - { label: "Иронический детектив", value: "85" }, - { label: "Исследования", value: "156" }, - { label: "Историческая литература", value: "3" }, - { label: "Исторический любовный роман", value: "518" }, - { label: "Историческое фэнтези", value: "90" }, - { label: "Киберпанк", value: "7" }, - { label: "Классическая литература", value: "9" }, - { label: "Книга-игра", value: "371" }, - { label: "Книги для родителей", value: "535" }, - { label: "Комедия", value: "80" }, - { label: "Конкурсные сборники", value: "349" }, - { label: "Короткий любовный роман", value: "517" }, - { label: "Космическая фантастика", value: "513" }, - { label: "Космоопера", value: "58" }, - { label: "Криминальная проза", value: "35" }, - { label: "Криптоистория", value: "400" }, - { label: "Ксенофантастика", value: "102" }, - { label: "Культура и искусство", value: "536" }, - { label: "Легенды", value: "161" }, - { label: "Лирика", value: "49" }, - { label: "ЛитРПГ", value: "62" }, - { label: "Любовная фантастика", value: "516" }, - { label: "Любовное фэнтези", value: "515" }, - { label: "Любовный роман", value: "46" }, - { label: "Магический реализм", value: "42" }, - { label: "Матриархат", value: "385" }, - { label: "Медицина", value: "57" }, - { label: "Мелодрама", value: "45" }, - { label: "Мемуары", value: "532" }, - { label: "Метафизика", value: "430" }, - { label: "Милитари", value: "126" }, - { label: "Мистика", value: "22" }, - { label: "Морские приключения", value: "211" }, - { label: "Мотивация", value: "135" }, - { label: "Научная фантастика", value: "122" }, - { label: "Научно-популярная", value: "43" }, - { label: "Ненаучная фантастика", value: "387" }, - { label: "Новелла", value: "498" }, - { label: "Нон-фикшн", value: "507" }, - { label: "Нуар", value: "121" }, - { label: "О войне", value: "136" }, - { label: "Оккультизм", value: "424" }, - { label: "Параллельные миры", value: "438" }, - { label: "Пародия", value: "108" }, - { label: "Подростковая фантастика", value: "27" }, - { label: "Подростковый роман", value: "388" }, - { label: "Подростковый ужастик", value: "110" }, - { label: "Постапокалипсис", value: "82" }, - { label: "Постапокалиптика", value: "67" }, - { label: "Поэзия", value: "44" }, - { label: "Превращение", value: "170" }, - { label: "Приключение", value: "497" }, - { label: "Приключения", value: "5" }, - { label: "Природа и животные", value: "538" }, - { label: "Проза", value: "95" }, - { label: "Производственный роман", value: "148" }, - { label: "Психология", value: "31" }, - { label: "Путешествия", value: "383" }, - { label: "Рассказы", value: "101" }, - { label: "Реализм", value: "422" }, - { label: "РеалРПГ", value: "442" }, - { label: "Религия", value: "188" }, - { label: "Робинзонада", value: "434" }, - { label: "Романтика", value: "130" }, - { label: "Романтическая фантастика", value: "501" }, - { label: "Русская классика", value: "523" }, - { label: "Рыцарский роман", value: "112" }, - { label: "Символизм", value: "439" }, - { label: "Сказки", value: "93" }, - { label: "Славянское фэнтези", value: "508" }, - { label: "Сновидения", value: "428" }, - { label: "Современный любовный роман", value: "519" }, - { label: "Социально-психологическая фантастика", value: "98" }, - { label: "Спорт", value: "147" }, - { label: "Справочники", value: "528" }, - { label: "Стимпанк", value: "56" }, - { label: "Субкультура", value: "209" }, - { label: "Сюрреализм", value: "26" }, - { label: "Тёмное фэнтези", value: "437" }, - { label: "Техномагия", value: "159" }, - { label: "Триллер", value: "6" }, - { label: "Трэш", value: "162" }, - { label: "Ужасы", value: "531" }, - { label: "Утопия", value: "191" }, - { label: "Учебник", value: "411" }, - { label: "Фантастика", value: "1" }, - { label: "Фантастический боевик", value: "38" }, - { label: "Фанфик", value: "111" }, - { label: "Философия", value: "461" }, - { label: "Фурри", value: "394" }, - { label: "Фэнтези", value: "2" }, - { label: "Хентай", value: "441" }, - { label: "Хобби и досуг", value: "530" }, - { label: "Хоррор", value: "86" }, - { label: "Чёрный юмор", value: "73" }, - { label: "Эзотерика", value: "30" }, - { label: "Экшн", value: "94" }, - { label: "Эпическое фэнтези", value: "499" }, - { label: "Эротика", value: "19" }, - { label: "Эссе", value: "353" }, - { label: "ЭТТИ", value: "502" }, - { label: "Юмористическая литература", value: "20" }, - { label: "Юмористическая фантастика", value: "514" }, - { label: "Юмористическое фэнтези", value: "521" }, - { label: "EVE - Миры Содружества", value: "99" }, + { label: 'Все', value: '' }, + { label: '16+', value: '204' }, + { label: '18+', value: '100' }, + { label: 'Авантюра', value: '347' }, + { label: 'Альтернативная история', value: '55' }, + { label: 'Ангст', value: '463' }, + { label: 'Аниме', value: '405' }, + { label: 'Антиутопия', value: '21' }, + { label: 'Апокалипсис', value: '368' }, + { label: 'Аудиокнига', value: '391' }, + { label: 'Байки', value: '435' }, + { label: 'Бизнес-книги', value: '522' }, + { label: 'Биография', value: '52' }, + { label: 'Биопанк', value: '379' }, + { label: 'Боевая фантастика', value: '512' }, + { label: 'Боевик', value: '61' }, + { label: 'Боевые искусства', value: '142' }, + { label: 'Бояръ-аниме', value: '433' }, + { label: 'Вбоквелл', value: '372' }, + { label: 'Вестерн', value: '96' }, + { label: 'Война миров', value: '137' }, + { label: 'Восточное фэнтези', value: '505' }, + { label: 'Городское фентези', value: '63' }, + { label: 'Городское фэнтези', value: '520' }, + { label: 'Готика', value: '83' }, + { label: 'Детективы', value: '4' }, + { label: 'Детские книги', value: '8' }, + { label: 'Документальные книги', value: '351' }, + { label: 'Дом и дача', value: '534' }, + { label: 'Дорожные истории', value: '41' }, + { label: 'Драма', value: '47' }, + { label: 'Древняя литература', value: '529' }, + { label: 'Женский психологический роман', value: '186' }, + { label: 'Женский роман', value: '141' }, + { label: 'Зарубежная классика', value: '524' }, + { label: 'Здоровье и красота', value: '533' }, + { label: 'Изучение языков', value: '537' }, + { label: 'Иронический детектив', value: '85' }, + { label: 'Исследования', value: '156' }, + { label: 'Историческая литература', value: '3' }, + { label: 'Исторический любовный роман', value: '518' }, + { label: 'Историческое фэнтези', value: '90' }, + { label: 'Киберпанк', value: '7' }, + { label: 'Классическая литература', value: '9' }, + { label: 'Книга-игра', value: '371' }, + { label: 'Книги для родителей', value: '535' }, + { label: 'Комедия', value: '80' }, + { label: 'Конкурсные сборники', value: '349' }, + { label: 'Короткий любовный роман', value: '517' }, + { label: 'Космическая фантастика', value: '513' }, + { label: 'Космоопера', value: '58' }, + { label: 'Криминальная проза', value: '35' }, + { label: 'Криптоистория', value: '400' }, + { label: 'Ксенофантастика', value: '102' }, + { label: 'Культура и искусство', value: '536' }, + { label: 'Легенды', value: '161' }, + { label: 'Лирика', value: '49' }, + { label: 'ЛитРПГ', value: '62' }, + { label: 'Любовная фантастика', value: '516' }, + { label: 'Любовное фэнтези', value: '515' }, + { label: 'Любовный роман', value: '46' }, + { label: 'Магический реализм', value: '42' }, + { label: 'Матриархат', value: '385' }, + { label: 'Медицина', value: '57' }, + { label: 'Мелодрама', value: '45' }, + { label: 'Мемуары', value: '532' }, + { label: 'Метафизика', value: '430' }, + { label: 'Милитари', value: '126' }, + { label: 'Мистика', value: '22' }, + { label: 'Морские приключения', value: '211' }, + { label: 'Мотивация', value: '135' }, + { label: 'Научная фантастика', value: '122' }, + { label: 'Научно-популярная', value: '43' }, + { label: 'Ненаучная фантастика', value: '387' }, + { label: 'Новелла', value: '498' }, + { label: 'Нон-фикшн', value: '507' }, + { label: 'Нуар', value: '121' }, + { label: 'О войне', value: '136' }, + { label: 'Оккультизм', value: '424' }, + { label: 'Параллельные миры', value: '438' }, + { label: 'Пародия', value: '108' }, + { label: 'Подростковая фантастика', value: '27' }, + { label: 'Подростковый роман', value: '388' }, + { label: 'Подростковый ужастик', value: '110' }, + { label: 'Постапокалипсис', value: '82' }, + { label: 'Постапокалиптика', value: '67' }, + { label: 'Поэзия', value: '44' }, + { label: 'Превращение', value: '170' }, + { label: 'Приключение', value: '497' }, + { label: 'Приключения', value: '5' }, + { label: 'Природа и животные', value: '538' }, + { label: 'Проза', value: '95' }, + { label: 'Производственный роман', value: '148' }, + { label: 'Психология', value: '31' }, + { label: 'Путешествия', value: '383' }, + { label: 'Рассказы', value: '101' }, + { label: 'Реализм', value: '422' }, + { label: 'РеалРПГ', value: '442' }, + { label: 'Религия', value: '188' }, + { label: 'Робинзонада', value: '434' }, + { label: 'Романтика', value: '130' }, + { label: 'Романтическая фантастика', value: '501' }, + { label: 'Русская классика', value: '523' }, + { label: 'Рыцарский роман', value: '112' }, + { label: 'Символизм', value: '439' }, + { label: 'Сказки', value: '93' }, + { label: 'Славянское фэнтези', value: '508' }, + { label: 'Сновидения', value: '428' }, + { label: 'Современный любовный роман', value: '519' }, + { label: 'Социально-психологическая фантастика', value: '98' }, + { label: 'Спорт', value: '147' }, + { label: 'Справочники', value: '528' }, + { label: 'Стимпанк', value: '56' }, + { label: 'Субкультура', value: '209' }, + { label: 'Сюрреализм', value: '26' }, + { label: 'Тёмное фэнтези', value: '437' }, + { label: 'Техномагия', value: '159' }, + { label: 'Триллер', value: '6' }, + { label: 'Трэш', value: '162' }, + { label: 'Ужасы', value: '531' }, + { label: 'Утопия', value: '191' }, + { label: 'Учебник', value: '411' }, + { label: 'Фантастика', value: '1' }, + { label: 'Фантастический боевик', value: '38' }, + { label: 'Фанфик', value: '111' }, + { label: 'Философия', value: '461' }, + { label: 'Фурри', value: '394' }, + { label: 'Фэнтези', value: '2' }, + { label: 'Хентай', value: '441' }, + { label: 'Хобби и досуг', value: '530' }, + { label: 'Хоррор', value: '86' }, + { label: 'Чёрный юмор', value: '73' }, + { label: 'Эзотерика', value: '30' }, + { label: 'Экшн', value: '94' }, + { label: 'Эпическое фэнтези', value: '499' }, + { label: 'Эротика', value: '19' }, + { label: 'Эссе', value: '353' }, + { label: 'ЭТТИ', value: '502' }, + { label: 'Юмористическая литература', value: '20' }, + { label: 'Юмористическая фантастика', value: '514' }, + { label: 'Юмористическое фэнтези', value: '521' }, + { label: 'EVE - Миры Содружества', value: '99' }, ], type: FilterTypes.Picker, }, @@ -359,26 +362,26 @@ class Zelluloza implements Plugin.PluginBase { } const alphabet: { [key: string]: string } = { - "~": "0", - "H": "1", - "^": "2", - "@": "3", - "f": "4", - "0": "5", - "5": "6", - "n": "7", - "r": "8", - "=": "9", - "W": "a", - "L": "b", - "7": "c", - " ": "d", - "u": "e", - "c": "f", + '~': '0', + 'H': '1', + '^': '2', + '@': '3', + 'f': '4', + '0': '5', + '5': '6', + 'n': '7', + 'r': '8', + '=': '9', + 'W': 'a', + 'L': 'b', + '7': 'c', + ' ': 'd', + 'u': 'e', + 'c': 'f', }; function decrypt(encrypt: string) { - if (!encrypt) return ""; + if (!encrypt) return ''; const hexArray = []; for (let j = 0; j < encrypt.length; j += 2) { @@ -387,12 +390,12 @@ function decrypt(encrypt: string) { hexArray.push(alphabet[firstChar] + alphabet[secondChar]); } - return "

" + hexToUtf8(hexArray) + "

"; + return '

' + hexToUtf8(hexArray) + '

'; } function hexToUtf8(hexArray: string[]) { let index = 0; - let result = ""; + let result = ''; while (index < hexArray.length) { const currentHex = parseInt(hexArray[index], 16) & 0xff; @@ -401,13 +404,13 @@ function hexToUtf8(hexArray: string[]) { if (currentHex < 16) { switch (currentHex) { case 9: - result += " "; + result += ' '; break; case 13: - result += "\r"; + result += '\r'; break; case 10: - result += "\n"; + result += '\n'; break; } } else { diff --git a/plugins/spanish/hasutl.broken.ts b/plugins/spanish/hasutl.broken.ts index 6d77bc4..ff385c6 100644 --- a/plugins/spanish/hasutl.broken.ts +++ b/plugins/spanish/hasutl.broken.ts @@ -1,145 +1,150 @@ -import { load as parseHTML } from "cheerio"; -import { fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { load as parseHTML } from 'cheerio'; +import { fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; class HasulTL implements Plugin.PluginBase { - id = "HasuTL"; - name = "Hasu Translations"; - icon = "src/es/hasutl/icon.png"; - site = "https://hasutl.wordpress.com/"; - filters?: Filters | undefined; - version = "1.0.0"; - baseUrl = this.site - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - let url = this.baseUrl + "light-novels-activas/"; + id = 'HasuTL'; + name = 'Hasu Translations'; + icon = 'src/es/hasutl/icon.png'; + site = 'https://hasutl.wordpress.com/'; + filters?: Filters | undefined; + version = '1.0.0'; + baseUrl = this.site; + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.baseUrl + 'light-novels-activas/'; - const result = await fetch(url); - const body = await result.text(); + const result = await fetch(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; + let novels: Plugin.NovelItem[] = []; - loadedCheerio("div.wp-block-columns").each(function () { - const novelName = loadedCheerio(this).find(".wp-block-button").text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); + loadedCheerio('div.wp-block-columns').each(function () { + const novelName = loadedCheerio(this).find('.wp-block-button').text(); + const novelCover = loadedCheerio(this).find('img').attr('src'); - let novelUrl = loadedCheerio(this) - .find(".wp-block-button > a") - .attr("href"); + let novelUrl = loadedCheerio(this) + .find('.wp-block-button > a') + .attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - novels.push(novel); - }); + novels.push(novel); + }); - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; + return novels; + } + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; - const result = await fetch(url); - const body = await result.text(); + const result = await fetch(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novel: Plugin.SourceNovel = { - url, + let novel: Plugin.SourceNovel = { + url, + }; + + novel.url = novelUrl; + + novel.name = loadedCheerio('.post-header').text(); + + novel.cover = loadedCheerio('.featured-media > img').attr('src'); + + let novelSummary = loadedCheerio('.post-content').find('p').html()!; + novel.summary = novelSummary; + + let novelChapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.wp-block-media-text__content') + .find('a') + .each(function () { + const chapterName = loadedCheerio(this).text().trim(); + + const releaseDate = null; + + let chapterUrl = loadedCheerio(this).attr('href'); + + if (!chapterUrl) return; + + const chapter = { + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, }; - novel.url = novelUrl; + novelChapters.push(chapter); + }); - novel.name = loadedCheerio(".post-header").text(); + novel.chapters = novelChapters; - novel.cover = loadedCheerio(".featured-media > img").attr("src"); + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const url = chapterUrl; - let novelSummary = loadedCheerio(".post-content").find("p").html()!; - novel.summary = novelSummary; + const result = await fetch(url); + const body = await result.text(); - let novelChapters: Plugin.ChapterItem[] = []; + let loadedCheerio = parseHTML(body); - loadedCheerio(".wp-block-media-text__content") - .find("a") - .each(function () { - const chapterName = loadedCheerio(this).text().trim(); + let chapterText = loadedCheerio('.post-content').html() || ''; - const releaseDate = null; + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.baseUrl}?s=${searchTerm}&post_type=wp-manga`; - let chapterUrl = loadedCheerio(this).attr("href"); + const result = await fetch(url); + const body = await result.text(); - if (!chapterUrl) return; + let loadedCheerio = parseHTML(body); - const chapter = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; + let novels: Plugin.NovelItem[] = []; - novelChapters.push(chapter); - }); + loadedCheerio('.post-container').each(function () { + const novelName = loadedCheerio(this).find('.post-header').text(); + if ( + !novelName.includes('Cap') && + !novelName.includes('Vol') && + !novelName.includes('Light Novels') + ) { + const novelCover = loadedCheerio(this).find('img').attr('src'); - novel.chapters = novelChapters; + let novelUrl = loadedCheerio(this).find('a').attr('href'); - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; + if (!novelUrl) return; - const result = await fetch(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - let chapterText = loadedCheerio(".post-content").html() || ''; - - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}?s=${searchTerm}&post_type=wp-manga`; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - const result = await fetch(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".post-container").each(function () { - const novelName = loadedCheerio(this).find(".post-header").text(); - if ( - !novelName.includes("Cap") && - !novelName.includes("Vol") && - !novelName.includes("Light Novels") - ) { - const novelCover = loadedCheerio(this).find("img").attr("src"); - - let novelUrl = loadedCheerio(this).find("a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - } - }); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + novels.push(novel); + } + }); + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new HasulTL(); diff --git a/plugins/spanish/novelasligera.broken.ts b/plugins/spanish/novelasligera.broken.ts index a0720f8..132c5fb 100644 --- a/plugins/spanish/novelasligera.broken.ts +++ b/plugins/spanish/novelasligera.broken.ts @@ -1,180 +1,183 @@ -import { load as parseHTML } from "cheerio"; -import { fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { load as parseHTML } from 'cheerio'; +import { fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; class Ligera implements Plugin.PluginBase { - id = "ligera.com"; - name = "Novelas Ligera"; - icon = "src/es/novelasligera/icon.png"; - site = "https://novelasligera.com/"; - filters?: Filters | undefined; - version = "1.0.0"; - baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - let url = this.baseUrl; + id = 'ligera.com'; + name = 'Novelas Ligera'; + icon = 'src/es/novelasligera/icon.png'; + site = 'https://novelasligera.com/'; + filters?: Filters | undefined; + version = '1.0.0'; + baseUrl = this.site; + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.baseUrl; - const result = await fetch(url); - const body = await result.text(); + const result = await fetch(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; + let novels: Plugin.NovelItem[] = []; - loadedCheerio(".elementor-column").each(function () { - const novelName = loadedCheerio(this) - .find(".widget-image-caption.wp-caption-text") - .text(); - if (novelName) { - const novelCover = loadedCheerio(this) - .find("a > img") - .attr("data-lazy-src"); + loadedCheerio('.elementor-column').each(function () { + const novelName = loadedCheerio(this) + .find('.widget-image-caption.wp-caption-text') + .text(); + if (novelName) { + const novelCover = loadedCheerio(this) + .find('a > img') + .attr('data-lazy-src'); - let novelUrl = loadedCheerio(this).find("a").attr("href"); - if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; + let novelUrl = loadedCheerio(this).find('a').attr('href'); + if (!novelUrl) return; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - novels.push(novel); - } + novels.push(novel); + } + }); + + return novels; + } + async parseNovelAndChapters(novelUrl: string): Promise { + const url = this.baseUrl + 'novela/' + novelUrl; + + // console.log(url); + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + let novel: Plugin.SourceNovel = { url }; + + novel.name = loadedCheerio('h1').text(); + + novel.cover = loadedCheerio('.elementor-widget-container') + .find('img') + .attr('src'); + + loadedCheerio('.elementor-row') + .find('p') + .each(function () { + if (loadedCheerio(this).text().includes('Autor:')) { + novel.author = loadedCheerio(this) + .text() + .replace('Autor:', '') + .trim(); + } + if (loadedCheerio(this).text().includes('Estado:')) { + novel.status = loadedCheerio(this) + .text() + .replace('Estado: ', '') + .trim(); + } + + if (loadedCheerio(this).text().includes('Género:')) { + loadedCheerio(this).find('span').remove(); + novel.genres = loadedCheerio(this).text().replace(/,\s/g, ','); + } + }); + + novel.summary = loadedCheerio( + '.elementor-text-editor.elementor-clearfix', + ).text(); + + let novelChapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.elementor-accordion-item').remove(); + + loadedCheerio('.elementor-tab-content') + .find('li') + .each(function () { + const chapterName = loadedCheerio(this).text(); + const releaseDate = null; + const chapterUrl = loadedCheerio(this).find('a').attr('href'); + if (!chapterUrl) return; + const chapter = { + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, + }; + + novelChapters.push(chapter); + }); + + novel.chapters = novelChapters; + + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const url = chapterUrl; + // console.log(url); + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + loadedCheerio('.osny-nightmode.osny-nightmode--left').remove(); + loadedCheerio('.code-block.code-block-1').remove(); + loadedCheerio('.adsb30').remove(); + loadedCheerio('.saboxplugin-wrap').remove(); + loadedCheerio('.wp-post-navigation').remove(); + + let chapterText = loadedCheerio('.entry-content').html() || ''; + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = this.baseUrl + '?s=' + searchTerm + '&post_type=wp-manga'; + // console.log(url); + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + let novels: Plugin.NovelItem[] = []; + + loadedCheerio('.inside-article').each(function () { + const novelCover = loadedCheerio(this).find('img').attr('src'); + let novelUrl = loadedCheerio(this).find('a').attr('href'); + + let novelName; + + if (novelUrl) { + novelName = novelUrl.replace(/-/g, ' ').replace(/^./, function (x) { + return x.toUpperCase(); }); + } - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = this.baseUrl + "novela/" + novelUrl; + novelUrl += '/'; - // console.log(url); + if (!novelName || !novelUrl) return; - const result = await fetch(url); - const body = await result.text(); + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - let loadedCheerio = parseHTML(body); + novels.push(novel); + }); - let novel: Plugin.SourceNovel = { url }; + novels = [{ ...novels[1] }]; - novel.name = loadedCheerio("h1").text(); - - novel.cover = loadedCheerio(".elementor-widget-container") - .find("img") - .attr("src"); - - loadedCheerio(".elementor-row") - .find("p") - .each(function () { - if (loadedCheerio(this).text().includes("Autor:")) { - novel.author = loadedCheerio(this) - .text() - .replace("Autor:", "") - .trim(); - } - if (loadedCheerio(this).text().includes("Estado:")) { - novel.status = loadedCheerio(this) - .text() - .replace("Estado: ", "") - .trim(); - } - - if (loadedCheerio(this).text().includes("Género:")) { - loadedCheerio(this).find("span").remove(); - novel.genres = loadedCheerio(this) - .text() - .replace(/,\s/g, ","); - } - }); - - novel.summary = loadedCheerio( - ".elementor-text-editor.elementor-clearfix" - ).text(); - - let novelChapters: Plugin.ChapterItem[] = []; - - loadedCheerio(".elementor-accordion-item").remove(); - - loadedCheerio(".elementor-tab-content") - .find("li") - .each(function () { - const chapterName = loadedCheerio(this).text(); - const releaseDate = null; - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - if (!chapterUrl) return; - const chapter = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; - - novelChapters.push(chapter); - }); - - novel.chapters = novelChapters; - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; - // console.log(url); - - const result = await fetch(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - loadedCheerio(".osny-nightmode.osny-nightmode--left").remove(); - loadedCheerio(".code-block.code-block-1").remove(); - loadedCheerio(".adsb30").remove(); - loadedCheerio(".saboxplugin-wrap").remove(); - loadedCheerio(".wp-post-navigation").remove(); - - let chapterText = loadedCheerio(".entry-content").html() || ''; - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = this.baseUrl + "?s=" + searchTerm + "&post_type=wp-manga"; - // console.log(url); - - const result = await fetch(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".inside-article").each(function () { - const novelCover = loadedCheerio(this).find("img").attr("src"); - let novelUrl = loadedCheerio(this).find("a").attr("href"); - - let novelName; - - if (novelUrl) { - novelName = novelUrl.replace(/-/g, " ").replace(/^./, function (x) { - return x.toUpperCase(); - }); - } - - novelUrl += "/"; - - if (!novelName || !novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - novels = [{ ...novels[1] }]; - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } -export default new Ligera(); \ No newline at end of file +export default new Ligera(); diff --git a/plugins/spanish/novelawuxia.broken.ts b/plugins/spanish/novelawuxia.broken.ts index 128b785..0b97d68 100644 --- a/plugins/spanish/novelawuxia.broken.ts +++ b/plugins/spanish/novelawuxia.broken.ts @@ -1,203 +1,200 @@ -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { load as parseHTML } from "cheerio"; -import { defaultCover } from "@libs/defaultCover"; -import { Filters } from "@libs/filterInputs"; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { Filters } from '@libs/filterInputs'; class ReinoWuxia implements Plugin.PluginBase { - id = "awuxia.com"; - name = "ReinoWuxia"; - icon = "src/es/novelawuxia/icon.png"; - filters?: Filters | undefined; - version = "1.0.0"; - site = "http://www.reinowuxia.com/" - baseUrl = this.site; - getNovelName(y: string | undefined) { - return y?.replace(/-/g, " ").replace(/(?:^|\s)\S/g, (a) => a.toUpperCase()); - } - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - let url = this.baseUrl + "p/todas-las-novelas.html"; + id = 'awuxia.com'; + name = 'ReinoWuxia'; + icon = 'src/es/novelawuxia/icon.png'; + filters?: Filters | undefined; + version = '1.0.0'; + site = 'http://www.reinowuxia.com/'; + baseUrl = this.site; + getNovelName(y: string | undefined) { + return y?.replace(/-/g, ' ').replace(/(?:^|\s)\S/g, a => a.toUpperCase()); + } + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.baseUrl + 'p/todas-las-novelas.html'; - const result = await fetchApi(url, { - method: "GET", - }); - const body = await result.text(); + const result = await fetchApi(url, { + method: 'GET', + }); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; + let novels: Plugin.NovelItem[] = []; - loadedCheerio(".post-body.entry-content") - .find("a") - .each( (idx, ele) => { - let novelName = loadedCheerio(ele) - .attr("href") - ?.split("/") - .pop() - ?.replace(".html", ""); - novelName = this.getNovelName(novelName); - const novelCover = loadedCheerio(ele).find("img").attr("src"); + loadedCheerio('.post-body.entry-content') + .find('a') + .each((idx, ele) => { + let novelName = loadedCheerio(ele) + .attr('href') + ?.split('/') + .pop() + ?.replace('.html', ''); + novelName = this.getNovelName(novelName); + const novelCover = loadedCheerio(ele).find('img').attr('src'); - let novelUrl = loadedCheerio(ele).attr("href"); + let novelUrl = loadedCheerio(ele).attr('href'); - if (!novelName || !novelUrl) return; + if (!novelName || !novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - novels.push(novel); - }); + novels.push(novel); + }); - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; + return novels; + } + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; - const result = await fetchApi(url); - const body = await result.text(); + const result = await fetchApi(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novel: Plugin.SourceNovel = { url }; + let novel: Plugin.SourceNovel = { url }; - novel.name = loadedCheerio("h1.post-title").text().trim(); + novel.name = loadedCheerio('h1.post-title').text().trim(); - novel.cover = loadedCheerio("div.separator").find("a").attr("href"); + novel.cover = loadedCheerio('div.separator').find('a').attr('href'); - novel.status = ""; + novel.status = ''; - loadedCheerio("div > b").each(function () { - const detailName = loadedCheerio(this).text(); - let detail = loadedCheerio(this)[0].nextSibling; + loadedCheerio('div > b').each(function () { + const detailName = loadedCheerio(this).text(); + let detail = loadedCheerio(this)[0].nextSibling; - if (detailName && detail) { - const text = loadedCheerio(detail).text(); + if (detailName && detail) { + const text = loadedCheerio(detail).text(); - if (detailName.includes("Autor")) { - novel.author = text.replace("Autor:", ""); - } + if (detailName.includes('Autor')) { + novel.author = text.replace('Autor:', ''); + } - if (detailName.includes("Estatus")) { - novel.status = text.replace("Estatus: ", ""); - } - if (detailName.includes("Géneros:")) { - novel.genres = text - .replace("Géneros: ", "") - .replace(/,\s/g, ","); - } + if (detailName.includes('Estatus')) { + novel.status = text.replace('Estatus: ', ''); + } + if (detailName.includes('Géneros:')) { + novel.genres = text.replace('Géneros: ', '').replace(/,\s/g, ','); + } + } + }); + + let novelChapters: Plugin.ChapterItem[] = []; + + loadedCheerio('div').each(function () { + const detailName = loadedCheerio(this).text(); + if (detailName.includes('Sinopsis')) { + novel.summary = + loadedCheerio(this).next().text() !== '' + ? loadedCheerio(this).next().text().replace('Sinopsis', '').trim() + : loadedCheerio(this) + .next() + .next() + .text() + .replace('Sinopsis', '') + .trim(); + } + + if (detailName.includes('Lista de Capítulos')) { + loadedCheerio(this) + .find('a') + .each(function (res) { + const chapterName = loadedCheerio(this).text(); + let chapterUrl = loadedCheerio(this).attr('href'); + const releaseDate = null; + + if ( + chapterName && + chapterUrl && + !novelChapters.some(chap => chap.name === chapterName) + ) { + const chapter = { + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, + }; + + novelChapters.push(chapter); } - }); + }); + } + }); - let novelChapters: Plugin.ChapterItem[] = []; + novel.chapters = novelChapters; - loadedCheerio("div").each(function () { - const detailName = loadedCheerio(this).text(); - if (detailName.includes("Sinopsis")) { - novel.summary = - loadedCheerio(this).next().text() !== "" - ? loadedCheerio(this) - .next() - .text() - .replace("Sinopsis", "") - .trim() - : loadedCheerio(this) - .next() - .next() - .text() - .replace("Sinopsis", "") - .trim(); - } + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const url = chapterUrl; - if (detailName.includes("Lista de Capítulos")) { - loadedCheerio(this) - .find("a") - .each(function (res) { - const chapterName = loadedCheerio(this).text(); - let chapterUrl = loadedCheerio(this).attr("href"); - const releaseDate = null; + const result = await fetchApi(url); + const body = await result.text(); - if ( - chapterName && - chapterUrl && - !novelChapters.some( - (chap) => chap.name === chapterName - ) - ) { - const chapter = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; + let loadedCheerio = parseHTML(body); - novelChapters.push(chapter); - } - }); - } - }); + let chapterText = loadedCheerio('.post-body.entry-content').html() || ''; - novel.chapters = novelChapters; + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.baseUrl}search?q=${searchTerm}`; - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; + const result = await fetchApi(url); + const body = await result.text(); - const result = await fetchApi(url); - const body = await result.text(); + let loadedCheerio = parseHTML(body); - let loadedCheerio = parseHTML(body); + let novels: Plugin.NovelItem[] = []; - let chapterText = loadedCheerio(".post-body.entry-content").html() || ''; + loadedCheerio('.date-outer').each((idx, ele) => { + let novelName = loadedCheerio(ele) + .find('a') + .attr('href') + ?.split('/') + .pop() + ?.replace(/-capitulo(.*?).html/, ''); - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.baseUrl}search?q=${searchTerm}`; + const novelUrl = novelName + '.html/'; - const result = await fetchApi(url); - const body = await result.text(); + novelName = this.getNovelName(novelName); - let loadedCheerio = parseHTML(body); + const exists = novels.some(novel => novel.name === novelName); - let novels: Plugin.NovelItem[] = []; + if (!exists) { + const novelCover = defaultCover; - loadedCheerio(".date-outer").each((idx, ele) => { - let novelName = loadedCheerio(ele) - .find("a") - .attr("href") - ?.split("/") - .pop() - ?.replace(/-capitulo(.*?).html/, ""); + if (!novelUrl || !novelName) return; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - const novelUrl = novelName + ".html/"; - - novelName = this.getNovelName(novelName); - - const exists = novels.some((novel) => novel.name === novelName); - - if (!exists) { - const novelCover = defaultCover; - - if (!novelUrl || !novelName) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - } - }); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + novels.push(novel); + } + }); + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } -export default new ReinoWuxia(); \ No newline at end of file +export default new ReinoWuxia(); diff --git a/plugins/spanish/oasistranslations.broken.ts b/plugins/spanish/oasistranslations.broken.ts index 34b2ac6..4be244c 100644 --- a/plugins/spanish/oasistranslations.broken.ts +++ b/plugins/spanish/oasistranslations.broken.ts @@ -1,171 +1,178 @@ -import { fetchFile, fetchApi } from "@libs/fetch"; -import { Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { load as parseHTML } from "cheerio"; +import { fetchFile, fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { load as parseHTML } from 'cheerio'; -export const id = "oasisTL.wp"; -export const name = "Oasis Translations"; -export const site = "https://oasistranslations.wordpress.com/"; -export const version = "1.0.0"; -export const icon = "src/es/oasistranslations/icon.png"; +export const id = 'oasisTL.wp'; +export const name = 'Oasis Translations'; +export const site = 'https://oasistranslations.wordpress.com/'; +export const version = '1.0.0'; +export const icon = 'src/es/oasistranslations/icon.png'; const baseUrl = site; class Oasis implements Plugin.PluginBase { - id = "oasisTL.wp"; - name = "Oasis Translations"; - site = "https://oasistranslations.wordpress.com/"; - version = "1.0.0"; - filters?: Filters | undefined; - icon = "src/es/oasistranslations/icon.png"; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - let url = baseUrl; + id = 'oasisTL.wp'; + name = 'Oasis Translations'; + site = 'https://oasistranslations.wordpress.com/'; + version = '1.0.0'; + filters?: Filters | undefined; + icon = 'src/es/oasistranslations/icon.png'; + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + let url = baseUrl; - const result = await fetchApi(url); - const body = await result.text(); + const result = await fetchApi(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; + let novels: Plugin.NovelItem[] = []; - loadedCheerio(".menu-item-1819") - .find(".sub-menu > li") - .each(function () { - const novelName = loadedCheerio(this).text(); - if (!novelName.match(/Activas|Finalizadas|Dropeadas/)) { - const novelCover = loadedCheerio(this).find("img").attr("src"); + loadedCheerio('.menu-item-1819') + .find('.sub-menu > li') + .each(function () { + const novelName = loadedCheerio(this).text(); + if (!novelName.match(/Activas|Finalizadas|Dropeadas/)) { + const novelCover = loadedCheerio(this).find('img').attr('src'); - let novelUrl = loadedCheerio(this).find("a").attr("href"); + let novelUrl = loadedCheerio(this).find('a').attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - novels.push(novel); - } - }); + novels.push(novel); + } + }); - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; + return novels; + } + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; - const result = await fetchApi(url); - const body = await result.text(); + const result = await fetchApi(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novel: Plugin.SourceNovel = { url }; + let novel: Plugin.SourceNovel = { url }; - novel.name = loadedCheerio("h1.entry-title") - .text() - .replace(/[\t\n]/g, "") - .trim(); + novel.name = loadedCheerio('h1.entry-title') + .text() + .replace(/[\t\n]/g, '') + .trim(); - novel.cover = loadedCheerio('img[loading="lazy"]').attr("src"); + novel.cover = loadedCheerio('img[loading="lazy"]').attr('src'); - loadedCheerio(".entry-content > p").each(function (res) { - if (loadedCheerio(this).text().includes("Autor")) { - const details = loadedCheerio(this).html()?.match(/<\/strong>(.|\n)*?
/g)?. - map((detail) => detail.replace(/|<\/strong>|
|:\s/g, "")); + loadedCheerio('.entry-content > p').each(function (res) { + if (loadedCheerio(this).text().includes('Autor')) { + const details = loadedCheerio(this) + .html() + ?.match(/<\/strong>(.|\n)*?
/g) + ?.map(detail => detail.replace(/|<\/strong>|
|:\s/g, '')); - novel.genres = ""; - if (details){ - novel.author = details[2]; - novel.genres = details[4].replace(/\s| /g, ""); - } - } - }); + novel.genres = ''; + if (details) { + novel.author = details[2]; + novel.genres = details[4].replace(/\s| /g, ''); + } + } + }); - // let novelSummary = $(this).next().html(); - novel.summary = ""; + // let novelSummary = $(this).next().html(); + novel.summary = ''; - let novelChapters: Plugin.ChapterItem[] = []; + let novelChapters: Plugin.ChapterItem[] = []; - // if ($(".entry-content").find("li").length) { - loadedCheerio(".entry-content") - .find("a") - .each(function () { - let chapterUrl = loadedCheerio(this).attr("href"); + // if ($(".entry-content").find("li").length) { + loadedCheerio('.entry-content') + .find('a') + .each(function () { + let chapterUrl = loadedCheerio(this).attr('href'); - if (chapterUrl && chapterUrl.includes(baseUrl)) { - const chapterName = loadedCheerio(this).text(); - const releaseDate = null; + if (chapterUrl && chapterUrl.includes(baseUrl)) { + const chapterName = loadedCheerio(this).text(); + const releaseDate = null; - const chapter = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; + const chapter = { + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, + }; - novelChapters.push(chapter); - } - }); + novelChapters.push(chapter); + } + }); - novel.chapters = novelChapters; + novel.chapters = novelChapters; - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const url = chapterUrl; - const result = await fetchApi(url); - const body = await result.text(); + const result = await fetchApi(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - loadedCheerio("div#jp-post-flair").remove(); + loadedCheerio('div#jp-post-flair').remove(); - let chapterText = loadedCheerio(".entry-content").html() || ''; + let chapterText = loadedCheerio('.entry-content').html() || ''; - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - searchTerm = searchTerm.toLowerCase(); + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + searchTerm = searchTerm.toLowerCase(); - let url = baseUrl; + let url = baseUrl; - const result = await fetchApi(url); - const body = await result.text(); + const result = await fetchApi(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; - loadedCheerio(".menu-item-1819") - .find(".sub-menu > li") - .each(function () { - const novelName = loadedCheerio(this).text(); - if (!novelName.match(/Activas|Finalizadas|Dropeadas/)) { - const novelCover = loadedCheerio(this).find("img").attr("src"); + let novels: Plugin.NovelItem[] = []; + loadedCheerio('.menu-item-1819') + .find('.sub-menu > li') + .each(function () { + const novelName = loadedCheerio(this).text(); + if (!novelName.match(/Activas|Finalizadas|Dropeadas/)) { + const novelCover = loadedCheerio(this).find('img').attr('src'); - let novelUrl = loadedCheerio(this).find("a").attr("href"); + let novelUrl = loadedCheerio(this).find('a').attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - novels.push(novel); - } - }); + novels.push(novel); + } + }); - novels = novels.filter((novel) => - novel.name.toLowerCase().includes(searchTerm) - ); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + novels = novels.filter(novel => + novel.name.toLowerCase().includes(searchTerm), + ); + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new Oasis(); diff --git a/plugins/spanish/skynovels.broken.ts b/plugins/spanish/skynovels.broken.ts index 5db2339..c952dce 100644 --- a/plugins/spanish/skynovels.broken.ts +++ b/plugins/spanish/skynovels.broken.ts @@ -1,93 +1,96 @@ -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; class SkyNovels implements Plugin.PluginBase { - id = "skynovels.net"; - name = "SkyNovels"; - site = "https://www.skynovels.net/"; - version = "1.0.0"; - icon = "src/es/skynovels/icon.png"; + id = 'skynovels.net'; + name = 'SkyNovels'; + site = 'https://www.skynovels.net/'; + version = '1.0.0'; + icon = 'src/es/skynovels/icon.png'; baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = "https://api.skynovels.net/api/novels?&q"; + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const url = 'https://api.skynovels.net/api/novels?&q'; const result = await fetchApi(url); - const body = await result.json() as response; + const body = (await result.json()) as response; const novels: Plugin.NovelItem[] = []; - body.novels?.forEach((res) => { - const novelName = res.nvl_title; - const novelCover = - "https://api.skynovels.net/api/get-image/" + - res.image + - "/novels/false"; - const novelUrl = - this.baseUrl + "novelas/" + res.id + "/" + res.nvl_name + "/"; + body.novels?.forEach(res => { + const novelName = res.nvl_title; + const novelCover = + 'https://api.skynovels.net/api/get-image/' + + res.image + + '/novels/false'; + const novelUrl = + this.baseUrl + 'novelas/' + res.id + '/' + res.nvl_name + '/'; - const novel = { name: novelName, url: novelUrl, cover: novelCover }; + const novel = { name: novelName, url: novelUrl, cover: novelCover }; - novels.push(novel); + novels.push(novel); }); return novels; } async parseNovelAndChapters(novelUrl: string): Promise { - const novelId = novelUrl.split("/")[4]; - const url = - "https://api.skynovels.net/api/novel/" + novelId + "/reading?&q"; + const novelId = novelUrl.split('/')[4]; + const url = + 'https://api.skynovels.net/api/novel/' + novelId + '/reading?&q'; - const result = await fetchApi(url); - const body = await result.json() as responseBook; + const result = await fetchApi(url); + const body = (await result.json()) as responseBook; - const item = body?.novel?.[0]; + const item = body?.novel?.[0]; - let novel: Plugin.SourceNovel = { url: novelUrl }; + let novel: Plugin.SourceNovel = { url: novelUrl }; - novel.name = item?.nvl_title; + novel.name = item?.nvl_title; - novel.cover = - "https://api.skynovels.net/api/get-image/" + - item?.image + - "/novels/false"; + novel.cover = + 'https://api.skynovels.net/api/get-image/' + + item?.image + + '/novels/false'; - let genres: string[] = []; - item?.genres?.forEach((genre) => genres.push(genre.genre_name)); - novel.genres = genres.join(","); - novel.author = item?.nvl_writer; - novel.summary = item?.nvl_content; - novel.status = item?.nvl_status; + let genres: string[] = []; + item?.genres?.forEach(genre => genres.push(genre.genre_name)); + novel.genres = genres.join(','); + novel.author = item?.nvl_writer; + novel.summary = item?.nvl_content; + novel.status = item?.nvl_status; - let novelChapters: Plugin.ChapterItem[] = []; + let novelChapters: Plugin.ChapterItem[] = []; - item?.volumes?.forEach((volume) => { - volume?.chapters?.forEach((chapter) => { - const chapterName = chapter.chp_index_title; - const releaseDate = new Date(chapter.createdAt).toDateString(); - const chapterUrl = novelUrl + chapter.id + "/" + chapter.chp_name; + item?.volumes?.forEach(volume => { + volume?.chapters?.forEach(chapter => { + const chapterName = chapter.chp_index_title; + const releaseDate = new Date(chapter.createdAt).toDateString(); + const chapterUrl = novelUrl + chapter.id + '/' + chapter.chp_name; - const chap = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; + const chap = { + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, + }; - novelChapters.push(chap); - }); - }); + novelChapters.push(chap); + }); + }); - novel.chapters = novelChapters; + novel.chapters = novelChapters; - return novel; + return novel; } async parseChapter(chapterUrl: string): Promise { - let chapterId: string = chapterUrl.split("/")[6]; + let chapterId: string = chapterUrl.split('/')[6]; const url = `https://api.skynovels.net/api/novel-chapter/${chapterId}`; const result = await fetchApi(url); - const body = await result.json() as responseChapter; + const body = (await result.json()) as responseChapter; const item = body?.chapter?.[0]; @@ -95,31 +98,34 @@ class SkyNovels implements Plugin.PluginBase { return chapterText; } - async searchNovels(searchTerm: string, pageNo: number): Promise { + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { searchTerm = searchTerm.toLowerCase(); - const url = "https://api.skynovels.net/api/novels?&q"; + const url = 'https://api.skynovels.net/api/novels?&q'; const result = await fetchApi(url); - const body = await result.json() as response;; + const body = (await result.json()) as response; - let results = body?.novels?.filter((novel) => - novel.nvl_title.toLowerCase().includes(searchTerm) + let results = body?.novels?.filter(novel => + novel.nvl_title.toLowerCase().includes(searchTerm), ); const novels: Plugin.NovelItem[] = []; - results?.forEach((res) => { - const novelName = res.nvl_title; - const novelCover = - "https://api.skynovels.net/api/get-image/" + - res.image + - "/novels/false"; - const novelUrl = - this.baseUrl + "novelas/" + res.id + "/" + res.nvl_name + "/"; + results?.forEach(res => { + const novelName = res.nvl_title; + const novelCover = + 'https://api.skynovels.net/api/get-image/' + + res.image + + '/novels/false'; + const novelUrl = + this.baseUrl + 'novelas/' + res.id + '/' + res.nvl_name + '/'; - const novel = { name: novelName, url: novelUrl, cover: novelCover }; + const novel = { name: novelName, url: novelUrl, cover: novelCover }; - novels.push(novel); + novels.push(novel); }); return novels; @@ -132,7 +138,7 @@ class SkyNovels implements Plugin.PluginBase { export default new SkyNovels(); interface response { - novels?: (NovelsEntity)[] | null; + novels?: NovelsEntity[] | null; } interface NovelsEntity { id: number; @@ -154,16 +160,15 @@ interface NovelsEntity { nvl_last_update: string; nvl_rating?: number | null; nvl_ratings_count: number; - genres?: (GenresEntity)[] | null; + genres?: GenresEntity[] | null; } interface GenresEntity { id: number; genre_name: string; } - interface responseBook { - novel?: (NovelEntity)[] | null; + novel?: NovelEntity[] | null; } interface NovelEntity { id: number; @@ -184,11 +189,11 @@ interface NovelEntity { nvl_chapters: number; nvl_last_update: string; nvl_rating: number; - bookmarks?: (BookmarksEntity)[] | null; - volumes?: (VolumesEntity)[] | null; - novel_ratings?: (NovelRatingsEntity)[] | null; - collaborators?: (CollaboratorsEntity)[] | null; - genres?: (GenresEntity)[] | null; + bookmarks?: BookmarksEntity[] | null; + volumes?: VolumesEntity[] | null; + novel_ratings?: NovelRatingsEntity[] | null; + collaborators?: CollaboratorsEntity[] | null; + genres?: GenresEntity[] | null; } interface BookmarksEntity { id: number; @@ -201,7 +206,7 @@ interface VolumesEntity { id: number; nvl_id: number; user_id?: number | null; - chapters?: (ChaptersEntity)[] | null; + chapters?: ChaptersEntity[] | null; } interface ChaptersEntity { id: number; @@ -233,9 +238,8 @@ interface CollaboratorsEntity { user_login: string; } - interface responseChapter { - chapter?: (ChapterEntity)[] | null; + chapter?: ChapterEntity[] | null; } interface ChapterEntity { id: number; @@ -256,9 +260,9 @@ interface ChapterEntity { nvl_name: string; user_login: string; reactions_count: number; - comments?: (null)[] | null; - reactions?: (null)[] | null; - total_reactions?: (TotalReactionsEntity)[] | null; + comments?: null[] | null; + reactions?: null[] | null; + total_reactions?: TotalReactionsEntity[] | null; } interface TotalReactionsEntity { reaction_id: number; diff --git a/plugins/spanish/tunovelaligera.ts b/plugins/spanish/tunovelaligera.ts index 5a6442f..b9d6be4 100644 --- a/plugins/spanish/tunovelaligera.ts +++ b/plugins/spanish/tunovelaligera.ts @@ -1,19 +1,19 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { defaultCover } from "@libs/defaultCover"; -import { NovelStatus } from "@libs/novelStatus"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; class TuNovelaLigera implements Plugin.PagePlugin { - id = "tunovelaligera"; - name = "TuNovelaLigera"; - icon = "src/es/tunovelaligera/icon.png"; - site = "https://tunovelaligera.com"; - version = "1.0.0"; + id = 'tunovelaligera'; + name = 'TuNovelaLigera'; + icon = 'src/es/tunovelaligera/icon.png'; + site = 'https://tunovelaligera.com'; + version = '1.0.0'; - async sleep (ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)) + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); } async popularNovels( pageNo: number, @@ -22,21 +22,23 @@ class TuNovelaLigera implements Plugin.PagePlugin { filters, }: Plugin.PopularNovelsOptions, ): Promise { - let link = this.site + let link = this.site; - link += filters?.genres?.value ? "/genero" + filters.genres.value : "/novelas"; + link += filters?.genres?.value + ? '/genero' + filters.genres.value + : '/novelas'; link += `/page/${pageNo}?m_orderby=`; - link += (showLatestNovels ? "latest" : filters?.order?.value || "rating"); + link += showLatestNovels ? 'latest' : filters?.order?.value || 'rating'; - const result = await fetchApi(link).then((res) => res.text()); + const result = await fetchApi(link).then(res => res.text()); const loadedCheerio = parseHTML(result); const novels: Plugin.NovelItem[] = []; - loadedCheerio(".page-item-detail").each((i, el) => { - const name = loadedCheerio(el).find(".h5 > a").text(); - const cover = loadedCheerio(el).find("img").attr("src"); - const url = loadedCheerio(el).find(".h5 > a").attr("href"); + loadedCheerio('.page-item-detail').each((i, el) => { + const name = loadedCheerio(el).find('.h5 > a').text(); + const cover = loadedCheerio(el).find('img').attr('src'); + const url = loadedCheerio(el).find('.h5 > a').attr('href'); if (!url) return; novels.push({ name, cover, path: url.replace(this.site, '') }); @@ -47,15 +49,15 @@ class TuNovelaLigera implements Plugin.PagePlugin { parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[] { const chapters: Plugin.ChapterItem[] = []; - loadedCheerio("#lcp_instance_0 li").each((i, el) => { + loadedCheerio('#lcp_instance_0 li').each((i, el) => { const chapterName = loadedCheerio(el) - .find("a") + .find('a') .text() - .replace(/[\t\n]/g, "") + .replace(/[\t\n]/g, '') .trim(); - const chapterUrl = loadedCheerio(el).find("a").attr("href"); - if(!chapterUrl) return; + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + if (!chapterUrl) return; chapters.push({ name: chapterName, path: chapterUrl.replace(this.site, ''), @@ -63,59 +65,61 @@ class TuNovelaLigera implements Plugin.PagePlugin { }); return chapters; } - - async parseNovel(novelPath: string): Promise { + + async parseNovel( + novelPath: string, + ): Promise { const novelUrl = this.site + novelPath; const result = await fetchApi(novelUrl); const body = await result.text(); let loadedCheerio = parseHTML(body); let lastPage = 1; - loadedCheerio('ul.lcp_paginator > li > a').each(function() { - const page = Number(this.attribs['title']); - if(page && page > lastPage) lastPage = page; + loadedCheerio('ul.lcp_paginator > li > a').each(function () { + const page = Number(this.attribs['title']); + if (page && page > lastPage) lastPage = page; }); - const novel: Plugin.SourceNovel & { totalPages: number; } = { + const novel: Plugin.SourceNovel & { totalPages: number } = { path: novelPath, chapters: [], totalPages: lastPage, - name: loadedCheerio(".post-title > h1").text().trim() + name: loadedCheerio('.post-title > h1').text().trim(), }; - loadedCheerio(".manga-title-badges").remove(); + loadedCheerio('.manga-title-badges').remove(); - let novelCover = loadedCheerio(".summary_image > a > img"); + let novelCover = loadedCheerio('.summary_image > a > img'); novel.cover = - novelCover.attr("data-src") || - novelCover.attr("src") || - novelCover.attr("data-cfsrc") || + novelCover.attr('data-src') || + novelCover.attr('src') || + novelCover.attr('data-cfsrc') || defaultCover; - loadedCheerio(".post-content_item").each(function () { + loadedCheerio('.post-content_item').each(function () { const detailName = loadedCheerio(this) - .find(".summary-heading > h5") + .find('.summary-heading > h5') .text() .trim(); - const detail = loadedCheerio(this).find(".summary-content").text().trim(); + const detail = loadedCheerio(this).find('.summary-content').text().trim(); switch (detailName) { - case "Género(s)": - novel.genres = detail.replace(/, /g, ","); + case 'Género(s)': + novel.genres = detail.replace(/, /g, ','); break; - case "Autor(es)": + case 'Autor(es)': novel.author = detail; break; - case "Estado": + case 'Estado': novel.status = - detail.includes("OnGoing") || detail.includes("Updating") + detail.includes('OnGoing') || detail.includes('Updating') ? NovelStatus.Ongoing : NovelStatus.Completed; break; } }); - novel.summary = loadedCheerio("div.summary__content > p").text().trim(); + novel.summary = loadedCheerio('div.summary__content > p').text().trim(); novel.chapters = this.parseChapters(loadedCheerio); @@ -130,22 +134,27 @@ class TuNovelaLigera implements Plugin.PagePlugin { let loadedCheerio = parseHTML(body); const latestChapterEle = loadedCheerio('#lcp_instance_0 li').first(); - const latestChapterUrl = loadedCheerio(latestChapterEle).find("a").attr("href"); - const latestChapterName = loadedCheerio(latestChapterEle).find("a") - .text() - .replace(/[\t\n]/g, "") - .trim(); - const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl ? { - path: latestChapterUrl.replace(this.site, ''), - name: latestChapterName - } : undefined + const latestChapterUrl = loadedCheerio(latestChapterEle) + .find('a') + .attr('href'); + const latestChapterName = loadedCheerio(latestChapterEle) + .find('a') + .text() + .replace(/[\t\n]/g, '') + .trim(); + const latestChapter: Plugin.ChapterItem | undefined = latestChapterUrl + ? { + path: latestChapterUrl.replace(this.site, ''), + name: latestChapterName, + } + : undefined; await this.sleep(1000); const pageText = await fetchApi(pageUrl).then(res => res.text()); const chapters = this.parseChapters(parseHTML(pageText)); return { chapters, - latestChapter - } + latestChapter, + }; } async parseChapter(chapterPath: string): Promise { const result = await fetchApi(this.site + chapterPath); @@ -153,14 +162,12 @@ class TuNovelaLigera implements Plugin.PagePlugin { const loadedCheerio = parseHTML(body); - const chapterText = loadedCheerio(".c-blog-post.post").html() || ""; + const chapterText = loadedCheerio('.c-blog-post.post').html() || ''; return chapterText; } - async searchNovels( - searchTerm: string, - ): Promise { + async searchNovels(searchTerm: string): Promise { const url = `${this.site}/?s=${searchTerm}&post_type=wp-manga`; const result = await fetchApi(url); @@ -170,10 +177,10 @@ class TuNovelaLigera implements Plugin.PagePlugin { const novels: Plugin.NovelItem[] = []; - loadedCheerio(".c-tabs-item__content").each((i, el) => { - const name = loadedCheerio(el).find(".h4 > a").text(); - const cover = loadedCheerio(el).find("img").attr("src"); - const url = loadedCheerio(el).find(".h4 > a").attr("href"); + loadedCheerio('.c-tabs-item__content').each((i, el) => { + const name = loadedCheerio(el).find('.h4 > a').text(); + const cover = loadedCheerio(el).find('img').attr('src'); + const url = loadedCheerio(el).find('.h4 > a').attr('href'); if (!url) return; novels.push({ name, cover, path: url.replace(this.site, '') }); @@ -188,70 +195,70 @@ class TuNovelaLigera implements Plugin.PagePlugin { filters = { order: { - value: "rating", - label: "Ordenado por", + value: 'rating', + label: 'Ordenado por', options: [ - { label: "Lo mas reciente", value: "latest" }, - { label: "A-Z", value: "alphabet" }, - { label: "Clasificación", value: "rating" }, - { label: "Trending", value: "trending" }, - { label: "Mas visto", value: "views" }, - { label: "Nuevo", value: "new-manga" }, + { label: 'Lo mas reciente', value: 'latest' }, + { label: 'A-Z', value: 'alphabet' }, + { label: 'Clasificación', value: 'rating' }, + { label: 'Trending', value: 'trending' }, + { label: 'Mas visto', value: 'views' }, + { label: 'Nuevo', value: 'new-manga' }, ], type: FilterTypes.Picker, }, genres: { - value: "", - label: "Generos", + value: '', + label: 'Generos', options: [ - { label: "None", value: "" }, - { label: "Acción", value: "accion" }, - { label: "Adulto", value: "adulto" }, - { label: "Artes Marciales", value: "artes-marciales" }, - { label: "Aventura", value: "aventura" }, - { label: "Ciencia Ficción", value: "ciencia-ficcion" }, - { label: "Comedia", value: "comedia" }, - { label: "Deportes", value: "deportes" }, - { label: "Drama", value: "drama" }, - { label: "Eastern Fantasy", value: "eastern-fantasy" }, - { label: "Ecchi", value: "ecchi" }, - { label: "FanFiction", value: "fan-fiction" }, - { label: "Fantasía", value: "fantasia" }, - { label: "Fantasía oriental", value: "fantasia-oriental" }, - { label: "Ficción Romántica", value: "ficcion-romantica" }, - { label: "Gender Bender", value: "gender-bender" }, - { label: "Harem", value: "harem" }, - { label: "Histórico", value: "historico" }, - { label: "Horror", value: "horror" }, - { label: "Josei", value: "josei" }, - { label: "Maduro", value: "maduro" }, - { label: "Mecha", value: "mecha" }, - { label: "Misterio", value: "misterio" }, - { label: "Novela China", value: "novela-china" }, - { label: "Novela FanFiction", value: "novela-fanfiction" }, - { label: "Novela Japonesa", value: "novela-japonesa" }, - { label: "Novela Koreana", value: "novela-koreana" }, - { label: "Novela ligera", value: "novela-ligera" }, - { label: "Novela original", value: "novela-original" }, - { label: "Novela Web", value: "web-novel" }, - { label: "Psicológico", value: "psicologico" }, - { label: "Realismo Mágico", value: "realismo-magico" }, - { label: "Recuento de vida", value: "recuento-de-vida" }, - { label: "Romance", value: "romance" }, - { label: "Romance contemporáneo", value: "romance-contemporaneo" }, - { label: "Romance Moderno", value: "romance-moderno" }, - { label: "Seinen", value: "seinen" }, - { label: "Shoujo", value: "shoujo" }, - { label: "Shounen", value: "shounen" }, - { label: "Sobrenatural", value: "sobrenatural" }, - { label: "Tragedia", value: "tragedia" }, - { label: "Vampiros", value: "vampiros" }, - { label: "Vida Escolar", value: "vida-escolar" }, - { label: "Western Fantasy", value: "western-fantasy" }, - { label: "Wuxia", value: "wuxia" }, - { label: "Xianxia", value: "xianxia" }, - { label: "Xuanhuan", value: "xuanhuan" }, - { label: "Yaoi", value: "yaoi" }, + { label: 'None', value: '' }, + { label: 'Acción', value: 'accion' }, + { label: 'Adulto', value: 'adulto' }, + { label: 'Artes Marciales', value: 'artes-marciales' }, + { label: 'Aventura', value: 'aventura' }, + { label: 'Ciencia Ficción', value: 'ciencia-ficcion' }, + { label: 'Comedia', value: 'comedia' }, + { label: 'Deportes', value: 'deportes' }, + { label: 'Drama', value: 'drama' }, + { label: 'Eastern Fantasy', value: 'eastern-fantasy' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'FanFiction', value: 'fan-fiction' }, + { label: 'Fantasía', value: 'fantasia' }, + { label: 'Fantasía oriental', value: 'fantasia-oriental' }, + { label: 'Ficción Romántica', value: 'ficcion-romantica' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Histórico', value: 'historico' }, + { label: 'Horror', value: 'horror' }, + { label: 'Josei', value: 'josei' }, + { label: 'Maduro', value: 'maduro' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Misterio', value: 'misterio' }, + { label: 'Novela China', value: 'novela-china' }, + { label: 'Novela FanFiction', value: 'novela-fanfiction' }, + { label: 'Novela Japonesa', value: 'novela-japonesa' }, + { label: 'Novela Koreana', value: 'novela-koreana' }, + { label: 'Novela ligera', value: 'novela-ligera' }, + { label: 'Novela original', value: 'novela-original' }, + { label: 'Novela Web', value: 'web-novel' }, + { label: 'Psicológico', value: 'psicologico' }, + { label: 'Realismo Mágico', value: 'realismo-magico' }, + { label: 'Recuento de vida', value: 'recuento-de-vida' }, + { label: 'Romance', value: 'romance' }, + { label: 'Romance contemporáneo', value: 'romance-contemporaneo' }, + { label: 'Romance Moderno', value: 'romance-moderno' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Sobrenatural', value: 'sobrenatural' }, + { label: 'Tragedia', value: 'tragedia' }, + { label: 'Vampiros', value: 'vampiros' }, + { label: 'Vida Escolar', value: 'vida-escolar' }, + { label: 'Western Fantasy', value: 'western-fantasy' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, ], type: FilterTypes.Picker, }, diff --git a/plugins/spanish/yukitls.broken.ts b/plugins/spanish/yukitls.broken.ts index 980b755..befb3ea 100644 --- a/plugins/spanish/yukitls.broken.ts +++ b/plugins/spanish/yukitls.broken.ts @@ -1,192 +1,197 @@ -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { load as parseHTML } from "cheerio"; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { load as parseHTML } from 'cheerio'; -export const id = "yuukitls.com"; -export const name = "Yuuki Tls"; -export const site = "https://yuukitls.com/"; -export const version = "1.0.0"; -export const icon = "src/es/yuukitls/icon.png"; +export const id = 'yuukitls.com'; +export const name = 'Yuuki Tls'; +export const site = 'https://yuukitls.com/'; +export const version = '1.0.0'; +export const icon = 'src/es/yuukitls/icon.png'; const baseUrl = site; class Yuuki implements Plugin.PluginBase { - id = "yuukitls.com"; - name = "Yuuki Tls"; - icon = "src/es/yuukitls/icon.png"; - site = "https://yuukitls.com/"; - filters?: Filters | undefined; - version = "1.0.0"; - baseUrl = this.site; + id = 'yuukitls.com'; + name = 'Yuuki Tls'; + icon = 'src/es/yuukitls/icon.png'; + site = 'https://yuukitls.com/'; + filters?: Filters | undefined; + version = '1.0.0'; + baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - let url = baseUrl; + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + let url = baseUrl; - const result = await fetchApi(url); - const body = await result.text(); + const result = await fetchApi(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; + let novels: Plugin.NovelItem[] = []; - loadedCheerio(".quadmenu-navbar-collapse ul li:nth-child(2)") - .find("li") - .each(function () { - const novelName = loadedCheerio(this) - .text() - .replace(/[\s\n]+/g, " "); - const novelCover = loadedCheerio(this).find("img").attr("src"); + loadedCheerio('.quadmenu-navbar-collapse ul li:nth-child(2)') + .find('li') + .each(function () { + const novelName = loadedCheerio(this) + .text() + .replace(/[\s\n]+/g, ' '); + const novelCover = loadedCheerio(this).find('img').attr('src'); - let novelUrl = loadedCheerio(this).find("a").attr("href"); - if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - - const result = await fetchApi(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - let novel: Plugin.SourceNovel = { - url, + let novelUrl = loadedCheerio(this).find('a').attr('href'); + if (!novelUrl) return; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, }; - novel.name = loadedCheerio("h1.entry-title") + novels.push(novel); + }); + + return novels; + } + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; + + const result = await fetchApi(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + let novel: Plugin.SourceNovel = { + url, + }; + + novel.name = loadedCheerio('h1.entry-title') + .text() + .replace(/[\t\n]/g, '') + .trim(); + + novel.cover = loadedCheerio('img[loading="lazy"]').attr('src'); + + loadedCheerio('.entry-content') + .find('div') + .each(function () { + if (loadedCheerio(this).text().includes('Escritor:')) { + novel.author = loadedCheerio(this) .text() - .replace(/[\t\n]/g, "") + .replace('Escritor: ', '') .trim(); - - novel.cover = loadedCheerio('img[loading="lazy"]').attr("src"); - - loadedCheerio(".entry-content") - .find("div") - .each(function () { - if (loadedCheerio(this).text().includes("Escritor:")) { - novel.author = loadedCheerio(this) - .text() - .replace("Escritor: ", "") - .trim(); - } - if (loadedCheerio(this).text().includes("Género:")) { - novel.genres = loadedCheerio(this) - .text() - .replace(/Género: |\s/g, ""); - } - - if (loadedCheerio(this).text().includes("Sinopsis:")) { - novel.summary = loadedCheerio(this).next().text(); - } - }); - - let novelChapters: Plugin.ChapterItem[] = []; - - if (loadedCheerio(".entry-content").find("li").length) { - loadedCheerio(".entry-content") - .find("li") - .each(function () { - let chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (chapterUrl && chapterUrl.includes(baseUrl)) { - const chapterName = loadedCheerio(this).text(); - const releaseDate = null; - - const chapter = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; - - novelChapters.push(chapter); - } - }); - } else { - loadedCheerio(".entry-content") - .find("p") - .each(function () { - let chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (chapterUrl && chapterUrl.includes(baseUrl)) { - const chapterName = loadedCheerio(this).text(); - const releaseDate = null; - - const chapter = { - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }; - - novelChapters.push(chapter); - } - }); + } + if (loadedCheerio(this).text().includes('Género:')) { + novel.genres = loadedCheerio(this) + .text() + .replace(/Género: |\s/g, ''); } - novel.chapters = novelChapters; + if (loadedCheerio(this).text().includes('Sinopsis:')) { + novel.summary = loadedCheerio(this).next().text(); + } + }); - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; + let novelChapters: Plugin.ChapterItem[] = []; - const result = await fetchApi(url); - const body = await result.text(); + if (loadedCheerio('.entry-content').find('li').length) { + loadedCheerio('.entry-content') + .find('li') + .each(function () { + let chapterUrl = loadedCheerio(this).find('a').attr('href'); - let loadedCheerio = parseHTML(body); - let chapterText = loadedCheerio(".entry-content").html() || ''; + if (chapterUrl && chapterUrl.includes(baseUrl)) { + const chapterName = loadedCheerio(this).text(); + const releaseDate = null; - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - searchTerm = searchTerm.toLowerCase(); + const chapter = { + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, + }; - let url = baseUrl; + novelChapters.push(chapter); + } + }); + } else { + loadedCheerio('.entry-content') + .find('p') + .each(function () { + let chapterUrl = loadedCheerio(this).find('a').attr('href'); - const result = await fetchApi(url); - const body = await result.text(); + if (chapterUrl && chapterUrl.includes(baseUrl)) { + const chapterName = loadedCheerio(this).text(); + const releaseDate = null; - let loadedCheerio = parseHTML(body); + const chapter = { + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, + }; - let novels: Plugin.NovelItem[] = []; - - loadedCheerio(".menu-item-2869") - .find(".menu-item.menu-item-type-post_type.menu-item-object-post") - .each(function () { - const novelName = loadedCheerio(this).text(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - - let novelUrl = loadedCheerio(this).find("a").attr("href"); - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - novels = novels.filter((novel) => - novel.name.toLowerCase().includes(searchTerm) - ); - - return novels; - } - fetchImage(url: string): Promise { - return fetchFile(url); + novelChapters.push(chapter); + } + }); } + novel.chapters = novelChapters; + + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const url = chapterUrl; + + const result = await fetchApi(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + let chapterText = loadedCheerio('.entry-content').html() || ''; + + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + searchTerm = searchTerm.toLowerCase(); + + let url = baseUrl; + + const result = await fetchApi(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + let novels: Plugin.NovelItem[] = []; + + loadedCheerio('.menu-item-2869') + .find('.menu-item.menu-item-type-post_type.menu-item-object-post') + .each(function () { + const novelName = loadedCheerio(this).text(); + const novelCover = loadedCheerio(this).find('img').attr('src'); + + let novelUrl = loadedCheerio(this).find('a').attr('href'); + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; + + novels.push(novel); + }); + + novels = novels.filter(novel => + novel.name.toLowerCase().includes(searchTerm), + ); + + return novels; + } + fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new Yuuki(); diff --git a/plugins/turkish/epiknovel.broken.ts b/plugins/turkish/epiknovel.broken.ts index 2569cdf..a143b43 100644 --- a/plugins/turkish/epiknovel.broken.ts +++ b/plugins/turkish/epiknovel.broken.ts @@ -1,169 +1,170 @@ -import { load as parseHTML } from "cheerio"; -import { fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; +import { load as parseHTML } from 'cheerio'; +import { fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; class EpikNovel implements Plugin.PluginBase { - id = "epiknovel.com"; - name = "EpikNovel"; - icon = "src/tr/epiknovel/icon.png"; - site = "https://www.epiknovel.com/"; - version = "1.0.0"; - baseUrl = this.site; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - let url = this.baseUrl + "seri-listesi?Sayfa=" + pageNo; + id = 'epiknovel.com'; + name = 'EpikNovel'; + icon = 'src/tr/epiknovel/icon.png'; + site = 'https://www.epiknovel.com/'; + version = '1.0.0'; + baseUrl = this.site; + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.baseUrl + 'seri-listesi?Sayfa=' + pageNo; - const result = await fetch(url); - const body = await result.text(); + const result = await fetch(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novels: Plugin.NovelItem[] = []; + let novels: Plugin.NovelItem[] = []; - loadedCheerio("div.col-lg-12.col-md-12").each(function () { - const novelName = loadedCheerio(this).find("h3").text(); - const novelCover = loadedCheerio(this).find("img").attr("data-src"); - const novelUrl = loadedCheerio(this).find("h3 > a").attr("href"); + loadedCheerio('div.col-lg-12.col-md-12').each(function () { + const novelName = loadedCheerio(this).find('h3').text(); + const novelCover = loadedCheerio(this).find('img').attr('data-src'); + const novelUrl = loadedCheerio(this).find('h3 > a').attr('href'); - if (!novelUrl) return; + if (!novelUrl) return; - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; - novels.push(novel); - }); + novels.push(novel); + }); - // console.log(novels); + // console.log(novels); - return novels; - } - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - // console.log(url); + return novels; + } + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; + // console.log(url); - const result = await fetch(url); - const body = await result.text(); + const result = await fetch(url); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - let novel: Plugin.SourceNovel = { url }; + let novel: Plugin.SourceNovel = { url }; - novel.name = loadedCheerio("h1#tables").text().trim(); + novel.name = loadedCheerio('h1#tables').text().trim(); - novel.cover = loadedCheerio("img.manga-cover").attr("src"); + novel.cover = loadedCheerio('img.manga-cover').attr('src'); - novel.summary = loadedCheerio( - "#wrapper > div.row > div.col-md-9 > div:nth-child(6) > p:nth-child(3)" - ) - .text() - .trim(); + novel.summary = loadedCheerio( + '#wrapper > div.row > div.col-md-9 > div:nth-child(6) > p:nth-child(3)', + ) + .text() + .trim(); - novel.status = loadedCheerio( - "#wrapper > div.row > div.col-md-9 > div.row > div.col-md-9 > h4:nth-child(3) > a" - ) - .text() - .trim(); + novel.status = loadedCheerio( + '#wrapper > div.row > div.col-md-9 > div.row > div.col-md-9 > h4:nth-child(3) > a', + ) + .text() + .trim(); - novel.author = loadedCheerio("#NovelInfo > p:nth-child(4)") - .text() - .replace(/Publisher:|\s/g, "") - .trim(); + novel.author = loadedCheerio('#NovelInfo > p:nth-child(4)') + .text() + .replace(/Publisher:|\s/g, '') + .trim(); - let novelChapters: Plugin.ChapterItem[] = []; + let novelChapters: Plugin.ChapterItem[] = []; - loadedCheerio("table").find("tr").first().remove(); + loadedCheerio('table').find('tr').first().remove(); - loadedCheerio("table") - .find("tr") - .each(function () { - const releaseDate = loadedCheerio(this) - .find("td:nth-child(3)") - .text(); + loadedCheerio('table') + .find('tr') + .each(function () { + const releaseDate = loadedCheerio(this).find('td:nth-child(3)').text(); - let chapterName = loadedCheerio(this) - .find("td:nth-child(1) > a") - .text(); + let chapterName = loadedCheerio(this) + .find('td:nth-child(1) > a') + .text(); - if ( - loadedCheerio(this).find("td:nth-child(1) > span").length > - 0 - ) { - chapterName = "🔒 " + chapterName; - } - - const chapterUrl = loadedCheerio(this) - .find(" td:nth-child(1) > a") - .attr("href"); - - if (!chapterUrl) return; - - novelChapters.push({ - name: chapterName, - url: chapterUrl, - releaseTime: releaseDate, - }); - }); - - novel.chapters = novelChapters; - // console.log(novel); - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const url = chapterUrl; - - // console.log(url); - - const result = await fetch(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - let chapterText = ''; - - if (result.url === "https://www.epiknovel.com/login") { - chapterText = "Premium Chapter"; - } else { - chapterText = loadedCheerio("div#icerik").html() || ''; + if (loadedCheerio(this).find('td:nth-child(1) > span').length > 0) { + chapterName = '🔒 ' + chapterName; } - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = this.baseUrl + "seri-listesi?q=" + searchTerm + '&Sayfa=' + pageNo; + const chapterUrl = loadedCheerio(this) + .find(' td:nth-child(1) > a') + .attr('href'); - const result = await fetch(url); - const body = await result.text(); + if (!chapterUrl) return; - let loadedCheerio = parseHTML(body); - - let novels: Plugin.NovelItem[] = []; - - loadedCheerio("div.col-lg-12.col-md-12").each(function () { - const novelName = loadedCheerio(this).find("h3").text(); - const novelCover = loadedCheerio(this).find("img").attr("data-src"); - const novelUrl = loadedCheerio(this).find("h3 > a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); + novelChapters.push({ + name: chapterName, + url: chapterUrl, + releaseTime: releaseDate, }); + }); - return novels; - } - fetchImage(url: string): Promise { - return fetchFile(url); + novel.chapters = novelChapters; + // console.log(novel); + + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const url = chapterUrl; + + // console.log(url); + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + let chapterText = ''; + + if (result.url === 'https://www.epiknovel.com/login') { + chapterText = 'Premium Chapter'; + } else { + chapterText = loadedCheerio('div#icerik').html() || ''; } + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = + this.baseUrl + 'seri-listesi?q=' + searchTerm + '&Sayfa=' + pageNo; + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + let novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.col-lg-12.col-md-12').each(function () { + const novelName = loadedCheerio(this).find('h3').text(); + const novelCover = loadedCheerio(this).find('img').attr('data-src'); + const novelUrl = loadedCheerio(this).find('h3 > a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + fetchImage(url: string): Promise { + return fetchFile(url); + } } -export default new EpikNovel(); \ No newline at end of file +export default new EpikNovel(); diff --git a/plugins/turkish/lshnovel.broken.ts b/plugins/turkish/lshnovel.broken.ts index 1bdad5f..a813f1a 100644 --- a/plugins/turkish/lshnovel.broken.ts +++ b/plugins/turkish/lshnovel.broken.ts @@ -1,310 +1,299 @@ -import { load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; - -class LSHNovel implements Plugin.PluginBase { - id = "lshnovel"; - name = "Liebe Schnee Hiver Novel"; - icon = "multisrc/wpmangastream/icons/lshnovel.png"; - site = "https://lshnovel.com/"; - version = "1.0.0"; - - async popularNovels( - pageNo: number, - { filters }: Plugin.PopularNovelsOptions - ): Promise { - let link = `${this.site}series/?page=${pageNo}`; - - if (filters.genres.value.length) { - link += filters.genres.value.map((i) => `&genre[]=${i}`).join(""); - } - - if (filters.type.value.length) - link += filters.type.value.map((i) => `&lang[]=${i}`).join(""); - - link += "&status=" + filters.status; - - link += "&order=" + filters.order; - - const headers = new Headers(); - const body = await fetchApi(link, { headers }).then((result) => - result.text() - ); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.bs").each(function () { - const novelName = loadedCheerio(this).find(".ntitle").text().trim(); - let image = loadedCheerio(this).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - - const novelUrl = loadedCheerio(this).find("a").attr("href"); - - if (!novelUrl) return; - - const novel = { - name: novelName, - cover: novelCover, - url: novelUrl, - }; - - novels.push(novel); - }); - - return novels; - } - - async parseNovelAndChapters(novelUrl: string): Promise { - const url = novelUrl; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - url, - chapters: [], - }; - - novel.name = loadedCheerio(".entry-title").text(); - - novel.cover = - loadedCheerio("img.wp-post-image").attr("data-src") || - loadedCheerio("img.wp-post-image").attr("src"); - - loadedCheerio("div.spe > span").each(function () { - const detailName = loadedCheerio(this).find("b").text().trim(); - const detail = loadedCheerio(this) - .find("b") - .remove() - .end() - .text() - .trim(); - - switch (detailName) { - case "Yazar:": - novel.author = detail; - break; - case "Seviye:": - novel.status = detail; - break; - } - }); - - novel.genres = loadedCheerio(".genxed") - .text() - .trim() - .replace(/\s/g, ","); - - loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove(); - novel.summary = loadedCheerio('div[itemprop="description"]') - .find("br") - .replaceWith("\n") - .end() - .text(); - - let chapter: Plugin.ChapterItem[] = []; - - loadedCheerio(".eplister") - .find("li") - .each(function () { - const chapterName = - loadedCheerio(this).find(".epl-num").text() + - " - " + - loadedCheerio(this).find(".epl-title").text(); - - const releaseDate = loadedCheerio(this) - .find(".epl-date") - .text() - .trim(); - - const chapterUrl = loadedCheerio(this).find("a").attr("href"); - - if (!chapterUrl) return; - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - url: chapterUrl, - }); - }); - - novel.chapters = chapter.reverse(); - - return novel; - } - async parseChapter(chapterUrl: string): Promise { - const headers = new Headers(); - const result = await fetchApi(chapterUrl, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - let chapterText = loadedCheerio(".epcontent").html() || ""; - - return chapterText; - } - - async searchNovels( - searchTerm: string, - pageNo?: number | undefined - ): Promise { - const url = `${this.site}?s=${searchTerm}`; - const headers = new Headers(); - const result = await fetchApi(url, { headers }); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio("article.bs").each(function () { - const novelName = loadedCheerio(this).find(".ntitle").text().trim(); - const novelCover = loadedCheerio(this).find("img").attr("src"); - const novelUrl = loadedCheerio(this).find("a").attr("href"); - if (!novelUrl) return; - - novels.push({ - name: novelName, - url: novelUrl, - cover: novelCover, - }); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return await fetchFile(url); - } - - filters = { - order: { - value: "popular", - label: "Önerilen", - options: [ - { label: "Varsayılan", value: "" }, - - { label: "A-Z", value: "title" }, - - { label: "Z-A", value: "titlereverse" }, - - { label: "Son Yüklemeler", value: "update" }, - - { label: "Son Eklenenler", value: "latest" }, - - { label: "Bestimiz", value: "popular" }, - ], - type: FilterTypes.Picker, - }, - status: { - label: "Statü", - value: "", - options: [ - { label: "Tümü", value: "" }, - - { label: "Ongoing", value: "ongoing" }, - - { label: "Hiatus", value: "hiatus" }, - - { label: "Completed", value: "completed" }, - ], - type: FilterTypes.Picker, - }, - type: { - value: [], - label: "Tür", - options: [ - { label: "Çeviri Novel", value: "ceviri-novel" }, - - { label: "Liz-Chan", value: "liz-chan" }, - - { label: "Manhwa", value: "manhwa" }, - - { label: "Orijinal Novel", value: "orijinal-novel" }, - - { label: "Web Novel", value: "web-novel" }, - ], - type: FilterTypes.CheckboxGroup, - }, - genres: { - value: [], - label: "Kategori", - options: [ - { label: "+18", value: "18" }, - - { label: "Action", value: "action" }, - - { label: "Adult", value: "adult" }, - - { label: "Aksiyon", value: "aksiyon" }, - - { label: "BL", value: "bl" }, - - { label: "Comedy", value: "comedy" }, - - { label: "Doğaüstü", value: "dogaustu" }, - - { label: "Dram", value: "dram" }, - - { label: "Drama", value: "drama" }, - - { label: "Ecchi", value: "ecchi" }, - - { label: "Fantastik", value: "fantastik" }, - - { label: "Fantasy", value: "fantasy" }, - - { label: "Gizem", value: "gizem" }, - - { label: "Harem", value: "harem" }, - - { label: "Historical", value: "historical" }, - - { label: "Josei", value: "josei" }, - - { label: "Macera", value: "macera" }, - - { label: "Manhwa", value: "manhwa" }, - - { label: "Martial Arts", value: "martial-arts" }, - - { label: "Mature", value: "mature" }, - - { label: "Novel", value: "novel" }, - - { label: "Okul", value: "okul" }, - - { label: "Psikolojik", value: "psikolojik" }, - - { label: "Psychological", value: "psychological" }, - - { label: "Reverse Harem", value: "reverse-harem" }, - - { label: "Romance", value: "romance" }, - - { label: "Romantik", value: "romantik" }, - - { label: "Shoujo", value: "shoujo" }, - - { label: "Slice Of Life", value: "slice-of-life" }, - - { label: "Smut", value: "smut" }, - - { label: "Supernatural", value: "supernatural" }, - - { label: "Tarihi", value: "tarihi" }, - - { label: "Tragedy", value: "tragedy" }, - - { label: "Yaoi", value: "yaoi" }, - ], - type: FilterTypes.CheckboxGroup, - }, - } satisfies Filters; -} - -export default new LSHNovel(); +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; + +class LSHNovel implements Plugin.PluginBase { + id = 'lshnovel'; + name = 'Liebe Schnee Hiver Novel'; + icon = 'multisrc/wpmangastream/icons/lshnovel.png'; + site = 'https://lshnovel.com/'; + version = '1.0.0'; + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}series/?page=${pageNo}`; + + if (filters.genres.value.length) { + link += filters.genres.value.map(i => `&genre[]=${i}`).join(''); + } + + if (filters.type.value.length) + link += filters.type.value.map(i => `&lang[]=${i}`).join(''); + + link += '&status=' + filters.status; + + link += '&order=' + filters.order; + + const headers = new Headers(); + const body = await fetchApi(link, { headers }).then(result => + result.text(), + ); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('article.bs').each(function () { + const novelName = loadedCheerio(this).find('.ntitle').text().trim(); + let image = loadedCheerio(this).find('img'); + const novelCover = image.attr('data-src') || image.attr('src'); + + const novelUrl = loadedCheerio(this).find('a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + url: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovelAndChapters(novelUrl: string): Promise { + const url = novelUrl; + const headers = new Headers(); + const result = await fetchApi(url, { headers }); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + url, + chapters: [], + }; + + novel.name = loadedCheerio('.entry-title').text(); + + novel.cover = + loadedCheerio('img.wp-post-image').attr('data-src') || + loadedCheerio('img.wp-post-image').attr('src'); + + loadedCheerio('div.spe > span').each(function () { + const detailName = loadedCheerio(this).find('b').text().trim(); + const detail = loadedCheerio(this).find('b').remove().end().text().trim(); + + switch (detailName) { + case 'Yazar:': + novel.author = detail; + break; + case 'Seviye:': + novel.status = detail; + break; + } + }); + + novel.genres = loadedCheerio('.genxed').text().trim().replace(/\s/g, ','); + + loadedCheerio('div[itemprop="description"] h3,p.a,strong').remove(); + novel.summary = loadedCheerio('div[itemprop="description"]') + .find('br') + .replaceWith('\n') + .end() + .text(); + + let chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('.eplister') + .find('li') + .each(function () { + const chapterName = + loadedCheerio(this).find('.epl-num').text() + + ' - ' + + loadedCheerio(this).find('.epl-title').text(); + + const releaseDate = loadedCheerio(this).find('.epl-date').text().trim(); + + const chapterUrl = loadedCheerio(this).find('a').attr('href'); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + releaseTime: releaseDate, + url: chapterUrl, + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const headers = new Headers(); + const result = await fetchApi(chapterUrl, { headers }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + let chapterText = loadedCheerio('.epcontent').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo?: number | undefined, + ): Promise { + const url = `${this.site}?s=${searchTerm}`; + const headers = new Headers(); + const result = await fetchApi(url, { headers }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('article.bs').each(function () { + const novelName = loadedCheerio(this).find('.ntitle').text().trim(); + const novelCover = loadedCheerio(this).find('img').attr('src'); + const novelUrl = loadedCheerio(this).find('a').attr('href'); + if (!novelUrl) return; + + novels.push({ + name: novelName, + url: novelUrl, + cover: novelCover, + }); + }); + + return novels; + } + async fetchImage(url: string): Promise { + return await fetchFile(url); + } + + filters = { + order: { + value: 'popular', + label: 'Önerilen', + options: [ + { label: 'Varsayılan', value: '' }, + + { label: 'A-Z', value: 'title' }, + + { label: 'Z-A', value: 'titlereverse' }, + + { label: 'Son Yüklemeler', value: 'update' }, + + { label: 'Son Eklenenler', value: 'latest' }, + + { label: 'Bestimiz', value: 'popular' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Statü', + value: '', + options: [ + { label: 'Tümü', value: '' }, + + { label: 'Ongoing', value: 'ongoing' }, + + { label: 'Hiatus', value: 'hiatus' }, + + { label: 'Completed', value: 'completed' }, + ], + type: FilterTypes.Picker, + }, + type: { + value: [], + label: 'Tür', + options: [ + { label: 'Çeviri Novel', value: 'ceviri-novel' }, + + { label: 'Liz-Chan', value: 'liz-chan' }, + + { label: 'Manhwa', value: 'manhwa' }, + + { label: 'Orijinal Novel', value: 'orijinal-novel' }, + + { label: 'Web Novel', value: 'web-novel' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genres: { + value: [], + label: 'Kategori', + options: [ + { label: '+18', value: '18' }, + + { label: 'Action', value: 'action' }, + + { label: 'Adult', value: 'adult' }, + + { label: 'Aksiyon', value: 'aksiyon' }, + + { label: 'BL', value: 'bl' }, + + { label: 'Comedy', value: 'comedy' }, + + { label: 'Doğaüstü', value: 'dogaustu' }, + + { label: 'Dram', value: 'dram' }, + + { label: 'Drama', value: 'drama' }, + + { label: 'Ecchi', value: 'ecchi' }, + + { label: 'Fantastik', value: 'fantastik' }, + + { label: 'Fantasy', value: 'fantasy' }, + + { label: 'Gizem', value: 'gizem' }, + + { label: 'Harem', value: 'harem' }, + + { label: 'Historical', value: 'historical' }, + + { label: 'Josei', value: 'josei' }, + + { label: 'Macera', value: 'macera' }, + + { label: 'Manhwa', value: 'manhwa' }, + + { label: 'Martial Arts', value: 'martial-arts' }, + + { label: 'Mature', value: 'mature' }, + + { label: 'Novel', value: 'novel' }, + + { label: 'Okul', value: 'okul' }, + + { label: 'Psikolojik', value: 'psikolojik' }, + + { label: 'Psychological', value: 'psychological' }, + + { label: 'Reverse Harem', value: 'reverse-harem' }, + + { label: 'Romance', value: 'romance' }, + + { label: 'Romantik', value: 'romantik' }, + + { label: 'Shoujo', value: 'shoujo' }, + + { label: 'Slice Of Life', value: 'slice-of-life' }, + + { label: 'Smut', value: 'smut' }, + + { label: 'Supernatural', value: 'supernatural' }, + + { label: 'Tarihi', value: 'tarihi' }, + + { label: 'Tragedy', value: 'tragedy' }, + + { label: 'Yaoi', value: 'yaoi' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new LSHNovel(); diff --git a/plugins/ukrainian/smakolykytl.ts b/plugins/ukrainian/smakolykytl.ts index cb14b6b..18d2856 100644 --- a/plugins/ukrainian/smakolykytl.ts +++ b/plugins/ukrainian/smakolykytl.ts @@ -1,32 +1,32 @@ -import { Plugin } from "@typings/plugin"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { NovelStatus } from "@libs/novelStatus"; -import dayjs from "dayjs"; +import { Plugin } from '@typings/plugin'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; class Smakolykytl implements Plugin.PluginBase { - id = "smakolykytl"; - name = "Смаколики"; - site = "https://smakolykytl.site/"; - version = "1.0.0"; - icon = "src/uk/smakolykytl/icon.png"; + id = 'smakolykytl'; + name = 'Смаколики'; + site = 'https://smakolykytl.site/'; + version = '1.0.0'; + icon = 'src/uk/smakolykytl/icon.png'; async popularNovels( pageNo: number, { showLatestNovels, filters }: Plugin.PopularNovelsOptions, ): Promise { const url = showLatestNovels - ? "https://api.smakolykytl.site/api/user/updates" - : "https://api.smakolykytl.site/api/user/projects"; + ? 'https://api.smakolykytl.site/api/user/updates' + : 'https://api.smakolykytl.site/api/user/projects'; const result = await fetchApi(url); const json = (await result.json()) as response; const novels: Plugin.NovelItem[] = []; - (json?.projects || json?.updates)?.forEach((novel) => + (json?.projects || json?.updates)?.forEach(novel => novels.push({ name: novel.title, cover: novel.image.url, - path: "titles/" + novel.id, + path: 'titles/' + novel.id, }), ); @@ -34,42 +34,42 @@ class Smakolykytl implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const id = novelPath.split("/").pop(); + const id = novelPath.split('/').pop(); const result = await fetchApi( - "https://api.smakolykytl.site/api/user/projects/" + id, + 'https://api.smakolykytl.site/api/user/projects/' + id, ); const book = (await result.json()) as response; const novel: Plugin.SourceNovel = { path: novelPath, - name: book?.project?.title || "", + name: book?.project?.title || '', cover: book?.project?.image?.url, summary: book?.project?.description, author: book?.project?.author, - status: book?.project?.status_translate.includes("Триває") + status: book?.project?.status_translate.includes('Триває') ? NovelStatus.Ongoing : NovelStatus.Completed, }; const tags = [book?.project?.genres, book?.project?.tags] .flat() - .map((tags) => tags?.title) - .filter((tags) => tags); + .map(tags => tags?.title) + .filter(tags => tags); if (tags.length) { - novel.genres = tags.join(", "); + novel.genres = tags.join(', '); } const chapters: Plugin.ChapterItem[] = []; const res = await fetchApi( - "https://api.smakolykytl.site/api/user/projects/" + id + "/books", + 'https://api.smakolykytl.site/api/user/projects/' + id + '/books', ); const data = (await res.json()) as response; - data?.books?.forEach((volume) => - volume?.chapters?.forEach((chapter) => + data?.books?.forEach(volume => + volume?.chapters?.forEach(chapter => chapters.push({ - name: volume.title + " " + chapter.title, - path: "read/" + chapter.id, - releaseTime: dayjs(chapter.modifiedAt).format("LLL"), + name: volume.title + ' ' + chapter.title, + path: 'read/' + chapter.id, + releaseTime: dayjs(chapter.modifiedAt).format('LLL'), chapterNumber: chapters.length + 1, }), ), @@ -80,33 +80,35 @@ class Smakolykytl implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const id = chapterPath.split("/").pop(); + const id = chapterPath.split('/').pop(); const result = await fetchApi( - "https://api.smakolykytl.site/api/user/chapters/" + id, + 'https://api.smakolykytl.site/api/user/chapters/' + id, ); const json = (await result.json()) as response; - const chapterRaw: HTML[] = JSON.parse(json?.chapter?.content || "[]"); + const chapterRaw: HTML[] = JSON.parse(json?.chapter?.content || '[]'); const chapterText = jsonToHtml(chapterRaw); return chapterText; } async searchNovels(searchTerm: string): Promise { - const result = await fetchApi("https://api.smakolykytl.site/api/user/projects"); + const result = await fetchApi( + 'https://api.smakolykytl.site/api/user/projects', + ); const json = (await result.json()) as response; const searchTitle = searchTerm.toLowerCase(); const novels: Plugin.NovelItem[] = []; - + json?.projects ?.filter( ({ title, id }) => title.toLowerCase().includes(searchTitle) || String(id) == searchTerm, ) - ?.forEach((novel) => + ?.forEach(novel => novels.push({ name: novel.title, cover: novel.image.url, - path: "titles/" + novel.id, + path: 'titles/' + novel.id, }), ); return novels; @@ -117,34 +119,34 @@ class Smakolykytl implements Plugin.PluginBase { export default new Smakolykytl(); -function jsonToHtml(json: HTML[], html: string = "") { - json.forEach((element) => { +function jsonToHtml(json: HTML[], html: string = '') { + json.forEach(element => { switch (element.type) { - case "hardBreak": - html += "
"; + case 'hardBreak': + html += '
'; break; - case "horizontalRule": - html += "
"; + case 'horizontalRule': + html += '
'; break; - case "image": + case 'image': if (element.attrs) { const attrs = Object.entries(element.attrs) - .filter((attr) => attr?.[1]) - .map((attr) => `${attr[0]}="${attr[1]}"`); - html += ""; + .filter(attr => attr?.[1]) + .map(attr => `${attr[0]}="${attr[1]}"`); + html += ''; } break; - case "paragraph": + case 'paragraph': html += - "

" + - (element.content ? jsonToHtml(element.content) : "
") + - "

"; + '

' + + (element.content ? jsonToHtml(element.content) : '
') + + '

'; break; - case "text": + case 'text': html += element.text; break; default: - html += JSON.stringify(element, null, "\t"); //maybe I missed something. + html += JSON.stringify(element, null, '\t'); //maybe I missed something. break; } }); diff --git a/plugins/vietnamese/LNHako.ts b/plugins/vietnamese/LNHako.ts index f18ab4d..af92eb2 100644 --- a/plugins/vietnamese/LNHako.ts +++ b/plugins/vietnamese/LNHako.ts @@ -1,259 +1,269 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { isUrlAbsolute } from "@libs/isAbsoluteUrl"; -import { NovelStatus } from "@libs/novelStatus"; -import { FilterTypes, Filters } from "@libs/filterInputs"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; +import { NovelStatus } from '@libs/novelStatus'; +import { FilterTypes, Filters } from '@libs/filterInputs'; class HakoPlugin implements Plugin.PluginBase { - id = "ln.hako"; - name = "Hako"; - icon = "src/vi/hakolightnovel/icon.png"; - site = "https://ln.hako.vn"; - version = "1.0.1"; - parseNovels(loadedCheerio: CheerioAPI) { - const novels: Plugin.NovelItem[] = []; - loadedCheerio("#mainpart .row .thumb-item-flow").each((index, ele) => { - let url = loadedCheerio(ele) - .find("div.thumb_attr.series-title > a") - .attr("href"); + id = 'ln.hako'; + name = 'Hako'; + icon = 'src/vi/hakolightnovel/icon.png'; + site = 'https://ln.hako.vn'; + version = '1.0.1'; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('#mainpart .row .thumb-item-flow').each((index, ele) => { + let url = loadedCheerio(ele) + .find('div.thumb_attr.series-title > a') + .attr('href'); - if (url) { - const name = loadedCheerio(ele) - .find(".series-title") - .text() - .trim(); - let cover = loadedCheerio(ele) - .find(".img-in-ratio") - .attr("data-bg"); + if (url) { + const name = loadedCheerio(ele).find('.series-title').text().trim(); + let cover = loadedCheerio(ele).find('.img-in-ratio').attr('data-bg'); - if (cover && !isUrlAbsolute(cover)) { - cover = this.site + cover; - } + if (cover && !isUrlAbsolute(cover)) { + cover = this.site + cover; + } - const novel = { name, path: url.replace(this.site, ''), cover }; + const novel = { name, path: url.replace(this.site, ''), cover }; - novels.push(novel); - } - }); - return novels; + novels.push(novel); + } + }); + return novels; + } + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site + '/danh-sach'; + if (filters.alphabet.value) { + link += '/' + filters.alphabet.value; } - async popularNovels( - pageNo: number, - {showLatestNovels, filters}: Plugin.PopularNovelsOptions - ): Promise { - let link = this.site + "/danh-sach" - if(filters.alphabet.value){ - link += "/" + filters.alphabet.value; - } - const params = new URLSearchParams(); - for(const novelType of filters.type.value){ - params.append(novelType, "1"); - } - for(const status of filters.status.value){ - params.append(status, "1"); - } - params.append("sapxep", filters.sort.value); - link += '?' + params.toString() + '&page=' + pageNo - const result = await fetch(link); - const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); + const params = new URLSearchParams(); + for (const novelType of filters.type.value) { + params.append(novelType, '1'); } - async parseNovel(novelPath: string): Promise { - const novel: Plugin.SourceNovel = { - path: novelPath, - name: 'Không có tiêu đề', - }; - const result = await fetch(this.site + novelPath); - const body = await result.text(); + for (const status of filters.status.value) { + params.append(status, '1'); + } + params.append('sapxep', filters.sort.value); + link += '?' + params.toString() + '&page=' + pageNo; + const result = await fetch(link); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Không có tiêu đề', + }; + const result = await fetch(this.site + novelPath); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + let loadedCheerio = parseHTML(body); - novel.name = loadedCheerio(".series-name").text().trim(); + novel.name = loadedCheerio('.series-name').text().trim(); - const background = - loadedCheerio(".series-cover > .a6-ratio > div").attr("style") || - ""; - const novelCover = background.substring( - background.indexOf("http"), - background.length - 2 - ); + const background = + loadedCheerio('.series-cover > .a6-ratio > div').attr('style') || ''; + const novelCover = background.substring( + background.indexOf('http'), + background.length - 2, + ); - novel.cover = novelCover - ? isUrlAbsolute(novelCover) - ? novelCover - : this.site + novelCover - : ""; + novel.cover = novelCover + ? isUrlAbsolute(novelCover) + ? novelCover + : this.site + novelCover + : ''; - novel.summary = loadedCheerio(".summary-content").text().trim(); + novel.summary = loadedCheerio('.summary-content').text().trim(); - novel.genres = loadedCheerio(".series-information > div:nth-child(1)") - .text() - .trim() - .split(/\n[\s\n]*/).join(','); - - novel.author = loadedCheerio('.series-information > div:nth-child(2) > .info-value') + novel.genres = loadedCheerio('.series-information > div:nth-child(1)') + .text() + .trim() + .split(/\n[\s\n]*/) + .join(','); + + novel.author = loadedCheerio( + '.series-information > div:nth-child(2) > .info-value', + ) + .text() + .trim(); + novel.artist = loadedCheerio( + '.series-information > div:nth-child(3) > .info-value', + ) + .text() + .trim(); + + novel.status = loadedCheerio( + '.series-information > div:nth-child(4) > .info-value', + ) + .text() + .trim(); + + switch (novel.status) { + case 'Đang tiến hành': + novel.status = NovelStatus.Ongoing; + break; + case 'Tạm ngưng': + novel.status = NovelStatus.OnHiatus; + break; + case 'Completed': + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Unknown; + } + + novel.chapters = []; + loadedCheerio('.volume-list').each((idx, ele) => { + const customPage = loadedCheerio(ele).find('.sect-title').text().trim(); + let num = 0, + part = 1; + loadedCheerio(ele) + .find('.list-chapters > li') + .each((idx, chapterEle) => { + const path = loadedCheerio(chapterEle).find('a').attr('href'); + const chapterName = loadedCheerio(chapterEle) + .find('.chapter-name') .text() .trim(); - novel.artist = loadedCheerio('.series-information > div:nth-child(3) > .info-value') + let chapterNumber = Number(chapterName.match(/Chương\s*(\d+)/i)?.[1]); + if (chapterNumber) { + num = chapterNumber; + part = 1; + } else { + chapterNumber = num + part / 10; + part++; + } + const chapterTime = loadedCheerio(chapterEle) + .find('.chapter-time') .text() - .trim(); - - novel.status = loadedCheerio('.series-information > div:nth-child(4) > .info-value') - .text() - .trim(); - - switch (novel.status) { - case 'Đang tiến hành': - novel.status = NovelStatus.Ongoing; - break; - case 'Tạm ngưng': - novel.status = NovelStatus.OnHiatus; - break; - case 'Completed': - novel.status = NovelStatus.Completed; - break; - default: - novel.status = NovelStatus.Unknown; - } - - novel.chapters = []; - loadedCheerio('.volume-list').each((idx, ele) => { - const customPage = loadedCheerio(ele).find('.sect-title').text().trim(); - let num = 0, part = 1; - loadedCheerio(ele).find('.list-chapters > li').each((idx, chapterEle) => { - const path = loadedCheerio(chapterEle).find("a").attr("href"); - const chapterName = loadedCheerio(chapterEle) - .find(".chapter-name") - .text() - .trim(); - let chapterNumber = Number(chapterName.match(/Chương\s*(\d+)/i)?.[1]); - if(chapterNumber){ - num = chapterNumber; - part = 1; - }else{ - chapterNumber = num + part / 10; - part ++; - } - const chapterTime = loadedCheerio(chapterEle) - .find(".chapter-time") - .text() - .split('/') - .map(x => Number(x)); - if(path){ - novel.chapters?.push( - { - page: customPage, - path, - name: chapterName, - releaseTime: new Date(chapterTime[2], chapterTime[1], chapterTime[0]).toISOString(), - chapterNumber: chapterNumber, - } - ); - } + .split('/') + .map(x => Number(x)); + if (path) { + novel.chapters?.push({ + page: customPage, + path, + name: chapterName, + releaseTime: new Date( + chapterTime[2], + chapterTime[1], + chapterTime[0], + ).toISOString(), + chapterNumber: chapterNumber, }); + } }); - return novel; - } - async parseChapter(chapterPath: string): Promise { - const result = await fetch(this.site + chapterPath); - const body = await result.text(); + }); + return novel; + } + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); + const body = await result.text(); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); - const chapterText = loadedCheerio("#chapter-content").html() || ""; + const chapterText = loadedCheerio('#chapter-content').html() || ''; - return chapterText; - } - async searchNovels( - searchTerm: string, - pageNo: number - ): Promise { - const url = this.site + "/tim-kiem?keywords=" + searchTerm + '&page=' + pageNo; - const result = await fetch(url); - const body = await result.text(); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio); - } - async fetchImage(url: string): Promise { - const headers = { - Referer: "https://ln.hako.vn", - }; - return await fetchFile(url, { headers: headers }); - } - filters = { - alphabet: { - type: FilterTypes.Picker, - value: "", - label: "Chữ cái", - options: [ - {label: 'Tất cả', value: ''}, - {label: 'Khác', value: 'khac'}, - {label: 'A', value: 'a'}, - {label: 'B', value: 'b'}, - {label: 'C', value: 'c'}, - {label: 'D', value: 'd'}, - {label: 'E', value: 'e'}, - {label: 'F', value: 'f'}, - {label: 'G', value: 'g'}, - {label: 'H', value: 'h'}, - {label: 'I', value: 'i'}, - {label: 'J', value: 'j'}, - {label: 'K', value: 'k'}, - {label: 'L', value: 'l'}, - {label: 'M', value: 'm'}, - {label: 'N', value: 'n'}, - {label: 'O', value: 'o'}, - {label: 'P', value: 'p'}, - {label: 'Q', value: 'q'}, - {label: 'R', value: 'r'}, - {label: 'S', value: 's'}, - {label: 'T', value: 't'}, - {label: 'U', value: 'u'}, - {label: 'V', value: 'v'}, - {label: 'W', value: 'w'}, - {label: 'X', value: 'x'}, - {label: 'Y', value: 'y'}, - {label: 'Z', value: 'z'}, - ] - }, - type: { - type: FilterTypes.CheckboxGroup, - label: "Phân loại", - value: [], - options: [ - {label: "Truyện dịch", value: "truyendich"}, - {label: "Truyện sáng tác", value: "sangtac"}, - {label: "Convert", value: "convert"} - ] - }, - status: { - type: FilterTypes.CheckboxGroup, - label: "Tình trạng", - value: [], - options: [ - {label: "Đang tiến hành", value: "dangtienhanh"}, - {label: "Tạm ngưng", value: "tamngung"}, - {label: "Đã hoàn thành", value: "hoanthanh"} - ] - }, - sort: { - type: FilterTypes.Picker, - label: "Sắp xếp", - value: "top", - options: [ - {label: "A-Z", value: "tentruyen"}, - {label: "Z-A", value: "tentruyenza"}, - {label: "Mới cập nhật", value: "capnhat"}, - {label: "Truyện mới", value: "truyenmoi"}, - {label: "Theo dõi", value: "theodoi"}, - {label: "Top toàn thời gian", value: "top"}, - {label: "Top tháng", value: "topthang"}, - {label: "Số từ", value: "sotu"}, - ] - } - } satisfies Filters; + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = + this.site + '/tim-kiem?keywords=' + searchTerm + '&page=' + pageNo; + const result = await fetch(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + async fetchImage(url: string): Promise { + const headers = { + Referer: 'https://ln.hako.vn', + }; + return await fetchFile(url, { headers: headers }); + } + filters = { + alphabet: { + type: FilterTypes.Picker, + value: '', + label: 'Chữ cái', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Khác', value: 'khac' }, + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + { label: 'D', value: 'd' }, + { label: 'E', value: 'e' }, + { label: 'F', value: 'f' }, + { label: 'G', value: 'g' }, + { label: 'H', value: 'h' }, + { label: 'I', value: 'i' }, + { label: 'J', value: 'j' }, + { label: 'K', value: 'k' }, + { label: 'L', value: 'l' }, + { label: 'M', value: 'm' }, + { label: 'N', value: 'n' }, + { label: 'O', value: 'o' }, + { label: 'P', value: 'p' }, + { label: 'Q', value: 'q' }, + { label: 'R', value: 'r' }, + { label: 'S', value: 's' }, + { label: 'T', value: 't' }, + { label: 'U', value: 'u' }, + { label: 'V', value: 'v' }, + { label: 'W', value: 'w' }, + { label: 'X', value: 'x' }, + { label: 'Y', value: 'y' }, + { label: 'Z', value: 'z' }, + ], + }, + type: { + type: FilterTypes.CheckboxGroup, + label: 'Phân loại', + value: [], + options: [ + { label: 'Truyện dịch', value: 'truyendich' }, + { label: 'Truyện sáng tác', value: 'sangtac' }, + { label: 'Convert', value: 'convert' }, + ], + }, + status: { + type: FilterTypes.CheckboxGroup, + label: 'Tình trạng', + value: [], + options: [ + { label: 'Đang tiến hành', value: 'dangtienhanh' }, + { label: 'Tạm ngưng', value: 'tamngung' }, + { label: 'Đã hoàn thành', value: 'hoanthanh' }, + ], + }, + sort: { + type: FilterTypes.Picker, + label: 'Sắp xếp', + value: 'top', + options: [ + { label: 'A-Z', value: 'tentruyen' }, + { label: 'Z-A', value: 'tentruyenza' }, + { label: 'Mới cập nhật', value: 'capnhat' }, + { label: 'Truyện mới', value: 'truyenmoi' }, + { label: 'Theo dõi', value: 'theodoi' }, + { label: 'Top toàn thời gian', value: 'top' }, + { label: 'Top tháng', value: 'topthang' }, + { label: 'Số từ', value: 'sotu' }, + ], + }, + } satisfies Filters; } export default new HakoPlugin(); diff --git a/plugins/vietnamese/Truyenconect.ts b/plugins/vietnamese/Truyenconect.ts index fa0767d..10dcb0e 100644 --- a/plugins/vietnamese/Truyenconect.ts +++ b/plugins/vietnamese/Truyenconect.ts @@ -1,299 +1,311 @@ - -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { FilterTypes, Filters } from "@libs/filterInputs"; -import { NovelStatus } from "@libs/novelStatus"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; interface VolumeData { - story: string; - navigation: string; - value: string + story: string; + navigation: string; + value: string; } interface ChapterSelect { - chap_selector: string; - eps_selector: boolean | string; + chap_selector: string; + eps_selector: boolean | string; } interface VolumeList { - chapters: Plugin.ChapterItem[], - volumes?: VolumeData[] + chapters: Plugin.ChapterItem[]; + volumes?: VolumeData[]; } class TruyenConect implements Plugin.PagePlugin { - id = "truyenconect"; - name = "Truyen Conect"; - icon = "src/vi/truyenconect/icon.png"; - site = "https://truyenconect.com"; - version = "1.0.0"; - async sleep (ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)) - } - parseNovels(loadedCheerio: CheerioAPI, selector: string){ - const novels: Plugin.NovelItem[] = []; - loadedCheerio(selector).each((idx, ele) => { - const url = loadedCheerio(ele).find('a').attr('href'); - if(url){ - novels.push({ - name: loadedCheerio(ele).find('img').attr('alt') || '', - path: url.replace(this.site, ''), - cover: loadedCheerio(ele).find('img').attr('data-src'), - }) - } + id = 'truyenconect'; + name = 'Truyen Conect'; + icon = 'src/vi/truyenconect/icon.png'; + site = 'https://truyenconect.com'; + version = '1.0.0'; + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + parseNovels(loadedCheerio: CheerioAPI, selector: string) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio(selector).each((idx, ele) => { + const url = loadedCheerio(ele).find('a').attr('href'); + if (url) { + novels.push({ + name: loadedCheerio(ele).find('img').attr('alt') || '', + path: url.replace(this.site, ''), + cover: loadedCheerio(ele).find('img').attr('data-src'), }); - return novels; + } + }); + return novels; + } + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site; + let selector = '.c-page__content > .grid9.block .item-thumb.c-image-hover'; + + if (showLatestNovels) { + selector = + '.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover'; + } else if (filters.category.value) { + link += '/' + filters.category.value; + selector = + 'table.manga-shortcodes.manga-chapters-listing td[width="10%"]'; + if (filters.category.value === 'the-loai') { + selector = '.item-thumb.hover-details.c-image-hover'; + link += '/' + filters.genre.value; + } + link += '?page=' + pageNo; } - async popularNovels(pageNo: number, {filters, showLatestNovels}: Plugin.PopularNovelsOptions): Promise { - let link = this.site; - let selector = '.c-page__content > .grid9.block .item-thumb.c-image-hover'; - - if(showLatestNovels){ - selector = '.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover'; - }else if(filters.category.value){ - link += '/' + filters.category.value; - selector = 'table.manga-shortcodes.manga-chapters-listing td[width="10%"]'; - if(filters.category.value === 'the-loai'){ - selector = '.item-thumb.hover-details.c-image-hover' - link += '/' + filters.genre.value; - } - link += '?page=' + pageNo; - } - const body = await fetch(link).then(r => r.text()); - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio, selector); + const body = await fetch(link).then(r => r.text()); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio, selector); + } + parseChapters(loadedCheerio: CheerioAPI) { + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('option').each((idx, ele) => { + let url = ele.attribs['value']; + if (!url) return; + const chapterId = url.match(/\/(\d+)-/)?.[1]; + if (chapterId) { + url = url.replace(chapterId + '-', '') + '-' + chapterId; + } + let num = url.match(/chuong-(\d+)/)?.[1]; + chapters.push({ + path: url.replace(this.site, ''), + name: loadedCheerio(ele).text().trim(), + chapterNumber: Number(num) || undefined, + }); + }); + return chapters.reverse(); + } + parseVolumes(loadedCheerio: CheerioAPI) { + const volumes: VolumeData[] = []; + loadedCheerio('option').each((idx, ele) => { + volumes.push({ + story: ele.attribs['data-story'], + navigation: ele.attribs['data-navigation'], + value: ele.attribs['value'], + }); + }); + return volumes; + } + async getVolumes(firstChapterUrl: string) { + const chapterId = firstChapterUrl.match(/-(\d+)$/)?.[1]; + if (!chapterId) throw new Error('Không tìm thấy chương'); + const url = this.site + '/truyen/get-chap-selector?chap=' + chapterId; + const data: ChapterSelect = await fetch(url).then(r => r.json()); + const volumeList: VolumeList = { + chapters: this.parseChapters(parseHTML(data.chap_selector)), + }; + if (data.eps_selector) { + volumeList.volumes = this.parseVolumes( + parseHTML(data.eps_selector as string), + ); } - parseChapters(loadedCheerio: CheerioAPI) { - const chapters: Plugin.ChapterItem[] = []; - loadedCheerio('option').each((idx, ele) => { - let url = ele.attribs['value']; - if(!url) return; - const chapterId = url.match(/\/(\d+)-/)?.[1]; - if(chapterId){ - url = url.replace(chapterId+'-', '') + '-' + chapterId; - } - let num = url.match(/chuong-(\d+)/)?.[1]; - chapters.push({ - path: url.replace(this.site, ''), - name: loadedCheerio(ele).text().trim(), - chapterNumber: Number(num) || undefined, - }) - }) - return chapters.reverse(); + return volumeList; + } + async parseNovel( + novelPath: string, + ): Promise { + const url = this.site + novelPath; + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + name: loadedCheerio('.post-title > h1').text().trim(), + path: novelPath, + chapters: [], + totalPages: 1, + }; + + novel.cover = loadedCheerio('.summary_image > a > img').attr('data-src'); + + loadedCheerio('.post-content_item').each(function () { + const detailName = loadedCheerio(this) + .find('.summary-heading > h5') + .text() + .trim(); + const detail = loadedCheerio(this).find('.summary-content').html(); + + if (!detail) return; + + switch (detailName) { + case 'Thể loại': + novel.genres = loadedCheerio(detail) + .children('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + break; + case 'Tác giả': + novel.author = loadedCheerio(detail).text().trim(); + break; + case 'Hoạ sĩ': + novel.artist = loadedCheerio(detail).text().trim(); + break; + case 'Trạng thái': + const status = detail.trim(); + if (status === 'Full') novel.status = NovelStatus.Completed; + else if (status === 'Tạm ngưng') novel.status = NovelStatus.OnHiatus; + else if (status === 'Đang tiến hành') + novel.status = NovelStatus.Ongoing; + else novel.status = NovelStatus.Unknown; + break; + } + }); + + loadedCheerio('.description-summary > div.summary__content > div').remove(); + + novel.summary = loadedCheerio('.description-summary > div.summary__content') + .text() + .trim(); + const firstChapLink = loadedCheerio('#init-links > a').first().attr('href'); + if (!firstChapLink) throw new Error('Không tìm thấy truyện'); + await this.sleep(1000); + const volumeList = await this.getVolumes(firstChapLink); + if (volumeList.volumes) { + novel.totalPages = volumeList.volumes.length; + novel.chapters = volumeList.chapters; + } else { + novel.totalPages = 1; + novel.chapters = volumeList.chapters; } - parseVolumes(loadedCheerio: CheerioAPI) { - const volumes: VolumeData[] = []; - loadedCheerio('option').each((idx, ele) => { - volumes.push({ - story: ele.attribs['data-story'], - navigation: ele.attribs['data-navigation'], - value: ele.attribs['value'], - }) - }) - return volumes; - } - async getVolumes (firstChapterUrl: string) { - const chapterId = firstChapterUrl.match(/-(\d+)$/)?.[1]; - if(!chapterId) throw new Error('Không tìm thấy chương'); - const url = this.site + '/truyen/get-chap-selector?chap=' + chapterId; - const data: ChapterSelect = await fetch(url).then(r => r.json()); - const volumeList: VolumeList = { - chapters: this.parseChapters(parseHTML(data.chap_selector)) - } - if(data.eps_selector){ - volumeList.volumes = this.parseVolumes(parseHTML(data.eps_selector as string)) - } - return volumeList - } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - const result = await fetch(url); - const body = await result.text(); + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath; + const result = await fetch(url); + const body = await result.text(); + let loadedCheerio = parseHTML(body); + const firstChapLink = loadedCheerio('#init-links > a').first().attr('href'); + if (!firstChapLink) throw new Error('Không tìm thấy truyện'); + await this.sleep(1000); + const volumeList = await this.getVolumes(firstChapLink); + const volumeIndex = Number(page) - 1; + if (!volumeList.volumes) throw new Error('Không tìm thấy truyện'); + if (volumeIndex >= volumeList.volumes.length) + throw new Error('Không tìm thấy volume'); + const volume = volumeList.volumes[volumeIndex]; + const query = `dataEpisodes=${volume.value}&datastory=${volume.story}&dataNavigation=${encodeURIComponent(volume.navigation)}`; + const chaptersUrl = `${this.site}/truyen/getreadingchapAction?${query}`; + await this.sleep(1000); + const res: { err: number; html: string } = await fetch(chaptersUrl).then( + r => r.json(), + ); + if (res.err) throw new Error(res.html); + return { + chapters: this.parseChapters(parseHTML(res.html)), + }; + } + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); + const body = await result.text(); - let loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); + let chapterText = loadedCheerio('.reading-content').html() || ''; - const novel: Plugin.SourceNovel & { totalPages: number; }= { - name: loadedCheerio(".post-title > h1").text().trim(), - path: novelPath, - chapters: [], - totalPages: 1 - }; + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}?key=${searchTerm}&page=${pageNo}`; + const result = await fetch(url); + const body = await result.text(); - novel.cover = loadedCheerio(".summary_image > a > img").attr( - "data-src" - ); + const loadedCheerio = parseHTML(body); - loadedCheerio(".post-content_item").each(function () { - const detailName = loadedCheerio(this) - .find(".summary-heading > h5") - .text() - .trim(); - const detail = loadedCheerio(this).find(".summary-content").html(); + const novels: Plugin.NovelItem[] = []; - if (!detail) return; + loadedCheerio('.tab-thumb.c-image-hover > a').each((idx, ele) => { + const novelName = ele.attribs['title']; + const novelCover = loadedCheerio(ele).find('img').first().attr('src'); + const novelUrl = ele.attribs['href']; - switch (detailName) { - case "Thể loại": - novel.genres = loadedCheerio(detail) - .children("a") - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - break; - case "Tác giả": - novel.author = loadedCheerio(detail).text().trim(); - break; - case "Hoạ sĩ": - novel.artist = loadedCheerio(detail).text().trim(); - break; - case "Trạng thái": - const status = detail.trim(); - if(status === 'Full') novel.status = NovelStatus.Completed; - else if(status === 'Tạm ngưng') novel.status = NovelStatus.OnHiatus; - else if(status === 'Đang tiến hành') novel.status = NovelStatus.Ongoing - else novel.status = NovelStatus.Unknown; - break; - } - }); + if (!novelUrl) return; - loadedCheerio( - ".description-summary > div.summary__content > div" - ).remove(); + novels.push({ + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }); + }); - novel.summary = loadedCheerio( - ".description-summary > div.summary__content" - ) - .text() - .trim(); - const firstChapLink = loadedCheerio('#init-links > a').first().attr('href'); - if(!firstChapLink) throw new Error('Không tìm thấy truyện'); - await this.sleep(1000); - const volumeList = await this.getVolumes(firstChapLink); - if(volumeList.volumes) { - novel.totalPages = volumeList.volumes.length; - novel.chapters = volumeList.chapters - }else{ - novel.totalPages = 1; - novel.chapters = volumeList.chapters; - } - return novel; - } - async parsePage(novelPath: string, page: string): Promise { - const url = this.site + novelPath; - const result = await fetch(url); - const body = await result.text(); - let loadedCheerio = parseHTML(body); - const firstChapLink = loadedCheerio('#init-links > a').first().attr('href'); - if(!firstChapLink) throw new Error('Không tìm thấy truyện'); - await this.sleep(1000); - const volumeList = await this.getVolumes(firstChapLink); - const volumeIndex = Number(page) - 1; - if(!volumeList.volumes) throw new Error("Không tìm thấy truyện"); - if(volumeIndex >= volumeList.volumes.length) throw new Error("Không tìm thấy volume"); - const volume = volumeList.volumes[volumeIndex]; - const query = `dataEpisodes=${volume.value}&datastory=${volume.story}&dataNavigation=${encodeURIComponent(volume.navigation)}`; - const chaptersUrl = `${this.site}/truyen/getreadingchapAction?${query}`; - await this.sleep(1000); - const res: {err: number, html: string} = await fetch(chaptersUrl).then(r => r.json()); - if(res.err) throw new Error(res.html); - return { - chapters: this.parseChapters(parseHTML(res.html)) - } - } - async parseChapter(chapterPath: string): Promise { - const result = await fetch(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - let chapterText = loadedCheerio(".reading-content").html() || ''; - - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const url = `${this.site}?key=${searchTerm}&page=${pageNo}`; - const result = await fetch(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio(".tab-thumb.c-image-hover > a").each((idx, ele) => { - const novelName = ele.attribs['title']; - const novelCover = loadedCheerio(ele).find('img').first().attr('src'); - const novelUrl = ele.attribs['href']; - - if (!novelUrl) return; - - novels.push({ - name: novelName, - path: novelUrl.replace(this.site, ''), - cover: novelCover, - }); - }); - - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - filters = { - category: { - label: "Lọc theo", - value: '', - type: FilterTypes.Picker, - options: [ - {label: '', value: ''}, - {label: 'Thể loại', value: 'the-loai'}, - {label: 'Truyện dịch', value: 'truyen-dich'}, - {label: 'Truyện convert', value: 'convert'} - ] - }, - genre: { - label: 'Thể loại', - value: 'action', - type: FilterTypes.Picker, - options: [ - {label: 'Action', value: 'action'}, - {label: 'Adult', value: 'adult'}, - {label: 'Adventure', value: 'adventure'}, - {label: 'Chinese novel', value: 'chinese-novel'}, - {label: 'Chuyển Sinh', value: 'chuyen-sinh'}, - {label: 'English Novel', value: 'english-novel'}, - {label: 'Harem', value: 'harem'}, - {label: 'Ecchi', value: 'ecchi'}, - {label: 'Fantasy', value: 'fantasy'}, - {label: 'Drama', value: 'drama'}, - {label: 'Game', value: 'game'}, - {label: 'Tiên hiệp', value: 'tien-hiep'}, - {label: 'Kiếm Hiệp', value: 'kiem-hiep'}, - {label: 'Ngôn Tình', value: 'ngon-tinh'}, - {label: 'Isekai', value: 'isekai'}, - {label: 'Lịch Sử', value: 'lich-su'}, - {label: 'Web Novel', value: 'web-novel'}, - {label: 'Xuyên không', value: 'xuyen-khong'}, - {label: 'Trọng sinh', value: 'trong-sinh'}, - {label: 'Trinh thám', value: 'trinh-tham'}, - {label: 'Dị giới', value: 'di-gioi'}, - {label: 'Huyền ảo', value: 'huyen-ao'}, - {label: 'Sắc Hiệp', value: 'sac-hiep'}, - {label: 'Dị năng', value: 'di-nang'}, - {label: 'Linh dị', value: 'linh-di'}, - {label: 'Đô thị', value: 'do-thi'}, - {label: 'Comedy', value: 'comedy'}, - {label: 'School Life', value: 'school-life'}, - {label: 'Romance', value: 'romance'}, - {label: 'Martial-arts', value: 'martial-arts'}, - {label: 'Light Novel', value: 'light-novel'}, - {label: 'Huyền huyễn', value: 'huyen-huyen'}, - {label: 'Kỳ Huyễn', value: 'ky-huyen'}, - {label: 'Khoa Huyễn', value: 'khoa-huyen'}, - {label: 'Võng Du', value: 'vong-du'}, - {label: 'Đồng Nhân', value: 'dong-nhan'}, - ] - } - } satisfies Filters; + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } + filters = { + category: { + label: 'Lọc theo', + value: '', + type: FilterTypes.Picker, + options: [ + { label: '', value: '' }, + { label: 'Thể loại', value: 'the-loai' }, + { label: 'Truyện dịch', value: 'truyen-dich' }, + { label: 'Truyện convert', value: 'convert' }, + ], + }, + genre: { + label: 'Thể loại', + value: 'action', + type: FilterTypes.Picker, + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Chinese novel', value: 'chinese-novel' }, + { label: 'Chuyển Sinh', value: 'chuyen-sinh' }, + { label: 'English Novel', value: 'english-novel' }, + { label: 'Harem', value: 'harem' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Game', value: 'game' }, + { label: 'Tiên hiệp', value: 'tien-hiep' }, + { label: 'Kiếm Hiệp', value: 'kiem-hiep' }, + { label: 'Ngôn Tình', value: 'ngon-tinh' }, + { label: 'Isekai', value: 'isekai' }, + { label: 'Lịch Sử', value: 'lich-su' }, + { label: 'Web Novel', value: 'web-novel' }, + { label: 'Xuyên không', value: 'xuyen-khong' }, + { label: 'Trọng sinh', value: 'trong-sinh' }, + { label: 'Trinh thám', value: 'trinh-tham' }, + { label: 'Dị giới', value: 'di-gioi' }, + { label: 'Huyền ảo', value: 'huyen-ao' }, + { label: 'Sắc Hiệp', value: 'sac-hiep' }, + { label: 'Dị năng', value: 'di-nang' }, + { label: 'Linh dị', value: 'linh-di' }, + { label: 'Đô thị', value: 'do-thi' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Romance', value: 'romance' }, + { label: 'Martial-arts', value: 'martial-arts' }, + { label: 'Light Novel', value: 'light-novel' }, + { label: 'Huyền huyễn', value: 'huyen-huyen' }, + { label: 'Kỳ Huyễn', value: 'ky-huyen' }, + { label: 'Khoa Huyễn', value: 'khoa-huyen' }, + { label: 'Võng Du', value: 'vong-du' }, + { label: 'Đồng Nhân', value: 'dong-nhan' }, + ], + }, + } satisfies Filters; } export default new TruyenConect(); diff --git a/plugins/vietnamese/lightnovelvn.ts b/plugins/vietnamese/lightnovelvn.ts index b05ecb7..7df5e7b 100644 --- a/plugins/vietnamese/lightnovelvn.ts +++ b/plugins/vietnamese/lightnovelvn.ts @@ -1,161 +1,178 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; -import { NovelStatus } from "@libs/novelStatus"; -import { NovelItem } from "../../test_web/static/js"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; +import { NovelItem } from '../../test_web/static/js'; interface SearchedNovel { - name: string; - slug: string; - coverUrl: string; + name: string; + slug: string; + coverUrl: string; } -interface SearchedResult { - data?: SearchedNovel[] +interface SearchedResult { + data?: SearchedNovel[]; } class LightNovelVN implements Plugin.PagePlugin { - id = "lightnovel.vn"; - name = "Light Novel VN"; - version = "1.0.0"; - icon = "src/vi/lightnovelvn/icon.png"; - filters?: Filters | undefined; - site = "https://lightnovel.vn"; - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.site}/truyen-hot-ds?page=${pageNo}`; - const body = await fetch(url).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each((idx, ele) => { - const novelName = loadedCheerio(ele).find('h3[itemprop="name"] > a').text().trim(); - const img = loadedCheerio(ele).find('noscript').html(); - const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1]; - const novelUrl = loadedCheerio(ele).find('h3[itemprop="name"] > a').attr('href'); - if(novelUrl) - novels.push({ - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ''), - }); + id = 'lightnovel.vn'; + name = 'Light Novel VN'; + version = '1.0.0'; + icon = 'src/vi/lightnovelvn/icon.png'; + filters?: Filters | undefined; + site = 'https://lightnovel.vn'; + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const url = `${this.site}/truyen-hot-ds?page=${pageNo}`; + const body = await fetch(url).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each( + (idx, ele) => { + const novelName = loadedCheerio(ele) + .find('h3[itemprop="name"] > a') + .text() + .trim(); + const img = loadedCheerio(ele).find('noscript').html(); + const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1]; + const novelUrl = loadedCheerio(ele) + .find('h3[itemprop="name"] > a') + .attr('href'); + if (novelUrl) + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + }, + ); + + return novels; + } + parseChapters(loadedCheerio: CheerioAPI) { + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('ul.chapter-list > li').each((idx, ele) => { + const chNum = Number(loadedCheerio(ele).find('div').first().text()); + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + const name = loadedCheerio(ele).find('a').text().trim(); + if (chapterUrl) { + chapters.push({ + path: chapterUrl.replace(this.site, ''), + name, + chapterNumber: chNum, }); - - return novels; + } + }); + return chapters; + } + async parseNovel( + novelPath: string, + ): Promise { + const url = this.site + novelPath; + const body = await fetch(url).then(r => r.text()); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + chapters: [], + name: 'Không có tiêu đề', + totalPages: 1, + }; + + novel.name = loadedCheerio('h1[itemprop="name"]').text().trim(); + + novel.cover = loadedCheerio('header div:nth-child(2) img') + .attr('srcset') + ?.split(/\s+/)[0]; + + const genres: string[] = []; + loadedCheerio('a[itemprop="genre"]').each(function () { + genres.push(loadedCheerio(this).text()); + }); + novel.genres = genres.join(','); + + novel.status = loadedCheerio('span.font-bold.text-size22:last').text(); + if (novel.status === 'Đang ra') { + novel.status = NovelStatus.Ongoing; + } else if (novel.status === 'Hoàn thành') { + novel.status = NovelStatus.Completed; + } else { + novel.status = NovelStatus.Unknown; } - parseChapters(loadedCheerio: CheerioAPI){ - const chapters: Plugin.ChapterItem[] = []; - loadedCheerio('ul.chapter-list > li').each((idx, ele) => { - const chNum = Number(loadedCheerio(ele).find('div').first().text()); - const chapterUrl = loadedCheerio(ele).find('a').attr('href'); - const name = loadedCheerio(ele).find('a').text().trim(); - if(chapterUrl){ - chapters.push({ - path: chapterUrl.replace(this.site, ''), - name, - chapterNumber: chNum - }) - } - }) - return chapters; - } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - const body = await fetch(url).then((r) => r.text()); + novel.author = loadedCheerio('a[itemprop="author"] > span').text(); - let loadedCheerio = parseHTML(body); + novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' '); + const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); + await delay(1000); + const chapterListUrl = url + '/danh-sach-chuong'; - const novel: Plugin.SourceNovel & { totalPages: number; } = { - path: novelPath, - chapters: [], - name: 'Không có tiêu đề', - totalPages: 1 - }; - - novel.name = loadedCheerio('h1[itemprop="name"]').text().trim(); - - novel.cover = loadedCheerio('header div:nth-child(2) img').attr('srcset')?.split(/\s+/)[0] - - const genres: string[] = []; - loadedCheerio('a[itemprop="genre"]').each(function() { - genres.push(loadedCheerio(this).text()); - }) - novel.genres = genres.join(','); - - novel.status = loadedCheerio('span.font-bold.text-size22:last').text(); - if(novel.status === 'Đang ra'){ - novel.status = NovelStatus.Ongoing; - }else if(novel.status === 'Hoàn thành'){ - novel.status = NovelStatus.Completed; - }else { - novel.status = NovelStatus.Unknown; + const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text()); + const loadedChapterList = parseHTML(chapterListBody); + novel.chapters = this.parseChapters(loadedChapterList); + loadedChapterList('nav[aria-label="Pagination"] a').each((index, ele) => { + const href = ele.attribs['href']; + if (href) { + const page = Number(href.match(/\?page=(\d+)/)?.[1]); + if (page && page > novel.totalPages) { + novel.totalPages = page; } - novel.author = loadedCheerio('a[itemprop="author"] > span').text(); + } + }); + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const url = `${this.site}${novelPath}/danh-sach-chuong?page=${page}`; + const chapterListBody = await fetchApi(url).then(r => r.text()); + const chapters = this.parseChapters(parseHTML(chapterListBody)); + return { + chapters, + }; + } + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); - novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' '); - const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); - await delay(1000); - const chapterListUrl = url + '/danh-sach-chuong'; + const loadedCheerio = parseHTML(body); - const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text()); - const loadedChapterList = parseHTML(chapterListBody); - novel.chapters = this.parseChapters(loadedChapterList); - loadedChapterList('nav[aria-label="Pagination"] a').each((index, ele) => { - const href = ele.attribs['href']; - if(href){ - const page = Number(href.match(/\?page=(\d+)/)?.[1]); - if(page && page > novel.totalPages){ - novel.totalPages = page; - } - } - }) - return novel; - } - async parsePage(novelPath: string, page: string): Promise { - const url = `${this.site}${novelPath}/danh-sach-chuong?page=${page}`; - const chapterListBody = await fetchApi(url).then(r => r.text()); - const chapters = this.parseChapters(parseHTML(chapterListBody)); + let chapterText = loadedCheerio('div.chapter-content').html() || ''; + + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo > 1) return []; + const url = `${this.site}/api/book-search`; + let formData = new FormData(); + formData.append('keyword', searchTerm); + + const result: SearchedResult = await fetchApi(url, { + method: 'POST', + body: formData, + }).then(r => r.json()); + + const novels: NovelItem[] = + result.data?.map(item => { return { - chapters - } - } - async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((r) => r.text()); - - const loadedCheerio = parseHTML(body); - - let chapterText = loadedCheerio("div.chapter-content").html() || ''; - - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - if(pageNo > 1) return []; - const url = `${this.site}/api/book-search`; - let formData = new FormData(); - formData.append("keyword", searchTerm); - - const result: SearchedResult = await fetchApi( - url, - { - method: "POST", - body: formData, - }, - ).then((r) => r.json()); - - const novels: NovelItem[] = result.data?.map(item => { - return { - name: item.name, - path: '/truyen/' + item.slug, - cover: (this.site + item.coverUrl).replace('default.jpg', '150.jpg?w=256&q=') - } - }) || []; - return novels; - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } - + name: item.name, + path: '/truyen/' + item.slug, + cover: (this.site + item.coverUrl).replace( + 'default.jpg', + '150.jpg?w=256&q=', + ), + }; + }) || []; + return novels; + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } -export default new LightNovelVN(); \ No newline at end of file +export default new LightNovelVN(); diff --git a/plugins/vietnamese/nettruyen.ts b/plugins/vietnamese/nettruyen.ts index 66227f2..7b3d815 100644 --- a/plugins/vietnamese/nettruyen.ts +++ b/plugins/vietnamese/nettruyen.ts @@ -1,108 +1,126 @@ -import { fetchFile } from "@libs/fetch"; -import { Filters } from "@libs/filterInputs"; -import { NovelStatus } from "@libs/novelStatus"; -import { Plugin } from "@typings/plugin"; -import { CheerioAPI, load as parseHTML } from "cheerio"; +import { fetchFile } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; +import { Plugin } from '@typings/plugin'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; class Nettruyen implements Plugin.PagePlugin { - id = 'nettruyen'; - name = 'Nettruyen'; - icon = 'src/vi/nettruyen/icon.png'; - site = 'https://nettruyen.com.vn'; - version = '1.0.0'; - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; - loadedCheerio('#listchuong > ul > li > a.thumb').each((idx, ele) => { - const path = ele.attribs['href']; - if(!path) return; - const name = ele.attribs['title']; - const cover = this.site + '/' + loadedCheerio(ele).find('img').first().attr('src'); - novels.push({ - path: '/' + path, - name, - cover - }) - }) - return novels; - } - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.site}/xem-nhieu/trang-${pageNo}.html`; - const body = await fetch(url).then(r => r.text()); - return this.parseNovels(parseHTML(body)); - } - parseChapters(loadedCheerio: CheerioAPI){ - const chapters: Plugin.ChapterItem[] = []; - loadedCheerio('ul.list-unstyled > li > a').each((idx, ele) => { - const path = ele.attribs['href']; - if(!path) return; - const name = ele.attribs['title']; - chapters.push({ - path: '/' + path, - name, - chapterNumber: Number(name.match(/Chương (\d+)/)?.[1]) - }) - }) - return chapters; - } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - const body = await fetch(url).then(r => r.text()); - const loadedCheerio = parseHTML(body); - const novel: Plugin.SourceNovel & { totalPages: number; } = { - path: novelPath, - name: loadedCheerio('.gioithieutruyen.profile-info > h1').text().trim(), - chapters: [], - totalPages: Number(loadedCheerio('.phantrang > span:nth-child(2)').text()) || 1, - } - novel.cover = this.site + '/' + loadedCheerio('.img-thumb > img').attr('src'); - loadedCheerio('.thongtintruyen.note.note-info').each((idx, ele) => { - const strong = loadedCheerio(ele).find('strong').text().trim(); - switch(strong){ - case 'Tác giả': - novel.author = loadedCheerio(ele).text().replace(/Tác giả\s+\n?:/, '') - break; - case 'Trạng thái': - const text = loadedCheerio(ele).text(); - if(text.includes('Đang ra')){ - novel.status = NovelStatus.Ongoing; - }else if(text.includes('Hoàn thành')){ - novel.status = NovelStatus.Completed; - }else { - novel.status = NovelStatus.Unknown; - } - break; - case 'Thể loại': - novel.genres = loadedCheerio('a > span').toArray().map((gEle) => loadedCheerio(gEle).text()).join(','); - break; - default: - break; - } - }); - novel.summary = loadedCheerio('.gioithieutruyen > .gioithieutruyen').text().trim() - novel.chapters = this.parseChapters(loadedCheerio); - return novel; - } - async parsePage(novelPath: string, page: string): Promise { - const id = novelPath.match(/-(\d+)\//)?.[1]; - if(!id) throw new Error("Cant parse page"); - const url = `${this.site}/ajax.chuong.php?id=${id}&page=${page}&url=${novelPath.replace(/\//g,'')}&loai=truyendich`; - const body = await fetch(url).then(r => r.text()); - const chapters = this.parseChapters(parseHTML(body)); - return { - chapters - } - } - async parseChapter(chapterPath: string): Promise { - const url = this.site + chapterPath; - const body = await fetch(url).then(r => r.text()); - const loadedCheerio = parseHTML(body); - return loadedCheerio('#noidungchap').html() || ''; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - throw new Error("Method not implemented."); - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + id = 'nettruyen'; + name = 'Nettruyen'; + icon = 'src/vi/nettruyen/icon.png'; + site = 'https://nettruyen.com.vn'; + version = '1.0.0'; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('#listchuong > ul > li > a.thumb').each((idx, ele) => { + const path = ele.attribs['href']; + if (!path) return; + const name = ele.attribs['title']; + const cover = + this.site + '/' + loadedCheerio(ele).find('img').first().attr('src'); + novels.push({ + path: '/' + path, + name, + cover, + }); + }); + return novels; + } + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const url = `${this.site}/xem-nhieu/trang-${pageNo}.html`; + const body = await fetch(url).then(r => r.text()); + return this.parseNovels(parseHTML(body)); + } + parseChapters(loadedCheerio: CheerioAPI) { + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('ul.list-unstyled > li > a').each((idx, ele) => { + const path = ele.attribs['href']; + if (!path) return; + const name = ele.attribs['title']; + chapters.push({ + path: '/' + path, + name, + chapterNumber: Number(name.match(/Chương (\d+)/)?.[1]), + }); + }); + return chapters; + } + async parseNovel( + novelPath: string, + ): Promise { + const url = this.site + novelPath; + const body = await fetch(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('.gioithieutruyen.profile-info > h1').text().trim(), + chapters: [], + totalPages: + Number(loadedCheerio('.phantrang > span:nth-child(2)').text()) || 1, + }; + novel.cover = + this.site + '/' + loadedCheerio('.img-thumb > img').attr('src'); + loadedCheerio('.thongtintruyen.note.note-info').each((idx, ele) => { + const strong = loadedCheerio(ele).find('strong').text().trim(); + switch (strong) { + case 'Tác giả': + novel.author = loadedCheerio(ele) + .text() + .replace(/Tác giả\s+\n?:/, ''); + break; + case 'Trạng thái': + const text = loadedCheerio(ele).text(); + if (text.includes('Đang ra')) { + novel.status = NovelStatus.Ongoing; + } else if (text.includes('Hoàn thành')) { + novel.status = NovelStatus.Completed; + } else { + novel.status = NovelStatus.Unknown; + } + break; + case 'Thể loại': + novel.genres = loadedCheerio('a > span') + .toArray() + .map(gEle => loadedCheerio(gEle).text()) + .join(','); + break; + default: + break; + } + }); + novel.summary = loadedCheerio('.gioithieutruyen > .gioithieutruyen') + .text() + .trim(); + novel.chapters = this.parseChapters(loadedCheerio); + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const id = novelPath.match(/-(\d+)\//)?.[1]; + if (!id) throw new Error('Cant parse page'); + const url = `${this.site}/ajax.chuong.php?id=${id}&page=${page}&url=${novelPath.replace(/\//g, '')}&loai=truyendich`; + const body = await fetch(url).then(r => r.text()); + const chapters = this.parseChapters(parseHTML(body)); + return { + chapters, + }; + } + async parseChapter(chapterPath: string): Promise { + const url = this.site + chapterPath; + const body = await fetch(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + return loadedCheerio('#noidungchap').html() || ''; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + throw new Error('Method not implemented.'); + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } -export default new Nettruyen(); \ No newline at end of file +export default new Nettruyen(); diff --git a/plugins/vietnamese/truyenchu.ts b/plugins/vietnamese/truyenchu.ts index e2bc9e3..667b714 100644 --- a/plugins/vietnamese/truyenchu.ts +++ b/plugins/vietnamese/truyenchu.ts @@ -1,147 +1,159 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; -import { NovelStatus } from "@libs/novelStatus"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; class TruyenFull implements Plugin.PagePlugin { - id = "truyenchu"; - name = "Truyện Chữ"; - icon = "src/vi/truyenchu/icon.png"; - site = "https://truyenchu.vn"; - version = "1.0.0"; + id = 'truyenchu'; + name = 'Truyện Chữ'; + icon = 'src/vi/truyenchu/icon.png'; + site = 'https://truyenchu.vn'; + version = '1.0.0'; - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; - loadedCheerio(".list-truyen .row").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find("h3.truyen-title > a") - .text(); + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('.list-truyen .row').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text(); - const novelCover = this.site + loadedCheerio(ele).find("div[data-classname='cover']").attr("data-image"); + const novelCover = + this.site + + loadedCheerio(ele) + .find("div[data-classname='cover']") + .attr('data-image'); - const novelUrl = loadedCheerio(ele) - .find("h3.truyen-title > a") - .attr("href"); - if(novelUrl){ - novels.push({ - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, '') - }); - } + const novelUrl = loadedCheerio(ele) + .find('h3.truyen-title > a') + .attr('href'); + if (novelUrl) { + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), }); - return novels; - } - parseChapters(html: string){ - const listChapterHTML = - html.match(/("list_chapter":\s?\\{0}"(.+)\\{0}"),/)?.[1]; - if(!listChapterHTML) throw new Error("Không tải được chương"); - const listChapter = JSON.parse(`{${listChapterHTML}}`); - const loadedChapterList = parseHTML(listChapter.list_chapter); - const chapters: Plugin.ChapterItem[] = []; - loadedChapterList('ul > li > a').each((idx, ele) => { - const path = ele.attribs['href'].replace(this.site, ''); - if(path){ - chapters.push({ - name: ele.attribs['title'], - path, - chapterNumber: Number(path.match(/\/chuong-(\d+)/)?.[1]) - }) - } + } + }); + return novels; + } + parseChapters(html: string) { + const listChapterHTML = html.match( + /("list_chapter":\s?\\{0}"(.+)\\{0}"),/, + )?.[1]; + if (!listChapterHTML) throw new Error('Không tải được chương'); + const listChapter = JSON.parse(`{${listChapterHTML}}`); + const loadedChapterList = parseHTML(listChapter.list_chapter); + const chapters: Plugin.ChapterItem[] = []; + loadedChapterList('ul > li > a').each((idx, ele) => { + const path = ele.attribs['href'].replace(this.site, ''); + if (path) { + chapters.push({ + name: ele.attribs['title'], + path, + chapterNumber: Number(path.match(/\/chuong-(\d+)/)?.[1]), }); - return chapters; + } + }); + return chapters; + } + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const url = `${this.site}/danh-sach/truyen-hot?page=${pageNo}`; + const result = await fetch(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio); + } + async parseNovel( + novelPath: string, + ): Promise { + const url = this.site + novelPath; + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + let lastPage = 1; + const regex = new RegExp(`${novelPath}\\?page=\\d+`, 'g'); + body.match(regex)?.forEach(v => { + const page = Number(v.match(/\?page=(\d+)/)?.[1]); + if (page && page > lastPage) { + lastPage = page; + } + }); + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('div.book > img').attr('alt') || 'Không có tiêu đề', + chapters: [], + totalPages: lastPage, + }; + + novel.cover = this.site + loadedCheerio('div.book > img').attr('src'); + + novel.summary = loadedCheerio('div.desc-text').text().trim(); + + novel.author = loadedCheerio('h3:contains("Tác giả:")') + .parent() + .contents() + .text() + .replace('Tác giả:', ''); + + novel.genres = loadedCheerio('h3:contains("Thể loại")') + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text(); + if (novel.status === 'Full') { + novel.status = NovelStatus.Completed; + } else if (novel.status === 'Đang ra') { + novel.status = NovelStatus.Ongoing; + } else { + novel.status = NovelStatus.Unknown; } - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.site}/danh-sach/truyen-hot?page=${pageNo}`; - const result = await fetch(url); - const body = await result.text(); - const loadedCheerio = parseHTML(body); + novel.chapters = this.parseChapters(body); + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const url = `${this.site}${novelPath}?page=${page}`; + const result = await fetch(url); + const body = await result.text(); - return this.parseNovels(loadedCheerio); - } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - - const result = await fetch(url); - const body = await result.text(); + const chapters = this.parseChapters(body); + return { + chapters, + }; + } + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); + const body = await result.text(); - let loadedCheerio = parseHTML(body); - let lastPage = 1; - const regex = new RegExp(`${novelPath}\\?page=\\d+`, 'g'); - body.match(regex)?.forEach(v => { - const page = Number(v.match(/\?page=(\d+)/)?.[1]); - if(page && page > lastPage){ - lastPage = page; - } - }); - const novel: Plugin.SourceNovel & {totalPages: number} = { - path: novelPath, - name: loadedCheerio("div.book > img").attr("alt") || 'Không có tiêu đề', - chapters: [], - totalPages: lastPage, - }; + const loadedCheerio = parseHTML(body); - novel.cover = this.site + loadedCheerio("div.book > img").attr("src"); + let chapterText = + (loadedCheerio('.chapter-title').html() || '') + + (loadedCheerio('#chapter-c').html() || ''); - novel.summary = loadedCheerio("div.desc-text").text().trim(); + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`; - novel.author = loadedCheerio('h3:contains("Tác giả:")') - .parent() - .contents() - .text() - .replace("Tác giả:", ""); - - novel.genres = loadedCheerio('h3:contains("Thể loại")') - .siblings() - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - - novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text(); - if(novel.status === "Full"){ - novel.status = NovelStatus.Completed; - }else if(novel.status === 'Đang ra'){ - novel.status = NovelStatus.Ongoing; - }else{ - novel.status = NovelStatus.Unknown; - } - novel.chapters = this.parseChapters(body); - return novel; - } - async parsePage(novelPath: string, page: string): Promise { - const url = `${this.site}${novelPath}?page=${page}`; - const result = await fetch(url); - const body = await result.text(); - - const chapters = this.parseChapters(body); - return { - chapters - } - } - async parseChapter(chapterPath: string): Promise { - const result = await fetch(this.site + chapterPath); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - let chapterText = (loadedCheerio(".chapter-title").html() || '') + (loadedCheerio('#chapter-c').html() || ''); - - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`; - - const result = await fetch(searchUrl); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio) - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + const result = await fetch(searchUrl); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new TruyenFull(); diff --git a/plugins/vietnamese/truyenfull.ts b/plugins/vietnamese/truyenfull.ts index 59638ed..82a9c09 100644 --- a/plugins/vietnamese/truyenfull.ts +++ b/plugins/vietnamese/truyenfull.ts @@ -1,139 +1,150 @@ -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; -import { NovelStatus } from "@libs/novelStatus"; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; class TruyenFull implements Plugin.PagePlugin { - id = "truyenfull"; - name = "Truyện Full"; - icon = "src/vi/truyenfull/icon.png"; - site = "https://truyenfull.vn"; - version = "1.0.0"; + id = 'truyenfull'; + name = 'Truyện Full'; + icon = 'src/vi/truyenfull/icon.png'; + site = 'https://truyenfull.vn'; + version = '1.0.0'; - parseNovels(loadedCheerio: CheerioAPI){ - const novels: Plugin.NovelItem[] = []; - loadedCheerio(".list-truyen .row").each((idx, ele) => { - const novelName = loadedCheerio(ele) - .find("h3.truyen-title > a") - .text(); + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('.list-truyen .row').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text(); - const novelCover = loadedCheerio(ele).find("div[data-classname='cover']").attr("data-image"); + const novelCover = loadedCheerio(ele) + .find("div[data-classname='cover']") + .attr('data-image'); - const novelUrl = loadedCheerio(ele) - .find("h3.truyen-title > a") - .attr("href"); - if(novelUrl){ - novels.push({ - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, '') - }); - } + const novelUrl = loadedCheerio(ele) + .find('h3.truyen-title > a') + .attr('href'); + if (novelUrl) { + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), }); - return novels; - } - parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[]{ - return loadedCheerio('ul.list-chapter > li > a').toArray().map((ele) => { - const path = ele.attribs['href'].replace(this.site, ''); - return { - name: loadedCheerio(ele).text().trim(), - path, - chapterNumber: Number(path.match(/\/chuong-(\d+)\//)?.[1]) - } - }) - } - async popularNovels(pageNo: number, options: Plugin.PopularNovelsOptions): Promise { - const url = `${this.site}/danh-sach/truyen-hot/trang-${pageNo}/`; - - const result = await fetch(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - - return this.parseNovels(loadedCheerio); - } - async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - - const result = await fetch(url); - const body = await result.text(); - - let loadedCheerio = parseHTML(body); - let lastPage = 1; - loadedCheerio('ul.pagination.pagination-sm > li > a').each(function() { - const page = Number(this.attribs['href'].match(/\/trang-(\d+)\//)?.[1]); - if(page && page > lastPage) lastPage = page; - }); - - const novel: Plugin.SourceNovel & {totalPages: number} = { - path: novelPath, - name: loadedCheerio("div.book > img").attr("alt") || 'Không có tiêu đề', - chapters: [], - totalPages: lastPage, - }; - - novel.cover = loadedCheerio("div.book > img").attr("src"); - - novel.summary = loadedCheerio("div.desc-text").text().trim(); - - novel.author = loadedCheerio('h3:contains("Tác giả:")') - .parent() - .contents() - .text() - .replace("Tác giả:", ""); - - novel.genres = loadedCheerio('h3:contains("Thể loại")') - .siblings() - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(","); - - novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text(); - if(novel.status === "Full"){ - novel.status = NovelStatus.Completed; - }else if(novel.status === 'Đang ra'){ - novel.status = NovelStatus.Ongoing; - }else{ - novel.status = NovelStatus.Unknown; - } - novel.chapters = this.parseChapters(loadedCheerio); - return novel; - } - async parsePage(novelPath: string, page: string): Promise { - const url = `${this.site}${novelPath}trang-${page}/#list-chapter`; - const result = await fetch(url); - const body = await result.text(); - - const loadedCheerio = parseHTML(body); - const chapters = this.parseChapters(loadedCheerio); + } + }); + return novels; + } + parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[] { + return loadedCheerio('ul.list-chapter > li > a') + .toArray() + .map(ele => { + const path = ele.attribs['href'].replace(this.site, ''); return { - chapters - } + name: loadedCheerio(ele).text().trim(), + path, + chapterNumber: Number(path.match(/\/chuong-(\d+)\//)?.[1]), + }; + }); + } + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const url = `${this.site}/danh-sach/truyen-hot/trang-${pageNo}/`; + + const result = await fetch(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio); + } + async parseNovel( + novelPath: string, + ): Promise { + const url = this.site + novelPath; + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + let lastPage = 1; + loadedCheerio('ul.pagination.pagination-sm > li > a').each(function () { + const page = Number(this.attribs['href'].match(/\/trang-(\d+)\//)?.[1]); + if (page && page > lastPage) lastPage = page; + }); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('div.book > img').attr('alt') || 'Không có tiêu đề', + chapters: [], + totalPages: lastPage, + }; + + novel.cover = loadedCheerio('div.book > img').attr('src'); + + novel.summary = loadedCheerio('div.desc-text').text().trim(); + + novel.author = loadedCheerio('h3:contains("Tác giả:")') + .parent() + .contents() + .text() + .replace('Tác giả:', ''); + + novel.genres = loadedCheerio('h3:contains("Thể loại")') + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text(); + if (novel.status === 'Full') { + novel.status = NovelStatus.Completed; + } else if (novel.status === 'Đang ra') { + novel.status = NovelStatus.Ongoing; + } else { + novel.status = NovelStatus.Unknown; } - async parseChapter(chapterPath: string): Promise { - const result = await fetch(this.site + chapterPath); - const body = await result.text(); + novel.chapters = this.parseChapters(loadedCheerio); + return novel; + } + async parsePage(novelPath: string, page: string): Promise { + const url = `${this.site}${novelPath}trang-${page}/#list-chapter`; + const result = await fetch(url); + const body = await result.text(); - const loadedCheerio = parseHTML(body); + const loadedCheerio = parseHTML(body); + const chapters = this.parseChapters(loadedCheerio); + return { + chapters, + }; + } + async parseChapter(chapterPath: string): Promise { + const result = await fetch(this.site + chapterPath); + const body = await result.text(); - let chapterText = (loadedCheerio(".chapter-title").html() || '') + (loadedCheerio('#chapter-c').html() || ''); + const loadedCheerio = parseHTML(body); - return chapterText; - } - async searchNovels(searchTerm: string, pageNo: number): Promise { - const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`; + let chapterText = + (loadedCheerio('.chapter-title').html() || '') + + (loadedCheerio('#chapter-c').html() || ''); - const result = await fetch(searchUrl); - const body = await result.text(); + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`; - const loadedCheerio = parseHTML(body); - return this.parseNovels(loadedCheerio) - } - async fetchImage(url: string): Promise { - return fetchFile(url); - } + const result = await fetch(searchUrl); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + async fetchImage(url: string): Promise { + return fetchFile(url); + } } export default new TruyenFull(); diff --git a/scripts/json_plugins.ts b/scripts/json_plugins.ts index 39d22e8..f1c3389 100644 --- a/scripts/json_plugins.ts +++ b/scripts/json_plugins.ts @@ -1,27 +1,27 @@ -require("module-alias/register"); +require('module-alias/register'); -import * as fs from "fs"; -import { languages } from "@libs/languages"; -import * as path from "path"; -import { Plugin } from "@typings/plugin"; -import { minify } from "./terser"; +import * as fs from 'fs'; +import { languages } from '@libs/languages'; +import * as path from 'path'; +import { Plugin } from '@typings/plugin'; +import { minify } from './terser'; const root = path.dirname(__dirname); -const outRoot = path.join(root, ".."); -const { execSync } = require("child_process"); -const REMOTE = execSync("git remote get-url origin") +const outRoot = path.join(root, '..'); +const { execSync } = require('child_process'); +const REMOTE = execSync('git remote get-url origin') .toString() - .replace(/[\s\n]/g, ""); -let CURRENT_BRANCH = "dist"; + .replace(/[\s\n]/g, ''); +let CURRENT_BRANCH = 'dist'; try { - CURRENT_BRANCH = execSync("git rev-parse --abbrev-ref HEAD") + CURRENT_BRANCH = execSync('git rev-parse --abbrev-ref HEAD') .toString() - .replace(/[\s\n]/g, ""); + .replace(/[\s\n]/g, ''); } catch { // } const matched = REMOTE.match(/([^:/]+?)\/([^/.]+)(\.git)?$/); -if (!matched) throw Error("Cant parse git url"); +if (!matched) throw Error('Cant parse git url'); const USERNAME = matched[1]; const REPO = matched[2]; const USER_CONTENT_LINK = `https://raw.githubusercontent.com/${USERNAME}/${REPO}/${CURRENT_BRANCH}`; @@ -40,29 +40,29 @@ const json: { iconUrl: string; }[]; } = {}; -if (!fs.existsSync(path.join(outRoot, ".dist"))) { - fs.mkdirSync(path.join(outRoot, ".dist")); +if (!fs.existsSync(path.join(outRoot, '.dist'))) { + fs.mkdirSync(path.join(outRoot, '.dist')); } -const jsonPath = path.join(outRoot, ".dist", "plugins.json"); -const jsonMinPath = path.join(outRoot, ".dist", "plugins.min.json"); +const jsonPath = path.join(outRoot, '.dist', 'plugins.json'); +const jsonMinPath = path.join(outRoot, '.dist', 'plugins.min.json'); const pluginSet = new Set(); let totalPlugins = 0; for (let language in languages) { // language with English name - const langPath = path.join(root, "plugins", language.toLowerCase()); + const langPath = path.join(root, 'plugins', language.toLowerCase()); if (!fs.existsSync(langPath)) continue; const plugins = fs.readdirSync(langPath); json[language] = []; - plugins.forEach((plugin) => { - if (plugin.startsWith(".")) return; + plugins.forEach(plugin => { + if (plugin.startsWith('.')) return; minify(path.join(langPath, plugin)); const instance: Plugin.PluginBase = require( - `../plugins/${language.toLowerCase()}/${plugin.split(".")[0]}`, + `../plugins/${language.toLowerCase()}/${plugin.split('.')[0]}`, ).default; const { id, name, site, version, icon } = instance; - const normalisedName = name.replace(/\[.*\]/, ""); + const normalisedName = name.replace(/\[.*\]/, ''); const info = { id, @@ -76,44 +76,44 @@ for (let language in languages) { if (pluginSet.has(id)) { console.log("There's already a plugin with id:", id); - throw new Error("2 or more plugins have the same id"); + throw new Error('2 or more plugins have the same id'); } else { pluginSet.add(id); } json[language].push(info); totalPlugins += 1; - console.log(name, "✅"); + console.log(name, '✅'); }); } for (let lang in json) json[lang].sort((a, b) => a.id.localeCompare(b.id)); fs.writeFileSync(jsonMinPath, JSON.stringify(json)); -fs.writeFileSync(jsonPath, JSON.stringify(json, null, "\t")); -if(CURRENT_BRANCH === 'dist'){ +fs.writeFileSync(jsonPath, JSON.stringify(json, null, '\t')); +if (CURRENT_BRANCH === 'dist') { fetch(`https://img.shields.io/badge/Plugins-${totalPlugins}-blue`) .then(res => res.text()) .then(data => { - fs.writeFileSync('total.svg', data, {encoding: 'utf-8'}); - }) + fs.writeFileSync('total.svg', data, { encoding: 'utf-8' }); + }); } // check for broken plugins for (let language in languages) { const tsFiles = fs.readdirSync( - path.join(root, "..", "plugins", language.toLocaleLowerCase()) + path.join(root, '..', 'plugins', language.toLocaleLowerCase()), ); tsFiles - .filter((f) => f.endsWith(".broken.ts")) - .forEach((fn) => { + .filter(f => f.endsWith('.broken.ts')) + .forEach(fn => { console.error( language.toLocaleLowerCase() + - "/" + - fn.replace(".broken.ts", "") + - " ❌" + '/' + + fn.replace('.broken.ts', '') + + ' ❌', ); }); } console.log(jsonPath); -console.log("Done ✅"); +console.log('Done ✅'); diff --git a/scripts/multisrc/generate.ts b/scripts/multisrc/generate.ts index 44d062a..ea3d752 100644 --- a/scripts/multisrc/generate.ts +++ b/scripts/multisrc/generate.ts @@ -1,62 +1,62 @@ -import path from "path"; -import fs from "fs"; +import path from 'path'; +import fs from 'fs'; type GeneratedScript = { - lang: string; - filename: string; - pluginScript: string; + lang: string; + filename: string; + pluginScript: string; }; export type ScrpitGeneratorFunction = (name: string) => GeneratedScript[]; const isScriptGenerator = (s: unknown): s is ScrpitGeneratorFunction => { - return !!s && typeof s === "function"; + return !!s && typeof s === 'function'; }; const generate = async (name: string): Promise => { - try { - const generateAll: ScrpitGeneratorFunction | unknown = - require(`./${name}/generator`).generateAll; - if (!isScriptGenerator(generateAll)) return false; - const sources = generateAll(name); - for (let source of sources) { - const { lang, filename, pluginScript } = source; - if (!lang || !filename || !pluginScript) { - console.warn( - name, - ": lang, filename, pluginScript are required!" - ); - continue; - } - const pluginsDir = path.join( - path.dirname(path.dirname(__dirname)), - "plugins" - ); - const filePath = path.join( - pluginsDir, - lang.toLowerCase(), - filename.replace(/[\s-\.]+/g, "") + `[${name}].ts` - ); - fs.writeFileSync(filePath, pluginScript, { encoding: "utf-8" }); - } - return true; - } catch (e) { - console.log(`${name} is broken! ${e}`); - return false; + try { + const generateAll: ScrpitGeneratorFunction | unknown = require( + `./${name}/generator`, + ).generateAll; + if (!isScriptGenerator(generateAll)) return false; + const sources = generateAll(name); + for (let source of sources) { + const { lang, filename, pluginScript } = source; + if (!lang || !filename || !pluginScript) { + console.warn(name, ': lang, filename, pluginScript are required!'); + continue; + } + const pluginsDir = path.join( + path.dirname(path.dirname(__dirname)), + 'plugins', + ); + const filePath = path.join( + pluginsDir, + lang.toLowerCase(), + filename.replace(/[\s-\.]+/g, '') + `[${name}].ts`, + ); + fs.writeFileSync(filePath, pluginScript, { encoding: 'utf-8' }); } + return true; + } catch (e) { + console.log(`${name} is broken! ${e}`); + return false; + } }; const run = async () => { - const sources = fs - .readdirSync(__dirname) - .filter((name) => - fs.lstatSync(path.join(__dirname, name)).isDirectory() && !name.endsWith('.broken') - ); + const sources = fs + .readdirSync(__dirname) + .filter( + name => + fs.lstatSync(path.join(__dirname, name)).isDirectory() && + !name.endsWith('.broken'), + ); - for (let name of sources) { - const success = await generate(name); - if (success) console.log(`[${name}] OK`); - } + for (let name of sources) { + const success = await generate(name); + if (success) console.log(`[${name}] OK`); + } }; run(); diff --git a/scripts/multisrc/hotnovelpub/filter_refresh.ts b/scripts/multisrc/hotnovelpub/filter_refresh.ts index 788b107..937057d 100644 --- a/scripts/multisrc/hotnovelpub/filter_refresh.ts +++ b/scripts/multisrc/hotnovelpub/filter_refresh.ts @@ -1,43 +1,43 @@ -import * as fs from "fs"; -import * as cheerio from "cheerio"; -import * as path from "path"; -import list from "./sources.json"; -import { HotNovelPubMetadata } from "./template"; +import * as fs from 'fs'; +import * as cheerio from 'cheerio'; +import * as path from 'path'; +import list from './sources.json'; +import { HotNovelPubMetadata } from './template'; async function getFilters(sources: HotNovelPubMetadata) { const filters: any = { sort: { - type: "Picker", - label: "Order", - value: "hot", + type: 'Picker', + label: 'Order', + value: 'hot', options: [], }, category: { - type: "Picker", - label: "category", - value: "", - options: [{ label: "NONE", value: "" }], + type: 'Picker', + label: 'category', + value: '', + options: [{ label: 'NONE', value: '' }], }, }; - const body = await fetch(sources.sourceSite).then((res) => res.text()); + const body = await fetch(sources.sourceSite).then(res => res.text()); const $: cheerio.CheerioAPI = cheerio.load(body); - $(".new-update").remove(); + $('.new-update').remove(); - $("section > div").each((i, el) => { - const id = $(el).find('a[class="see-all"]').attr("href"); + $('section > div').each((i, el) => { + const id = $(el).find('a[class="see-all"]').attr('href'); if (id) { filters.sort.options.push({ label: $(el).find('[class="section-title"]').text().trim(), - value: id.split("/").pop(), + value: id.split('/').pop(), }); } }); - filters.category.label = $(".category-title").text().trim(); - const apiSite = sources.sourceSite.replace("://", "://api."); - const jsonRaw = await fetch(apiSite + "/categories", { + filters.category.label = $('.category-title').text().trim(); + const apiSite = sources.sourceSite.replace('://', '://api.'); + const jsonRaw = await fetch(apiSite + '/categories', { headers: { - lang: sources.options?.lang || "en", + lang: sources.options?.lang || 'en', }, }); const json = (await jsonRaw.json()) as response; @@ -45,7 +45,7 @@ async function getFilters(sources: HotNovelPubMetadata) { if (json.data?.length) { json.data .sort((a, b) => a.name.localeCompare(b.name)) - .forEach((category) => + .forEach(category => filters.category.options.push({ label: category.name, value: category.slug, @@ -69,13 +69,13 @@ interface DataEntity { async function start() { const result = []; for (const sources of list) { - console.log("updating the filters in", sources.sourceName); + console.log('updating the filters in', sources.sourceName); const NewFilters = await getFilters(sources as any); sources.filters = NewFilters; result.push(sources); } fs.writeFileSync( - path.join(__dirname, "sources.json"), + path.join(__dirname, 'sources.json'), JSON.stringify(result, null, 2), ); } diff --git a/scripts/multisrc/hotnovelpub/generator.ts b/scripts/multisrc/hotnovelpub/generator.ts index 8632210..470306e 100644 --- a/scripts/multisrc/hotnovelpub/generator.ts +++ b/scripts/multisrc/hotnovelpub/generator.ts @@ -1,21 +1,21 @@ -import { FilterTypes, Filters } from "../../../libs/filterInputs"; -import { ScrpitGeneratorFunction } from "../generate"; -import list from "./sources.json"; -import { HotNovelPubMetadata } from "./template"; -import { readFileSync } from "fs"; -import path from "path"; +import { FilterTypes, Filters } from '../../../libs/filterInputs'; +import { ScrpitGeneratorFunction } from '../generate'; +import list from './sources.json'; +import { HotNovelPubMetadata } from './template'; +import { readFileSync } from 'fs'; +import path from 'path'; const lang: { [key: string]: string } = { - en: "english", - ru: "russian", - es: "spanish", - pt: "portuguese", - th: "turkish", + en: 'english', + ru: 'russian', + es: 'spanish', + pt: 'portuguese', + th: 'turkish', }; export const generateAll: ScrpitGeneratorFunction = function (name) { return list - .map((p) => { + .map(p => { const filters: Filters = {}; for (const k in p.filters) { const f = p.filters[k as keyof typeof p.filters]; @@ -36,8 +36,9 @@ export const generateAll: ScrpitGeneratorFunction = function (name) { const generator = function generator(metadata: HotNovelPubMetadata) { const HotNovelPubTemplate = readFileSync( - path.join(__dirname, "template.ts"), { - encoding: "utf-8", + path.join(__dirname, 'template.ts'), + { + encoding: 'utf-8', }, ); @@ -51,7 +52,7 @@ export default plugin; `.trim(); return { - lang: lang[metadata?.options?.lang || "en"] || "english", + lang: lang[metadata?.options?.lang || 'en'] || 'english', filename: metadata.sourceName, pluginScript, }; diff --git a/scripts/multisrc/hotnovelpub/template.ts b/scripts/multisrc/hotnovelpub/template.ts index 87fb93d..806e37d 100644 --- a/scripts/multisrc/hotnovelpub/template.ts +++ b/scripts/multisrc/hotnovelpub/template.ts @@ -1,9 +1,9 @@ -import { fetchFile, fetchApi } from "@libs/fetch"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { fetchFile, fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; export interface HotNovelPubMetadata { id: string; @@ -32,23 +32,23 @@ class HotNovelPubPlugin implements Plugin.PluginBase { this.name = metadata.sourceName; this.icon = `multisrc/hotnovelpub/icons/${metadata.id}.png`; this.site = metadata.sourceSite; - this.apiSite = metadata.sourceSite.replace("://", "://api."); - this.version = "1.0.0"; + this.apiSite = metadata.sourceSite.replace('://', '://api.'); + this.version = '1.0.0'; this.filters = metadata.filters; - this.lang = metadata.options?.lang || "en"; + this.lang = metadata.options?.lang || 'en'; } async popularNovels( pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.apiSite + "/books/"; - url += showLatestNovels ? "new" : filters?.sort?.value || "hot"; + let url = this.apiSite + '/books/'; + url += showLatestNovels ? 'new' : filters?.sort?.value || 'hot'; if (filters?.category?.value) { - url = this.apiSite + "/category/" + filters.category.value; + url = this.apiSite + '/category/' + filters.category.value; } - url += "/?page=" + (pageNo - 1) + "&limit=20"; + url += '/?page=' + (pageNo - 1) + '&limit=20'; const result = await fetchApi(url, { headers: { @@ -59,11 +59,11 @@ class HotNovelPubPlugin implements Plugin.PluginBase { const novels: Plugin.NovelItem[] = []; if (json.status && json.data.books.data?.length) { - json.data.books.data.forEach((novel) => + json.data.books.data.forEach(novel => novels.push({ name: novel.name, cover: this.site + novel.image, - path: "/" + novel.slug, + path: '/' + novel.slug, }), ); } @@ -71,7 +71,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const result = await fetchApi(this.apiSite + "/book" + novelPath, { + const result = await fetchApi(this.apiSite + '/book' + novelPath, { headers: { lang: this.lang, }, @@ -85,13 +85,13 @@ class HotNovelPubPlugin implements Plugin.PluginBase { summary: json.data.book.authorize.description, author: json.data.book.authorize.name, status: - json.data.book.status === "updating" + json.data.book.status === 'updating' ? NovelStatus.Ongoing : NovelStatus.Completed, }; if (json.data.tags.tags_name?.length) { - novel.genres = json.data.tags.tags_name.join(","); + novel.genres = json.data.tags.tags_name.join(','); } if (json.data.chapters?.length) { @@ -99,7 +99,7 @@ class HotNovelPubPlugin implements Plugin.PluginBase { json.data.chapters.forEach((chapter, chapterIndex) => chapters.push({ name: chapter.title, - path: "/" + chapter.slug, + path: '/' + chapter.slug, releaseTime: undefined, chapterNumber: (chapter.index || chapterIndex) + 1, }), @@ -111,50 +111,52 @@ class HotNovelPubPlugin implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); - let chapterText = loadedCheerio("#content-item").html() || ""; + let chapterText = loadedCheerio('#content-item').html() || ''; if (chapterText) { const result = await fetchApi( - this.site + "/server/getContent?slug=" + chapterPath, + this.site + '/server/getContent?slug=' + chapterPath, ); const json = (await result.json()) as ChapterType; if (json.data) { chapterText += json.data - .map((item) => "

" + item + "

") - .join("") - .replace(/\n/g, "

") - .replace(/\s/g, " "); + .map(item => '

' + item + '

') + .join('') + .replace(/\n/g, '

') + .replace(/\s/g, ' '); } } - return chapterText.replace(/\.copy right hot novel pub/g, ""); + return chapterText.replace(/\.copy right hot novel pub/g, ''); } async searchNovels( searchTerm: string, pageNo: number, ): Promise { - const result = await fetchApi(this.apiSite + "/search", { + const result = await fetchApi(this.apiSite + '/search', { headers: { - "Content-Type": "application/json;charset=utf-8", + 'Content-Type': 'application/json;charset=utf-8', Referer: this.site, Origin: this.site, lang: this.lang, }, - method: "POST", + method: 'POST', body: JSON.stringify({ key_search: searchTerm }), }); const json = (await result.json()) as responseSearch; const novels: Plugin.NovelItem[] = []; if (json.status && json.data.books?.length) { - json.data.books.forEach((novel) => + json.data.books.forEach(novel => novels.push({ name: novel.name, cover: undefined, - path: "/" + novel.slug, + path: '/' + novel.slug, }), ); } diff --git a/scripts/multisrc/ifreedom/generator.ts b/scripts/multisrc/ifreedom/generator.ts index 0dc8c4c..4269d31 100644 --- a/scripts/multisrc/ifreedom/generator.ts +++ b/scripts/multisrc/ifreedom/generator.ts @@ -1,10 +1,10 @@ -import { Filters } from "../../../libs/filterInputs"; -import { ScrpitGeneratorFunction } from "../generate"; -import list from "./sources.json"; -import defaultSettings from "./settings.json"; -import { IfreedomMetadata } from "./template"; -import { readFileSync } from "fs"; -import path from "path"; +import { Filters } from '../../../libs/filterInputs'; +import { ScrpitGeneratorFunction } from '../generate'; +import list from './sources.json'; +import defaultSettings from './settings.json'; +import { IfreedomMetadata } from './template'; +import { readFileSync } from 'fs'; +import path from 'path'; export const generateAll: ScrpitGeneratorFunction = function (name) { return list.map((metadata: IfreedomMetadata) => { @@ -15,8 +15,8 @@ export const generateAll: ScrpitGeneratorFunction = function (name) { }; const generator = function generator(metadata: IfreedomMetadata) { - const IfreedomTemplate = readFileSync(path.join(__dirname, "template.ts"), { - encoding: "utf-8", + const IfreedomTemplate = readFileSync(path.join(__dirname, 'template.ts'), { + encoding: 'utf-8', }); const pluginScript = ` @@ -29,7 +29,7 @@ export default plugin; `.trim(); return { - lang: "russian", + lang: 'russian', filename: metadata.sourceName, pluginScript, }; diff --git a/scripts/multisrc/ifreedom/template.ts b/scripts/multisrc/ifreedom/template.ts index 13d0ef1..517063d 100644 --- a/scripts/multisrc/ifreedom/template.ts +++ b/scripts/multisrc/ifreedom/template.ts @@ -1,9 +1,9 @@ -import { fetchFile, fetchApi } from "@libs/fetch"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { fetchFile, fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; export interface IfreedomMetadata { id: string; @@ -25,7 +25,7 @@ class IfreedomPlugin implements Plugin.PluginBase { this.name = metadata.sourceName; this.icon = `multisrc/ifreedom/icons/${metadata.id}.png`; this.site = metadata.sourceSite; - this.version = "1.0.0"; + this.version = '1.0.0'; this.filters = metadata.filters; } @@ -36,87 +36,95 @@ class IfreedomPlugin implements Plugin.PluginBase { showLatestNovels, }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/vse-knigi/?sort=" + - (showLatestNovels ? "По дате обновления" : filters?.sort?.value || "По рейтингу"); + let url = + this.site + + '/vse-knigi/?sort=' + + (showLatestNovels + ? 'По дате обновления' + : filters?.sort?.value || 'По рейтингу'); Object.entries(filters || {}).forEach(([type, { value }]) => { if (value instanceof Array && value.length) { - url += "&" + type + "[]=" + value.join("&" + type + "[]="); + url += '&' + type + '[]=' + value.join('&' + type + '[]='); } }); - url += "&bpage=" + page; + url += '&bpage=' + page; - const body = await fetchApi(url).then((res) => res.text()); + const body = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = loadedCheerio( - "div.one-book-home > div.img-home a", + 'div.one-book-home > div.img-home a', ) .map((index, element) => ({ - name: loadedCheerio(element).attr("title") || "", - cover: loadedCheerio(element).find("img").attr("src"), + name: loadedCheerio(element).attr('title') || '', + cover: loadedCheerio(element).find('img').attr('src'), path: - loadedCheerio(element).attr("href")?.replace?.(this.site, "") || "", + loadedCheerio(element).attr('href')?.replace?.(this.site, '') || '', })) .get() - .filter((novel) => novel.name && novel.path); + .filter(novel => novel.name && novel.path); return novels; } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const body = await fetchApi(this.site + novelPath).then(res => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, - name: loadedCheerio(".entry-title").text(), - cover: loadedCheerio(".img-ranobe > img").attr("src"), - summary: loadedCheerio('meta[name="description"]').attr("content"), + name: loadedCheerio('.entry-title').text(), + cover: loadedCheerio('.img-ranobe > img').attr('src'), + summary: loadedCheerio('meta[name="description"]').attr('content'), }; - loadedCheerio("div.data-ranobe").each(function () { - switch (loadedCheerio(this).find("b").text()) { - case "Автор": + loadedCheerio('div.data-ranobe').each(function () { + switch (loadedCheerio(this).find('b').text()) { + case 'Автор': novel.author = loadedCheerio(this) - .find("div.data-value") + .find('div.data-value') .text() .trim(); break; - case "Жанры": - novel.genres = loadedCheerio("div.data-value > a") + case 'Жанры': + novel.genres = loadedCheerio('div.data-value > a') .map((index, element) => loadedCheerio(element).text()?.trim()) .get() - .join(","); + .join(','); break; - case "Статус книги": - novel.status = loadedCheerio("div.data-value") + case 'Статус книги': + novel.status = loadedCheerio('div.data-value') .text() - .includes("активен") + .includes('активен') ? NovelStatus.Ongoing : NovelStatus.Completed; break; } }); - if (novel.author == "Не указан") delete novel.author; + if (novel.author == 'Не указан') delete novel.author; const chapters: Plugin.ChapterItem[] = []; - const totalChapters = loadedCheerio("div.li-ranobe").length; + const totalChapters = loadedCheerio('div.li-ranobe').length; - loadedCheerio("div.li-ranobe").each((chapterIndex, element) => { - const name = loadedCheerio(element).find("a").text(); - const url = loadedCheerio(element).find("a").attr("href"); - if (!loadedCheerio(element).find("label.buy-ranobe").length && name && url) { + loadedCheerio('div.li-ranobe').each((chapterIndex, element) => { + const name = loadedCheerio(element).find('a').text(); + const url = loadedCheerio(element).find('a').attr('href'); + if ( + !loadedCheerio(element).find('label.buy-ranobe').length && + name && + url + ) { const releaseDate = loadedCheerio(element) - .find("div.li-col2-ranobe") + .find('div.li-col2-ranobe') .text() .trim(); chapters.push({ name, - path: url.replace(this.site, ""), + path: url.replace(this.site, ''), releaseTime: this.parseDate(releaseDate), chapterNumber: totalChapters - chapterIndex, }); @@ -128,21 +136,23 @@ class IfreedomPlugin implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); - loadedCheerio(".entry-content img").each((index, element) => { - const srcset = loadedCheerio(element).attr("srcset")?.split?.(" "); + loadedCheerio('.entry-content img').each((index, element) => { + const srcset = loadedCheerio(element).attr('srcset')?.split?.(' '); if (srcset?.length) { - loadedCheerio(element).removeAttr("srcset"); - const bestlink: string[] = srcset.filter((url) => url.startsWith("http")); + loadedCheerio(element).removeAttr('srcset'); + const bestlink: string[] = srcset.filter(url => url.startsWith('http')); if (bestlink[bestlink.length - 1]) { - loadedCheerio(element).attr("src", bestlink[bestlink.length - 1]); + loadedCheerio(element).attr('src', bestlink[bestlink.length - 1]); } } }); - const chapterText = loadedCheerio(".entry-content").html() || ""; + const chapterText = loadedCheerio('.entry-content').html() || ''; return chapterText; } @@ -151,25 +161,26 @@ class IfreedomPlugin implements Plugin.PluginBase { page: number | undefined = 1, ): Promise { const url = - this.site + "/vse-knigi/?searchname=" + searchTerm + "&bpage=" + page; - const result = await fetchApi(url).then((res) => res.text()); + this.site + '/vse-knigi/?searchname=' + searchTerm + '&bpage=' + page; + const result = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(result); const novels: Plugin.NovelItem[] = loadedCheerio( - "div.one-book-home > div.img-home a", + 'div.one-book-home > div.img-home a', ) .map((index, element) => ({ - name: loadedCheerio(element).attr("title") || "", - cover: loadedCheerio(element).find("img").attr("src"), - path: loadedCheerio(element).attr("href")?.replace?.(this.site, "") || "", + name: loadedCheerio(element).attr('title') || '', + cover: loadedCheerio(element).find('img').attr('src'), + path: + loadedCheerio(element).attr('href')?.replace?.(this.site, '') || '', })) .get() - .filter((novel) => novel.name && novel.path); + .filter(novel => novel.name && novel.path); return novels; } - parseDate = (dateString: string | undefined = "") => { + parseDate = (dateString: string | undefined = '') => { const months: { [key: string]: number } = { января: 1, февраля: 2, @@ -185,16 +196,16 @@ class IfreedomPlugin implements Plugin.PluginBase { декабря: 12, }; - if (dateString.includes(".")) { - const [day, month, year] = dateString.split("."); + if (dateString.includes('.')) { + const [day, month, year] = dateString.split('.'); if (day && month && year) { - return dayjs(year + "-" + month + "-" + day).format("LL"); + return dayjs(year + '-' + month + '-' + day).format('LL'); } - } else if (dateString.includes(" ")) { - const [day, month] = dateString.split(" "); + } else if (dateString.includes(' ')) { + const [day, month] = dateString.split(' '); if (day && months[month]) { const year = new Date().getFullYear(); - return dayjs(year + "-" + months[month] + "-" + day).format("LL"); + return dayjs(year + '-' + months[month] + '-' + day).format('LL'); } } return dateString || null; diff --git a/scripts/multisrc/lightnovelwp/generator.ts b/scripts/multisrc/lightnovelwp/generator.ts index 57988aa..ab4ee19 100644 --- a/scripts/multisrc/lightnovelwp/generator.ts +++ b/scripts/multisrc/lightnovelwp/generator.ts @@ -1,8 +1,8 @@ -import { ScrpitGeneratorFunction } from "../generate"; -import { LightNovelWPMetadata } from "./template"; -import list from "./sources.json"; -import { readFileSync } from "fs"; -import path from "path"; +import { ScrpitGeneratorFunction } from '../generate'; +import { LightNovelWPMetadata } from './template'; +import list from './sources.json'; +import { readFileSync } from 'fs'; +import path from 'path'; export const generateAll: ScrpitGeneratorFunction = function (name) { return list.map((source: LightNovelWPMetadata) => { @@ -11,15 +11,20 @@ export const generateAll: ScrpitGeneratorFunction = function (name) { filters = require(`./filters/${source.id}`); source.filters = filters.filters; } catch (e) {} - console.log(`[${name}] Generating: ${source.id}${" ".repeat(20 - source.id.length)} ${source.filters ? "🔎with filters🔍" : "🚫no filters🚫"}`); + console.log( + `[${name}] Generating: ${source.id}${' '.repeat(20 - source.id.length)} ${source.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`, + ); return generator(source); }); }; const generator = function generator(source: LightNovelWPMetadata) { - const LightNovelWPTemplate = readFileSync(path.join(__dirname, "template.ts"),{ - encoding: "utf-8", - }); + const LightNovelWPTemplate = readFileSync( + path.join(__dirname, 'template.ts'), + { + encoding: 'utf-8', + }, + ); const pluginScript = ` ${LightNovelWPTemplate} @@ -28,7 +33,7 @@ export default plugin; `.trim(); return { - lang: source.options?.lang || "English", + lang: source.options?.lang || 'English', filename: source.sourceName, pluginScript, }; diff --git a/scripts/multisrc/lightnovelwp/get_filters.ts b/scripts/multisrc/lightnovelwp/get_filters.ts index 8d51920..8456d3a 100644 --- a/scripts/multisrc/lightnovelwp/get_filters.ts +++ b/scripts/multisrc/lightnovelwp/get_filters.ts @@ -3,99 +3,123 @@ import * as cheerio from 'cheerio'; import * as path from 'path'; function getFilters(name: string, html: string) { - const $:cheerio.CheerioAPI = cheerio.load(html); - const filters: any = {filters: { - "genre[]": { - type: "Checkbox", - label: "Genre", - value: [], - options: [] - }, - "type[]": { - type: "Checkbox", - label: "Type", - value: [], - options: [] - }, - "status": { - type: "Picker", - label: "Status", - value: "", - options: [] - }, - "order": { - type: "Picker", - label: "Order", - value: "", - options: [] - }, - }}; + const $: cheerio.CheerioAPI = cheerio.load(html); + const filters: any = { + filters: { + 'genre[]': { + type: 'Checkbox', + label: 'Genre', + value: [], + options: [], + }, + 'type[]': { + type: 'Checkbox', + label: 'Type', + value: [], + options: [], + }, + 'status': { + type: 'Picker', + label: 'Status', + value: '', + options: [], + }, + 'order': { + type: 'Picker', + label: 'Order', + value: '', + options: [], + }, + }, + }; - const filtersContainer = $('div.quickfilter').find("ul"); - filtersContainer.each((i, el) => { - const filterName = Object.keys(filters.filters)[i]; - filters.filters[filterName].label = $(el).prev().contents().first().text().trim(); - $(el).find("li").each((j, li) => { - filters.filters[filterName].options.push({ - label: $(li).text().trim(), - value: decodeURI($(li).find("input").attr("value") || "") - }); - }); - }); - fs.writeFileSync( - path.join(__dirname, 'filters', name + ".json"), - JSON.stringify(filters, null, 2) - ); + const filtersContainer = $('div.quickfilter').find('ul'); + filtersContainer.each((i, el) => { + const filterName = Object.keys(filters.filters)[i]; + filters.filters[filterName].label = $(el) + .prev() + .contents() + .first() + .text() + .trim(); + $(el) + .find('li') + .each((j, li) => { + filters.filters[filterName].options.push({ + label: $(li).text().trim(), + value: decodeURI($(li).find('input').attr('value') || ''), + }); + }); + }); + fs.writeFileSync( + path.join(__dirname, 'filters', name + '.json'), + JSON.stringify(filters, null, 2), + ); } async function getFiltersFromURL(name: string, url: string) { - const response = await fetch(url + "/series/"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}, while fetching ${response.url}`); - } - const html = await response.text(); - try { - getFilters(name, html); - } catch (e) { - console.error("Error while getting filters from", url); - } + const response = await fetch(url + '/series/'); + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status}, while fetching ${response.url}`, + ); + } + const html = await response.text(); + try { + getFilters(name, html); + } catch (e) { + console.error('Error while getting filters from', url); + } } +async function askGetFilter() { + const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout, + }); -async function askGetFilter(){ - const readline = require('readline').createInterface({ - input: process.stdin, - output: process.stdout - }); - - const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; - await readline.question('Enter the id of the site (same one as in sources.json): ', async (name: string) => { - await readline.question(EREASE_PREV_LINE + 'Do you want to get the filters from a URL or the html text? (if url dosen\'t work try html) (url/html): ', async (method: string) => { - if (method.toLowerCase() === "url") { - await readline.question(EREASE_PREV_LINE + 'Enter the URL (same one as in sources.json): ', async (url: string) => { - try { - await getFiltersFromURL(name, url); - } catch (e: any) { - console.error("Error while getting filters from", url); - console.log(e.message || e); - } - readline.close(); - }); - } else { - process.stdout.write(EREASE_PREV_LINE + `Enter the html text from the page at {sourceSite}/series (at the end press ENTER then press CTRL+C) -(to make it faster you can run \`$(\"div.quickfilter\").parent().html()\` in the console to get only the important html part): `) - let html = ""; - readline.on('SIGINT', () => { - console.log("Creating filters file"); - getFilters(name, html); - readline.close(); - }); - readline.on('line', (line: string) => { - html += line + "\n"; - }); - } - }); - }); + const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; + await readline.question( + 'Enter the id of the site (same one as in sources.json): ', + async (name: string) => { + await readline.question( + EREASE_PREV_LINE + + "Do you want to get the filters from a URL or the html text? (if url dosen't work try html) (url/html): ", + async (method: string) => { + if (method.toLowerCase() === 'url') { + await readline.question( + EREASE_PREV_LINE + + 'Enter the URL (same one as in sources.json): ', + async (url: string) => { + try { + await getFiltersFromURL(name, url); + } catch (e: any) { + console.error('Error while getting filters from', url); + console.log(e.message || e); + } + readline.close(); + }, + ); + } else { + process.stdout.write( + EREASE_PREV_LINE + + `Enter the html text from the page at {sourceSite}/series (at the end press ENTER then press CTRL+C) +(to make it faster you can run \`$(\"div.quickfilter\").parent().html()\` in the console to get only the important html part): `, + ); + let html = ''; + readline.on('SIGINT', () => { + console.log('Creating filters file'); + getFilters(name, html); + readline.close(); + }); + readline.on('line', (line: string) => { + html += line + '\n'; + }); + } + }, + ); + }, + ); } askGetFilter(); diff --git a/scripts/multisrc/lightnovelwp/template.ts b/scripts/multisrc/lightnovelwp/template.ts index 4542453..c2470b9 100644 --- a/scripts/multisrc/lightnovelwp/template.ts +++ b/scripts/multisrc/lightnovelwp/template.ts @@ -1,9 +1,9 @@ -import { CheerioAPI, load } from "cheerio"; -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; -import { defaultCover } from "@libs/defaultCover"; -import { Filters } from "@libs/filterInputs"; +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { defaultCover } from '@libs/defaultCover'; +import { Filters } from '@libs/filterInputs'; interface LightNovelWPOptions { reverseChapters?: boolean; @@ -40,50 +40,64 @@ class LightNovelWPPlugin implements Plugin.PluginBase { } getHostname(url: string): string { - return url.split("/")[2]; + return url.split('/')[2]; } async getCheerio(url: string, search: boolean): Promise { const r = await fetchApi(url); - if (!r.ok && search != true) throw new Error("Could not reach site (" + r.status + ") try to open in webview."); + if (!r.ok && search != true) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); const $ = load(await r.text()); - const title = $("title").text().trim(); - if (this.getHostname(url) != this.getHostname(r.url) || - title == "Bot Verification" || title == "You are being redirected..." || title == "Un instant..." || title == "Just a moment..." || title == "Redirecting...") - throw new Error("Captcha error, please open in webview"); + const title = $('title').text().trim(); + if ( + this.getHostname(url) != this.getHostname(r.url) || + title == 'Bot Verification' || + title == 'You are being redirected...' || + title == 'Un instant...' || + title == 'Just a moment...' || + title == 'Redirecting...' + ) + throw new Error('Captcha error, please open in webview'); - return ($); + return $; } parseNovels($: CheerioAPI): Plugin.NovelItem[] { const novels: Plugin.NovelItem[] = []; - $("div.listupd > article").each((i, elem) => { - const novelName = $(elem).find("h2").text(); - const image = $(elem).find("img"); - const novelUrl = $(elem).find("a").attr("href"); + $('div.listupd > article').each((i, elem) => { + const novelName = $(elem).find('h2').text(); + const image = $(elem).find('img'); + const novelUrl = $(elem).find('a').attr('href'); if (novelUrl) { novels.push({ name: novelName, - cover: image.attr("data-src") || image.attr("src") || defaultCover, - path: novelUrl.replace(this.site, ""), + cover: image.attr('data-src') || image.attr('src') || defaultCover, + path: novelUrl.replace(this.site, ''), }); } }); return novels; } - async popularNovels(pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions ): Promise { - let url = this.site + "/series/?page=" + pageNo; + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.site + '/series/?page=' + pageNo; if (!filters) filters = {}; - if (showLatestNovels) url += "&order=latest"; + if (showLatestNovels) url += '&order=latest'; for (const key in filters) { - if (typeof filters[key].value === "object") + if (typeof filters[key].value === 'object') for (const value of filters[key].value as string[]) url += `&${key}=${value}`; - else if (filters[key].value) - url += `&${key}=${filters[key].value}`; + else if (filters[key].value) url += `&${key}=${filters[key].value}`; } const $ = await this.getCheerio(url, false); return this.parseNovels($); @@ -92,19 +106,22 @@ class LightNovelWPPlugin implements Plugin.PluginBase { async parseNovel(novelPath: string): Promise { const $ = await this.getCheerio(this.site + novelPath, false); - const novel: Plugin.SourceNovel = { path: novelPath.replace(this.site, ""), name: "Untitled" }; + const novel: Plugin.SourceNovel = { + path: novelPath.replace(this.site, ''), + name: 'Untitled', + }; - novel.name = $("h1.entry-title").text().trim(); - const image = $("img.wp-post-image"); - novel.cover = image.attr("data-src") || image.attr("src") || defaultCover; - switch ($("div.sertostat > span").attr("class")?.toLowerCase() || "") { - case "completed": + novel.name = $('h1.entry-title').text().trim(); + const image = $('img.wp-post-image'); + novel.cover = image.attr('data-src') || image.attr('src') || defaultCover; + switch ($('div.sertostat > span').attr('class')?.toLowerCase() || '') { + case 'completed': novel.status = NovelStatus.Completed; break; - case "ongoing": + case 'ongoing': novel.status = NovelStatus.Ongoing; break; - case "hiatus": + case 'hiatus': novel.status = NovelStatus.OnHiatus; break; default: @@ -112,49 +129,55 @@ class LightNovelWPPlugin implements Plugin.PluginBase { break; } - let details = $("div.serl"); - if (!details.length) details = $("div.spe > span"); + let details = $('div.serl'); + if (!details.length) details = $('div.spe > span'); details.each(function () { - const detailName = $(this).contents().first().text().replace(":", "").trim().toLowerCase(); + const detailName = $(this) + .contents() + .first() + .text() + .replace(':', '') + .trim() + .toLowerCase(); const detail = $(this).contents().last().text().trim().toLowerCase(); switch (detailName) { - case "الكاتب": - case "author": - case "auteur": - case "autor": - case "yazar": + case 'الكاتب': + case 'author': + case 'auteur': + case 'autor': + case 'yazar': novel.author = detail; break; - case "الحالة": - case "status": - case "statut": - case "estado": - case "durum": + case 'الحالة': + case 'status': + case 'statut': + case 'estado': + case 'durum': switch (detail) { - case "مكتملة": - case "completed": - case "complété": - case "completo": - case "completado": - case "tamamlandı": + case 'مكتملة': + case 'completed': + case 'complété': + case 'completo': + case 'completado': + case 'tamamlandı': novel.status = NovelStatus.Completed; break; - case "مستمرة": - case "ongoing": - case "en cours": - case "em andamento": - case "en progreso": - case "devam ediyor": + case 'مستمرة': + case 'ongoing': + case 'en cours': + case 'em andamento': + case 'en progreso': + case 'devam ediyor': novel.status = NovelStatus.Ongoing; break; - case "متوقفة": - case "hiatus": - case "en pause": - case "hiato": - case "pausa": - case "pausado": - case "duraklatıldı": + case 'متوقفة': + case 'hiatus': + case 'en pause': + case 'hiato': + case 'pausa': + case 'pausado': + case 'duraklatıldı': novel.status = NovelStatus.OnHiatus; break; default: @@ -162,40 +185,48 @@ class LightNovelWPPlugin implements Plugin.PluginBase { break; } break; - case "الفنان": - case "artist": - case "artiste": - case "artista": - case "çizer": + case 'الفنان': + case 'artist': + case 'artiste': + case 'artista': + case 'çizer': novel.artist = detail; break; } }); - let genres = $(".sertogenre") - if (!genres.length) genres = $(".genxed") + let genres = $('.sertogenre'); + if (!genres.length) genres = $('.genxed'); novel.genres = genres - .children("a") + .children('a') .map((i, el) => $(el).text()) .toArray() - .join(","); + .join(','); - let summary = $(".sersys > p").map((i, el) => $(el).text().trim()).toArray(); - if (!summary.length) summary = $(".entry-content > p").map((i, el) => $(el).text().trim()).toArray(); - novel.summary = summary.join("\n"); + let summary = $('.sersys > p') + .map((i, el) => $(el).text().trim()) + .toArray(); + if (!summary.length) + summary = $('.entry-content > p') + .map((i, el) => $(el).text().trim()) + .toArray(); + novel.summary = summary.join('\n'); const chapters: Plugin.ChapterItem[] = []; - $(".eplister li").each((i, elem) => { - const chapterName = $(elem).find(".epl-num").text() + " " + $(elem).find(".epl-title").text(); - const chapterUrl = $(elem).find("a").attr("href") || ""; - const releaseTime = $(elem).find(".epl-date").text().trim(); + $('.eplister li').each((i, elem) => { + const chapterName = + $(elem).find('.epl-num').text() + + ' ' + + $(elem).find('.epl-title').text(); + const chapterUrl = $(elem).find('a').attr('href') || ''; + const releaseTime = $(elem).find('.epl-date').text().trim(); let isFreeChapter: boolean; - switch ($(elem).find(".epl-price").text().trim().toLowerCase()) { - case "free": - case "gratuit": - case "مجاني": - case "livre": - case "": + switch ($(elem).find('.epl-price').text().trim().toLowerCase()) { + case 'free': + case 'gratuit': + case 'مجاني': + case 'livre': + case '': isFreeChapter = true; break; default: @@ -205,7 +236,7 @@ class LightNovelWPPlugin implements Plugin.PluginBase { if (isFreeChapter) chapters.push({ name: chapterName, - path: chapterUrl.replace(this.site, ""), + path: chapterUrl.replace(this.site, ''), releaseTime: releaseTime, }); }); @@ -218,16 +249,24 @@ class LightNovelWPPlugin implements Plugin.PluginBase { async parseChapter(chapterPath: string): Promise { const $ = await this.getCheerio(this.site + chapterPath, false); - if (this.id == "kolnovel") { - let ignore = $("article > style").text().trim().split(","); - ignore.push(...(ignore.pop()?.match(/^\.\w+/) || [])); - ignore.map((tag) => $(`p${tag}`).remove()); + if (this.id == 'kolnovel') { + let ignore = $('article > style').text().trim().split(','); + ignore.push(...(ignore.pop()?.match(/^\.\w+/) || [])); + ignore.map(tag => $(`p${tag}`).remove()); } - return $(".epcontent p").map((i, el) => $(el)).toArray().join("\n") || ""; + return ( + $('.epcontent p') + .map((i, el) => $(el)) + .toArray() + .join('\n') || '' + ); } - async searchNovels(searchTerm: string, page: number): Promise { - const url = this.site + "page/" + page + "/?s=" + searchTerm; + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const url = this.site + 'page/' + page + '/?s=' + searchTerm; const $ = await this.getCheerio(url, true); return this.parseNovels($); } diff --git a/scripts/multisrc/madara/generator.ts b/scripts/multisrc/madara/generator.ts index 537cd88..64f9c8c 100644 --- a/scripts/multisrc/madara/generator.ts +++ b/scripts/multisrc/madara/generator.ts @@ -1,36 +1,37 @@ -import { FilterTypes, Filters } from "../../../libs/filterInputs"; -import { ScrpitGeneratorFunction } from "../generate"; -import list from "./sources.json"; -import { MadaraMetadata } from "./template"; -import { readFileSync } from "fs"; -import path from "path"; +import { FilterTypes, Filters } from '../../../libs/filterInputs'; +import { ScrpitGeneratorFunction } from '../generate'; +import list from './sources.json'; +import { MadaraMetadata } from './template'; +import { readFileSync } from 'fs'; +import path from 'path'; export const generateAll: ScrpitGeneratorFunction = function (name) { - return list.map((metadata: MadaraMetadata) => { - let filters: any = {}; - try { - filters = require(`./filters/${metadata.id}`); - metadata.filters = filters.filters; - } - catch (e) {} - console.log(`[${name}] Generating: ${metadata.id}${" ".repeat(20 - metadata.id.length)} ${metadata.filters ? "🔎with filters🔍" : "🚫no filters🚫"}`); - return generator(metadata); - }); + return list.map((metadata: MadaraMetadata) => { + let filters: any = {}; + try { + filters = require(`./filters/${metadata.id}`); + metadata.filters = filters.filters; + } catch (e) {} + console.log( + `[${name}] Generating: ${metadata.id}${' '.repeat(20 - metadata.id.length)} ${metadata.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`, + ); + return generator(metadata); + }); }; const generator = function generator(metadata: MadaraMetadata) { - const madaraTemplate = readFileSync(path.join(__dirname, "template.ts"), { - encoding: "utf-8", - }); + const madaraTemplate = readFileSync(path.join(__dirname, 'template.ts'), { + encoding: 'utf-8', + }); - const pluginScript = ` + const pluginScript = ` ${madaraTemplate} const plugin = new MadaraPlugin(${JSON.stringify(metadata)}); export default plugin; `.trim(); - return { - lang: metadata.options?.lang || "English", - filename: metadata.sourceName, - pluginScript, - }; + return { + lang: metadata.options?.lang || 'English', + filename: metadata.sourceName, + pluginScript, + }; }; diff --git a/scripts/multisrc/madara/get_filters.ts b/scripts/multisrc/madara/get_filters.ts index 176bbae..7c0cddc 100644 --- a/scripts/multisrc/madara/get_filters.ts +++ b/scripts/multisrc/madara/get_filters.ts @@ -3,177 +3,208 @@ import * as cheerio from 'cheerio'; import * as path from 'path'; function getFilters(name: string, html: string) { - const $:cheerio.CheerioAPI = cheerio.load(html); - const filters: any = {filters: { - "genre[]": { - type: "Checkbox", - label: "Genre", - value: [], - options: [] - }, - "op": { - type: "Switch", - label: "Having ALL selected genres", - value: false - }, - "author": { - type: "Text", - label: "Author", - value: "" - }, - "artist": { - type: "Text", - label: "Artist", - value: "" - }, - "release": { - type: "Text", - label: "Year of Released", - value: "" - }, - "adult": { - type: "Picker", - label: "Adult content", - value: "", - options: [], - }, - "status[]": { - type: "Checkbox", - label: "Status", - value: [], - options: [] - }, - "m_orderby": { - type: "Picker", - label: "Order by", - value: "", - options: [] - }, - }}; + const $: cheerio.CheerioAPI = cheerio.load(html); + const filters: any = { + filters: { + 'genre[]': { + type: 'Checkbox', + label: 'Genre', + value: [], + options: [], + }, + 'op': { + type: 'Switch', + label: 'Having ALL selected genres', + value: false, + }, + 'author': { + type: 'Text', + label: 'Author', + value: '', + }, + 'artist': { + type: 'Text', + label: 'Artist', + value: '', + }, + 'release': { + type: 'Text', + label: 'Year of Released', + value: '', + }, + 'adult': { + type: 'Picker', + label: 'Adult content', + value: '', + options: [], + }, + 'status[]': { + type: 'Checkbox', + label: 'Status', + value: [], + options: [], + }, + 'm_orderby': { + type: 'Picker', + label: 'Order by', + value: '', + options: [], + }, + }, + }; - const divs = $("form.search-advanced-form > div"); + const divs = $('form.search-advanced-form > div'); - // ==================== Genre ==================== - const genreDiv = divs.eq(0); - genreDiv.find("label").each((i, el) => { - const option = { - label: $(el).text().trim(), - value: decodeURI($(el).attr("for") || "") - }; - filters.filters["genre[]"].options.push(option); - }); + // ==================== Genre ==================== + const genreDiv = divs.eq(0); + genreDiv.find('label').each((i, el) => { + const option = { + label: $(el).text().trim(), + value: decodeURI($(el).attr('for') || ''), + }; + filters.filters['genre[]'].options.push(option); + }); - // ===================== Op ====================== - const statusDiv = divs.eq(1); - filters.filters["op"].label = statusDiv.find("option").eq(1) - .text().replace("AND", "").replace("(", "").replace(")", "").trim(); + // ===================== Op ====================== + const statusDiv = divs.eq(1); + filters.filters['op'].label = statusDiv + .find('option') + .eq(1) + .text() + .replace('AND', '') + .replace('(', '') + .replace(')', '') + .trim(); - // ==================== Author ==================== - const authorDiv = divs.eq(2); - filters.filters["author"].label = authorDiv.find("span").text().trim(); + // ==================== Author ==================== + const authorDiv = divs.eq(2); + filters.filters['author'].label = authorDiv.find('span').text().trim(); - // ==================== Artist ==================== - const artistDiv = divs.eq(3); - filters.filters["artist"].label = artistDiv.find("span").text().trim(); + // ==================== Artist ==================== + const artistDiv = divs.eq(3); + filters.filters['artist'].label = artistDiv.find('span').text().trim(); - // ==================== Release ==================== - const releaseDiv = divs.eq(4); - filters.filters["release"].label = releaseDiv.find("span").text().trim(); + // ==================== Release ==================== + const releaseDiv = divs.eq(4); + filters.filters['release'].label = releaseDiv.find('span').text().trim(); - // ==================== Adult ==================== - const adultDiv = divs.eq(5); - filters.filters["adult"].label = adultDiv.find("span").text().trim(); - adultDiv.find("option").each((i, el) => { - const option = { - label: $(el).text().trim(), - value: decodeURI($(el).attr("value") || "") - }; - filters.filters["adult"].options.push(option); - }); + // ==================== Adult ==================== + const adultDiv = divs.eq(5); + filters.filters['adult'].label = adultDiv.find('span').text().trim(); + adultDiv.find('option').each((i, el) => { + const option = { + label: $(el).text().trim(), + value: decodeURI($(el).attr('value') || ''), + }; + filters.filters['adult'].options.push(option); + }); - // ==================== Status ==================== - const statusDiv2 = divs.eq(6); - filters.filters["status[]"].label = statusDiv2.find("span").text().trim(); - statusDiv2.find("label").each((i, el) => { - const option = { - label: $(el).text().trim(), - value: $(el).attr("for") - }; - filters.filters["status[]"].options.push(option); - }); + // ==================== Status ==================== + const statusDiv2 = divs.eq(6); + filters.filters['status[]'].label = statusDiv2.find('span').text().trim(); + statusDiv2.find('label').each((i, el) => { + const option = { + label: $(el).text().trim(), + value: $(el).attr('for'), + }; + filters.filters['status[]'].options.push(option); + }); - // ==================== Order by ==================== - const orderByDiv = $("div.c-nav-tabs").eq(0); - filters.filters["m_orderby"].label = orderByDiv.find("span").text().trim(); - orderByDiv.find("a").each((i, el) => { - const option = { - label: $(el).text().trim(), - value: decodeURI(($(el).attr("href")?.split("=").length == 3 ? $(el).attr("href")?.split("=")[2] : "") || "") - }; - filters.filters["m_orderby"].options.push(option); - }); + // ==================== Order by ==================== + const orderByDiv = $('div.c-nav-tabs').eq(0); + filters.filters['m_orderby'].label = orderByDiv.find('span').text().trim(); + orderByDiv.find('a').each((i, el) => { + const option = { + label: $(el).text().trim(), + value: decodeURI( + ($(el).attr('href')?.split('=').length == 3 + ? $(el).attr('href')?.split('=')[2] + : '') || '', + ), + }; + filters.filters['m_orderby'].options.push(option); + }); - if (filters.filters["m_orderby"].options.length == 0 || - filters.filters["status[]"].options.length == 0 || - filters.filters["adult"].options.length == 0 || - filters.filters["genre[]"].options.length == 0) { - console.error(`🚨Error in filters for ${name} please fix manually (${path.join(__dirname, 'filters', name + ".json")})🚨`); - } + if ( + filters.filters['m_orderby'].options.length == 0 || + filters.filters['status[]'].options.length == 0 || + filters.filters['adult'].options.length == 0 || + filters.filters['genre[]'].options.length == 0 + ) { + console.error( + `🚨Error in filters for ${name} please fix manually (${path.join(__dirname, 'filters', name + '.json')})🚨`, + ); + } - fs.writeFileSync( - path.join(__dirname, 'filters', name + ".json"), - JSON.stringify(filters, null, 2) - ); - console.log(`✅Filters created successfully for ${name}✅`); + fs.writeFileSync( + path.join(__dirname, 'filters', name + '.json'), + JSON.stringify(filters, null, 2), + ); + console.log(`✅Filters created successfully for ${name}✅`); } async function getFiltersFromURL(name: string, url: string) { - const response = await fetch(url + "/?s=&post_type=wp-manga"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}, while fetching ${response.url}`); - } - const html = await response.text(); - try { - getFilters(name, html); - } catch (e) { - console.error("Error while getting filters from", url); - } + const response = await fetch(url + '/?s=&post_type=wp-manga'); + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status}, while fetching ${response.url}`, + ); + } + const html = await response.text(); + try { + getFilters(name, html); + } catch (e) { + console.error('Error while getting filters from', url); + } } +async function askGetFilter() { + const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout, + }); -async function askGetFilter(){ - const readline = require('readline').createInterface({ - input: process.stdin, - output: process.stdout - }); - - const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; - await readline.question('Enter the id of the site (same one as in sources.json): ', async (name: string) => { - await readline.question(EREASE_PREV_LINE + 'Do you want to get the filters from a URL or the html text? (if url dosen\'t work try html) (url/html): ', async (method: string) => { - if (method.toLowerCase() === "url") { - await readline.question(EREASE_PREV_LINE + 'Enter the URL (same one as in sources.json): ', async (url: string) => { - try { - await getFiltersFromURL(name, url); - } catch (e: any) { - console.error("Error while getting filters from", url); - console.log(e.message || e); - } - readline.close(); - }); - } else { - process.stdout.write(EREASE_PREV_LINE + `Enter the html text from the page at {sourceSite}/?s=&post_type=wp-manga (at the end press ENTER then press CTRL+C): `) - let html = ""; - readline.on('SIGINT', () => { - console.log("Stopeed reading input, creating filters file"); - getFilters(name, html); - readline.close(); - }); - readline.on('line', (line: string) => { - html += line + "\n"; - }); - } - }); - }); + const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; + await readline.question( + 'Enter the id of the site (same one as in sources.json): ', + async (name: string) => { + await readline.question( + EREASE_PREV_LINE + + "Do you want to get the filters from a URL or the html text? (if url dosen't work try html) (url/html): ", + async (method: string) => { + if (method.toLowerCase() === 'url') { + await readline.question( + EREASE_PREV_LINE + + 'Enter the URL (same one as in sources.json): ', + async (url: string) => { + try { + await getFiltersFromURL(name, url); + } catch (e: any) { + console.error('Error while getting filters from', url); + console.log(e.message || e); + } + readline.close(); + }, + ); + } else { + process.stdout.write( + EREASE_PREV_LINE + + `Enter the html text from the page at {sourceSite}/?s=&post_type=wp-manga (at the end press ENTER then press CTRL+C): `, + ); + let html = ''; + readline.on('SIGINT', () => { + console.log('Stopeed reading input, creating filters file'); + getFilters(name, html); + readline.close(); + }); + readline.on('line', (line: string) => { + html += line + '\n'; + }); + } + }, + ); + }, + ); } askGetFilter(); diff --git a/scripts/multisrc/madara/template.ts b/scripts/multisrc/madara/template.ts index e02eb5d..c81776c 100644 --- a/scripts/multisrc/madara/template.ts +++ b/scripts/multisrc/madara/template.ts @@ -1,13 +1,13 @@ -import { fetchApi, fetchFile } from "@libs/fetch"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { CheerioAPI, load as parseHTML } from "cheerio"; -import { defaultCover } from "@libs/defaultCover"; -import { NovelStatus } from "@libs/novelStatus"; -import dayjs from "dayjs"; +import { fetchApi, fetchFile } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; const includesAny = (str: string, keywords: string[]) => - new RegExp(keywords.join("|")).test(str); + new RegExp(keywords.join('|')).test(str); interface MadaraOptions { useNewChapterEndpoint?: boolean; @@ -45,157 +45,178 @@ class MadaraPlugin implements Plugin.PluginBase { } getHostname(url: string): string { - return url.split("/")[2]; + return url.split('/')[2]; } async getCheerio(url: string): Promise { const r = await fetchApi(url); - if (!r.ok) throw new Error("Could not reach site (" + r.status + ") try to open in webview."); + if (!r.ok) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); const $ = parseHTML(await r.text()); - const title = $("title").text().trim(); - if (this.getHostname(url) != this.getHostname(r.url) || - title == "Bot Verification" || title == "You are being redirected..." || title == "Un instant..." || title == "Just a moment..." || title == "Redirecting...") - throw new Error("Captcha error, please open in webview"); - return ($); + const title = $('title').text().trim(); + if ( + this.getHostname(url) != this.getHostname(r.url) || + title == 'Bot Verification' || + title == 'You are being redirected...' || + title == 'Un instant...' || + title == 'Just a moment...' || + title == 'Redirecting...' + ) + throw new Error('Captcha error, please open in webview'); + return $; } parseNovels(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { const novels: Plugin.NovelItem[] = []; - loadedCheerio(".manga-title-badges").remove(); - - loadedCheerio(".page-item-detail, .c-tabs-item__content").each((index, element) => { - const novelName = loadedCheerio(element).find(".post-title").text().trim(); - const novelUrl = loadedCheerio(element).find(".post-title").find("a").attr("href") || ""; - if (!novelName || !novelUrl) return; - const image = loadedCheerio(element).find("img"); - const novelCover = image.attr("data-src") || image.attr("src"); - const novel: Plugin.NovelItem = { - name: novelName, - cover: novelCover, - path: novelUrl.replace(this.site, ""), - }; - novels.push(novel); - }); + loadedCheerio('.manga-title-badges').remove(); + + loadedCheerio('.page-item-detail, .c-tabs-item__content').each( + (index, element) => { + const novelName = loadedCheerio(element) + .find('.post-title') + .text() + .trim(); + const novelUrl = + loadedCheerio(element).find('.post-title').find('a').attr('href') || + ''; + if (!novelName || !novelUrl) return; + const image = loadedCheerio(element).find('img'); + const novelCover = image.attr('data-src') || image.attr('src'); + const novel: Plugin.NovelItem = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + }, + ); return novels; } - async popularNovels(pageNo: number,{ filters, showLatestNovels }: Plugin.PopularNovelsOptions): Promise { - let url = this.site + "/page/" + pageNo + "/?s=&post_type=wp-manga"; + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions, + ): Promise { + let url = this.site + '/page/' + pageNo + '/?s=&post_type=wp-manga'; if (!filters) filters = {}; - if (showLatestNovels) url += "&m_orderby=latest"; + if (showLatestNovels) url += '&m_orderby=latest'; for (const key in filters) { - if (typeof filters[key].value === "object") + if (typeof filters[key].value === 'object') for (const value of filters[key].value as string[]) url += `&${key}=${value}`; - else if (filters[key].value) - url += `&${key}=${filters[key].value}`; + else if (filters[key].value) url += `&${key}=${filters[key].value}`; } const loadedCheerio = await this.getCheerio(url); return this.parseNovels(loadedCheerio); } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const body = await fetchApi(this.site + novelPath).then(res => res.text()); let loadedCheerio = parseHTML(body); - loadedCheerio(".manga-title-badges, #manga-title span").remove(); + loadedCheerio('.manga-title-badges, #manga-title span').remove(); const novel: Plugin.SourceNovel = { path: novelPath, name: - loadedCheerio(".post-title h1").text().trim() || - loadedCheerio("#manga-title h1").text(), + loadedCheerio('.post-title h1').text().trim() || + loadedCheerio('#manga-title h1').text(), }; novel.cover = - loadedCheerio(".summary_image > a > img").attr("data-lazy-src") || - loadedCheerio(".summary_image > a > img").attr("data-src") || - loadedCheerio(".summary_image > a > img").attr("src") || - loadedCheerio(".summary_image > a > img").attr("src") || + loadedCheerio('.summary_image > a > img').attr('data-lazy-src') || + loadedCheerio('.summary_image > a > img').attr('data-src') || + loadedCheerio('.summary_image > a > img').attr('src') || + loadedCheerio('.summary_image > a > img').attr('src') || defaultCover; - loadedCheerio(".post-content_item, .post-content").each(function () { - const detailName = loadedCheerio(this).find("h5").text().trim(); - const detail = loadedCheerio(this).find(".summary-content").text().trim(); + loadedCheerio('.post-content_item, .post-content').each(function () { + const detailName = loadedCheerio(this).find('h5').text().trim(); + const detail = loadedCheerio(this).find('.summary-content').text().trim(); switch (detailName) { - case "Genre(s)": - case "Género(s)": - case "التصنيفات": + case 'Genre(s)': + case 'Género(s)': + case 'التصنيفات': novel.genres = detail; break; - case "Author(s)": - case "Autor(es)": - case "المؤلف": - case "المؤلف (ين)": + case 'Author(s)': + case 'Autor(es)': + case 'المؤلف': + case 'المؤلف (ين)': novel.author = detail; break; - case "Status": - case "Estado": + case 'Status': + case 'Estado': novel.status = - detail.includes("OnGoing") || detail.includes("مستمرة") + detail.includes('OnGoing') || detail.includes('مستمرة') ? NovelStatus.Ongoing : NovelStatus.Completed; break; } }); - loadedCheerio("div.summary__content .code-block,script").remove(); + loadedCheerio('div.summary__content .code-block,script').remove(); novel.summary = - loadedCheerio("div.summary__content").text().trim() || - loadedCheerio("#tab-manga-about").text().trim() || + loadedCheerio('div.summary__content').text().trim() || + loadedCheerio('#tab-manga-about').text().trim() || loadedCheerio('.post-content_item h5:contains("Summary")') .next() .text() .trim(); const chapters: Plugin.ChapterItem[] = []; - let html = ""; + let html = ''; if (this.options?.useNewChapterEndpoint) { - html = await fetchApi(this.site + novelPath + "ajax/chapters/", { - method: "POST", - }).then((res) => res.text()); + html = await fetchApi(this.site + novelPath + 'ajax/chapters/', { + method: 'POST', + }).then(res => res.text()); } else { const novelId = - loadedCheerio(".rating-post-id").attr("value") || - loadedCheerio("#manga-chapters-holder").attr("data-id") || - ""; + loadedCheerio('.rating-post-id').attr('value') || + loadedCheerio('#manga-chapters-holder').attr('data-id') || + ''; const formData = new FormData(); - formData.append("action", "manga_get_chapters"); - formData.append("manga", novelId); + formData.append('action', 'manga_get_chapters'); + formData.append('manga', novelId); - html = await fetchApi(this.site + "wp-admin/admin-ajax.php", { - method: "POST", + html = await fetchApi(this.site + 'wp-admin/admin-ajax.php', { + method: 'POST', body: formData, - }).then((res) => res.text()); + }).then(res => res.text()); } - if (html !== "0") { + if (html !== '0') { loadedCheerio = parseHTML(html); } - const totalChapters = loadedCheerio(".wp-manga-chapter").length; - loadedCheerio(".wp-manga-chapter").each((chapterIndex, element) => { - const chapterName = loadedCheerio(element).find("a").text().trim(); + const totalChapters = loadedCheerio('.wp-manga-chapter').length; + loadedCheerio('.wp-manga-chapter').each((chapterIndex, element) => { + const chapterName = loadedCheerio(element).find('a').text().trim(); let releaseDate = loadedCheerio(element) - .find("span.chapter-release-date") + .find('span.chapter-release-date') .text() .trim(); if (releaseDate) { releaseDate = this.parseData(releaseDate); } else { - releaseDate = dayjs().format("LL"); + releaseDate = dayjs().format('LL'); } - let chapterUrl = loadedCheerio(element).find("a").attr("href") || ""; + let chapterUrl = loadedCheerio(element).find('a').attr('href') || ''; chapters.push({ name: chapterName, - path: chapterUrl.replace(this.site, ""), + path: chapterUrl.replace(this.site, ''), releaseTime: releaseDate || null, chapterNumber: totalChapters - chapterIndex, }); @@ -206,21 +227,32 @@ class MadaraPlugin implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); const chapterText = - loadedCheerio(".text-left").html() || - loadedCheerio(".text-right").html() || - loadedCheerio(".entry-content").html() || - loadedCheerio(".c-blog-post > div > div:nth-child(2)").html() || - ""; + loadedCheerio('.text-left').html() || + loadedCheerio('.text-right').html() || + loadedCheerio('.entry-content').html() || + loadedCheerio('.c-blog-post > div > div:nth-child(2)').html() || + ''; return chapterText; } - async searchNovels(searchTerm: string, pageNo?: number | undefined): Promise { - const url = this.site + "page/" + pageNo + "/?s=" + searchTerm + "&post_type=wp-manga"; + async searchNovels( + searchTerm: string, + pageNo?: number | undefined, + ): Promise { + const url = + this.site + + 'page/' + + pageNo + + '/?s=' + + searchTerm + + '&post_type=wp-manga'; const loadedCheerio = await this.getCheerio(url); return this.parseNovels(loadedCheerio); } @@ -229,66 +261,66 @@ class MadaraPlugin implements Plugin.PluginBase { parseData = (date: string) => { const dayJSDate = dayjs(); // today - const timeAgo = date.match(/\d+/)?.[0] || ""; + const timeAgo = date.match(/\d+/)?.[0] || ''; const timeAgoInt = parseInt(timeAgo, 10); if (!timeAgo) return date; // there is no number! - if (includesAny(date, ["detik", "segundo", "second", "วินาที"])) { - dayJSDate.subtract(timeAgoInt, "second"); // go back N seconds + if (includesAny(date, ['detik', 'segundo', 'second', 'วินาที'])) { + dayJSDate.subtract(timeAgoInt, 'second'); // go back N seconds } else if ( includesAny(date, [ - "menit", - "dakika", - "min", - "minute", - "minuto", - "นาที", - "دقائق", + 'menit', + 'dakika', + 'min', + 'minute', + 'minuto', + 'นาที', + 'دقائق', ]) ) { - dayJSDate.subtract(timeAgoInt, "minute"); // go back N minute + dayJSDate.subtract(timeAgoInt, 'minute'); // go back N minute } else if ( includesAny(date, [ - "jam", - "saat", - "heure", - "hora", - "hour", - "ชั่วโมง", - "giờ", - "ore", - "ساعة", - "小时", + 'jam', + 'saat', + 'heure', + 'hora', + 'hour', + 'ชั่วโมง', + 'giờ', + 'ore', + 'ساعة', + '小时', ]) ) { - dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours + dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours } else if ( includesAny(date, [ - "hari", - "gün", - "jour", - "día", - "dia", - "day", - "วัน", - "ngày", - "giorni", - "أيام", - "天", + 'hari', + 'gün', + 'jour', + 'día', + 'dia', + 'day', + 'วัน', + 'ngày', + 'giorni', + 'أيام', + '天', ]) ) { - dayJSDate.subtract(timeAgoInt, "days"); // go back N days - } else if (includesAny(date, ["week", "semana"])) { - dayJSDate.subtract(timeAgoInt, "week"); // go back N a week - } else if (includesAny(date, ["month", "mes"])) { - dayJSDate.subtract(timeAgoInt, "month"); // go back N months - } else if (includesAny(date, ["year", "año"])) { - dayJSDate.subtract(timeAgoInt, "year"); // go back N years + dayJSDate.subtract(timeAgoInt, 'days'); // go back N days + } else if (includesAny(date, ['week', 'semana'])) { + dayJSDate.subtract(timeAgoInt, 'week'); // go back N a week + } else if (includesAny(date, ['month', 'mes'])) { + dayJSDate.subtract(timeAgoInt, 'month'); // go back N months + } else if (includesAny(date, ['year', 'año'])) { + dayJSDate.subtract(timeAgoInt, 'year'); // go back N years } else { return date; } - return dayJSDate.format("LL"); + return dayJSDate.format('LL'); }; } diff --git a/scripts/multisrc/readwn/generator.ts b/scripts/multisrc/readwn/generator.ts index 026fb00..90f9f52 100644 --- a/scripts/multisrc/readwn/generator.ts +++ b/scripts/multisrc/readwn/generator.ts @@ -1,16 +1,18 @@ -import { FilterTypes, Filters } from "../../../libs/filterInputs"; -import { ScrpitGeneratorFunction } from "../generate"; -import list from "./sources.json"; -import defaultSettings from "./settings.json"; -import { ReadwnMetadata } from "./template"; -import { readFileSync } from "fs"; -import path from "path"; +import { FilterTypes, Filters } from '../../../libs/filterInputs'; +import { ScrpitGeneratorFunction } from '../generate'; +import list from './sources.json'; +import defaultSettings from './settings.json'; +import { ReadwnMetadata } from './template'; +import { readFileSync } from 'fs'; +import path from 'path'; export const generateAll: ScrpitGeneratorFunction = function (name) { return list - .map((p) => { + .map(p => { p.filters.tags.options.unshift(...defaultSettings.filters.tags.options); - p.filters.genres.options.unshift(...defaultSettings.filters.genres.options); + p.filters.genres.options.unshift( + ...defaultSettings.filters.genres.options, + ); p.filters = Object.assign(defaultSettings.filters, p.filters); const d = false; @@ -33,8 +35,8 @@ export const generateAll: ScrpitGeneratorFunction = function (name) { }; const generator = function generator(metadata: ReadwnMetadata) { - const readwnTemplate = readFileSync(path.join(__dirname, "template.ts"), { - encoding: "utf-8", + const readwnTemplate = readFileSync(path.join(__dirname, 'template.ts'), { + encoding: 'utf-8', }); //metadata.filters = { @@ -45,14 +47,15 @@ const generator = function generator(metadata: ReadwnMetadata) { const pluginScript = ` ${readwnTemplate} -const plugin = new ReadwnPlugin(${ - JSON.stringify(metadata).replace(/"type":"([^"]+)"/g, '"type":FilterTypes.$1') -}); +const plugin = new ReadwnPlugin(${JSON.stringify(metadata).replace( + /"type":"([^"]+)"/g, + '"type":FilterTypes.$1', + )}); export default plugin; `.trim(); return { - lang: "english", + lang: 'english', filename: metadata.sourceName, pluginScript, }; diff --git a/scripts/multisrc/readwn/template.ts b/scripts/multisrc/readwn/template.ts index 5b028cd..86c476b 100644 --- a/scripts/multisrc/readwn/template.ts +++ b/scripts/multisrc/readwn/template.ts @@ -1,10 +1,10 @@ -import { fetchFile, fetchApi } from "@libs/fetch"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; -import qs from "qs"; +import { fetchFile, fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; +import qs from 'qs'; export interface ReadwnMetadata { id: string; @@ -26,7 +26,7 @@ class ReadwnPlugin implements Plugin.PluginBase { this.name = metadata.sourceName; this.icon = `multisrc/readwn/icons/${metadata.id}.png`; this.site = metadata.sourceSite; - this.version = "1.0.0"; + this.version = '1.0.0'; this.filters = metadata.filters; } @@ -34,115 +34,115 @@ class ReadwnPlugin implements Plugin.PluginBase { pageNo: number, { filters, showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { - let url = this.site + "/list/"; - url += (filters?.genres?.value || "all") + "/"; - url += (filters?.status?.value || "all") + "-"; - url += showLatestNovels ? "lastdotime" : filters?.sort?.value || "newstime"; - url += "-" + (pageNo - 1) + ".html"; + let url = this.site + '/list/'; + url += (filters?.genres?.value || 'all') + '/'; + url += (filters?.status?.value || 'all') + '-'; + url += showLatestNovels ? 'lastdotime' : filters?.sort?.value || 'newstime'; + url += '-' + (pageNo - 1) + '.html'; if (filters?.tags?.value) { //only 1 page - url = this.site + "/tags/" + filters.tags.value + "-0.html"; + url = this.site + '/tags/' + filters.tags.value + '-0.html'; } - const body = await fetchApi(url).then((res) => res.text()); + const body = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(body); - const novels: Plugin.NovelItem[] = loadedCheerio("li.novel-item") + const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item') .map((index, element) => ({ - name: loadedCheerio(element).find("h4").text() || "", + name: loadedCheerio(element).find('h4').text() || '', cover: this.site + - loadedCheerio(element).find(".novel-cover > img").attr("data-src"), - path: loadedCheerio(element).find("a").attr("href") || "", + loadedCheerio(element).find('.novel-cover > img').attr('data-src'), + path: loadedCheerio(element).find('a').attr('href') || '', })) .get() - .filter((novel) => novel.name && novel.path); + .filter(novel => novel.name && novel.path); return novels; } async parseNovel(novelPath: string): Promise { - const body = await fetchApi(this.site + novelPath).then((res) => res.text()); + const body = await fetchApi(this.site + novelPath).then(res => res.text()); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { path: novelPath, - name: loadedCheerio("h1.novel-title").text() || "", + name: loadedCheerio('h1.novel-title').text() || '', }; - novel.author = loadedCheerio("span[itemprop=author]").text(); + novel.author = loadedCheerio('span[itemprop=author]').text(); novel.cover = - this.site + loadedCheerio("figure.cover > img").attr("data-src"); + this.site + loadedCheerio('figure.cover > img').attr('data-src'); - novel.summary = loadedCheerio(".summary") + novel.summary = loadedCheerio('.summary') .text() - .replace("Summary", "") + .replace('Summary', '') .trim(); - novel.genres = loadedCheerio("div.categories > ul > li") + novel.genres = loadedCheerio('div.categories > ul > li') .map((index, element) => loadedCheerio(element).text()?.trim()) .get() - .join(","); + .join(','); - loadedCheerio("div.header-stats > span").each(function () { - if (loadedCheerio(this).find("small").text() === "Status") { + loadedCheerio('div.header-stats > span').each(function () { + if (loadedCheerio(this).find('small').text() === 'Status') { novel.status = - loadedCheerio(this).find("strong").text() === "Ongoing" + loadedCheerio(this).find('strong').text() === 'Ongoing' ? NovelStatus.Ongoing : NovelStatus.Completed; } }); const latestChapterNo = parseInt( - loadedCheerio(".header-stats") - .find("span > strong") + loadedCheerio('.header-stats') + .find('span > strong') .first() .text() .trim(), ); - const chapters: Plugin.ChapterItem[] = loadedCheerio(".chapter-list li") + const chapters: Plugin.ChapterItem[] = loadedCheerio('.chapter-list li') .map((chapterIndex, element) => { const name = loadedCheerio(element) - .find("a .chapter-title") + .find('a .chapter-title') .text() .trim(); - const path = loadedCheerio(element).find("a").attr("href")?.trim(); + const path = loadedCheerio(element).find('a').attr('href')?.trim(); if (!name || !path) return null; let releaseTime = loadedCheerio(element) - .find("a .chapter-update") + .find('a .chapter-update') .text() .trim(); - if (releaseTime?.includes?.("ago")) { - const timeAgo = releaseTime.match(/\d+/)?.[0] || "0"; + if (releaseTime?.includes?.('ago')) { + const timeAgo = releaseTime.match(/\d+/)?.[0] || '0'; const timeAgoInt = parseInt(timeAgo, 10); if (timeAgoInt) { const dayJSDate = dayjs(); // today if ( - releaseTime.includes("hours ago") || - releaseTime.includes("hour ago") + releaseTime.includes('hours ago') || + releaseTime.includes('hour ago') ) { - dayJSDate.subtract(timeAgoInt, "hours"); // go back N hours + dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours } if ( - releaseTime.includes("days ago") || - releaseTime.includes("day ago") + releaseTime.includes('days ago') || + releaseTime.includes('day ago') ) { - dayJSDate.subtract(timeAgoInt, "days"); // go back N days + dayJSDate.subtract(timeAgoInt, 'days'); // go back N days } if ( - releaseTime.includes("months ago") || - releaseTime.includes("month ago") + releaseTime.includes('months ago') || + releaseTime.includes('month ago') ) { - dayJSDate.subtract(timeAgoInt, "months"); // go back N months + dayJSDate.subtract(timeAgoInt, 'months'); // go back N months } - releaseTime = dayJSDate.format("LL"); + releaseTime = dayJSDate.format('LL'); } } @@ -154,18 +154,22 @@ class ReadwnPlugin implements Plugin.PluginBase { }; }) .get() - .filter((chapter) => chapter); + .filter(chapter => chapter); if (latestChapterNo > chapters.length) { const lastChapterNo = parseInt( - chapters[chapters.length - 1].path.match(/_(\d+)\.html/)?.[1] || "", + chapters[chapters.length - 1].path.match(/_(\d+)\.html/)?.[1] || '', 10, ); - for (let i = (lastChapterNo || chapters.length) + 1; i <= latestChapterNo; i++) { + for ( + let i = (lastChapterNo || chapters.length) + 1; + i <= latestChapterNo; + i++ + ) { chapters.push({ - name: "Chapter " + i, - path: novelPath.replace(".html", "_" + i + ".html"), + name: 'Chapter ' + i, + path: novelPath.replace('.html', '_' + i + '.html'), releaseTime: null, chapterNumber: i, }); @@ -177,38 +181,40 @@ class ReadwnPlugin implements Plugin.PluginBase { } async parseChapter(chapterPath: string): Promise { - const body = await fetchApi(this.site + chapterPath).then((res) => res.text()); + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); const loadedCheerio = parseHTML(body); - const chapterText = loadedCheerio(".chapter-content").html() || ""; + const chapterText = loadedCheerio('.chapter-content').html() || ''; return chapterText; } async searchNovels(searchTerm: string): Promise { - const result = await fetchApi(this.site + "/e/search/index.php", { + const result = await fetchApi(this.site + '/e/search/index.php', { headers: { - "Content-Type": "application/x-www-form-urlencoded", - Referer: this.site + "/search.html", + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.site + '/search.html', Origin: this.site, }, - method: "POST", + method: 'POST', body: qs.stringify({ - show: "title", + show: 'title', tempid: 1, - tbname: "news", + tbname: 'news', keyboard: searchTerm, }), - }).then((res) => res.text()); + }).then(res => res.text()); const loadedCheerio = parseHTML(result); - const novels: Plugin.NovelItem[] = loadedCheerio("li.novel-item") + const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item') .map((index, element) => ({ - name: loadedCheerio(element).find("h4").text() || "", - cover: this.site + loadedCheerio(element).find("img").attr("data-src"), - path: loadedCheerio(element).find("a").attr("href") || "", + name: loadedCheerio(element).find('h4').text() || '', + cover: this.site + loadedCheerio(element).find('img').attr('data-src'), + path: loadedCheerio(element).find('a').attr('href') || '', })) .get() - .filter((novel) => novel.name && novel.path); + .filter(novel => novel.name && novel.path); return novels; } diff --git a/scripts/multisrc/rulate/generator.ts b/scripts/multisrc/rulate/generator.ts index 40e6d68..ead050c 100644 --- a/scripts/multisrc/rulate/generator.ts +++ b/scripts/multisrc/rulate/generator.ts @@ -1,14 +1,14 @@ -import { Filters, FilterTypes } from "../../../libs/filterInputs"; -import { ScrpitGeneratorFunction } from "../generate"; -import list from "./sources.json"; -import defaultSettings from "./settings.json"; -import { RulateMetadata } from "./template"; -import { readFileSync } from "fs"; -import path from "path"; +import { Filters, FilterTypes } from '../../../libs/filterInputs'; +import { ScrpitGeneratorFunction } from '../generate'; +import list from './sources.json'; +import defaultSettings from './settings.json'; +import { RulateMetadata } from './template'; +import { readFileSync } from 'fs'; +import path from 'path'; export const generateAll: ScrpitGeneratorFunction = function (name) { return list - .map((p) => { + .map(p => { if (p.filters) { p.filters.cat.options.unshift(...defaultSettings.filters.cat.options); } @@ -34,21 +34,21 @@ export const generateAll: ScrpitGeneratorFunction = function (name) { }; const generator = function generator(metadata: RulateMetadata) { - const rulateTemplate = readFileSync(path.join(__dirname, "template.ts"), { - encoding: "utf-8", + const rulateTemplate = readFileSync(path.join(__dirname, 'template.ts'), { + encoding: 'utf-8', }); const pluginScript = ` ${rulateTemplate} const plugin = new RulatePlugin(${JSON.stringify(metadata) .replace(/"type":"([^"]+)"/g, '"type":FilterTypes.$1') - .replace(/\.XCheckbox/g, ".ExcludableCheckboxGroup") //remember to redo - .replace(/\.Checkbox/g, ".CheckboxGroup")}); + .replace(/\.XCheckbox/g, '.ExcludableCheckboxGroup') //remember to redo + .replace(/\.Checkbox/g, '.CheckboxGroup')}); export default plugin; `.trim(); return { - lang: "russian", + lang: 'russian', filename: metadata.sourceName, pluginScript, }; diff --git a/scripts/multisrc/rulate/template.ts b/scripts/multisrc/rulate/template.ts index de185be..e4b93e7 100644 --- a/scripts/multisrc/rulate/template.ts +++ b/scripts/multisrc/rulate/template.ts @@ -1,9 +1,9 @@ -import { fetchFile, fetchApi } from "@libs/fetch"; -import { Filters, FilterTypes } from "@libs/filterInputs"; -import { Plugin } from "@typings/plugin"; -import { NovelStatus } from "@libs/novelStatus"; -import { load as parseHTML } from "cheerio"; -import dayjs from "dayjs"; +import { fetchFile, fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@typings/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; export interface RulateMetadata { id: string; @@ -25,7 +25,7 @@ class RulatePlugin implements Plugin.PluginBase { this.name = metadata.sourceName; this.icon = `multisrc/rulate/icons/${metadata.id}.png`; this.site = metadata.sourceSite; - this.version = "1.0.0"; + this.version = '1.0.0'; this.filters = metadata.filters; } @@ -34,35 +34,37 @@ class RulatePlugin implements Plugin.PluginBase { { filters, showLatestNovels }: Plugin.PopularNovelsOptions, ): Promise { const novels: Plugin.NovelItem[] = []; - let url = this.site + "/search?t="; - url += "&cat=" + (filters?.cat?.value || "0"); - url += "&s_lang=" + (filters?.s_lang?.value || "0"); - url += "&t_lang=" + (filters?.t_lang?.value || "0"); - url += "&type=" + (filters?.type?.value || "0"); - url += "&sort=" + (showLatestNovels ? "4" : filters?.sort?.value || "6"); - url += "&atmosphere=" + (filters?.atmosphere?.value || "0"); - url += "&adult=" + (filters?.adult?.value || "0"); + let url = this.site + '/search?t='; + url += '&cat=' + (filters?.cat?.value || '0'); + url += '&s_lang=' + (filters?.s_lang?.value || '0'); + url += '&t_lang=' + (filters?.t_lang?.value || '0'); + url += '&type=' + (filters?.type?.value || '0'); + url += '&sort=' + (showLatestNovels ? '4' : filters?.sort?.value || '6'); + url += '&atmosphere=' + (filters?.atmosphere?.value || '0'); + url += '&adult=' + (filters?.adult?.value || '0'); Object.entries(filters || {}).forEach(([type, { value }]) => { if (value instanceof Array && value.length) { - url += "&" + value - .map((val) => (type == "extra" ? val + "=1" : type + "[]=" + val)) - .join("&"); + url += + '&' + + value + .map(val => (type == 'extra' ? val + '=1' : type + '[]=' + val)) + .join('&'); } }); - url += "&Book_page=" + pageNo; + url += '&Book_page=' + pageNo; - const body = await fetchApi(url).then((res) => res.text()); + const body = await fetchApi(url).then(res => res.text()); const loadedCheerio = parseHTML(body); loadedCheerio( 'ul[class="search-results"] > li:not([class="ad_type_catalog"])', ).each((index, element) => { - loadedCheerio(element).find("p > a").text(); - const name = loadedCheerio(element).find("p > a").text(); - const cover = loadedCheerio(element).find("img").attr("src"); - const path = loadedCheerio(element).find("p > a").attr("href"); + loadedCheerio(element).find('p > a').text(); + const name = loadedCheerio(element).find('p > a').text(); + const cover = loadedCheerio(element).find('img').attr('src'); + const path = loadedCheerio(element).find('p > a').attr('href'); if (!name || !path) return; novels.push({ name, cover: this.site + cover, path }); @@ -73,13 +75,13 @@ class RulatePlugin implements Plugin.PluginBase { async parseNovel(novelPath: string): Promise { let result = await fetchApi(this.site + novelPath); - if (result.url.includes("mature?path=")) { + if (result.url.includes('mature?path=')) { const formData = new FormData(); - formData.append("path", novelPath); - formData.append("ok", "Да"); + formData.append('path', novelPath); + formData.append('ok', 'Да'); await fetchApi(result.url, { - method: "POST", + method: 'POST', body: formData, }); @@ -96,41 +98,41 @@ class RulatePlugin implements Plugin.PluginBase { .text() .trim(), }; - if (novel.name?.includes?.("[")) { - novel.name = novel.name.split("[")[0].trim(); + if (novel.name?.includes?.('[')) { + novel.name = novel.name.split('[')[0].trim(); } novel.cover = - this.site + loadedCheerio('div[class="images"] > div img').attr("src"); - novel.summary = loadedCheerio("#Info > div:nth-child(3), .book-description") + this.site + loadedCheerio('div[class="images"] > div img').attr('src'); + novel.summary = loadedCheerio('#Info > div:nth-child(3), .book-description') .text() .trim(); novel.author = loadedCheerio( - ".book-stats-icons_author > span:nth-child(2) > a:nth-child(1)", + '.book-stats-icons_author > span:nth-child(2) > a:nth-child(1)', ).text(); const genres: string[] = []; - loadedCheerio("div.span5 > p, .span5 > div:nth-child(2) > p").each( + loadedCheerio('div.span5 > p, .span5 > div:nth-child(2) > p').each( function () { - switch (loadedCheerio(this).find("strong").text()) { - case "Автор:": - novel.author = loadedCheerio(this).find("em > a").text().trim(); + switch (loadedCheerio(this).find('strong').text()) { + case 'Автор:': + novel.author = loadedCheerio(this).find('em > a').text().trim(); break; - case "Выпуск:": + case 'Выпуск:': novel.status = - loadedCheerio(this).find("em").text().trim() === "продолжается" + loadedCheerio(this).find('em').text().trim() === 'продолжается' ? NovelStatus.Ongoing : NovelStatus.Completed; break; - case "Тэги:": + case 'Тэги:': loadedCheerio(this) - .find("em > a") + .find('em > a') .each(function () { genres.push(loadedCheerio(this).text()); }); break; - case "Жанры:": + case 'Жанры:': loadedCheerio(this) - .find("em > a") + .find('em > a') .each(function () { genres.push(loadedCheerio(this).text()); }); @@ -140,23 +142,23 @@ class RulatePlugin implements Plugin.PluginBase { ); if (genres.length) { - novel.genres = genres.reverse().join(","); + novel.genres = genres.reverse().join(','); } const chapters: Plugin.ChapterItem[] = []; - loadedCheerio("table > tbody > tr.chapter_row").each( + loadedCheerio('table > tbody > tr.chapter_row').each( (chapterIndex, element) => { const chapterName = loadedCheerio(element) .find('td[class="t"] > a') .text() .trim(); const releaseDate = loadedCheerio(element) - .find("td > span") - .attr("title") + .find('td > span') + .attr('title') ?.trim(); const chapterUrl = loadedCheerio(element) .find('td[class="t"] > a') - .attr("href"); + .attr('href'); if ( !loadedCheerio(element).find('td > span[class="disabled"]').length && @@ -179,12 +181,12 @@ class RulatePlugin implements Plugin.PluginBase { async parseChapter(chapterPath: string): Promise { let result = await fetchApi(this.site + chapterPath); - if (result.url.includes("mature?path=")) { + if (result.url.includes('mature?path=')) { const formData = new FormData(); - formData.append("ok", "Да"); + formData.append('ok', 'Да'); await fetchApi(result.url, { - method: "POST", + method: 'POST', body: formData, }); @@ -193,26 +195,26 @@ class RulatePlugin implements Plugin.PluginBase { const body = await result.text(); const loadedCheerio = parseHTML(body); - loadedCheerio(".content-text img").each((index, element) => { - if (!loadedCheerio(element).attr("src")?.startsWith("http")) { - const src = loadedCheerio(element).attr("src"); - loadedCheerio(element).attr("src", this.site + src); + loadedCheerio('.content-text img').each((index, element) => { + if (!loadedCheerio(element).attr('src')?.startsWith('http')) { + const src = loadedCheerio(element).attr('src'); + loadedCheerio(element).attr('src', this.site + src); } }); - const chapterText = loadedCheerio(".content-text").html(); - return chapterText || ""; + const chapterText = loadedCheerio('.content-text').html(); + return chapterText || ''; } async searchNovels(searchTerm: string): Promise { const novels: Plugin.NovelItem[] = []; const result = await fetchApi( - this.site + "/search/autocomplete?query=" + searchTerm, + this.site + '/search/autocomplete?query=' + searchTerm, ); const json = (await result.json()) as response[]; - json.forEach((novel) => { - const name = novel.title_one + " / " + novel.title_two; + json.forEach(novel => { + const name = novel.title_one + ' / ' + novel.title_two; if (!novel.url) return; novels.push({ @@ -225,24 +227,26 @@ class RulatePlugin implements Plugin.PluginBase { return novels; } - parseDate = (dateString: string | undefined = "") => { + parseDate = (dateString: string | undefined = '') => { const months: { [key: string]: number } = { - "янв.": 1, - "февр.": 2, - "мар.": 3, - "апр.": 4, + 'янв.': 1, + 'февр.': 2, + 'мар.': 3, + 'апр.': 4, мая: 5, - "июн.": 6, - "июл.": 7, - "авг.": 8, - "сент.": 9, - "окт.": 10, - "нояб.": 11, - "дек.": 12, + 'июн.': 6, + 'июл.': 7, + 'авг.': 8, + 'сент.': 9, + 'окт.': 10, + 'нояб.': 11, + 'дек.': 12, }; - const [day, month, year, , time] = dateString.split(" "); + const [day, month, year, , time] = dateString.split(' '); if (day && months[month] && year && time) { - return dayjs(year + "-" + months[month] + "-" + day + " " + time).format("LLL"); + return dayjs(year + '-' + months[month] + '-' + day + ' ' + time).format( + 'LLL', + ); } return dateString || null; }; diff --git a/scripts/terser.ts b/scripts/terser.ts index 90fc5f9..a02cf07 100644 --- a/scripts/terser.ts +++ b/scripts/terser.ts @@ -1,5 +1,5 @@ -const { minify_sync } = require("terser"); -import * as fs from "fs"; +const { minify_sync } = require('terser'); +import * as fs from 'fs'; export const minify = function (path: string) { const code = fs.readFileSync(path).toString(); diff --git a/test_web/api/plugins.ts b/test_web/api/plugins.ts index 80b5b21..f8cf440 100644 --- a/test_web/api/plugins.ts +++ b/test_web/api/plugins.ts @@ -1,100 +1,98 @@ -import fs from "fs"; -import { languages } from "@libs/languages"; -import path from "path"; -import { PluginList } from "@typings/types"; -import { Plugin } from "@typings/plugin"; -import { Filters } from "@libs/filterInputs"; -const root = path.dirname(require?.main?.filename || ""); +import fs from 'fs'; +import { languages } from '@libs/languages'; +import path from 'path'; +import { PluginList } from '@typings/types'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; +const root = path.dirname(require?.main?.filename || ''); export const fetchHeaders: Record = {}; export const all_plugins = (): PluginList => { - const res: PluginList = {}; - for (let languageEnglish in languages) { - //language with English name - const languageNative = - languages[languageEnglish as keyof typeof languages]; - const langPath = path.join( - root, - "plugins", - languageEnglish.toLowerCase() - ); - if (!fs.existsSync(langPath)) continue; - const plugins = fs.readdirSync(langPath); - res[languageEnglish] = []; - plugins.forEach((plugin) => { - if (plugin.startsWith(".")) return; - const requirePath = `@plugins/${languageEnglish.toLowerCase()}/${ - plugin.split(".")[0] - }`; - const instance = require(requirePath).default; - const { id, name, version, icon } = instance; - const info: Plugin.PluginItem = { - id, - name: name + (plugin.match(/(\[.+\])/)?.[1] || ''), - lang: languageNative, - version, - requirePath, - icon, - }; // lang: language with native name - res[languageEnglish].push(info); - }); - } - return res; + const res: PluginList = {}; + for (let languageEnglish in languages) { + //language with English name + const languageNative = languages[languageEnglish as keyof typeof languages]; + const langPath = path.join(root, 'plugins', languageEnglish.toLowerCase()); + if (!fs.existsSync(langPath)) continue; + const plugins = fs.readdirSync(langPath); + res[languageEnglish] = []; + plugins.forEach(plugin => { + if (plugin.startsWith('.')) return; + const requirePath = `@plugins/${languageEnglish.toLowerCase()}/${ + plugin.split('.')[0] + }`; + const instance = require(requirePath).default; + const { id, name, version, icon } = instance; + const info: Plugin.PluginItem = { + id, + name: name + (plugin.match(/(\[.+\])/)?.[1] || ''), + lang: languageNative, + version, + requirePath, + icon, + }; // lang: language with native name + res[languageEnglish].push(info); + }); + } + return res; }; const getPlugin = async ( - requirePath: string + requirePath: string, ): Promise => { - const plugin = await require(requirePath).default; - return plugin; + const plugin = await require(requirePath).default; + return plugin; }; export const getFilter = async (pluginRequirePath: string) => - (await getPlugin(pluginRequirePath))?.filters; + (await getPlugin(pluginRequirePath))?.filters; export const popularNovels = async ( - pluginRequirePath: string, - page: number, - options: Plugin.PopularNovelsOptions + pluginRequirePath: string, + page: number, + options: Plugin.PopularNovelsOptions, ) => (await getPlugin(pluginRequirePath))?.popularNovels(page, options); export const searchNovels = async ( - pluginRequirePath: string, - page: number, - searchTerm: string + pluginRequirePath: string, + page: number, + searchTerm: string, ) => (await getPlugin(pluginRequirePath))?.searchNovels(searchTerm, page); export const parseNovel = async ( - pluginRequirePath: string, - novelPath: string + pluginRequirePath: string, + novelPath: string, ) => (await getPlugin(pluginRequirePath))?.parseNovel(novelPath); -export const hasParsePage = async ( - pluginRequirePath: string, -) => (await getPlugin(pluginRequirePath) as Plugin.PagePlugin | null)?.parsePage; +export const hasParsePage = async (pluginRequirePath: string) => + ((await getPlugin(pluginRequirePath)) as Plugin.PagePlugin | null)?.parsePage; -export const parsePage = async( - pluginRequirePath: string, - novelPath: string, - page: string -) => (await getPlugin(pluginRequirePath) as Plugin.PagePlugin | null)?.parsePage(novelPath, page); +export const parsePage = async ( + pluginRequirePath: string, + novelPath: string, + page: string, +) => + ((await getPlugin(pluginRequirePath)) as Plugin.PagePlugin | null)?.parsePage( + novelPath, + page, + ); export const parseChapter = async ( - pluginRequirePath: string, - chapterPath: string + pluginRequirePath: string, + chapterPath: string, ) => (await getPlugin(pluginRequirePath))?.parseChapter(chapterPath); export const fetchImage = async (pluginRequirePath: string, url: string) => - (await getPlugin(pluginRequirePath))?.fetchImage(url); + (await getPlugin(pluginRequirePath))?.fetchImage(url); export const resolveUrl = async ( - pluginRequirePath: string, - path: string, - isNovel?: boolean, + pluginRequirePath: string, + path: string, + isNovel?: boolean, ) => { - const plugin = await getPlugin(pluginRequirePath); - if (!plugin) return path; - if (plugin.resolveUrl) return plugin.resolveUrl(path, isNovel); - return plugin.site + path; -}; \ No newline at end of file + const plugin = await getPlugin(pluginRequirePath); + if (!plugin) return path; + if (plugin.resolveUrl) return plugin.resolveUrl(path, isNovel); + return plugin.site + path; +}; diff --git a/test_web/static/js/accordion.js b/test_web/static/js/accordion.js index 01534c2..8c87846 100644 --- a/test_web/static/js/accordion.js +++ b/test_web/static/js/accordion.js @@ -1,91 +1,91 @@ -/** @ts-check */ - -class AccordionContent extends HTMLElement { - constructor() { - super(); - this.gridElement = document.createElement("div"); - } - get content() { - return this.gridElement; - } - connectedCallback() { - // create a div that'll animate the collapsing - this.gridElement.append(...Array.from(this.childNodes)); - this.gridElement.classList.add("accordion-animation-content"); - this.innerHTML = ""; - this.append(this.gridElement); - } -} - -class AccordionHeader extends HTMLElement { - constructor() { - super(); - console.log("contructor?"); - this.btn = document.createElement("button"); - } - get html() { - return this.btn.innerHTML; - } - set html(val) { - this.btn.innerHTML = val; - } - get text() { - return this.btn.innerText; - } - set text(val) { - this.btn.innerText = val; - } - get parentBox() { - return this.parentElement; - } - connectedCallback() { - const childNodes = Array.from(this.childNodes); - this.innerHTML = ""; - this.btn.append(...childNodes); - this.append(this.btn); - this.btn.addEventListener("click", this._btnClicked.bind(this)); - } - _btnClicked() { - this.parentBox?.toggle(); - } -} - -class AccordionBox extends HTMLElement { - constructor() { - super(); - } - get collapsed() { - return this.hasAttribute("collapsed"); - } - set collapsed(val) { - if (val) { - this.setAttribute("collapsed", ""); - } else { - this.removeAttribute("collapsed"); - } - } - toggle() { - this.collapsed = !this.collapsed; - } -} - -class Accordion extends HTMLElement { - constructor() { - super(); - } -} - -window.customElements.define("main-accordion", Accordion); -window.customElements.define("accordion-box", AccordionBox); -window.customElements.define("accordion-header", AccordionHeader); -window.customElements.define("accordion-content", AccordionContent); - -// Export types for use in index.js -const module = {}; - -module.exports = { - AccordionBox, - Accordion, - AccordionContent, - AccordionHeader, -}; +/** @ts-check */ + +class AccordionContent extends HTMLElement { + constructor() { + super(); + this.gridElement = document.createElement('div'); + } + get content() { + return this.gridElement; + } + connectedCallback() { + // create a div that'll animate the collapsing + this.gridElement.append(...Array.from(this.childNodes)); + this.gridElement.classList.add('accordion-animation-content'); + this.innerHTML = ''; + this.append(this.gridElement); + } +} + +class AccordionHeader extends HTMLElement { + constructor() { + super(); + console.log('contructor?'); + this.btn = document.createElement('button'); + } + get html() { + return this.btn.innerHTML; + } + set html(val) { + this.btn.innerHTML = val; + } + get text() { + return this.btn.innerText; + } + set text(val) { + this.btn.innerText = val; + } + get parentBox() { + return this.parentElement; + } + connectedCallback() { + const childNodes = Array.from(this.childNodes); + this.innerHTML = ''; + this.btn.append(...childNodes); + this.append(this.btn); + this.btn.addEventListener('click', this._btnClicked.bind(this)); + } + _btnClicked() { + this.parentBox?.toggle(); + } +} + +class AccordionBox extends HTMLElement { + constructor() { + super(); + } + get collapsed() { + return this.hasAttribute('collapsed'); + } + set collapsed(val) { + if (val) { + this.setAttribute('collapsed', ''); + } else { + this.removeAttribute('collapsed'); + } + } + toggle() { + this.collapsed = !this.collapsed; + } +} + +class Accordion extends HTMLElement { + constructor() { + super(); + } +} + +window.customElements.define('main-accordion', Accordion); +window.customElements.define('accordion-box', AccordionBox); +window.customElements.define('accordion-header', AccordionHeader); +window.customElements.define('accordion-content', AccordionContent); + +// Export types for use in index.js +const module = {}; + +module.exports = { + AccordionBox, + Accordion, + AccordionContent, + AccordionHeader, +}; diff --git a/test_web/static/js/bootstrap.min.js b/test_web/static/js/bootstrap.min.js index a2eac27..10b2d4d 100644 --- a/test_web/static/js/bootstrap.min.js +++ b/test_web/static/js/bootstrap.min.js @@ -1,9 +1,2950 @@ /*! - * Bootstrap v4.3.1 (https://getbootstrap.com/) - * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right

',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t this._items.length - 1 || t < 0)) + if (this._isSliding) + g(this._element).one(Q.SLID, function () { + return e.to(t); + }); + else { + if (n === t) return this.pause(), void this.cycle(); + var i = n < t ? W : q; + this._slide(i, this._items[t]); + } + }), + (t.dispose = function () { + g(this._element).off(H), + g.removeData(this._element, j), + (this._items = null), + (this._config = null), + (this._element = null), + (this._interval = null), + (this._isPaused = null), + (this._isSliding = null), + (this._activeElement = null), + (this._indicatorsElement = null); + }), + (t._getConfig = function (t) { + return (t = l({}, F, t)), _.typeCheckConfig(L, t, U), t; + }), + (t._handleSwipe = function () { + var t = Math.abs(this.touchDeltaX); + if (!(t <= 40)) { + var e = t / this.touchDeltaX; + 0 < e && this.prev(), e < 0 && this.next(); + } + }), + (t._addEventListeners = function () { + var e = this; + this._config.keyboard && + g(this._element).on(Q.KEYDOWN, function (t) { + return e._keydown(t); + }), + 'hover' === this._config.pause && + g(this._element) + .on(Q.MOUSEENTER, function (t) { + return e.pause(t); + }) + .on(Q.MOUSELEAVE, function (t) { + return e.cycle(t); + }), + this._config.touch && this._addTouchEventListeners(); + }), + (t._addTouchEventListeners = function () { + var n = this; + if (this._touchSupported) { + var e = function (t) { + n._pointerEvent && at[t.originalEvent.pointerType.toUpperCase()] + ? (n.touchStartX = t.originalEvent.clientX) + : n._pointerEvent || + (n.touchStartX = t.originalEvent.touches[0].clientX); + }, + i = function (t) { + n._pointerEvent && + at[t.originalEvent.pointerType.toUpperCase()] && + (n.touchDeltaX = t.originalEvent.clientX - n.touchStartX), + n._handleSwipe(), + 'hover' === n._config.pause && + (n.pause(), + n.touchTimeout && clearTimeout(n.touchTimeout), + (n.touchTimeout = setTimeout(function (t) { + return n.cycle(t); + }, 500 + n._config.interval))); + }; + g(this._element.querySelectorAll(nt)).on( + Q.DRAG_START, + function (t) { + return t.preventDefault(); + }, + ), + this._pointerEvent + ? (g(this._element).on(Q.POINTERDOWN, function (t) { + return e(t); + }), + g(this._element).on(Q.POINTERUP, function (t) { + return i(t); + }), + this._element.classList.add(J)) + : (g(this._element).on(Q.TOUCHSTART, function (t) { + return e(t); + }), + g(this._element).on(Q.TOUCHMOVE, function (t) { + var e; + (e = t).originalEvent.touches && + 1 < e.originalEvent.touches.length + ? (n.touchDeltaX = 0) + : (n.touchDeltaX = + e.originalEvent.touches[0].clientX - n.touchStartX); + }), + g(this._element).on(Q.TOUCHEND, function (t) { + return i(t); + })); + } + }), + (t._keydown = function (t) { + if (!/input|textarea/i.test(t.target.tagName)) + switch (t.which) { + case 37: + t.preventDefault(), this.prev(); + break; + case 39: + t.preventDefault(), this.next(); + } + }), + (t._getItemIndex = function (t) { + return ( + (this._items = + t && t.parentNode + ? [].slice.call(t.parentNode.querySelectorAll(et)) + : []), + this._items.indexOf(t) + ); + }), + (t._getItemByDirection = function (t, e) { + var n = t === W, + i = t === q, + o = this._getItemIndex(e), + r = this._items.length - 1; + if (((i && 0 === o) || (n && o === r)) && !this._config.wrap) + return e; + var s = (o + (t === q ? -1 : 1)) % this._items.length; + return -1 === s + ? this._items[this._items.length - 1] + : this._items[s]; + }), + (t._triggerSlideEvent = function (t, e) { + var n = this._getItemIndex(t), + i = this._getItemIndex(this._element.querySelector(tt)), + o = g.Event(Q.SLIDE, { + relatedTarget: t, + direction: e, + from: i, + to: n, + }); + return g(this._element).trigger(o), o; + }), + (t._setActiveIndicatorElement = function (t) { + if (this._indicatorsElement) { + var e = [].slice.call(this._indicatorsElement.querySelectorAll(Z)); + g(e).removeClass(V); + var n = this._indicatorsElement.children[this._getItemIndex(t)]; + n && g(n).addClass(V); + } + }), + (t._slide = function (t, e) { + var n, + i, + o, + r = this, + s = this._element.querySelector(tt), + a = this._getItemIndex(s), + l = e || (s && this._getItemByDirection(t, s)), + c = this._getItemIndex(l), + h = Boolean(this._interval); + if ( + ((o = t === W ? ((n = X), (i = $), M) : ((n = z), (i = G), K)), + l && g(l).hasClass(V)) + ) + this._isSliding = !1; + else if ( + !this._triggerSlideEvent(l, o).isDefaultPrevented() && + s && + l + ) { + (this._isSliding = !0), + h && this.pause(), + this._setActiveIndicatorElement(l); + var u = g.Event(Q.SLID, { + relatedTarget: l, + direction: o, + from: a, + to: c, + }); + if (g(this._element).hasClass(Y)) { + g(l).addClass(i), _.reflow(l), g(s).addClass(n), g(l).addClass(n); + var f = parseInt(l.getAttribute('data-interval'), 10); + this._config.interval = f + ? ((this._config.defaultInterval = + this._config.defaultInterval || this._config.interval), + f) + : this._config.defaultInterval || this._config.interval; + var d = _.getTransitionDurationFromElement(s); + g(s) + .one(_.TRANSITION_END, function () { + g(l) + .removeClass(n + ' ' + i) + .addClass(V), + g(s).removeClass(V + ' ' + i + ' ' + n), + (r._isSliding = !1), + setTimeout(function () { + return g(r._element).trigger(u); + }, 0); + }) + .emulateTransitionEnd(d); + } else + g(s).removeClass(V), + g(l).addClass(V), + (this._isSliding = !1), + g(this._element).trigger(u); + h && this.cycle(); + } + }), + (r._jQueryInterface = function (i) { + return this.each(function () { + var t = g(this).data(j), + e = l({}, F, g(this).data()); + 'object' == typeof i && (e = l({}, e, i)); + var n = 'string' == typeof i ? i : e.slide; + if ( + (t || ((t = new r(this, e)), g(this).data(j, t)), + 'number' == typeof i) + ) + t.to(i); + else if ('string' == typeof n) { + if ('undefined' == typeof t[n]) + throw new TypeError('No method named "' + n + '"'); + t[n](); + } else e.interval && e.ride && (t.pause(), t.cycle()); + }); + }), + (r._dataApiClickHandler = function (t) { + var e = _.getSelectorFromElement(this); + if (e) { + var n = g(e)[0]; + if (n && g(n).hasClass(B)) { + var i = l({}, g(n).data(), g(this).data()), + o = this.getAttribute('data-slide-to'); + o && (i.interval = !1), + r._jQueryInterface.call(g(n), i), + o && g(n).data(j).to(o), + t.preventDefault(); + } + } + }), + s(r, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + { + key: 'Default', + get: function () { + return F; + }, + }, + ]), + r + ); + })(); + g(document).on(Q.CLICK_DATA_API, rt, lt._dataApiClickHandler), + g(window).on(Q.LOAD_DATA_API, function () { + for ( + var t = [].slice.call(document.querySelectorAll(st)), + e = 0, + n = t.length; + e < n; + e++ + ) { + var i = g(t[e]); + lt._jQueryInterface.call(i, i.data()); + } + }), + (g.fn[L] = lt._jQueryInterface), + (g.fn[L].Constructor = lt), + (g.fn[L].noConflict = function () { + return (g.fn[L] = x), lt._jQueryInterface; + }); + var ct = 'collapse', + ht = 'bs.collapse', + ut = '.' + ht, + ft = g.fn[ct], + dt = { toggle: !0, parent: '' }, + gt = { toggle: 'boolean', parent: '(string|element)' }, + _t = { + SHOW: 'show' + ut, + SHOWN: 'shown' + ut, + HIDE: 'hide' + ut, + HIDDEN: 'hidden' + ut, + CLICK_DATA_API: 'click' + ut + '.data-api', + }, + mt = 'show', + pt = 'collapse', + vt = 'collapsing', + yt = 'collapsed', + Et = 'width', + Ct = 'height', + Tt = '.show, .collapsing', + St = '[data-toggle="collapse"]', + bt = (function () { + function a(e, t) { + (this._isTransitioning = !1), + (this._element = e), + (this._config = this._getConfig(t)), + (this._triggerArray = [].slice.call( + document.querySelectorAll( + '[data-toggle="collapse"][href="#' + + e.id + + '"],[data-toggle="collapse"][data-target="#' + + e.id + + '"]', + ), + )); + for ( + var n = [].slice.call(document.querySelectorAll(St)), + i = 0, + o = n.length; + i < o; + i++ + ) { + var r = n[i], + s = _.getSelectorFromElement(r), + a = [].slice + .call(document.querySelectorAll(s)) + .filter(function (t) { + return t === e; + }); + null !== s && + 0 < a.length && + ((this._selector = s), this._triggerArray.push(r)); + } + (this._parent = this._config.parent ? this._getParent() : null), + this._config.parent || + this._addAriaAndCollapsedClass(this._element, this._triggerArray), + this._config.toggle && this.toggle(); + } + var t = a.prototype; + return ( + (t.toggle = function () { + g(this._element).hasClass(mt) ? this.hide() : this.show(); + }), + (t.show = function () { + var t, + e, + n = this; + if ( + !this._isTransitioning && + !g(this._element).hasClass(mt) && + (this._parent && + 0 === + (t = [].slice + .call(this._parent.querySelectorAll(Tt)) + .filter(function (t) { + return 'string' == typeof n._config.parent + ? t.getAttribute('data-parent') === n._config.parent + : t.classList.contains(pt); + })).length && + (t = null), + !( + t && + (e = g(t).not(this._selector).data(ht)) && + e._isTransitioning + )) + ) { + var i = g.Event(_t.SHOW); + if ((g(this._element).trigger(i), !i.isDefaultPrevented())) { + t && + (a._jQueryInterface.call(g(t).not(this._selector), 'hide'), + e || g(t).data(ht, null)); + var o = this._getDimension(); + g(this._element).removeClass(pt).addClass(vt), + (this._element.style[o] = 0), + this._triggerArray.length && + g(this._triggerArray) + .removeClass(yt) + .attr('aria-expanded', !0), + this.setTransitioning(!0); + var r = 'scroll' + (o[0].toUpperCase() + o.slice(1)), + s = _.getTransitionDurationFromElement(this._element); + g(this._element) + .one(_.TRANSITION_END, function () { + g(n._element).removeClass(vt).addClass(pt).addClass(mt), + (n._element.style[o] = ''), + n.setTransitioning(!1), + g(n._element).trigger(_t.SHOWN); + }) + .emulateTransitionEnd(s), + (this._element.style[o] = this._element[r] + 'px'); + } + } + }), + (t.hide = function () { + var t = this; + if (!this._isTransitioning && g(this._element).hasClass(mt)) { + var e = g.Event(_t.HIDE); + if ((g(this._element).trigger(e), !e.isDefaultPrevented())) { + var n = this._getDimension(); + (this._element.style[n] = + this._element.getBoundingClientRect()[n] + 'px'), + _.reflow(this._element), + g(this._element).addClass(vt).removeClass(pt).removeClass(mt); + var i = this._triggerArray.length; + if (0 < i) + for (var o = 0; o < i; o++) { + var r = this._triggerArray[o], + s = _.getSelectorFromElement(r); + if (null !== s) + g([].slice.call(document.querySelectorAll(s))).hasClass( + mt, + ) || g(r).addClass(yt).attr('aria-expanded', !1); + } + this.setTransitioning(!0); + this._element.style[n] = ''; + var a = _.getTransitionDurationFromElement(this._element); + g(this._element) + .one(_.TRANSITION_END, function () { + t.setTransitioning(!1), + g(t._element) + .removeClass(vt) + .addClass(pt) + .trigger(_t.HIDDEN); + }) + .emulateTransitionEnd(a); + } + } + }), + (t.setTransitioning = function (t) { + this._isTransitioning = t; + }), + (t.dispose = function () { + g.removeData(this._element, ht), + (this._config = null), + (this._parent = null), + (this._element = null), + (this._triggerArray = null), + (this._isTransitioning = null); + }), + (t._getConfig = function (t) { + return ( + ((t = l({}, dt, t)).toggle = Boolean(t.toggle)), + _.typeCheckConfig(ct, t, gt), + t + ); + }), + (t._getDimension = function () { + return g(this._element).hasClass(Et) ? Et : Ct; + }), + (t._getParent = function () { + var t, + n = this; + _.isElement(this._config.parent) + ? ((t = this._config.parent), + 'undefined' != typeof this._config.parent.jquery && + (t = this._config.parent[0])) + : (t = document.querySelector(this._config.parent)); + var e = + '[data-toggle="collapse"][data-parent="' + + this._config.parent + + '"]', + i = [].slice.call(t.querySelectorAll(e)); + return ( + g(i).each(function (t, e) { + n._addAriaAndCollapsedClass(a._getTargetFromElement(e), [e]); + }), + t + ); + }), + (t._addAriaAndCollapsedClass = function (t, e) { + var n = g(t).hasClass(mt); + e.length && g(e).toggleClass(yt, !n).attr('aria-expanded', n); + }), + (a._getTargetFromElement = function (t) { + var e = _.getSelectorFromElement(t); + return e ? document.querySelector(e) : null; + }), + (a._jQueryInterface = function (i) { + return this.each(function () { + var t = g(this), + e = t.data(ht), + n = l({}, dt, t.data(), 'object' == typeof i && i ? i : {}); + if ( + (!e && n.toggle && /show|hide/.test(i) && (n.toggle = !1), + e || ((e = new a(this, n)), t.data(ht, e)), + 'string' == typeof i) + ) { + if ('undefined' == typeof e[i]) + throw new TypeError('No method named "' + i + '"'); + e[i](); + } + }); + }), + s(a, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + { + key: 'Default', + get: function () { + return dt; + }, + }, + ]), + a + ); + })(); + g(document).on(_t.CLICK_DATA_API, St, function (t) { + 'A' === t.currentTarget.tagName && t.preventDefault(); + var n = g(this), + e = _.getSelectorFromElement(this), + i = [].slice.call(document.querySelectorAll(e)); + g(i).each(function () { + var t = g(this), + e = t.data(ht) ? 'toggle' : n.data(); + bt._jQueryInterface.call(t, e); + }); + }), + (g.fn[ct] = bt._jQueryInterface), + (g.fn[ct].Constructor = bt), + (g.fn[ct].noConflict = function () { + return (g.fn[ct] = ft), bt._jQueryInterface; + }); + var It = 'dropdown', + Dt = 'bs.dropdown', + wt = '.' + Dt, + At = '.data-api', + Nt = g.fn[It], + Ot = new RegExp('38|40|27'), + kt = { + HIDE: 'hide' + wt, + HIDDEN: 'hidden' + wt, + SHOW: 'show' + wt, + SHOWN: 'shown' + wt, + CLICK: 'click' + wt, + CLICK_DATA_API: 'click' + wt + At, + KEYDOWN_DATA_API: 'keydown' + wt + At, + KEYUP_DATA_API: 'keyup' + wt + At, + }, + Pt = 'disabled', + Lt = 'show', + jt = 'dropup', + Ht = 'dropright', + Rt = 'dropleft', + xt = 'dropdown-menu-right', + Ft = 'position-static', + Ut = '[data-toggle="dropdown"]', + Wt = '.dropdown form', + qt = '.dropdown-menu', + Mt = '.navbar-nav', + Kt = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)', + Qt = 'top-start', + Bt = 'top-end', + Vt = 'bottom-start', + Yt = 'bottom-end', + zt = 'right-start', + Xt = 'left-start', + $t = { + offset: 0, + flip: !0, + boundary: 'scrollParent', + reference: 'toggle', + display: 'dynamic', + }, + Gt = { + offset: '(number|string|function)', + flip: 'boolean', + boundary: '(string|element)', + reference: '(string|element)', + display: 'string', + }, + Jt = (function () { + function c(t, e) { + (this._element = t), + (this._popper = null), + (this._config = this._getConfig(e)), + (this._menu = this._getMenuElement()), + (this._inNavbar = this._detectNavbar()), + this._addEventListeners(); + } + var t = c.prototype; + return ( + (t.toggle = function () { + if (!this._element.disabled && !g(this._element).hasClass(Pt)) { + var t = c._getParentFromElement(this._element), + e = g(this._menu).hasClass(Lt); + if ((c._clearMenus(), !e)) { + var n = { relatedTarget: this._element }, + i = g.Event(kt.SHOW, n); + if ((g(t).trigger(i), !i.isDefaultPrevented())) { + if (!this._inNavbar) { + if ('undefined' == typeof u) + throw new TypeError( + "Bootstrap's dropdowns require Popper.js (https://popper.js.org/)", + ); + var o = this._element; + 'parent' === this._config.reference + ? (o = t) + : _.isElement(this._config.reference) && + ((o = this._config.reference), + 'undefined' != typeof this._config.reference.jquery && + (o = this._config.reference[0])), + 'scrollParent' !== this._config.boundary && + g(t).addClass(Ft), + (this._popper = new u( + o, + this._menu, + this._getPopperConfig(), + )); + } + 'ontouchstart' in document.documentElement && + 0 === g(t).closest(Mt).length && + g(document.body).children().on('mouseover', null, g.noop), + this._element.focus(), + this._element.setAttribute('aria-expanded', !0), + g(this._menu).toggleClass(Lt), + g(t).toggleClass(Lt).trigger(g.Event(kt.SHOWN, n)); + } + } + } + }), + (t.show = function () { + if ( + !( + this._element.disabled || + g(this._element).hasClass(Pt) || + g(this._menu).hasClass(Lt) + ) + ) { + var t = { relatedTarget: this._element }, + e = g.Event(kt.SHOW, t), + n = c._getParentFromElement(this._element); + g(n).trigger(e), + e.isDefaultPrevented() || + (g(this._menu).toggleClass(Lt), + g(n).toggleClass(Lt).trigger(g.Event(kt.SHOWN, t))); + } + }), + (t.hide = function () { + if ( + !this._element.disabled && + !g(this._element).hasClass(Pt) && + g(this._menu).hasClass(Lt) + ) { + var t = { relatedTarget: this._element }, + e = g.Event(kt.HIDE, t), + n = c._getParentFromElement(this._element); + g(n).trigger(e), + e.isDefaultPrevented() || + (g(this._menu).toggleClass(Lt), + g(n).toggleClass(Lt).trigger(g.Event(kt.HIDDEN, t))); + } + }), + (t.dispose = function () { + g.removeData(this._element, Dt), + g(this._element).off(wt), + (this._element = null), + (this._menu = null) !== this._popper && + (this._popper.destroy(), (this._popper = null)); + }), + (t.update = function () { + (this._inNavbar = this._detectNavbar()), + null !== this._popper && this._popper.scheduleUpdate(); + }), + (t._addEventListeners = function () { + var e = this; + g(this._element).on(kt.CLICK, function (t) { + t.preventDefault(), t.stopPropagation(), e.toggle(); + }); + }), + (t._getConfig = function (t) { + return ( + (t = l({}, this.constructor.Default, g(this._element).data(), t)), + _.typeCheckConfig(It, t, this.constructor.DefaultType), + t + ); + }), + (t._getMenuElement = function () { + if (!this._menu) { + var t = c._getParentFromElement(this._element); + t && (this._menu = t.querySelector(qt)); + } + return this._menu; + }), + (t._getPlacement = function () { + var t = g(this._element.parentNode), + e = Vt; + return ( + t.hasClass(jt) + ? ((e = Qt), g(this._menu).hasClass(xt) && (e = Bt)) + : t.hasClass(Ht) + ? (e = zt) + : t.hasClass(Rt) + ? (e = Xt) + : g(this._menu).hasClass(xt) && (e = Yt), + e + ); + }), + (t._detectNavbar = function () { + return 0 < g(this._element).closest('.navbar').length; + }), + (t._getOffset = function () { + var e = this, + t = {}; + return ( + 'function' == typeof this._config.offset + ? (t.fn = function (t) { + return ( + (t.offsets = l( + {}, + t.offsets, + e._config.offset(t.offsets, e._element) || {}, + )), + t + ); + }) + : (t.offset = this._config.offset), + t + ); + }), + (t._getPopperConfig = function () { + var t = { + placement: this._getPlacement(), + modifiers: { + offset: this._getOffset(), + flip: { enabled: this._config.flip }, + preventOverflow: { boundariesElement: this._config.boundary }, + }, + }; + return ( + 'static' === this._config.display && + (t.modifiers.applyStyle = { enabled: !1 }), + t + ); + }), + (c._jQueryInterface = function (e) { + return this.each(function () { + var t = g(this).data(Dt); + if ( + (t || + ((t = new c(this, 'object' == typeof e ? e : null)), + g(this).data(Dt, t)), + 'string' == typeof e) + ) { + if ('undefined' == typeof t[e]) + throw new TypeError('No method named "' + e + '"'); + t[e](); + } + }); + }), + (c._clearMenus = function (t) { + if (!t || (3 !== t.which && ('keyup' !== t.type || 9 === t.which))) + for ( + var e = [].slice.call(document.querySelectorAll(Ut)), + n = 0, + i = e.length; + n < i; + n++ + ) { + var o = c._getParentFromElement(e[n]), + r = g(e[n]).data(Dt), + s = { relatedTarget: e[n] }; + if ((t && 'click' === t.type && (s.clickEvent = t), r)) { + var a = r._menu; + if ( + g(o).hasClass(Lt) && + !( + t && + (('click' === t.type && + /input|textarea/i.test(t.target.tagName)) || + ('keyup' === t.type && 9 === t.which)) && + g.contains(o, t.target) + ) + ) { + var l = g.Event(kt.HIDE, s); + g(o).trigger(l), + l.isDefaultPrevented() || + ('ontouchstart' in document.documentElement && + g(document.body) + .children() + .off('mouseover', null, g.noop), + e[n].setAttribute('aria-expanded', 'false'), + g(a).removeClass(Lt), + g(o).removeClass(Lt).trigger(g.Event(kt.HIDDEN, s))); + } + } + } + }), + (c._getParentFromElement = function (t) { + var e, + n = _.getSelectorFromElement(t); + return n && (e = document.querySelector(n)), e || t.parentNode; + }), + (c._dataApiKeydownHandler = function (t) { + if ( + (/input|textarea/i.test(t.target.tagName) + ? !( + 32 === t.which || + (27 !== t.which && + ((40 !== t.which && 38 !== t.which) || + g(t.target).closest(qt).length)) + ) + : Ot.test(t.which)) && + (t.preventDefault(), + t.stopPropagation(), + !this.disabled && !g(this).hasClass(Pt)) + ) { + var e = c._getParentFromElement(this), + n = g(e).hasClass(Lt); + if (n && (!n || (27 !== t.which && 32 !== t.which))) { + var i = [].slice.call(e.querySelectorAll(Kt)); + if (0 !== i.length) { + var o = i.indexOf(t.target); + 38 === t.which && 0 < o && o--, + 40 === t.which && o < i.length - 1 && o++, + o < 0 && (o = 0), + i[o].focus(); + } + } else { + if (27 === t.which) { + var r = e.querySelector(Ut); + g(r).trigger('focus'); + } + g(this).trigger('click'); + } + } + }), + s(c, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + { + key: 'Default', + get: function () { + return $t; + }, + }, + { + key: 'DefaultType', + get: function () { + return Gt; + }, + }, + ]), + c + ); + })(); + g(document) + .on(kt.KEYDOWN_DATA_API, Ut, Jt._dataApiKeydownHandler) + .on(kt.KEYDOWN_DATA_API, qt, Jt._dataApiKeydownHandler) + .on(kt.CLICK_DATA_API + ' ' + kt.KEYUP_DATA_API, Jt._clearMenus) + .on(kt.CLICK_DATA_API, Ut, function (t) { + t.preventDefault(), + t.stopPropagation(), + Jt._jQueryInterface.call(g(this), 'toggle'); + }) + .on(kt.CLICK_DATA_API, Wt, function (t) { + t.stopPropagation(); + }), + (g.fn[It] = Jt._jQueryInterface), + (g.fn[It].Constructor = Jt), + (g.fn[It].noConflict = function () { + return (g.fn[It] = Nt), Jt._jQueryInterface; + }); + var Zt = 'modal', + te = 'bs.modal', + ee = '.' + te, + ne = g.fn[Zt], + ie = { backdrop: !0, keyboard: !0, focus: !0, show: !0 }, + oe = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + focus: 'boolean', + show: 'boolean', + }, + re = { + HIDE: 'hide' + ee, + HIDDEN: 'hidden' + ee, + SHOW: 'show' + ee, + SHOWN: 'shown' + ee, + FOCUSIN: 'focusin' + ee, + RESIZE: 'resize' + ee, + CLICK_DISMISS: 'click.dismiss' + ee, + KEYDOWN_DISMISS: 'keydown.dismiss' + ee, + MOUSEUP_DISMISS: 'mouseup.dismiss' + ee, + MOUSEDOWN_DISMISS: 'mousedown.dismiss' + ee, + CLICK_DATA_API: 'click' + ee + '.data-api', + }, + se = 'modal-dialog-scrollable', + ae = 'modal-scrollbar-measure', + le = 'modal-backdrop', + ce = 'modal-open', + he = 'fade', + ue = 'show', + fe = '.modal-dialog', + de = '.modal-body', + ge = '[data-toggle="modal"]', + _e = '[data-dismiss="modal"]', + me = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', + pe = '.sticky-top', + ve = (function () { + function o(t, e) { + (this._config = this._getConfig(e)), + (this._element = t), + (this._dialog = t.querySelector(fe)), + (this._backdrop = null), + (this._isShown = !1), + (this._isBodyOverflowing = !1), + (this._ignoreBackdropClick = !1), + (this._isTransitioning = !1), + (this._scrollbarWidth = 0); + } + var t = o.prototype; + return ( + (t.toggle = function (t) { + return this._isShown ? this.hide() : this.show(t); + }), + (t.show = function (t) { + var e = this; + if (!this._isShown && !this._isTransitioning) { + g(this._element).hasClass(he) && (this._isTransitioning = !0); + var n = g.Event(re.SHOW, { relatedTarget: t }); + g(this._element).trigger(n), + this._isShown || + n.isDefaultPrevented() || + ((this._isShown = !0), + this._checkScrollbar(), + this._setScrollbar(), + this._adjustDialog(), + this._setEscapeEvent(), + this._setResizeEvent(), + g(this._element).on(re.CLICK_DISMISS, _e, function (t) { + return e.hide(t); + }), + g(this._dialog).on(re.MOUSEDOWN_DISMISS, function () { + g(e._element).one(re.MOUSEUP_DISMISS, function (t) { + g(t.target).is(e._element) && (e._ignoreBackdropClick = !0); + }); + }), + this._showBackdrop(function () { + return e._showElement(t); + })); + } + }), + (t.hide = function (t) { + var e = this; + if ( + (t && t.preventDefault(), this._isShown && !this._isTransitioning) + ) { + var n = g.Event(re.HIDE); + if ( + (g(this._element).trigger(n), + this._isShown && !n.isDefaultPrevented()) + ) { + this._isShown = !1; + var i = g(this._element).hasClass(he); + if ( + (i && (this._isTransitioning = !0), + this._setEscapeEvent(), + this._setResizeEvent(), + g(document).off(re.FOCUSIN), + g(this._element).removeClass(ue), + g(this._element).off(re.CLICK_DISMISS), + g(this._dialog).off(re.MOUSEDOWN_DISMISS), + i) + ) { + var o = _.getTransitionDurationFromElement(this._element); + g(this._element) + .one(_.TRANSITION_END, function (t) { + return e._hideModal(t); + }) + .emulateTransitionEnd(o); + } else this._hideModal(); + } + } + }), + (t.dispose = function () { + [window, this._element, this._dialog].forEach(function (t) { + return g(t).off(ee); + }), + g(document).off(re.FOCUSIN), + g.removeData(this._element, te), + (this._config = null), + (this._element = null), + (this._dialog = null), + (this._backdrop = null), + (this._isShown = null), + (this._isBodyOverflowing = null), + (this._ignoreBackdropClick = null), + (this._isTransitioning = null), + (this._scrollbarWidth = null); + }), + (t.handleUpdate = function () { + this._adjustDialog(); + }), + (t._getConfig = function (t) { + return (t = l({}, ie, t)), _.typeCheckConfig(Zt, t, oe), t; + }), + (t._showElement = function (t) { + var e = this, + n = g(this._element).hasClass(he); + (this._element.parentNode && + this._element.parentNode.nodeType === Node.ELEMENT_NODE) || + document.body.appendChild(this._element), + (this._element.style.display = 'block'), + this._element.removeAttribute('aria-hidden'), + this._element.setAttribute('aria-modal', !0), + g(this._dialog).hasClass(se) + ? (this._dialog.querySelector(de).scrollTop = 0) + : (this._element.scrollTop = 0), + n && _.reflow(this._element), + g(this._element).addClass(ue), + this._config.focus && this._enforceFocus(); + var i = g.Event(re.SHOWN, { relatedTarget: t }), + o = function () { + e._config.focus && e._element.focus(), + (e._isTransitioning = !1), + g(e._element).trigger(i); + }; + if (n) { + var r = _.getTransitionDurationFromElement(this._dialog); + g(this._dialog).one(_.TRANSITION_END, o).emulateTransitionEnd(r); + } else o(); + }), + (t._enforceFocus = function () { + var e = this; + g(document) + .off(re.FOCUSIN) + .on(re.FOCUSIN, function (t) { + document !== t.target && + e._element !== t.target && + 0 === g(e._element).has(t.target).length && + e._element.focus(); + }); + }), + (t._setEscapeEvent = function () { + var e = this; + this._isShown && this._config.keyboard + ? g(this._element).on(re.KEYDOWN_DISMISS, function (t) { + 27 === t.which && (t.preventDefault(), e.hide()); + }) + : this._isShown || g(this._element).off(re.KEYDOWN_DISMISS); + }), + (t._setResizeEvent = function () { + var e = this; + this._isShown + ? g(window).on(re.RESIZE, function (t) { + return e.handleUpdate(t); + }) + : g(window).off(re.RESIZE); + }), + (t._hideModal = function () { + var t = this; + (this._element.style.display = 'none'), + this._element.setAttribute('aria-hidden', !0), + this._element.removeAttribute('aria-modal'), + (this._isTransitioning = !1), + this._showBackdrop(function () { + g(document.body).removeClass(ce), + t._resetAdjustments(), + t._resetScrollbar(), + g(t._element).trigger(re.HIDDEN); + }); + }), + (t._removeBackdrop = function () { + this._backdrop && + (g(this._backdrop).remove(), (this._backdrop = null)); + }), + (t._showBackdrop = function (t) { + var e = this, + n = g(this._element).hasClass(he) ? he : ''; + if (this._isShown && this._config.backdrop) { + if ( + ((this._backdrop = document.createElement('div')), + (this._backdrop.className = le), + n && this._backdrop.classList.add(n), + g(this._backdrop).appendTo(document.body), + g(this._element).on(re.CLICK_DISMISS, function (t) { + e._ignoreBackdropClick + ? (e._ignoreBackdropClick = !1) + : t.target === t.currentTarget && + ('static' === e._config.backdrop + ? e._element.focus() + : e.hide()); + }), + n && _.reflow(this._backdrop), + g(this._backdrop).addClass(ue), + !t) + ) + return; + if (!n) return void t(); + var i = _.getTransitionDurationFromElement(this._backdrop); + g(this._backdrop).one(_.TRANSITION_END, t).emulateTransitionEnd(i); + } else if (!this._isShown && this._backdrop) { + g(this._backdrop).removeClass(ue); + var o = function () { + e._removeBackdrop(), t && t(); + }; + if (g(this._element).hasClass(he)) { + var r = _.getTransitionDurationFromElement(this._backdrop); + g(this._backdrop) + .one(_.TRANSITION_END, o) + .emulateTransitionEnd(r); + } else o(); + } else t && t(); + }), + (t._adjustDialog = function () { + var t = + this._element.scrollHeight > document.documentElement.clientHeight; + !this._isBodyOverflowing && + t && + (this._element.style.paddingLeft = this._scrollbarWidth + 'px'), + this._isBodyOverflowing && + !t && + (this._element.style.paddingRight = this._scrollbarWidth + 'px'); + }), + (t._resetAdjustments = function () { + (this._element.style.paddingLeft = ''), + (this._element.style.paddingRight = ''); + }), + (t._checkScrollbar = function () { + var t = document.body.getBoundingClientRect(); + (this._isBodyOverflowing = t.left + t.right < window.innerWidth), + (this._scrollbarWidth = this._getScrollbarWidth()); + }), + (t._setScrollbar = function () { + var o = this; + if (this._isBodyOverflowing) { + var t = [].slice.call(document.querySelectorAll(me)), + e = [].slice.call(document.querySelectorAll(pe)); + g(t).each(function (t, e) { + var n = e.style.paddingRight, + i = g(e).css('padding-right'); + g(e) + .data('padding-right', n) + .css('padding-right', parseFloat(i) + o._scrollbarWidth + 'px'); + }), + g(e).each(function (t, e) { + var n = e.style.marginRight, + i = g(e).css('margin-right'); + g(e) + .data('margin-right', n) + .css( + 'margin-right', + parseFloat(i) - o._scrollbarWidth + 'px', + ); + }); + var n = document.body.style.paddingRight, + i = g(document.body).css('padding-right'); + g(document.body) + .data('padding-right', n) + .css( + 'padding-right', + parseFloat(i) + this._scrollbarWidth + 'px', + ); + } + g(document.body).addClass(ce); + }), + (t._resetScrollbar = function () { + var t = [].slice.call(document.querySelectorAll(me)); + g(t).each(function (t, e) { + var n = g(e).data('padding-right'); + g(e).removeData('padding-right'), (e.style.paddingRight = n || ''); + }); + var e = [].slice.call(document.querySelectorAll('' + pe)); + g(e).each(function (t, e) { + var n = g(e).data('margin-right'); + 'undefined' != typeof n && + g(e).css('margin-right', n).removeData('margin-right'); + }); + var n = g(document.body).data('padding-right'); + g(document.body).removeData('padding-right'), + (document.body.style.paddingRight = n || ''); + }), + (t._getScrollbarWidth = function () { + var t = document.createElement('div'); + (t.className = ae), document.body.appendChild(t); + var e = t.getBoundingClientRect().width - t.clientWidth; + return document.body.removeChild(t), e; + }), + (o._jQueryInterface = function (n, i) { + return this.each(function () { + var t = g(this).data(te), + e = l({}, ie, g(this).data(), 'object' == typeof n && n ? n : {}); + if ( + (t || ((t = new o(this, e)), g(this).data(te, t)), + 'string' == typeof n) + ) { + if ('undefined' == typeof t[n]) + throw new TypeError('No method named "' + n + '"'); + t[n](i); + } else e.show && t.show(i); + }); + }), + s(o, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + { + key: 'Default', + get: function () { + return ie; + }, + }, + ]), + o + ); + })(); + g(document).on(re.CLICK_DATA_API, ge, function (t) { + var e, + n = this, + i = _.getSelectorFromElement(this); + i && (e = document.querySelector(i)); + var o = g(e).data(te) ? 'toggle' : l({}, g(e).data(), g(this).data()); + ('A' !== this.tagName && 'AREA' !== this.tagName) || t.preventDefault(); + var r = g(e).one(re.SHOW, function (t) { + t.isDefaultPrevented() || + r.one(re.HIDDEN, function () { + g(n).is(':visible') && n.focus(); + }); + }); + ve._jQueryInterface.call(g(e), o, this); + }), + (g.fn[Zt] = ve._jQueryInterface), + (g.fn[Zt].Constructor = ve), + (g.fn[Zt].noConflict = function () { + return (g.fn[Zt] = ne), ve._jQueryInterface; + }); + var ye = [ + 'background', + 'cite', + 'href', + 'itemtype', + 'longdesc', + 'poster', + 'src', + 'xlink:href', + ], + Ee = { + '*': ['class', 'dir', 'id', 'lang', 'role', /^aria-[\w-]*$/i], + a: ['target', 'href', 'title', 'rel'], + area: [], + b: [], + br: [], + col: [], + code: [], + div: [], + em: [], + hr: [], + h1: [], + h2: [], + h3: [], + h4: [], + h5: [], + h6: [], + i: [], + img: ['src', 'alt', 'title', 'width', 'height'], + li: [], + ol: [], + p: [], + pre: [], + s: [], + small: [], + span: [], + sub: [], + sup: [], + strong: [], + u: [], + ul: [], + }, + Ce = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi, + Te = + /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i; + function Se(t, s, e) { + if (0 === t.length) return t; + if (e && 'function' == typeof e) return e(t); + for ( + var n = new window.DOMParser().parseFromString(t, 'text/html'), + a = Object.keys(s), + l = [].slice.call(n.body.querySelectorAll('*')), + i = function (t, e) { + var n = l[t], + i = n.nodeName.toLowerCase(); + if (-1 === a.indexOf(n.nodeName.toLowerCase())) + return n.parentNode.removeChild(n), 'continue'; + var o = [].slice.call(n.attributes), + r = [].concat(s['*'] || [], s[i] || []); + o.forEach(function (t) { + (function (t, e) { + var n = t.nodeName.toLowerCase(); + if (-1 !== e.indexOf(n)) + return ( + -1 === ye.indexOf(n) || + Boolean(t.nodeValue.match(Ce) || t.nodeValue.match(Te)) + ); + for ( + var i = e.filter(function (t) { + return t instanceof RegExp; + }), + o = 0, + r = i.length; + o < r; + o++ + ) + if (n.match(i[o])) return !0; + return !1; + })(t, r) || n.removeAttribute(t.nodeName); + }); + }, + o = 0, + r = l.length; + o < r; + o++ + ) + i(o); + return n.body.innerHTML; + } + var be = 'tooltip', + Ie = 'bs.tooltip', + De = '.' + Ie, + we = g.fn[be], + Ae = 'bs-tooltip', + Ne = new RegExp('(^|\\s)' + Ae + '\\S+', 'g'), + Oe = ['sanitize', 'whiteList', 'sanitizeFn'], + ke = { + animation: 'boolean', + template: 'string', + title: '(string|element|function)', + trigger: 'string', + delay: '(number|object)', + html: 'boolean', + selector: '(string|boolean)', + placement: '(string|function)', + offset: '(number|string|function)', + container: '(string|element|boolean)', + fallbackPlacement: '(string|array)', + boundary: '(string|element)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + whiteList: 'object', + }, + Pe = { + AUTO: 'auto', + TOP: 'top', + RIGHT: 'right', + BOTTOM: 'bottom', + LEFT: 'left', + }, + Le = { + animation: !0, + template: + '', + trigger: 'hover focus', + title: '', + delay: 0, + html: !1, + selector: !1, + placement: 'top', + offset: 0, + container: !1, + fallbackPlacement: 'flip', + boundary: 'scrollParent', + sanitize: !0, + sanitizeFn: null, + whiteList: Ee, + }, + je = 'show', + He = 'out', + Re = { + HIDE: 'hide' + De, + HIDDEN: 'hidden' + De, + SHOW: 'show' + De, + SHOWN: 'shown' + De, + INSERTED: 'inserted' + De, + CLICK: 'click' + De, + FOCUSIN: 'focusin' + De, + FOCUSOUT: 'focusout' + De, + MOUSEENTER: 'mouseenter' + De, + MOUSELEAVE: 'mouseleave' + De, + }, + xe = 'fade', + Fe = 'show', + Ue = '.tooltip-inner', + We = '.arrow', + qe = 'hover', + Me = 'focus', + Ke = 'click', + Qe = 'manual', + Be = (function () { + function i(t, e) { + if ('undefined' == typeof u) + throw new TypeError( + "Bootstrap's tooltips require Popper.js (https://popper.js.org/)", + ); + (this._isEnabled = !0), + (this._timeout = 0), + (this._hoverState = ''), + (this._activeTrigger = {}), + (this._popper = null), + (this.element = t), + (this.config = this._getConfig(e)), + (this.tip = null), + this._setListeners(); + } + var t = i.prototype; + return ( + (t.enable = function () { + this._isEnabled = !0; + }), + (t.disable = function () { + this._isEnabled = !1; + }), + (t.toggleEnabled = function () { + this._isEnabled = !this._isEnabled; + }), + (t.toggle = function (t) { + if (this._isEnabled) + if (t) { + var e = this.constructor.DATA_KEY, + n = g(t.currentTarget).data(e); + n || + ((n = new this.constructor( + t.currentTarget, + this._getDelegateConfig(), + )), + g(t.currentTarget).data(e, n)), + (n._activeTrigger.click = !n._activeTrigger.click), + n._isWithActiveTrigger() + ? n._enter(null, n) + : n._leave(null, n); + } else { + if (g(this.getTipElement()).hasClass(Fe)) + return void this._leave(null, this); + this._enter(null, this); + } + }), + (t.dispose = function () { + clearTimeout(this._timeout), + g.removeData(this.element, this.constructor.DATA_KEY), + g(this.element).off(this.constructor.EVENT_KEY), + g(this.element).closest('.modal').off('hide.bs.modal'), + this.tip && g(this.tip).remove(), + (this._isEnabled = null), + (this._timeout = null), + (this._hoverState = null), + (this._activeTrigger = null) !== this._popper && + this._popper.destroy(), + (this._popper = null), + (this.element = null), + (this.config = null), + (this.tip = null); + }), + (t.show = function () { + var e = this; + if ('none' === g(this.element).css('display')) + throw new Error('Please use show on visible elements'); + var t = g.Event(this.constructor.Event.SHOW); + if (this.isWithContent() && this._isEnabled) { + g(this.element).trigger(t); + var n = _.findShadowRoot(this.element), + i = g.contains( + null !== n ? n : this.element.ownerDocument.documentElement, + this.element, + ); + if (t.isDefaultPrevented() || !i) return; + var o = this.getTipElement(), + r = _.getUID(this.constructor.NAME); + o.setAttribute('id', r), + this.element.setAttribute('aria-describedby', r), + this.setContent(), + this.config.animation && g(o).addClass(xe); + var s = + 'function' == typeof this.config.placement + ? this.config.placement.call(this, o, this.element) + : this.config.placement, + a = this._getAttachment(s); + this.addAttachmentClass(a); + var l = this._getContainer(); + g(o).data(this.constructor.DATA_KEY, this), + g.contains( + this.element.ownerDocument.documentElement, + this.tip, + ) || g(o).appendTo(l), + g(this.element).trigger(this.constructor.Event.INSERTED), + (this._popper = new u(this.element, o, { + placement: a, + modifiers: { + offset: this._getOffset(), + flip: { behavior: this.config.fallbackPlacement }, + arrow: { element: We }, + preventOverflow: { boundariesElement: this.config.boundary }, + }, + onCreate: function (t) { + t.originalPlacement !== t.placement && + e._handlePopperPlacementChange(t); + }, + onUpdate: function (t) { + return e._handlePopperPlacementChange(t); + }, + })), + g(o).addClass(Fe), + 'ontouchstart' in document.documentElement && + g(document.body).children().on('mouseover', null, g.noop); + var c = function () { + e.config.animation && e._fixTransition(); + var t = e._hoverState; + (e._hoverState = null), + g(e.element).trigger(e.constructor.Event.SHOWN), + t === He && e._leave(null, e); + }; + if (g(this.tip).hasClass(xe)) { + var h = _.getTransitionDurationFromElement(this.tip); + g(this.tip).one(_.TRANSITION_END, c).emulateTransitionEnd(h); + } else c(); + } + }), + (t.hide = function (t) { + var e = this, + n = this.getTipElement(), + i = g.Event(this.constructor.Event.HIDE), + o = function () { + e._hoverState !== je && + n.parentNode && + n.parentNode.removeChild(n), + e._cleanTipClass(), + e.element.removeAttribute('aria-describedby'), + g(e.element).trigger(e.constructor.Event.HIDDEN), + null !== e._popper && e._popper.destroy(), + t && t(); + }; + if ((g(this.element).trigger(i), !i.isDefaultPrevented())) { + if ( + (g(n).removeClass(Fe), + 'ontouchstart' in document.documentElement && + g(document.body).children().off('mouseover', null, g.noop), + (this._activeTrigger[Ke] = !1), + (this._activeTrigger[Me] = !1), + (this._activeTrigger[qe] = !1), + g(this.tip).hasClass(xe)) + ) { + var r = _.getTransitionDurationFromElement(n); + g(n).one(_.TRANSITION_END, o).emulateTransitionEnd(r); + } else o(); + this._hoverState = ''; + } + }), + (t.update = function () { + null !== this._popper && this._popper.scheduleUpdate(); + }), + (t.isWithContent = function () { + return Boolean(this.getTitle()); + }), + (t.addAttachmentClass = function (t) { + g(this.getTipElement()).addClass(Ae + '-' + t); + }), + (t.getTipElement = function () { + return (this.tip = this.tip || g(this.config.template)[0]), this.tip; + }), + (t.setContent = function () { + var t = this.getTipElement(); + this.setElementContent(g(t.querySelectorAll(Ue)), this.getTitle()), + g(t).removeClass(xe + ' ' + Fe); + }), + (t.setElementContent = function (t, e) { + 'object' != typeof e || (!e.nodeType && !e.jquery) + ? this.config.html + ? (this.config.sanitize && + (e = Se(e, this.config.whiteList, this.config.sanitizeFn)), + t.html(e)) + : t.text(e) + : this.config.html + ? g(e).parent().is(t) || t.empty().append(e) + : t.text(g(e).text()); + }), + (t.getTitle = function () { + var t = this.element.getAttribute('data-original-title'); + return ( + t || + (t = + 'function' == typeof this.config.title + ? this.config.title.call(this.element) + : this.config.title), + t + ); + }), + (t._getOffset = function () { + var e = this, + t = {}; + return ( + 'function' == typeof this.config.offset + ? (t.fn = function (t) { + return ( + (t.offsets = l( + {}, + t.offsets, + e.config.offset(t.offsets, e.element) || {}, + )), + t + ); + }) + : (t.offset = this.config.offset), + t + ); + }), + (t._getContainer = function () { + return !1 === this.config.container + ? document.body + : _.isElement(this.config.container) + ? g(this.config.container) + : g(document).find(this.config.container); + }), + (t._getAttachment = function (t) { + return Pe[t.toUpperCase()]; + }), + (t._setListeners = function () { + var i = this; + this.config.trigger.split(' ').forEach(function (t) { + if ('click' === t) + g(i.element).on( + i.constructor.Event.CLICK, + i.config.selector, + function (t) { + return i.toggle(t); + }, + ); + else if (t !== Qe) { + var e = + t === qe + ? i.constructor.Event.MOUSEENTER + : i.constructor.Event.FOCUSIN, + n = + t === qe + ? i.constructor.Event.MOUSELEAVE + : i.constructor.Event.FOCUSOUT; + g(i.element) + .on(e, i.config.selector, function (t) { + return i._enter(t); + }) + .on(n, i.config.selector, function (t) { + return i._leave(t); + }); + } + }), + g(this.element) + .closest('.modal') + .on('hide.bs.modal', function () { + i.element && i.hide(); + }), + this.config.selector + ? (this.config = l({}, this.config, { + trigger: 'manual', + selector: '', + })) + : this._fixTitle(); + }), + (t._fixTitle = function () { + var t = typeof this.element.getAttribute('data-original-title'); + (this.element.getAttribute('title') || 'string' !== t) && + (this.element.setAttribute( + 'data-original-title', + this.element.getAttribute('title') || '', + ), + this.element.setAttribute('title', '')); + }), + (t._enter = function (t, e) { + var n = this.constructor.DATA_KEY; + (e = e || g(t.currentTarget).data(n)) || + ((e = new this.constructor( + t.currentTarget, + this._getDelegateConfig(), + )), + g(t.currentTarget).data(n, e)), + t && (e._activeTrigger['focusin' === t.type ? Me : qe] = !0), + g(e.getTipElement()).hasClass(Fe) || e._hoverState === je + ? (e._hoverState = je) + : (clearTimeout(e._timeout), + (e._hoverState = je), + e.config.delay && e.config.delay.show + ? (e._timeout = setTimeout(function () { + e._hoverState === je && e.show(); + }, e.config.delay.show)) + : e.show()); + }), + (t._leave = function (t, e) { + var n = this.constructor.DATA_KEY; + (e = e || g(t.currentTarget).data(n)) || + ((e = new this.constructor( + t.currentTarget, + this._getDelegateConfig(), + )), + g(t.currentTarget).data(n, e)), + t && (e._activeTrigger['focusout' === t.type ? Me : qe] = !1), + e._isWithActiveTrigger() || + (clearTimeout(e._timeout), + (e._hoverState = He), + e.config.delay && e.config.delay.hide + ? (e._timeout = setTimeout(function () { + e._hoverState === He && e.hide(); + }, e.config.delay.hide)) + : e.hide()); + }), + (t._isWithActiveTrigger = function () { + for (var t in this._activeTrigger) + if (this._activeTrigger[t]) return !0; + return !1; + }), + (t._getConfig = function (t) { + var e = g(this.element).data(); + return ( + Object.keys(e).forEach(function (t) { + -1 !== Oe.indexOf(t) && delete e[t]; + }), + 'number' == + typeof (t = l( + {}, + this.constructor.Default, + e, + 'object' == typeof t && t ? t : {}, + )).delay && (t.delay = { show: t.delay, hide: t.delay }), + 'number' == typeof t.title && (t.title = t.title.toString()), + 'number' == typeof t.content && (t.content = t.content.toString()), + _.typeCheckConfig(be, t, this.constructor.DefaultType), + t.sanitize && + (t.template = Se(t.template, t.whiteList, t.sanitizeFn)), + t + ); + }), + (t._getDelegateConfig = function () { + var t = {}; + if (this.config) + for (var e in this.config) + this.constructor.Default[e] !== this.config[e] && + (t[e] = this.config[e]); + return t; + }), + (t._cleanTipClass = function () { + var t = g(this.getTipElement()), + e = t.attr('class').match(Ne); + null !== e && e.length && t.removeClass(e.join('')); + }), + (t._handlePopperPlacementChange = function (t) { + var e = t.instance; + (this.tip = e.popper), + this._cleanTipClass(), + this.addAttachmentClass(this._getAttachment(t.placement)); + }), + (t._fixTransition = function () { + var t = this.getTipElement(), + e = this.config.animation; + null === t.getAttribute('x-placement') && + (g(t).removeClass(xe), + (this.config.animation = !1), + this.hide(), + this.show(), + (this.config.animation = e)); + }), + (i._jQueryInterface = function (n) { + return this.each(function () { + var t = g(this).data(Ie), + e = 'object' == typeof n && n; + if ( + (t || !/dispose|hide/.test(n)) && + (t || ((t = new i(this, e)), g(this).data(Ie, t)), + 'string' == typeof n) + ) { + if ('undefined' == typeof t[n]) + throw new TypeError('No method named "' + n + '"'); + t[n](); + } + }); + }), + s(i, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + { + key: 'Default', + get: function () { + return Le; + }, + }, + { + key: 'NAME', + get: function () { + return be; + }, + }, + { + key: 'DATA_KEY', + get: function () { + return Ie; + }, + }, + { + key: 'Event', + get: function () { + return Re; + }, + }, + { + key: 'EVENT_KEY', + get: function () { + return De; + }, + }, + { + key: 'DefaultType', + get: function () { + return ke; + }, + }, + ]), + i + ); + })(); + (g.fn[be] = Be._jQueryInterface), + (g.fn[be].Constructor = Be), + (g.fn[be].noConflict = function () { + return (g.fn[be] = we), Be._jQueryInterface; + }); + var Ve = 'popover', + Ye = 'bs.popover', + ze = '.' + Ye, + Xe = g.fn[Ve], + $e = 'bs-popover', + Ge = new RegExp('(^|\\s)' + $e + '\\S+', 'g'), + Je = l({}, Be.Default, { + placement: 'right', + trigger: 'click', + content: '', + template: + '', + }), + Ze = l({}, Be.DefaultType, { content: '(string|element|function)' }), + tn = 'fade', + en = 'show', + nn = '.popover-header', + on = '.popover-body', + rn = { + HIDE: 'hide' + ze, + HIDDEN: 'hidden' + ze, + SHOW: 'show' + ze, + SHOWN: 'shown' + ze, + INSERTED: 'inserted' + ze, + CLICK: 'click' + ze, + FOCUSIN: 'focusin' + ze, + FOCUSOUT: 'focusout' + ze, + MOUSEENTER: 'mouseenter' + ze, + MOUSELEAVE: 'mouseleave' + ze, + }, + sn = (function (t) { + var e, n; + function i() { + return t.apply(this, arguments) || this; + } + (n = t), + ((e = i).prototype = Object.create(n.prototype)), + ((e.prototype.constructor = e).__proto__ = n); + var o = i.prototype; + return ( + (o.isWithContent = function () { + return this.getTitle() || this._getContent(); + }), + (o.addAttachmentClass = function (t) { + g(this.getTipElement()).addClass($e + '-' + t); + }), + (o.getTipElement = function () { + return (this.tip = this.tip || g(this.config.template)[0]), this.tip; + }), + (o.setContent = function () { + var t = g(this.getTipElement()); + this.setElementContent(t.find(nn), this.getTitle()); + var e = this._getContent(); + 'function' == typeof e && (e = e.call(this.element)), + this.setElementContent(t.find(on), e), + t.removeClass(tn + ' ' + en); + }), + (o._getContent = function () { + return ( + this.element.getAttribute('data-content') || this.config.content + ); + }), + (o._cleanTipClass = function () { + var t = g(this.getTipElement()), + e = t.attr('class').match(Ge); + null !== e && 0 < e.length && t.removeClass(e.join('')); + }), + (i._jQueryInterface = function (n) { + return this.each(function () { + var t = g(this).data(Ye), + e = 'object' == typeof n ? n : null; + if ( + (t || !/dispose|hide/.test(n)) && + (t || ((t = new i(this, e)), g(this).data(Ye, t)), + 'string' == typeof n) + ) { + if ('undefined' == typeof t[n]) + throw new TypeError('No method named "' + n + '"'); + t[n](); + } + }); + }), + s(i, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + { + key: 'Default', + get: function () { + return Je; + }, + }, + { + key: 'NAME', + get: function () { + return Ve; + }, + }, + { + key: 'DATA_KEY', + get: function () { + return Ye; + }, + }, + { + key: 'Event', + get: function () { + return rn; + }, + }, + { + key: 'EVENT_KEY', + get: function () { + return ze; + }, + }, + { + key: 'DefaultType', + get: function () { + return Ze; + }, + }, + ]), + i + ); + })(Be); + (g.fn[Ve] = sn._jQueryInterface), + (g.fn[Ve].Constructor = sn), + (g.fn[Ve].noConflict = function () { + return (g.fn[Ve] = Xe), sn._jQueryInterface; + }); + var an = 'scrollspy', + ln = 'bs.scrollspy', + cn = '.' + ln, + hn = g.fn[an], + un = { offset: 10, method: 'auto', target: '' }, + fn = { offset: 'number', method: 'string', target: '(string|element)' }, + dn = { + ACTIVATE: 'activate' + cn, + SCROLL: 'scroll' + cn, + LOAD_DATA_API: 'load' + cn + '.data-api', + }, + gn = 'dropdown-item', + _n = 'active', + mn = '[data-spy="scroll"]', + pn = '.nav, .list-group', + vn = '.nav-link', + yn = '.nav-item', + En = '.list-group-item', + Cn = '.dropdown', + Tn = '.dropdown-item', + Sn = '.dropdown-toggle', + bn = 'offset', + In = 'position', + Dn = (function () { + function n(t, e) { + var n = this; + (this._element = t), + (this._scrollElement = 'BODY' === t.tagName ? window : t), + (this._config = this._getConfig(e)), + (this._selector = + this._config.target + + ' ' + + vn + + ',' + + this._config.target + + ' ' + + En + + ',' + + this._config.target + + ' ' + + Tn), + (this._offsets = []), + (this._targets = []), + (this._activeTarget = null), + (this._scrollHeight = 0), + g(this._scrollElement).on(dn.SCROLL, function (t) { + return n._process(t); + }), + this.refresh(), + this._process(); + } + var t = n.prototype; + return ( + (t.refresh = function () { + var e = this, + t = this._scrollElement === this._scrollElement.window ? bn : In, + o = 'auto' === this._config.method ? t : this._config.method, + r = o === In ? this._getScrollTop() : 0; + (this._offsets = []), + (this._targets = []), + (this._scrollHeight = this._getScrollHeight()), + [].slice + .call(document.querySelectorAll(this._selector)) + .map(function (t) { + var e, + n = _.getSelectorFromElement(t); + if ((n && (e = document.querySelector(n)), e)) { + var i = e.getBoundingClientRect(); + if (i.width || i.height) return [g(e)[o]().top + r, n]; + } + return null; + }) + .filter(function (t) { + return t; + }) + .sort(function (t, e) { + return t[0] - e[0]; + }) + .forEach(function (t) { + e._offsets.push(t[0]), e._targets.push(t[1]); + }); + }), + (t.dispose = function () { + g.removeData(this._element, ln), + g(this._scrollElement).off(cn), + (this._element = null), + (this._scrollElement = null), + (this._config = null), + (this._selector = null), + (this._offsets = null), + (this._targets = null), + (this._activeTarget = null), + (this._scrollHeight = null); + }), + (t._getConfig = function (t) { + if ( + 'string' != + typeof (t = l({}, un, 'object' == typeof t && t ? t : {})).target + ) { + var e = g(t.target).attr('id'); + e || ((e = _.getUID(an)), g(t.target).attr('id', e)), + (t.target = '#' + e); + } + return _.typeCheckConfig(an, t, fn), t; + }), + (t._getScrollTop = function () { + return this._scrollElement === window + ? this._scrollElement.pageYOffset + : this._scrollElement.scrollTop; + }), + (t._getScrollHeight = function () { + return ( + this._scrollElement.scrollHeight || + Math.max( + document.body.scrollHeight, + document.documentElement.scrollHeight, + ) + ); + }), + (t._getOffsetHeight = function () { + return this._scrollElement === window + ? window.innerHeight + : this._scrollElement.getBoundingClientRect().height; + }), + (t._process = function () { + var t = this._getScrollTop() + this._config.offset, + e = this._getScrollHeight(), + n = this._config.offset + e - this._getOffsetHeight(); + if ((this._scrollHeight !== e && this.refresh(), n <= t)) { + var i = this._targets[this._targets.length - 1]; + this._activeTarget !== i && this._activate(i); + } else { + if ( + this._activeTarget && + t < this._offsets[0] && + 0 < this._offsets[0] + ) + return (this._activeTarget = null), void this._clear(); + for (var o = this._offsets.length; o--; ) { + this._activeTarget !== this._targets[o] && + t >= this._offsets[o] && + ('undefined' == typeof this._offsets[o + 1] || + t < this._offsets[o + 1]) && + this._activate(this._targets[o]); + } + } + }), + (t._activate = function (e) { + (this._activeTarget = e), this._clear(); + var t = this._selector.split(',').map(function (t) { + return ( + t + '[data-target="' + e + '"],' + t + '[href="' + e + '"]' + ); + }), + n = g([].slice.call(document.querySelectorAll(t.join(',')))); + n.hasClass(gn) + ? (n.closest(Cn).find(Sn).addClass(_n), n.addClass(_n)) + : (n.addClass(_n), + n + .parents(pn) + .prev(vn + ', ' + En) + .addClass(_n), + n.parents(pn).prev(yn).children(vn).addClass(_n)), + g(this._scrollElement).trigger(dn.ACTIVATE, { relatedTarget: e }); + }), + (t._clear = function () { + [].slice + .call(document.querySelectorAll(this._selector)) + .filter(function (t) { + return t.classList.contains(_n); + }) + .forEach(function (t) { + return t.classList.remove(_n); + }); + }), + (n._jQueryInterface = function (e) { + return this.each(function () { + var t = g(this).data(ln); + if ( + (t || + ((t = new n(this, 'object' == typeof e && e)), + g(this).data(ln, t)), + 'string' == typeof e) + ) { + if ('undefined' == typeof t[e]) + throw new TypeError('No method named "' + e + '"'); + t[e](); + } + }); + }), + s(n, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + { + key: 'Default', + get: function () { + return un; + }, + }, + ]), + n + ); + })(); + g(window).on(dn.LOAD_DATA_API, function () { + for ( + var t = [].slice.call(document.querySelectorAll(mn)), e = t.length; + e--; + + ) { + var n = g(t[e]); + Dn._jQueryInterface.call(n, n.data()); + } + }), + (g.fn[an] = Dn._jQueryInterface), + (g.fn[an].Constructor = Dn), + (g.fn[an].noConflict = function () { + return (g.fn[an] = hn), Dn._jQueryInterface; + }); + var wn = 'bs.tab', + An = '.' + wn, + Nn = g.fn.tab, + On = { + HIDE: 'hide' + An, + HIDDEN: 'hidden' + An, + SHOW: 'show' + An, + SHOWN: 'shown' + An, + CLICK_DATA_API: 'click' + An + '.data-api', + }, + kn = 'dropdown-menu', + Pn = 'active', + Ln = 'disabled', + jn = 'fade', + Hn = 'show', + Rn = '.dropdown', + xn = '.nav, .list-group', + Fn = '.active', + Un = '> li > .active', + Wn = '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]', + qn = '.dropdown-toggle', + Mn = '> .dropdown-menu .active', + Kn = (function () { + function i(t) { + this._element = t; + } + var t = i.prototype; + return ( + (t.show = function () { + var n = this; + if ( + !( + (this._element.parentNode && + this._element.parentNode.nodeType === Node.ELEMENT_NODE && + g(this._element).hasClass(Pn)) || + g(this._element).hasClass(Ln) + ) + ) { + var t, + i, + e = g(this._element).closest(xn)[0], + o = _.getSelectorFromElement(this._element); + if (e) { + var r = 'UL' === e.nodeName || 'OL' === e.nodeName ? Un : Fn; + i = (i = g.makeArray(g(e).find(r)))[i.length - 1]; + } + var s = g.Event(On.HIDE, { relatedTarget: this._element }), + a = g.Event(On.SHOW, { relatedTarget: i }); + if ( + (i && g(i).trigger(s), + g(this._element).trigger(a), + !a.isDefaultPrevented() && !s.isDefaultPrevented()) + ) { + o && (t = document.querySelector(o)), + this._activate(this._element, e); + var l = function () { + var t = g.Event(On.HIDDEN, { relatedTarget: n._element }), + e = g.Event(On.SHOWN, { relatedTarget: i }); + g(i).trigger(t), g(n._element).trigger(e); + }; + t ? this._activate(t, t.parentNode, l) : l(); + } + } + }), + (t.dispose = function () { + g.removeData(this._element, wn), (this._element = null); + }), + (t._activate = function (t, e, n) { + var i = this, + o = ( + !e || ('UL' !== e.nodeName && 'OL' !== e.nodeName) + ? g(e).children(Fn) + : g(e).find(Un) + )[0], + r = n && o && g(o).hasClass(jn), + s = function () { + return i._transitionComplete(t, o, n); + }; + if (o && r) { + var a = _.getTransitionDurationFromElement(o); + g(o) + .removeClass(Hn) + .one(_.TRANSITION_END, s) + .emulateTransitionEnd(a); + } else s(); + }), + (t._transitionComplete = function (t, e, n) { + if (e) { + g(e).removeClass(Pn); + var i = g(e.parentNode).find(Mn)[0]; + i && g(i).removeClass(Pn), + 'tab' === e.getAttribute('role') && + e.setAttribute('aria-selected', !1); + } + if ( + (g(t).addClass(Pn), + 'tab' === t.getAttribute('role') && + t.setAttribute('aria-selected', !0), + _.reflow(t), + t.classList.contains(jn) && t.classList.add(Hn), + t.parentNode && g(t.parentNode).hasClass(kn)) + ) { + var o = g(t).closest(Rn)[0]; + if (o) { + var r = [].slice.call(o.querySelectorAll(qn)); + g(r).addClass(Pn); + } + t.setAttribute('aria-expanded', !0); + } + n && n(); + }), + (i._jQueryInterface = function (n) { + return this.each(function () { + var t = g(this), + e = t.data(wn); + if ( + (e || ((e = new i(this)), t.data(wn, e)), 'string' == typeof n) + ) { + if ('undefined' == typeof e[n]) + throw new TypeError('No method named "' + n + '"'); + e[n](); + } + }); + }), + s(i, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + ]), + i + ); + })(); + g(document).on(On.CLICK_DATA_API, Wn, function (t) { + t.preventDefault(), Kn._jQueryInterface.call(g(this), 'show'); + }), + (g.fn.tab = Kn._jQueryInterface), + (g.fn.tab.Constructor = Kn), + (g.fn.tab.noConflict = function () { + return (g.fn.tab = Nn), Kn._jQueryInterface; + }); + var Qn = 'toast', + Bn = 'bs.toast', + Vn = '.' + Bn, + Yn = g.fn[Qn], + zn = { + CLICK_DISMISS: 'click.dismiss' + Vn, + HIDE: 'hide' + Vn, + HIDDEN: 'hidden' + Vn, + SHOW: 'show' + Vn, + SHOWN: 'shown' + Vn, + }, + Xn = 'fade', + $n = 'hide', + Gn = 'show', + Jn = 'showing', + Zn = { animation: 'boolean', autohide: 'boolean', delay: 'number' }, + ti = { animation: !0, autohide: !0, delay: 500 }, + ei = '[data-dismiss="toast"]', + ni = (function () { + function i(t, e) { + (this._element = t), + (this._config = this._getConfig(e)), + (this._timeout = null), + this._setListeners(); + } + var t = i.prototype; + return ( + (t.show = function () { + var t = this; + g(this._element).trigger(zn.SHOW), + this._config.animation && this._element.classList.add(Xn); + var e = function () { + t._element.classList.remove(Jn), + t._element.classList.add(Gn), + g(t._element).trigger(zn.SHOWN), + t._config.autohide && t.hide(); + }; + if ( + (this._element.classList.remove($n), + this._element.classList.add(Jn), + this._config.animation) + ) { + var n = _.getTransitionDurationFromElement(this._element); + g(this._element).one(_.TRANSITION_END, e).emulateTransitionEnd(n); + } else e(); + }), + (t.hide = function (t) { + var e = this; + this._element.classList.contains(Gn) && + (g(this._element).trigger(zn.HIDE), + t + ? this._close() + : (this._timeout = setTimeout(function () { + e._close(); + }, this._config.delay))); + }), + (t.dispose = function () { + clearTimeout(this._timeout), + (this._timeout = null), + this._element.classList.contains(Gn) && + this._element.classList.remove(Gn), + g(this._element).off(zn.CLICK_DISMISS), + g.removeData(this._element, Bn), + (this._element = null), + (this._config = null); + }), + (t._getConfig = function (t) { + return ( + (t = l( + {}, + ti, + g(this._element).data(), + 'object' == typeof t && t ? t : {}, + )), + _.typeCheckConfig(Qn, t, this.constructor.DefaultType), + t + ); + }), + (t._setListeners = function () { + var t = this; + g(this._element).on(zn.CLICK_DISMISS, ei, function () { + return t.hide(!0); + }); + }), + (t._close = function () { + var t = this, + e = function () { + t._element.classList.add($n), g(t._element).trigger(zn.HIDDEN); + }; + if ((this._element.classList.remove(Gn), this._config.animation)) { + var n = _.getTransitionDurationFromElement(this._element); + g(this._element).one(_.TRANSITION_END, e).emulateTransitionEnd(n); + } else e(); + }), + (i._jQueryInterface = function (n) { + return this.each(function () { + var t = g(this), + e = t.data(Bn); + if ( + (e || + ((e = new i(this, 'object' == typeof n && n)), t.data(Bn, e)), + 'string' == typeof n) + ) { + if ('undefined' == typeof e[n]) + throw new TypeError('No method named "' + n + '"'); + e[n](this); + } + }); + }), + s(i, null, [ + { + key: 'VERSION', + get: function () { + return '4.3.1'; + }, + }, + { + key: 'DefaultType', + get: function () { + return Zn; + }, + }, + { + key: 'Default', + get: function () { + return ti; + }, + }, + ]), + i + ); + })(); + (g.fn[Qn] = ni._jQueryInterface), + (g.fn[Qn].Constructor = ni), + (g.fn[Qn].noConflict = function () { + return (g.fn[Qn] = Yn), ni._jQueryInterface; + }), + (function () { + if ('undefined' == typeof g) + throw new TypeError( + "Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.", + ); + var t = g.fn.jquery.split(' ')[0].split('.'); + if ( + (t[0] < 2 && t[1] < 9) || + (1 === t[0] && 9 === t[1] && t[2] < 1) || + 4 <= t[0] + ) + throw new Error( + "Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0", + ); + })(), + (t.Util = _), + (t.Alert = p), + (t.Button = P), + (t.Carousel = lt), + (t.Collapse = bt), + (t.Dropdown = Jt), + (t.Modal = ve), + (t.Popover = sn), + (t.Scrollspy = Dn), + (t.Tab = Kn), + (t.Toast = ni), + (t.Tooltip = Be), + Object.defineProperty(t, '__esModule', { value: !0 }); +}); //# sourceMappingURL=bootstrap.min.js.map /*! @@ -12,7 +2953,2091 @@ * Copyright 2012-2018 SnapAppointments, LLC * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) */ -!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){!function(a){"use strict";function b(a,b){return a.length===b.length&&a.every(function(a,c){return a===b[c]})}function c(a){var b,c=[],d=a&&a.options;if(a.multiple)for(var e=0,f=d.length;e]+>/g,"")),d&&(j=f(j)),j=j.toUpperCase(),g="contains"===c?j.indexOf(b)>=0:j.startsWith(b)))break}return g}function e(a){return parseInt(a,10)||0}function f(b){var c=[{re:/[\xC0-\xC6]/g,ch:"A"},{re:/[\xE0-\xE6]/g,ch:"a"},{re:/[\xC8-\xCB]/g,ch:"E"},{re:/[\xE8-\xEB]/g,ch:"e"},{re:/[\xCC-\xCF]/g,ch:"I"},{re:/[\xEC-\xEF]/g,ch:"i"},{re:/[\xD2-\xD6]/g,ch:"O"},{re:/[\xF2-\xF6]/g,ch:"o"},{re:/[\xD9-\xDC]/g,ch:"U"},{re:/[\xF9-\xFC]/g,ch:"u"},{re:/[\xC7-\xE7]/g,ch:"c"},{re:/[\xD1]/g,ch:"N"},{re:/[\xF1]/g,ch:"n"}];return a.each(c,function(){b=b?b.replace(this.re,this.ch):""}),b}function g(b){var c=arguments,d=b;[].shift.apply(c);var e,f=this.each(function(){var b=a(this);if(b.is("select")){var f=b.data("selectpicker"),g="object"==typeof d&&d;if(f){if(g)for(var h in g)g.hasOwnProperty(h)&&(f.options[h]=g[h])}else{var i=a.extend({},x.DEFAULTS,a.fn.selectpicker.defaults||{},b.data(),g);i.template=a.extend({},x.DEFAULTS.template,a.fn.selectpicker.defaults?a.fn.selectpicker.defaults.template:{},b.data().template,g.template),b.data("selectpicker",f=new x(this,i))}"string"==typeof d&&(e=f[d]instanceof Function?f[d].apply(f,c):f.options[d])}});return void 0!==e?e:f}var h=document.createElement("_");if(h.classList.toggle("c3",!1),h.classList.contains("c3")){var i=DOMTokenList.prototype.toggle;DOMTokenList.prototype.toggle=function(a,b){return 1 in arguments&&!this.contains(a)==!b?b:i.call(this,a)}}String.prototype.startsWith||function(){var a=function(){try{var a={},b=Object.defineProperty,c=b(a,a,a)&&b}catch(a){}return c}(),b={}.toString,c=function(a){if(null==this)throw new TypeError;var c=String(this);if(a&&"[object RegExp]"==b.call(a))throw new TypeError;var d=c.length,e=String(a),f=e.length,g=arguments.length>1?arguments[1]:void 0,h=g?Number(g):0;h!=h&&(h=0);var i=Math.min(Math.max(h,0),d);if(f+i>d)return!1;for(var j=-1;++j":">",'"':""","'":"'","`":"`"},n={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},o=function(a){var b=function(b){return a[b]},c="(?:"+Object.keys(a).join("|")+")",d=RegExp(c),e=RegExp(c,"g");return function(a){return a=null==a?"":""+a,d.test(a)?a.replace(e,b):a}},p=o(m),q=o(n),r={32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",65:"A",66:"B",67:"C",68:"D",69:"E",70:"F",71:"G",72:"H",73:"I",74:"J",75:"K",76:"L",77:"M",78:"N",79:"O",80:"P",81:"Q",82:"R",83:"S",84:"T",85:"U",86:"V",87:"W",88:"X",89:"Y",90:"Z",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9"},s={ESCAPE:27,ENTER:13,SPACE:32,TAB:9,ARROW_UP:38,ARROW_DOWN:40},t={};try{t.full=(a.fn.dropdown.Constructor.VERSION||"").split(" ")[0].split("."),t.major=t.full[0]}catch(a){console.error("There was an issue retrieving Bootstrap's version. Ensure Bootstrap is being loaded before bootstrap-select and there is no namespace collision.",a),t.major="3"}var u={DISABLED:"disabled",DIVIDER:"4"===t.major?"dropdown-divider":"divider",SHOW:"4"===t.major?"show":"open",DROPUP:"dropup",MENURIGHT:"dropdown-menu-right",MENULEFT:"dropdown-menu-left",BUTTONCLASS:"4"===t.major?"btn-light":"btn-default",POPOVERHEADER:"4"===t.major?"popover-header":"popover-title"},v=new RegExp(s.ARROW_UP+"|"+s.ARROW_DOWN),w=new RegExp("^"+s.TAB+"$|"+s.ESCAPE),x=(new RegExp(s.ENTER+"|"+s.SPACE),function(b,c){var d=this;j.useDefault||(a.valHooks.select.set=j._set,j.useDefault=!0),this.$element=a(b),this.$newElement=null,this.$button=null,this.$menu=null,this.options=c,this.selectpicker={main:{map:{newIndex:{},originalIndex:{}}},current:{map:{}},search:{map:{}},view:{},keydown:{keyHistory:"",resetKeyHistory:{start:function(){return setTimeout(function(){d.selectpicker.keydown.keyHistory=""},800)}}}},null===this.options.title&&(this.options.title=this.$element.attr("title"));var e=this.options.windowPadding;"number"==typeof e&&(this.options.windowPadding=[e,e,e,e]),this.val=x.prototype.val,this.render=x.prototype.render,this.refresh=x.prototype.refresh,this.setStyle=x.prototype.setStyle,this.selectAll=x.prototype.selectAll,this.deselectAll=x.prototype.deselectAll,this.destroy=x.prototype.destroy,this.remove=x.prototype.remove,this.show=x.prototype.show,this.hide=x.prototype.hide,this.init()});x.VERSION="1.13.1",x.DEFAULTS={noneSelectedText:"Nothing selected",noneResultsText:"No results matched {0}",countSelectedText:function(a,b){return 1==a?"{0} item selected":"{0} items selected"},maxOptionsText:function(a,b){return[1==a?"Limit reached ({n} item max)":"Limit reached ({n} items max)",1==b?"Group limit reached ({n} item max)":"Group limit reached ({n} items max)"]},selectAllText:"Select All",deselectAllText:"Deselect All",doneButton:!1,doneButtonText:"Close",multipleSeparator:", ",styleBase:"btn",style:"btn-default",size:"auto",title:null,selectedTextFormat:"values",width:!1,container:!1,hideDisabled:!1,showSubtext:!1,showIcon:!0,showContent:!0,dropupAuto:!0,header:!1,liveSearch:!1,liveSearchPlaceholder:null,liveSearchNormalize:!1,liveSearchStyle:"contains",actionsBox:!1,iconBase:"glyphicon",tickIcon:"glyphicon-ok",showTick:!1,template:{caret:''},maxOptions:!1,mobile:!1,selectOnTab:!1,dropdownAlignRight:!1,windowPadding:0,virtualScroll:600},"4"===t.major&&(x.DEFAULTS.style="btn-light",x.DEFAULTS.iconBase="",x.DEFAULTS.tickIcon="bs-ok-default"),x.prototype={constructor:x,init:function(){var a=this,b=this.$element.attr("id");this.$element.addClass("bs-select-hidden"),this.multiple=this.$element.prop("multiple"),this.autofocus=this.$element.prop("autofocus"),this.$newElement=this.createDropdown(),this.createLi(),this.$element.after(this.$newElement).prependTo(this.$newElement),this.$button=this.$newElement.children("button"),this.$menu=this.$newElement.children(".dropdown-menu"),this.$menuInner=this.$menu.children(".inner"),this.$searchbox=this.$menu.find("input"),this.$element.removeClass("bs-select-hidden"),!0===this.options.dropdownAlignRight&&this.$menu.addClass(u.MENURIGHT),void 0!==b&&this.$button.attr("data-id",b),this.checkDisabled(),this.clickListener(),this.options.liveSearch&&this.liveSearchListener(),this.render(),this.setStyle(),this.setWidth(),this.options.container?this.selectPosition():this.$element.on("hide.bs.select",function(){if(a.isVirtual()){var b=a.$menuInner[0],c=b.firstChild.cloneNode(!1);b.replaceChild(c,b.firstChild),b.scrollTop=0}}),this.$menu.data("this",this),this.$newElement.data("this",this),this.options.mobile&&this.mobile(),this.$newElement.on({"hide.bs.dropdown":function(b){a.$menuInner.attr("aria-expanded",!1),a.$element.trigger("hide.bs.select",b)},"hidden.bs.dropdown":function(b){a.$element.trigger("hidden.bs.select",b)},"show.bs.dropdown":function(b){a.$menuInner.attr("aria-expanded",!0),a.$element.trigger("show.bs.select",b)},"shown.bs.dropdown":function(b){a.$element.trigger("shown.bs.select",b)}}),a.$element[0].hasAttribute("required")&&this.$element.on("invalid",function(){a.$button.addClass("bs-invalid"),a.$element.on({"shown.bs.select":function(){a.$element.val(a.$element.val()).off("shown.bs.select")},"rendered.bs.select":function(){this.validity.valid&&a.$button.removeClass("bs-invalid"),a.$element.off("rendered.bs.select")}}),a.$button.on("blur.bs.select",function(){a.$element.focus().blur(),a.$button.off("blur.bs.select")})}),setTimeout(function(){a.$element.trigger("loaded.bs.select")})},createDropdown:function(){var b=this.multiple||this.options.showTick?" show-tick":"",c=this.autofocus?" autofocus":"",d=this.options.header?'
'+this.options.header+"
":"",e=this.options.liveSearch?'':"",f=this.multiple&&this.options.actionsBox?'
":"",g=this.multiple&&this.options.doneButton?'
":"",h='";return a(h)},setPositionData:function(){this.selectpicker.view.canHighlight=[];for(var a=0;a=this.options.virtualScroll||!0===this.options.virtualScroll},createView:function(c,d){function e(a,d){var e,j,k,l,m,n,o,p=f.selectpicker.current.elements.length,q=[],r=void 0,s=!0,t=f.isVirtual();f.selectpicker.view.scrollTop=a,!0===t&&f.sizeInfo.hasScrollBar&&f.$menu[0].offsetWidth>f.sizeInfo.totalMenuWidth&&(f.sizeInfo.menuWidth=f.$menu[0].offsetWidth,f.sizeInfo.totalMenuWidth=f.sizeInfo.menuWidth+f.sizeInfo.scrollBarWidth,f.$menu.css("min-width",f.sizeInfo.menuWidth)),e=Math.ceil(f.sizeInfo.menuInnerHeight/f.sizeInfo.liHeight*1.5),j=Math.round(p/e)||1;for(var u=0;up-1?0:f.selectpicker.current.data[p-1].position-f.selectpicker.current.data[f.selectpicker.view.position1-1].position,y.firstChild.style.marginTop=w+"px",y.firstChild.style.marginBottom=x+"px"),y.firstChild.appendChild(z)}if(f.prevActiveIndex=f.activeIndex,f.options.liveSearch){if(c&&d){var D,E=0;f.selectpicker.view.canHighlight[E]||(E=1+f.selectpicker.view.canHighlight.slice(1).indexOf(!0)),D=f.selectpicker.view.visibleElements[E],f.selectpicker.view.currentActive&&(f.selectpicker.view.currentActive.classList.remove("active"),f.selectpicker.view.currentActive.firstChild&&f.selectpicker.view.currentActive.firstChild.classList.remove("active")),D&&(D.classList.add("active"),D.firstChild&&D.firstChild.classList.add("active")),f.activeIndex=f.selectpicker.current.map.originalIndex[E]}}else f.$menuInner.focus()}d=d||0;var f=this;this.selectpicker.current=c?this.selectpicker.search:this.selectpicker.main;var g,h,i=[];this.setPositionData(),e(d,!0),this.$menuInner.off("scroll.createView").on("scroll.createView",function(a,b){f.noScroll||e(this.scrollTop,b),f.noScroll=!1}),a(window).off("resize.createView").on("resize.createView",function(){e(f.$menuInner[0].scrollTop)})},createLi:function(){var b,c=this,d=[],e=0,f=0,g=[],h=0,i=0,j=-1;this.selectpicker.view.titleOption||(this.selectpicker.view.titleOption=document.createElement("option"));var k={span:document.createElement("span"),subtext:document.createElement("small"),a:document.createElement("a"),li:document.createElement("li"),whitespace:document.createTextNode(" ")},l=k.span.cloneNode(!1),m=document.createDocumentFragment();l.className=c.options.iconBase+" "+c.options.tickIcon+" check-mark",k.a.appendChild(l),k.a.setAttribute("role","option"),k.subtext.className="text-muted",k.text=k.span.cloneNode(!1),k.text.className="text";var n=function(a,b,c,d){var e=k.li.cloneNode(!1);return a&&(1===a.nodeType||11===a.nodeType?e.appendChild(a):e.innerHTML=a),void 0!==c&&""!==c&&(e.className=c),void 0!==d&&null!==d&&e.classList.add("optgroup-"+d),e},o=function(a,b,c){var d=k.a.cloneNode(!0);return a&&(11===a.nodeType?d.appendChild(a):d.insertAdjacentHTML("beforeend",a)),void 0!==b&""!==b&&(d.className=b),"4"===t.major&&d.classList.add("dropdown-item"),c&&d.setAttribute("style",c),d},q=function(a){var b,d,e=k.text.cloneNode(!1);if(a.optionContent)e.innerHTML=a.optionContent;else{if(e.textContent=a.text,a.optionIcon){var f=k.whitespace.cloneNode(!1);d=k.span.cloneNode(!1),d.className=c.options.iconBase+" "+a.optionIcon,m.appendChild(d),m.appendChild(f)}a.optionSubtext&&(b=k.subtext.cloneNode(!1),b.innerHTML=a.optionSubtext,e.appendChild(b))}return m.appendChild(e),m},r=function(a){var b,d,e=k.text.cloneNode(!1);if(e.textContent=a.labelEscaped,a.labelIcon){var f=k.whitespace.cloneNode(!1);d=k.span.cloneNode(!1),d.className=c.options.iconBase+" "+a.labelIcon,m.appendChild(d),m.appendChild(f)}return a.labelSubtext&&(b=k.subtext.cloneNode(!1),b.textContent=a.labelSubtext,e.appendChild(b)),m.appendChild(e),m};if(this.options.title&&!this.multiple){j--;var s=this.$element[0],v=!1,w=!this.selectpicker.view.titleOption.parentNode;if(w){this.selectpicker.view.titleOption.className="bs-title-option",this.selectpicker.view.titleOption.value="";v=void 0===a(s.options[s.selectedIndex]).attr("selected")&&void 0===this.$element.data("selected")}(w||0!==this.selectpicker.view.titleOption.index)&&s.insertBefore(this.selectpicker.view.titleOption,s.firstChild),v&&(s.selectedIndex=0)}var x=this.$element.find("option");x.each(function(k){var l=a(this);if(j++,!l.hasClass("bs-title-option")){var m,s,t=l.data(),v=this.className||"",w=p(this.style.cssText),y=t.content,z=this.textContent,A=t.tokens,B=t.subtext,C=t.icon,D=l.parent(),E=D[0],F="OPTGROUP"===E.tagName,G=F&&E.disabled,H=this.disabled||G,I=this.previousElementSibling&&"OPTGROUP"===this.previousElementSibling.tagName,J=D.data();if(!0===t.hidden||c.options.hideDisabled&&(H&&!F||G)){if(m=t.prevHiddenIndex,l.next().data("prevHiddenIndex",void 0!==m?m:k),j--,!I&&void 0!==m){var K=x[m].previousElementSibling;K&&"OPTGROUP"===K.tagName&&!K.disabled&&(I=!0)}return void(I&&"divider"!==g[g.length-1].type&&(j++,d.push(n(!1,0,u.DIVIDER,h+"div")),g.push({type:"divider",optID:h,originalIndex:k})))}if(F&&!0!==t.divider){if(c.options.hideDisabled&&H){if(void 0===J.allOptionsDisabled){var L=D.children();D.data("allOptionsDisabled",L.filter(":disabled").length===L.length)}if(D.data("allOptionsDisabled"))return void j--}var M=" "+E.className||"";if(!this.previousElementSibling){h+=1;var N=E.label,O=p(N),P=J.subtext,Q=J.icon;0!==k&&d.length>0&&(j++,d.push(n(!1,0,u.DIVIDER,h+"div")),g.push({type:"divider",optID:h,originalIndex:k})),j++;var R=r({labelEscaped:O,labelSubtext:P,labelIcon:Q});d.push(n(R,0,"dropdown-header"+M,h)),g.push({content:O,subtext:P,type:"optgroup-label",optID:h,originalIndex:k}),i=j-1}if(c.options.hideDisabled&&H||!0===t.hidden)return void j--;s=q({text:z,optionContent:y,optionSubtext:B,optionIcon:C}),d.push(n(o(s,"opt "+v+M,w),0,"",h)),g.push({content:y||z,subtext:B,tokens:A,type:"option",optID:h,headerIndex:i,lastIndex:i+E.childElementCount,originalIndex:k,data:t}),e++}else if(!0===t.divider)d.push(n(!1,0,"divider")),g.push({type:"divider",originalIndex:k});else{if(!I&&c.options.hideDisabled&&void 0!==(m=t.prevHiddenIndex)){var K=x[m].previousElementSibling;K&&"OPTGROUP"===K.tagName&&!K.disabled&&(I=!0)}I&&"divider"!==g[g.length-1].type&&(j++,d.push(n(!1,0,u.DIVIDER,h+"div")),g.push({type:"divider",optID:h,originalIndex:k})),s=q({text:z,optionContent:y,optionSubtext:B,optionIcon:C}),d.push(n(o(s,v,w))),g.push({content:y||z,subtext:B,tokens:A,type:"option",originalIndex:k,data:t}),e++}c.selectpicker.main.map.newIndex[k]=j,c.selectpicker.main.map.originalIndex[j]=k;var S=g[g.length-1];S.disabled=H;var T=0;S.content&&(T+=S.content.length),S.subtext&&(T+=S.subtext.length),C&&(T+=1),T>f&&(f=T,b=d[d.length-1])}}),this.selectpicker.main.elements=d,this.selectpicker.main.data=g,this.selectpicker.current=this.selectpicker.main,this.selectpicker.view.widestOption=b,this.selectpicker.view.availableOptionsCount=e},findLis:function(){return this.$menuInner.find(".inner > li")},render:function(){var a=this,b=this.$element.find("option"),c=[],d=[];this.togglePlaceholder(),this.tabIndex();for(var e=0,f=this.selectpicker.main.elements.length;e ':"";i=a.options.showSubtext&&k.subtext&&!a.multiple?' '+k.subtext+"":"",j=h.title?h.title:k.content&&a.options.showContent?k.content.toString():l+h.innerHTML.trim()+i,d.push(j)}}var m=this.multiple?d.join(this.options.multipleSeparator):d[0];if(c.length>50&&(m+="..."),this.multiple&&-1!==this.options.selectedTextFormat.indexOf("count")){var n=this.options.selectedTextFormat.split(">");if(n.length>1&&c.length>n[1]||1===n.length&&c.length>=2){var o=this.selectpicker.view.availableOptionsCount;m=("function"==typeof this.options.countSelectedText?this.options.countSelectedText(c.length,o):this.options.countSelectedText).replace("{0}",c.length.toString()).replace("{1}",o.toString())}}void 0==this.options.title&&(this.options.title=this.$element.attr("title")),"static"==this.options.selectedTextFormat&&(m=this.options.title),m||(m=void 0!==this.options.title?this.options.title:this.options.noneSelectedText),this.$button[0].title=q(m.replace(/<[^>]*>?/g,"").trim()),this.$button.find(".filter-option-inner-inner")[0].innerHTML=m,this.$element.trigger("rendered.bs.select")},setStyle:function(a,b){this.$element.attr("class")&&this.$newElement.addClass(this.$element.attr("class").replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi,""));var c=a||this.options.style;"add"==b?this.$button.addClass(c):"remove"==b?this.$button.removeClass(c):(this.$button.removeClass(this.options.style),this.$button.addClass(c))},liHeight:function(b){if(b||!1!==this.options.size&&!this.sizeInfo){this.sizeInfo||(this.sizeInfo={});var c=document.createElement("div"),d=document.createElement("div"),f=document.createElement("div"),g=document.createElement("ul"),h=document.createElement("li"),i=document.createElement("li"),j=document.createElement("li"),k=document.createElement("a"),l=document.createElement("span"),m=this.options.header&&this.$menu.find("."+u.POPOVERHEADER).length>0?this.$menu.find("."+u.POPOVERHEADER)[0].cloneNode(!0):null,n=this.options.liveSearch?document.createElement("div"):null,o=this.options.actionsBox&&this.multiple&&this.$menu.find(".bs-actionsbox").length>0?this.$menu.find(".bs-actionsbox")[0].cloneNode(!0):null,p=this.options.doneButton&&this.multiple&&this.$menu.find(".bs-donebutton").length>0?this.$menu.find(".bs-donebutton")[0].cloneNode(!0):null;if(this.sizeInfo.selectWidth=this.$newElement[0].offsetWidth,l.className="text",k.className="dropdown-item",c.className=this.$menu[0].parentNode.className+" "+u.SHOW,c.style.width=this.sizeInfo.selectWidth+"px",d.className="dropdown-menu "+u.SHOW,f.className="inner "+u.SHOW,g.className="dropdown-menu inner "+("4"===t.major?u.SHOW:""),h.className=u.DIVIDER,i.className="dropdown-header",l.appendChild(document.createTextNode("Inner text")),k.appendChild(l),j.appendChild(k),i.appendChild(l.cloneNode(!0)),this.selectpicker.view.widestOption&&g.appendChild(this.selectpicker.view.widestOption.cloneNode(!0)),g.appendChild(j),g.appendChild(h),g.appendChild(i),m&&d.appendChild(m),n){var q=document.createElement("input");n.className="bs-searchbox",q.className="form-control",n.appendChild(q),d.appendChild(n)}o&&d.appendChild(o),f.appendChild(g),d.appendChild(f),p&&d.appendChild(p),c.appendChild(d),document.body.appendChild(c);var r,s=k.offsetHeight,v=i?i.offsetHeight:0,w=m?m.offsetHeight:0,x=n?n.offsetHeight:0,y=o?o.offsetHeight:0,z=p?p.offsetHeight:0,A=a(h).outerHeight(!0),B=!!window.getComputedStyle&&window.getComputedStyle(d),C=d.offsetWidth,D=B?null:a(d),E={vert:e(B?B.paddingTop:D.css("paddingTop"))+e(B?B.paddingBottom:D.css("paddingBottom"))+e(B?B.borderTopWidth:D.css("borderTopWidth"))+e(B?B.borderBottomWidth:D.css("borderBottomWidth")),horiz:e(B?B.paddingLeft:D.css("paddingLeft"))+e(B?B.paddingRight:D.css("paddingRight"))+e(B?B.borderLeftWidth:D.css("borderLeftWidth"))+e(B?B.borderRightWidth:D.css("borderRightWidth"))},F={vert:E.vert+e(B?B.marginTop:D.css("marginTop"))+e(B?B.marginBottom:D.css("marginBottom"))+2,horiz:E.horiz+e(B?B.marginLeft:D.css("marginLeft"))+e(B?B.marginRight:D.css("marginRight"))+2};f.style.overflowY="scroll",r=d.offsetWidth-C,document.body.removeChild(c),this.sizeInfo.liHeight=s,this.sizeInfo.dropdownHeaderHeight=v,this.sizeInfo.headerHeight=w,this.sizeInfo.searchHeight=x,this.sizeInfo.actionsHeight=y,this.sizeInfo.doneButtonHeight=z,this.sizeInfo.dividerHeight=A,this.sizeInfo.menuPadding=E,this.sizeInfo.menuExtras=F,this.sizeInfo.menuWidth=C,this.sizeInfo.totalMenuWidth=this.sizeInfo.menuWidth,this.sizeInfo.scrollBarWidth=r,this.sizeInfo.selectHeight=this.$newElement[0].offsetHeight,this.setPositionData()}},getSelectPosition:function(){var b,c=this,d=a(window),e=c.$newElement.offset(),f=a(c.options.container);c.options.container&&!f.is("body")?(b=f.offset(),b.top+=parseInt(f.css("borderTopWidth")),b.left+=parseInt(f.css("borderLeftWidth"))):b={top:0,left:0};var g=c.options.windowPadding;this.sizeInfo.selectOffsetTop=e.top-b.top-d.scrollTop(),this.sizeInfo.selectOffsetBot=d.height()-this.sizeInfo.selectOffsetTop-this.sizeInfo.selectHeight-b.top-g[2],this.sizeInfo.selectOffsetLeft=e.left-b.left-d.scrollLeft(),this.sizeInfo.selectOffsetRight=d.width()-this.sizeInfo.selectOffsetLeft-this.sizeInfo.selectWidth-b.left-g[1],this.sizeInfo.selectOffsetTop-=g[0],this.sizeInfo.selectOffsetLeft-=g[3]},setMenuSize:function(a){this.getSelectPosition();var b,c,d,e,f,g,h,i=this.sizeInfo.selectWidth,j=this.sizeInfo.liHeight,k=this.sizeInfo.headerHeight,l=this.sizeInfo.searchHeight,m=this.sizeInfo.actionsHeight,n=this.sizeInfo.doneButtonHeight,o=this.sizeInfo.dividerHeight,p=this.sizeInfo.menuPadding,q=0;if(this.options.dropupAuto&&(h=j*this.selectpicker.current.elements.length+p.vert,this.$newElement.toggleClass(u.DROPUP,this.sizeInfo.selectOffsetTop-this.sizeInfo.selectOffsetBot>this.sizeInfo.menuExtras.vert&&h+this.sizeInfo.menuExtras.vert+50>this.sizeInfo.selectOffsetBot)),"auto"===this.options.size)e=this.selectpicker.current.elements.length>3?3*this.sizeInfo.liHeight+this.sizeInfo.menuExtras.vert-2:0,c=this.sizeInfo.selectOffsetBot-this.sizeInfo.menuExtras.vert,d=e+k+l+m+n,g=Math.max(e-p.vert,0),this.$newElement.hasClass(u.DROPUP)&&(c=this.sizeInfo.selectOffsetTop-this.sizeInfo.menuExtras.vert),f=c,b=c-k-l-m-n-p.vert;else if(this.options.size&&"auto"!=this.options.size&&this.selectpicker.current.elements.length>this.options.size){for(var r=0;rthis.sizeInfo.selectOffsetRight&&this.sizeInfo.selectOffsetRightthis.sizeInfo.menuInnerHeight&&(this.sizeInfo.hasScrollBar=!0,this.sizeInfo.totalMenuWidth=this.sizeInfo.menuWidth+this.sizeInfo.scrollBarWidth,this.$menu.css("min-width",this.sizeInfo.totalMenuWidth)),this.dropdown&&this.dropdown._popper&&this.dropdown._popper.update()},setSize:function(b){if(this.liHeight(b),this.options.header&&this.$menu.css("padding-top",0),!1!==this.options.size){var c,d=this,e=a(window),f=0;this.setMenuSize(),"auto"===this.options.size?(this.$searchbox.off("input.setMenuSize propertychange.setMenuSize").on("input.setMenuSize propertychange.setMenuSize",function(){return d.setMenuSize()}),e.off("resize.setMenuSize scroll.setMenuSize").on("resize.setMenuSize scroll.setMenuSize",function(){return d.setMenuSize()})):this.options.size&&"auto"!=this.options.size&&this.selectpicker.current.elements.length>this.options.size&&(this.$searchbox.off("input.setMenuSize propertychange.setMenuSize"),e.off("resize.setMenuSize scroll.setMenuSize")),b?f=this.$menuInner[0].scrollTop:d.multiple||"number"==typeof(c=d.selectpicker.main.map.newIndex[d.$element[0].selectedIndex])&&!1!==d.options.size&&(f=d.sizeInfo.liHeight*c,f=f-d.sizeInfo.menuInnerHeight/2+d.sizeInfo.liHeight/2),d.createView(!1,f)}},setWidth:function(){var a=this;"auto"===this.options.width?requestAnimationFrame(function(){a.$menu.css("min-width","0"),a.liHeight(),a.setMenuSize();var b=a.$newElement.clone().appendTo("body"),c=b.css("width","auto").children("button").outerWidth();b.remove(),a.sizeInfo.selectWidth=Math.max(a.sizeInfo.totalMenuWidth,c),a.$newElement.css("width",a.sizeInfo.selectWidth+"px")}):"fit"===this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width","").addClass("fit-width")):this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width",this.options.width)):(this.$menu.css("min-width",""),this.$newElement.css("width","")),this.$newElement.hasClass("fit-width")&&"fit"!==this.options.width&&this.$newElement.removeClass("fit-width")},selectPosition:function(){this.$bsContainer=a('
');var b,c,d,e=this,f=a(this.options.container),g=function(a){var g={};e.$bsContainer.addClass(a.attr("class").replace(/form-control|fit-width/gi,"")).toggleClass(u.DROPUP,a.hasClass(u.DROPUP)),b=a.offset(),f.is("body")?c={top:0,left:0}:(c=f.offset(),c.top+=parseInt(f.css("borderTopWidth"))-f.scrollTop(),c.left+=parseInt(f.css("borderLeftWidth"))-f.scrollLeft()),d=a.hasClass(u.DROPUP)?0:a[0].offsetHeight,t.major<4&&(g.top=b.top-c.top+d,g.left=b.left-c.left),g.width=a[0].offsetWidth,e.$bsContainer.css(g)};this.$button.on("click.bs.dropdown.data-api",function(){e.isDisabled()||(g(e.$newElement),e.$bsContainer.appendTo(e.options.container).toggleClass(u.SHOW,!e.$button.hasClass(u.SHOW)).append(e.$menu))}),a(window).on("resize scroll",function(){g(e.$newElement)}),this.$element.on("hide.bs.select",function(){e.$menu.data("height",e.$menu.height()),e.$bsContainer.detach()})},setOptionStatus:function(){var a=this,b=this.$element.find("option");if(a.noScroll=!1,a.selectpicker.view.visibleElements&&a.selectpicker.view.visibleElements.length)for(var c=0;c3&&!b.dropdown&&(b.dropdown=b.$button.data("bs.dropdown"),b.dropdown._menu=b.$menu[0])}),this.$button.on("click.bs.dropdown.data-api",function(){b.$newElement.hasClass(u.SHOW)||b.setSize()}),this.$element.on("shown.bs.select",function(){b.$menuInner[0].scrollTop!==b.selectpicker.view.scrollTop&&(b.$menuInner[0].scrollTop=b.selectpicker.view.scrollTop),b.options.liveSearch?b.$searchbox.focus():b.$menuInner.focus()}),this.$menuInner.on("click","li a",function(d,e){var f=a(this),g=b.isVirtual()?b.selectpicker.view.position0:0,h=b.selectpicker.current.map.originalIndex[f.parent().index()+g],i=c(b.$element[0]),j=b.$element.prop("selectedIndex"),l=!0;if(b.multiple&&1!==b.options.maxOptions&&d.stopPropagation(),d.preventDefault(),!b.isDisabled()&&!f.parent().hasClass(u.DISABLED)){var m=b.$element.find("option"),n=m.eq(h),o=n.prop("selected"),p=n.parent("optgroup"),q=b.options.maxOptions,r=p.data("maxOptions")||!1;if(h===b.activeIndex&&(e=!0),e||(b.prevActiveIndex=b.activeIndex,b.activeIndex=void 0),b.multiple){if(n.prop("selected",!o),b.setSelected(h,!o),f.blur(),!1!==q||!1!==r){var s=q
');x[2]&&(y=y.replace("{var}",x[2][q>1?0:1]),z=z.replace("{var}",x[2][r>1?0:1])),n.prop("selected",!1),b.$menu.append(A),q&&s&&(A.append(a("
"+y+"
")),l=!1,b.$element.trigger("maxReached.bs.select")),r&&t&&(A.append(a("
"+z+"
")),l=!1,b.$element.trigger("maxReachedGrp.bs.select")),setTimeout(function(){b.setSelected(h,!1)},10),A.delay(750).fadeOut(300,function(){a(this).remove()})}}}else m.prop("selected",!1),n.prop("selected",!0),b.setSelected(h,!0);!b.multiple||b.multiple&&1===b.options.maxOptions?b.$button.focus():b.options.liveSearch&&b.$searchbox.focus(),l&&(i!=c(b.$element[0])&&b.multiple||j!=b.$element.prop("selectedIndex")&&!b.multiple)&&(k=[h,n.prop("selected"),i],b.$element.triggerNative("change"))}}),this.$menu.on("click","li."+u.DISABLED+" a, ."+u.POPOVERHEADER+", ."+u.POPOVERHEADER+" :not(.close)",function(c){c.currentTarget==this&&(c.preventDefault(),c.stopPropagation(),b.options.liveSearch&&!a(c.target).hasClass("close")?b.$searchbox.focus():b.$button.focus())}),this.$menuInner.on("click",".divider, .dropdown-header",function(a){a.preventDefault(),a.stopPropagation(),b.options.liveSearch?b.$searchbox.focus():b.$button.focus()}),this.$menu.on("click","."+u.POPOVERHEADER+" .close",function(){b.$button.click()}),this.$searchbox.on("click",function(a){a.stopPropagation()}),this.$menu.on("click",".actions-btn",function(c){b.options.liveSearch?b.$searchbox.focus():b.$button.focus(),c.preventDefault(),c.stopPropagation(),a(this).hasClass("bs-select-all")?b.selectAll():b.deselectAll()}),this.$element.on({change:function(){b.render(),b.$element.trigger("changed.bs.select",k),k=null},focus:function(){b.$button.focus()}})},liveSearchListener:function(){var a=this,b=document.createElement("li");this.$button.on("click.bs.dropdown.data-api",function(){a.$searchbox.val()&&a.$searchbox.val("")}),this.$searchbox.on("click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api",function(a){a.stopPropagation()}),this.$searchbox.on("input propertychange",function(){var c=a.$searchbox.val();if(a.selectpicker.search.map.newIndex={},a.selectpicker.search.map.originalIndex={},a.selectpicker.search.elements=[],a.selectpicker.search.data=[],c){var e,f=[],g=c.toUpperCase(),h={},i=[],j=a._searchStyle(),k=a.options.liveSearchNormalize;a._$lisSelected=a.$menuInner.find(".selected");for(var e=0;e0&&(h[l.headerIndex-1]=!0,i.push(l.headerIndex-1)),h[l.headerIndex]=!0,i.push(l.headerIndex),h[l.lastIndex+1]=!0),h[e]&&"optgroup-label"!==l.type&&i.push(e)}for(var e=0,m=i.length;e=48&&b.which<=57||b.which>=96&&b.which<=105||b.which>=65&&b.which<=90)&&k.$button.trigger("click.bs.dropdown.data-api"),b.which===s.ESCAPE&&e&&(b.preventDefault(),k.$button.trigger("click.bs.dropdown.data-api").focus()),o){if(!l.length)return;c=!0===q?l.index(l.filter(".active")):k.selectpicker.current.map.newIndex[k.activeIndex],void 0===c&&(c=-1),-1!==c&&(f=k.selectpicker.current.elements[c+t],f.classList.remove("active"),f.firstChild&&f.firstChild.classList.remove("active")),b.which===s.ARROW_UP?(-1!==c&&c--,c+t<0&&(c+=l.length),k.selectpicker.view.canHighlight[c+t]||-1===(c=k.selectpicker.view.canHighlight.slice(0,c+t).lastIndexOf(!0)-t)&&(c=l.length-1)):(b.which===s.ARROW_DOWN||n)&&(c++,c+t>=k.selectpicker.view.canHighlight.length&&(c=0),k.selectpicker.view.canHighlight[c+t]||(c=c+1+k.selectpicker.view.canHighlight.slice(c+t+1).indexOf(!0))),b.preventDefault();var x=t+c;b.which===s.ARROW_UP?0===t&&c===l.length-1?(k.$menuInner[0].scrollTop=k.$menuInner[0].scrollHeight,x=k.selectpicker.current.elements.length-1):(g=k.selectpicker.current.data[x],h=g.position-g.height,m=hp)),f=k.selectpicker.current.elements[x],f.classList.add("active"),f.firstChild&&f.firstChild.classList.add("active"),k.activeIndex=k.selectpicker.current.map.originalIndex[x],k.selectpicker.view.currentActive=f,m&&(k.$menuInner[0].scrollTop=h),k.options.liveSearch?k.$searchbox.focus():i.focus()}else if(!i.is("input")&&!w.test(b.which)||b.which===s.SPACE&&k.selectpicker.keydown.keyHistory){var y,z,A=[];b.preventDefault(),k.selectpicker.keydown.keyHistory+=r[b.which],k.selectpicker.keydown.resetKeyHistory.cancel&&clearTimeout(k.selectpicker.keydown.resetKeyHistory.cancel),k.selectpicker.keydown.resetKeyHistory.cancel=k.selectpicker.keydown.resetKeyHistory.start(),z=k.selectpicker.keydown.keyHistory,/^(.)\1+$/.test(z)&&(z=z.charAt(0));for(var B=0;B0?(h=g.position-g.height,m=!0):(h=g.position-k.sizeInfo.menuInnerHeight,m=g.position>p+k.sizeInfo.menuInnerHeight),f=k.selectpicker.current.elements[y],f.classList.add("active"),f.firstChild&&f.firstChild.classList.add("active"),k.activeIndex=A[E],f.firstChild.focus(),m&&(k.$menuInner[0].scrollTop=h),i.focus()}}e&&(b.which===s.SPACE&&!k.selectpicker.keydown.keyHistory||b.which===s.ENTER||b.which===s.TAB&&k.options.selectOnTab)&&(b.which!==s.SPACE&&b.preventDefault(),k.options.liveSearch&&b.which===s.SPACE||(k.$menuInner.find(".active a").trigger("click",!0),i.focus(),k.options.liveSearch||(b.preventDefault(),a(document).data("spaceSelect",!0))))},mobile:function(){this.$element.addClass("mobile-device")},refresh:function(){var b=a.extend({},this.options,this.$element.data());this.options=b,this.selectpicker.main.map.newIndex={},this.selectpicker.main.map.originalIndex={},this.createLi(),this.checkDisabled(),this.render(),this.setStyle(),this.setWidth(),this.setSize(!0),this.$element.trigger("refreshed.bs.select")},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(".bs.select").removeData("selectpicker").removeClass("bs-select-hidden selectpicker")}};var y=a.fn.selectpicker;a.fn.selectpicker=g,a.fn.selectpicker.Constructor=x,a.fn.selectpicker.noConflict=function(){return a.fn.selectpicker=y,this},a(document).off("keydown.bs.dropdown.data-api").on("keydown.bs.select",'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input',x.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input',function(a){a.stopPropagation()}),a(window).on("load.bs.select.data-api",function(){a(".selectpicker").each(function(){var b=a(this);g.call(b,b.data())})})}(a)}); - //# sourceMappingURL=bootstrap-select.js.map - \ No newline at end of file +!(function (a, b) { + 'function' == typeof define && define.amd + ? define(['jquery'], function (a) { + return b(a); + }) + : 'object' == typeof module && module.exports + ? (module.exports = b(require('jquery'))) + : b(a.jQuery); +})(this, function (a) { + !(function (a) { + 'use strict'; + function b(a, b) { + return ( + a.length === b.length && + a.every(function (a, c) { + return a === b[c]; + }) + ); + } + function c(a) { + var b, + c = [], + d = a && a.options; + if (a.multiple) + for (var e = 0, f = d.length; e < f; e++) + (b = d[e]), b.selected && c.push(b.value || b.text); + else c = a.value; + return c; + } + function d(a, b, c, d) { + for ( + var e = ['content', 'subtext', 'tokens'], g = !1, h = 0; + h < e.length; + h++ + ) { + var i = e[h], + j = a[i]; + if ( + j && + ((j = j.toString()), + 'content' === i && (j = j.replace(/<[^>]+>/g, '')), + d && (j = f(j)), + (j = j.toUpperCase()), + (g = 'contains' === c ? j.indexOf(b) >= 0 : j.startsWith(b))) + ) + break; + } + return g; + } + function e(a) { + return parseInt(a, 10) || 0; + } + function f(b) { + var c = [ + { re: /[\xC0-\xC6]/g, ch: 'A' }, + { re: /[\xE0-\xE6]/g, ch: 'a' }, + { re: /[\xC8-\xCB]/g, ch: 'E' }, + { re: /[\xE8-\xEB]/g, ch: 'e' }, + { re: /[\xCC-\xCF]/g, ch: 'I' }, + { re: /[\xEC-\xEF]/g, ch: 'i' }, + { re: /[\xD2-\xD6]/g, ch: 'O' }, + { re: /[\xF2-\xF6]/g, ch: 'o' }, + { re: /[\xD9-\xDC]/g, ch: 'U' }, + { re: /[\xF9-\xFC]/g, ch: 'u' }, + { re: /[\xC7-\xE7]/g, ch: 'c' }, + { re: /[\xD1]/g, ch: 'N' }, + { re: /[\xF1]/g, ch: 'n' }, + ]; + return ( + a.each(c, function () { + b = b ? b.replace(this.re, this.ch) : ''; + }), + b + ); + } + function g(b) { + var c = arguments, + d = b; + [].shift.apply(c); + var e, + f = this.each(function () { + var b = a(this); + if (b.is('select')) { + var f = b.data('selectpicker'), + g = 'object' == typeof d && d; + if (f) { + if (g) + for (var h in g) g.hasOwnProperty(h) && (f.options[h] = g[h]); + } else { + var i = a.extend( + {}, + x.DEFAULTS, + a.fn.selectpicker.defaults || {}, + b.data(), + g, + ); + (i.template = a.extend( + {}, + x.DEFAULTS.template, + a.fn.selectpicker.defaults + ? a.fn.selectpicker.defaults.template + : {}, + b.data().template, + g.template, + )), + b.data('selectpicker', (f = new x(this, i))); + } + 'string' == typeof d && + (e = f[d] instanceof Function ? f[d].apply(f, c) : f.options[d]); + } + }); + return void 0 !== e ? e : f; + } + var h = document.createElement('_'); + if ((h.classList.toggle('c3', !1), h.classList.contains('c3'))) { + var i = DOMTokenList.prototype.toggle; + DOMTokenList.prototype.toggle = function (a, b) { + return 1 in arguments && !this.contains(a) == !b ? b : i.call(this, a); + }; + } + String.prototype.startsWith || + (function () { + var a = (function () { + try { + var a = {}, + b = Object.defineProperty, + c = b(a, a, a) && b; + } catch (a) {} + return c; + })(), + b = {}.toString, + c = function (a) { + if (null == this) throw new TypeError(); + var c = String(this); + if (a && '[object RegExp]' == b.call(a)) throw new TypeError(); + var d = c.length, + e = String(a), + f = e.length, + g = arguments.length > 1 ? arguments[1] : void 0, + h = g ? Number(g) : 0; + h != h && (h = 0); + var i = Math.min(Math.max(h, 0), d); + if (f + i > d) return !1; + for (var j = -1; ++j < f; ) + if (c.charCodeAt(i + j) != e.charCodeAt(j)) return !1; + return !0; + }; + a + ? a(String.prototype, 'startsWith', { + value: c, + configurable: !0, + writable: !0, + }) + : (String.prototype.startsWith = c); + })(), + Object.keys || + (Object.keys = function (a, b, c) { + c = []; + for (b in a) c.hasOwnProperty.call(a, b) && c.push(b); + return c; + }); + var j = { useDefault: !1, _set: a.valHooks.select.set }; + a.valHooks.select.set = function (b, c) { + return ( + c && !j.useDefault && a(b).data('selected', !0), + j._set.apply(this, arguments) + ); + }; + var k = null, + l = (function () { + try { + return new Event('change'), !0; + } catch (a) { + return !1; + } + })(); + a.fn.triggerNative = function (a) { + var b, + c = this[0]; + c.dispatchEvent + ? (l + ? (b = new Event(a, { bubbles: !0 })) + : ((b = document.createEvent('Event')), b.initEvent(a, !0, !1)), + c.dispatchEvent(b)) + : c.fireEvent + ? ((b = document.createEventObject()), + (b.eventType = a), + c.fireEvent('on' + a, b)) + : this.trigger(a); + }; + var m = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`', + }, + n = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + '`': '`', + }, + o = function (a) { + var b = function (b) { + return a[b]; + }, + c = '(?:' + Object.keys(a).join('|') + ')', + d = RegExp(c), + e = RegExp(c, 'g'); + return function (a) { + return (a = null == a ? '' : '' + a), d.test(a) ? a.replace(e, b) : a; + }; + }, + p = o(m), + q = o(n), + r = { + 32: ' ', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 59: ';', + 65: 'A', + 66: 'B', + 67: 'C', + 68: 'D', + 69: 'E', + 70: 'F', + 71: 'G', + 72: 'H', + 73: 'I', + 74: 'J', + 75: 'K', + 76: 'L', + 77: 'M', + 78: 'N', + 79: 'O', + 80: 'P', + 81: 'Q', + 82: 'R', + 83: 'S', + 84: 'T', + 85: 'U', + 86: 'V', + 87: 'W', + 88: 'X', + 89: 'Y', + 90: 'Z', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9', + }, + s = { + ESCAPE: 27, + ENTER: 13, + SPACE: 32, + TAB: 9, + ARROW_UP: 38, + ARROW_DOWN: 40, + }, + t = {}; + try { + (t.full = (a.fn.dropdown.Constructor.VERSION || '') + .split(' ')[0] + .split('.')), + (t.major = t.full[0]); + } catch (a) { + console.error( + "There was an issue retrieving Bootstrap's version. Ensure Bootstrap is being loaded before bootstrap-select and there is no namespace collision.", + a, + ), + (t.major = '3'); + } + var u = { + DISABLED: 'disabled', + DIVIDER: '4' === t.major ? 'dropdown-divider' : 'divider', + SHOW: '4' === t.major ? 'show' : 'open', + DROPUP: 'dropup', + MENURIGHT: 'dropdown-menu-right', + MENULEFT: 'dropdown-menu-left', + BUTTONCLASS: '4' === t.major ? 'btn-light' : 'btn-default', + POPOVERHEADER: '4' === t.major ? 'popover-header' : 'popover-title', + }, + v = new RegExp(s.ARROW_UP + '|' + s.ARROW_DOWN), + w = new RegExp('^' + s.TAB + '$|' + s.ESCAPE), + x = + (new RegExp(s.ENTER + '|' + s.SPACE), + function (b, c) { + var d = this; + j.useDefault || + ((a.valHooks.select.set = j._set), (j.useDefault = !0)), + (this.$element = a(b)), + (this.$newElement = null), + (this.$button = null), + (this.$menu = null), + (this.options = c), + (this.selectpicker = { + main: { map: { newIndex: {}, originalIndex: {} } }, + current: { map: {} }, + search: { map: {} }, + view: {}, + keydown: { + keyHistory: '', + resetKeyHistory: { + start: function () { + return setTimeout(function () { + d.selectpicker.keydown.keyHistory = ''; + }, 800); + }, + }, + }, + }), + null === this.options.title && + (this.options.title = this.$element.attr('title')); + var e = this.options.windowPadding; + 'number' == typeof e && (this.options.windowPadding = [e, e, e, e]), + (this.val = x.prototype.val), + (this.render = x.prototype.render), + (this.refresh = x.prototype.refresh), + (this.setStyle = x.prototype.setStyle), + (this.selectAll = x.prototype.selectAll), + (this.deselectAll = x.prototype.deselectAll), + (this.destroy = x.prototype.destroy), + (this.remove = x.prototype.remove), + (this.show = x.prototype.show), + (this.hide = x.prototype.hide), + this.init(); + }); + (x.VERSION = '1.13.1'), + (x.DEFAULTS = { + noneSelectedText: 'Nothing selected', + noneResultsText: 'No results matched {0}', + countSelectedText: function (a, b) { + return 1 == a ? '{0} item selected' : '{0} items selected'; + }, + maxOptionsText: function (a, b) { + return [ + 1 == a + ? 'Limit reached ({n} item max)' + : 'Limit reached ({n} items max)', + 1 == b + ? 'Group limit reached ({n} item max)' + : 'Group limit reached ({n} items max)', + ]; + }, + selectAllText: 'Select All', + deselectAllText: 'Deselect All', + doneButton: !1, + doneButtonText: 'Close', + multipleSeparator: ', ', + styleBase: 'btn', + style: 'btn-default', + size: 'auto', + title: null, + selectedTextFormat: 'values', + width: !1, + container: !1, + hideDisabled: !1, + showSubtext: !1, + showIcon: !0, + showContent: !0, + dropupAuto: !0, + header: !1, + liveSearch: !1, + liveSearchPlaceholder: null, + liveSearchNormalize: !1, + liveSearchStyle: 'contains', + actionsBox: !1, + iconBase: 'glyphicon', + tickIcon: 'glyphicon-ok', + showTick: !1, + template: { caret: '' }, + maxOptions: !1, + mobile: !1, + selectOnTab: !1, + dropdownAlignRight: !1, + windowPadding: 0, + virtualScroll: 600, + }), + '4' === t.major && + ((x.DEFAULTS.style = 'btn-light'), + (x.DEFAULTS.iconBase = ''), + (x.DEFAULTS.tickIcon = 'bs-ok-default')), + (x.prototype = { + constructor: x, + init: function () { + var a = this, + b = this.$element.attr('id'); + this.$element.addClass('bs-select-hidden'), + (this.multiple = this.$element.prop('multiple')), + (this.autofocus = this.$element.prop('autofocus')), + (this.$newElement = this.createDropdown()), + this.createLi(), + this.$element.after(this.$newElement).prependTo(this.$newElement), + (this.$button = this.$newElement.children('button')), + (this.$menu = this.$newElement.children('.dropdown-menu')), + (this.$menuInner = this.$menu.children('.inner')), + (this.$searchbox = this.$menu.find('input')), + this.$element.removeClass('bs-select-hidden'), + !0 === this.options.dropdownAlignRight && + this.$menu.addClass(u.MENURIGHT), + void 0 !== b && this.$button.attr('data-id', b), + this.checkDisabled(), + this.clickListener(), + this.options.liveSearch && this.liveSearchListener(), + this.render(), + this.setStyle(), + this.setWidth(), + this.options.container + ? this.selectPosition() + : this.$element.on('hide.bs.select', function () { + if (a.isVirtual()) { + var b = a.$menuInner[0], + c = b.firstChild.cloneNode(!1); + b.replaceChild(c, b.firstChild), (b.scrollTop = 0); + } + }), + this.$menu.data('this', this), + this.$newElement.data('this', this), + this.options.mobile && this.mobile(), + this.$newElement.on({ + 'hide.bs.dropdown': function (b) { + a.$menuInner.attr('aria-expanded', !1), + a.$element.trigger('hide.bs.select', b); + }, + 'hidden.bs.dropdown': function (b) { + a.$element.trigger('hidden.bs.select', b); + }, + 'show.bs.dropdown': function (b) { + a.$menuInner.attr('aria-expanded', !0), + a.$element.trigger('show.bs.select', b); + }, + 'shown.bs.dropdown': function (b) { + a.$element.trigger('shown.bs.select', b); + }, + }), + a.$element[0].hasAttribute('required') && + this.$element.on('invalid', function () { + a.$button.addClass('bs-invalid'), + a.$element.on({ + 'shown.bs.select': function () { + a.$element.val(a.$element.val()).off('shown.bs.select'); + }, + 'rendered.bs.select': function () { + this.validity.valid && + a.$button.removeClass('bs-invalid'), + a.$element.off('rendered.bs.select'); + }, + }), + a.$button.on('blur.bs.select', function () { + a.$element.focus().blur(), a.$button.off('blur.bs.select'); + }); + }), + setTimeout(function () { + a.$element.trigger('loaded.bs.select'); + }); + }, + createDropdown: function () { + var b = this.multiple || this.options.showTick ? ' show-tick' : '', + c = this.autofocus ? ' autofocus' : '', + d = this.options.header + ? '
' + + this.options.header + + '
' + : '', + e = this.options.liveSearch + ? '' + : '', + f = + this.multiple && this.options.actionsBox + ? '
' + : '', + g = + this.multiple && this.options.doneButton + ? '
' + : '', + h = + ''; + return a(h); + }, + setPositionData: function () { + this.selectpicker.view.canHighlight = []; + for (var a = 0; a < this.selectpicker.current.data.length; a++) { + var b = this.selectpicker.current.data[a], + c = !0; + 'divider' === b.type + ? ((c = !1), (b.height = this.sizeInfo.dividerHeight)) + : 'optgroup-label' === b.type + ? ((c = !1), (b.height = this.sizeInfo.dropdownHeaderHeight)) + : (b.height = this.sizeInfo.liHeight), + b.disabled && (c = !1), + this.selectpicker.view.canHighlight.push(c), + (b.position = + (0 === a ? 0 : this.selectpicker.current.data[a - 1].position) + + b.height); + } + }, + isVirtual: function () { + return ( + (!1 !== this.options.virtualScroll && + this.selectpicker.main.elements.length >= + this.options.virtualScroll) || + !0 === this.options.virtualScroll + ); + }, + createView: function (c, d) { + function e(a, d) { + var e, + j, + k, + l, + m, + n, + o, + p = f.selectpicker.current.elements.length, + q = [], + r = void 0, + s = !0, + t = f.isVirtual(); + (f.selectpicker.view.scrollTop = a), + !0 === t && + f.sizeInfo.hasScrollBar && + f.$menu[0].offsetWidth > f.sizeInfo.totalMenuWidth && + ((f.sizeInfo.menuWidth = f.$menu[0].offsetWidth), + (f.sizeInfo.totalMenuWidth = + f.sizeInfo.menuWidth + f.sizeInfo.scrollBarWidth), + f.$menu.css('min-width', f.sizeInfo.menuWidth)), + (e = Math.ceil( + (f.sizeInfo.menuInnerHeight / f.sizeInfo.liHeight) * 1.5, + )), + (j = Math.round(p / e) || 1); + for (var u = 0; u < j; u++) { + var v = (u + 1) * e; + if ( + (u === j - 1 && (v = p), (q[u] = [u * e + (u ? 1 : 0), v]), !p) + ) + break; + void 0 === r && + a <= + f.selectpicker.current.data[v - 1].position - + f.sizeInfo.menuInnerHeight && + (r = u); + } + if ( + (void 0 === r && (r = 0), + (m = [ + f.selectpicker.view.position0, + f.selectpicker.view.position1, + ]), + (k = Math.max(0, r - 1)), + (l = Math.min(j - 1, r + 1)), + (f.selectpicker.view.position0 = Math.max(0, q[k][0]) || 0), + (f.selectpicker.view.position1 = Math.min(p, q[l][1]) || 0), + (n = + m[0] !== f.selectpicker.view.position0 || + m[1] !== f.selectpicker.view.position1), + void 0 !== f.activeIndex && + ((h = + f.selectpicker.current.elements[ + f.selectpicker.current.map.newIndex[f.prevActiveIndex] + ]), + (i = + f.selectpicker.current.elements[ + f.selectpicker.current.map.newIndex[f.activeIndex] + ]), + (g = + f.selectpicker.current.elements[ + f.selectpicker.current.map.newIndex[f.selectedIndex] + ]), + d && + (f.activeIndex !== f.selectedIndex && + (i.classList.remove('active'), + i.firstChild && i.firstChild.classList.remove('active')), + (f.activeIndex = void 0)), + f.activeIndex && + f.activeIndex !== f.selectedIndex && + g && + g.length && + (g.classList.remove('active'), + g.firstChild && g.firstChild.classList.remove('active'))), + void 0 !== f.prevActiveIndex && + f.prevActiveIndex !== f.activeIndex && + f.prevActiveIndex !== f.selectedIndex && + h && + h.length && + (h.classList.remove('active'), + h.firstChild && h.firstChild.classList.remove('active')), + (d || n) && + ((o = f.selectpicker.view.visibleElements + ? f.selectpicker.view.visibleElements.slice() + : []), + (f.selectpicker.view.visibleElements = + f.selectpicker.current.elements.slice( + f.selectpicker.view.position0, + f.selectpicker.view.position1, + )), + f.setOptionStatus(), + (c || (!1 === t && d)) && + (s = !b(o, f.selectpicker.view.visibleElements)), + (d || !0 === t) && s)) + ) { + var w, + x, + y = f.$menuInner[0], + z = document.createDocumentFragment(), + A = y.firstChild.cloneNode(!1), + B = + !0 === t + ? f.selectpicker.view.visibleElements + : f.selectpicker.current.elements; + y.replaceChild(A, y.firstChild); + for (var u = 0, C = B.length; u < C; u++) z.appendChild(B[u]); + !0 === t && + ((w = + 0 === f.selectpicker.view.position0 + ? 0 + : f.selectpicker.current.data[ + f.selectpicker.view.position0 - 1 + ].position), + (x = + f.selectpicker.view.position1 > p - 1 + ? 0 + : f.selectpicker.current.data[p - 1].position - + f.selectpicker.current.data[ + f.selectpicker.view.position1 - 1 + ].position), + (y.firstChild.style.marginTop = w + 'px'), + (y.firstChild.style.marginBottom = x + 'px')), + y.firstChild.appendChild(z); + } + if (((f.prevActiveIndex = f.activeIndex), f.options.liveSearch)) { + if (c && d) { + var D, + E = 0; + f.selectpicker.view.canHighlight[E] || + (E = + 1 + f.selectpicker.view.canHighlight.slice(1).indexOf(!0)), + (D = f.selectpicker.view.visibleElements[E]), + f.selectpicker.view.currentActive && + (f.selectpicker.view.currentActive.classList.remove( + 'active', + ), + f.selectpicker.view.currentActive.firstChild && + f.selectpicker.view.currentActive.firstChild.classList.remove( + 'active', + )), + D && + (D.classList.add('active'), + D.firstChild && D.firstChild.classList.add('active')), + (f.activeIndex = f.selectpicker.current.map.originalIndex[E]); + } + } else f.$menuInner.focus(); + } + d = d || 0; + var f = this; + this.selectpicker.current = c + ? this.selectpicker.search + : this.selectpicker.main; + var g, + h, + i = []; + this.setPositionData(), + e(d, !0), + this.$menuInner + .off('scroll.createView') + .on('scroll.createView', function (a, b) { + f.noScroll || e(this.scrollTop, b), (f.noScroll = !1); + }), + a(window) + .off('resize.createView') + .on('resize.createView', function () { + e(f.$menuInner[0].scrollTop); + }); + }, + createLi: function () { + var b, + c = this, + d = [], + e = 0, + f = 0, + g = [], + h = 0, + i = 0, + j = -1; + this.selectpicker.view.titleOption || + (this.selectpicker.view.titleOption = + document.createElement('option')); + var k = { + span: document.createElement('span'), + subtext: document.createElement('small'), + a: document.createElement('a'), + li: document.createElement('li'), + whitespace: document.createTextNode(' '), + }, + l = k.span.cloneNode(!1), + m = document.createDocumentFragment(); + (l.className = + c.options.iconBase + ' ' + c.options.tickIcon + ' check-mark'), + k.a.appendChild(l), + k.a.setAttribute('role', 'option'), + (k.subtext.className = 'text-muted'), + (k.text = k.span.cloneNode(!1)), + (k.text.className = 'text'); + var n = function (a, b, c, d) { + var e = k.li.cloneNode(!1); + return ( + a && + (1 === a.nodeType || 11 === a.nodeType + ? e.appendChild(a) + : (e.innerHTML = a)), + void 0 !== c && '' !== c && (e.className = c), + void 0 !== d && null !== d && e.classList.add('optgroup-' + d), + e + ); + }, + o = function (a, b, c) { + var d = k.a.cloneNode(!0); + return ( + a && + (11 === a.nodeType + ? d.appendChild(a) + : d.insertAdjacentHTML('beforeend', a)), + (void 0 !== b) & ('' !== b) && (d.className = b), + '4' === t.major && d.classList.add('dropdown-item'), + c && d.setAttribute('style', c), + d + ); + }, + q = function (a) { + var b, + d, + e = k.text.cloneNode(!1); + if (a.optionContent) e.innerHTML = a.optionContent; + else { + if (((e.textContent = a.text), a.optionIcon)) { + var f = k.whitespace.cloneNode(!1); + (d = k.span.cloneNode(!1)), + (d.className = c.options.iconBase + ' ' + a.optionIcon), + m.appendChild(d), + m.appendChild(f); + } + a.optionSubtext && + ((b = k.subtext.cloneNode(!1)), + (b.innerHTML = a.optionSubtext), + e.appendChild(b)); + } + return m.appendChild(e), m; + }, + r = function (a) { + var b, + d, + e = k.text.cloneNode(!1); + if (((e.textContent = a.labelEscaped), a.labelIcon)) { + var f = k.whitespace.cloneNode(!1); + (d = k.span.cloneNode(!1)), + (d.className = c.options.iconBase + ' ' + a.labelIcon), + m.appendChild(d), + m.appendChild(f); + } + return ( + a.labelSubtext && + ((b = k.subtext.cloneNode(!1)), + (b.textContent = a.labelSubtext), + e.appendChild(b)), + m.appendChild(e), + m + ); + }; + if (this.options.title && !this.multiple) { + j--; + var s = this.$element[0], + v = !1, + w = !this.selectpicker.view.titleOption.parentNode; + if (w) { + (this.selectpicker.view.titleOption.className = + 'bs-title-option'), + (this.selectpicker.view.titleOption.value = ''); + v = + void 0 === a(s.options[s.selectedIndex]).attr('selected') && + void 0 === this.$element.data('selected'); + } + (w || 0 !== this.selectpicker.view.titleOption.index) && + s.insertBefore(this.selectpicker.view.titleOption, s.firstChild), + v && (s.selectedIndex = 0); + } + var x = this.$element.find('option'); + x.each(function (k) { + var l = a(this); + if ((j++, !l.hasClass('bs-title-option'))) { + var m, + s, + t = l.data(), + v = this.className || '', + w = p(this.style.cssText), + y = t.content, + z = this.textContent, + A = t.tokens, + B = t.subtext, + C = t.icon, + D = l.parent(), + E = D[0], + F = 'OPTGROUP' === E.tagName, + G = F && E.disabled, + H = this.disabled || G, + I = + this.previousElementSibling && + 'OPTGROUP' === this.previousElementSibling.tagName, + J = D.data(); + if ( + !0 === t.hidden || + (c.options.hideDisabled && ((H && !F) || G)) + ) { + if ( + ((m = t.prevHiddenIndex), + l.next().data('prevHiddenIndex', void 0 !== m ? m : k), + j--, + !I && void 0 !== m) + ) { + var K = x[m].previousElementSibling; + K && 'OPTGROUP' === K.tagName && !K.disabled && (I = !0); + } + return void ( + I && + 'divider' !== g[g.length - 1].type && + (j++, + d.push(n(!1, 0, u.DIVIDER, h + 'div')), + g.push({ type: 'divider', optID: h, originalIndex: k })) + ); + } + if (F && !0 !== t.divider) { + if (c.options.hideDisabled && H) { + if (void 0 === J.allOptionsDisabled) { + var L = D.children(); + D.data( + 'allOptionsDisabled', + L.filter(':disabled').length === L.length, + ); + } + if (D.data('allOptionsDisabled')) return void j--; + } + var M = ' ' + E.className || ''; + if (!this.previousElementSibling) { + h += 1; + var N = E.label, + O = p(N), + P = J.subtext, + Q = J.icon; + 0 !== k && + d.length > 0 && + (j++, + d.push(n(!1, 0, u.DIVIDER, h + 'div')), + g.push({ type: 'divider', optID: h, originalIndex: k })), + j++; + var R = r({ labelEscaped: O, labelSubtext: P, labelIcon: Q }); + d.push(n(R, 0, 'dropdown-header' + M, h)), + g.push({ + content: O, + subtext: P, + type: 'optgroup-label', + optID: h, + originalIndex: k, + }), + (i = j - 1); + } + if ((c.options.hideDisabled && H) || !0 === t.hidden) + return void j--; + (s = q({ + text: z, + optionContent: y, + optionSubtext: B, + optionIcon: C, + })), + d.push(n(o(s, 'opt ' + v + M, w), 0, '', h)), + g.push({ + content: y || z, + subtext: B, + tokens: A, + type: 'option', + optID: h, + headerIndex: i, + lastIndex: i + E.childElementCount, + originalIndex: k, + data: t, + }), + e++; + } else if (!0 === t.divider) + d.push(n(!1, 0, 'divider')), + g.push({ type: 'divider', originalIndex: k }); + else { + if ( + !I && + c.options.hideDisabled && + void 0 !== (m = t.prevHiddenIndex) + ) { + var K = x[m].previousElementSibling; + K && 'OPTGROUP' === K.tagName && !K.disabled && (I = !0); + } + I && + 'divider' !== g[g.length - 1].type && + (j++, + d.push(n(!1, 0, u.DIVIDER, h + 'div')), + g.push({ type: 'divider', optID: h, originalIndex: k })), + (s = q({ + text: z, + optionContent: y, + optionSubtext: B, + optionIcon: C, + })), + d.push(n(o(s, v, w))), + g.push({ + content: y || z, + subtext: B, + tokens: A, + type: 'option', + originalIndex: k, + data: t, + }), + e++; + } + (c.selectpicker.main.map.newIndex[k] = j), + (c.selectpicker.main.map.originalIndex[j] = k); + var S = g[g.length - 1]; + S.disabled = H; + var T = 0; + S.content && (T += S.content.length), + S.subtext && (T += S.subtext.length), + C && (T += 1), + T > f && ((f = T), (b = d[d.length - 1])); + } + }), + (this.selectpicker.main.elements = d), + (this.selectpicker.main.data = g), + (this.selectpicker.current = this.selectpicker.main), + (this.selectpicker.view.widestOption = b), + (this.selectpicker.view.availableOptionsCount = e); + }, + findLis: function () { + return this.$menuInner.find('.inner > li'); + }, + render: function () { + var a = this, + b = this.$element.find('option'), + c = [], + d = []; + this.togglePlaceholder(), this.tabIndex(); + for ( + var e = 0, f = this.selectpicker.main.elements.length; + e < f; + e++ + ) { + var g = this.selectpicker.main.map.originalIndex[e], + h = b[g]; + if ( + h && + h.selected && + (c.push(h), + (d.length < 100 && 'count' !== a.options.selectedTextFormat) || + 1 === c.length) + ) { + if ( + a.options.hideDisabled && + (h.disabled || + ('OPTGROUP' === h.parentNode.tagName && + h.parentNode.disabled)) + ) + return; + var i, + j, + k = this.selectpicker.main.data[e].data, + l = + k.icon && a.options.showIcon + ? ' ' + : ''; + (i = + a.options.showSubtext && k.subtext && !a.multiple + ? ' ' + k.subtext + '' + : ''), + (j = h.title + ? h.title + : k.content && a.options.showContent + ? k.content.toString() + : l + h.innerHTML.trim() + i), + d.push(j); + } + } + var m = this.multiple ? d.join(this.options.multipleSeparator) : d[0]; + if ( + (c.length > 50 && (m += '...'), + this.multiple && + -1 !== this.options.selectedTextFormat.indexOf('count')) + ) { + var n = this.options.selectedTextFormat.split('>'); + if ( + (n.length > 1 && c.length > n[1]) || + (1 === n.length && c.length >= 2) + ) { + var o = this.selectpicker.view.availableOptionsCount; + m = ( + 'function' == typeof this.options.countSelectedText + ? this.options.countSelectedText(c.length, o) + : this.options.countSelectedText + ) + .replace('{0}', c.length.toString()) + .replace('{1}', o.toString()); + } + } + void 0 == this.options.title && + (this.options.title = this.$element.attr('title')), + 'static' == this.options.selectedTextFormat && + (m = this.options.title), + m || + (m = + void 0 !== this.options.title + ? this.options.title + : this.options.noneSelectedText), + (this.$button[0].title = q(m.replace(/<[^>]*>?/g, '').trim())), + (this.$button.find('.filter-option-inner-inner')[0].innerHTML = m), + this.$element.trigger('rendered.bs.select'); + }, + setStyle: function (a, b) { + this.$element.attr('class') && + this.$newElement.addClass( + this.$element + .attr('class') + .replace( + /selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, + '', + ), + ); + var c = a || this.options.style; + 'add' == b + ? this.$button.addClass(c) + : 'remove' == b + ? this.$button.removeClass(c) + : (this.$button.removeClass(this.options.style), + this.$button.addClass(c)); + }, + liHeight: function (b) { + if (b || (!1 !== this.options.size && !this.sizeInfo)) { + this.sizeInfo || (this.sizeInfo = {}); + var c = document.createElement('div'), + d = document.createElement('div'), + f = document.createElement('div'), + g = document.createElement('ul'), + h = document.createElement('li'), + i = document.createElement('li'), + j = document.createElement('li'), + k = document.createElement('a'), + l = document.createElement('span'), + m = + this.options.header && + this.$menu.find('.' + u.POPOVERHEADER).length > 0 + ? this.$menu.find('.' + u.POPOVERHEADER)[0].cloneNode(!0) + : null, + n = this.options.liveSearch + ? document.createElement('div') + : null, + o = + this.options.actionsBox && + this.multiple && + this.$menu.find('.bs-actionsbox').length > 0 + ? this.$menu.find('.bs-actionsbox')[0].cloneNode(!0) + : null, + p = + this.options.doneButton && + this.multiple && + this.$menu.find('.bs-donebutton').length > 0 + ? this.$menu.find('.bs-donebutton')[0].cloneNode(!0) + : null; + if ( + ((this.sizeInfo.selectWidth = this.$newElement[0].offsetWidth), + (l.className = 'text'), + (k.className = 'dropdown-item'), + (c.className = this.$menu[0].parentNode.className + ' ' + u.SHOW), + (c.style.width = this.sizeInfo.selectWidth + 'px'), + (d.className = 'dropdown-menu ' + u.SHOW), + (f.className = 'inner ' + u.SHOW), + (g.className = + 'dropdown-menu inner ' + ('4' === t.major ? u.SHOW : '')), + (h.className = u.DIVIDER), + (i.className = 'dropdown-header'), + l.appendChild(document.createTextNode('Inner text')), + k.appendChild(l), + j.appendChild(k), + i.appendChild(l.cloneNode(!0)), + this.selectpicker.view.widestOption && + g.appendChild( + this.selectpicker.view.widestOption.cloneNode(!0), + ), + g.appendChild(j), + g.appendChild(h), + g.appendChild(i), + m && d.appendChild(m), + n) + ) { + var q = document.createElement('input'); + (n.className = 'bs-searchbox'), + (q.className = 'form-control'), + n.appendChild(q), + d.appendChild(n); + } + o && d.appendChild(o), + f.appendChild(g), + d.appendChild(f), + p && d.appendChild(p), + c.appendChild(d), + document.body.appendChild(c); + var r, + s = k.offsetHeight, + v = i ? i.offsetHeight : 0, + w = m ? m.offsetHeight : 0, + x = n ? n.offsetHeight : 0, + y = o ? o.offsetHeight : 0, + z = p ? p.offsetHeight : 0, + A = a(h).outerHeight(!0), + B = !!window.getComputedStyle && window.getComputedStyle(d), + C = d.offsetWidth, + D = B ? null : a(d), + E = { + vert: + e(B ? B.paddingTop : D.css('paddingTop')) + + e(B ? B.paddingBottom : D.css('paddingBottom')) + + e(B ? B.borderTopWidth : D.css('borderTopWidth')) + + e(B ? B.borderBottomWidth : D.css('borderBottomWidth')), + horiz: + e(B ? B.paddingLeft : D.css('paddingLeft')) + + e(B ? B.paddingRight : D.css('paddingRight')) + + e(B ? B.borderLeftWidth : D.css('borderLeftWidth')) + + e(B ? B.borderRightWidth : D.css('borderRightWidth')), + }, + F = { + vert: + E.vert + + e(B ? B.marginTop : D.css('marginTop')) + + e(B ? B.marginBottom : D.css('marginBottom')) + + 2, + horiz: + E.horiz + + e(B ? B.marginLeft : D.css('marginLeft')) + + e(B ? B.marginRight : D.css('marginRight')) + + 2, + }; + (f.style.overflowY = 'scroll'), + (r = d.offsetWidth - C), + document.body.removeChild(c), + (this.sizeInfo.liHeight = s), + (this.sizeInfo.dropdownHeaderHeight = v), + (this.sizeInfo.headerHeight = w), + (this.sizeInfo.searchHeight = x), + (this.sizeInfo.actionsHeight = y), + (this.sizeInfo.doneButtonHeight = z), + (this.sizeInfo.dividerHeight = A), + (this.sizeInfo.menuPadding = E), + (this.sizeInfo.menuExtras = F), + (this.sizeInfo.menuWidth = C), + (this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth), + (this.sizeInfo.scrollBarWidth = r), + (this.sizeInfo.selectHeight = this.$newElement[0].offsetHeight), + this.setPositionData(); + } + }, + getSelectPosition: function () { + var b, + c = this, + d = a(window), + e = c.$newElement.offset(), + f = a(c.options.container); + c.options.container && !f.is('body') + ? ((b = f.offset()), + (b.top += parseInt(f.css('borderTopWidth'))), + (b.left += parseInt(f.css('borderLeftWidth')))) + : (b = { top: 0, left: 0 }); + var g = c.options.windowPadding; + (this.sizeInfo.selectOffsetTop = e.top - b.top - d.scrollTop()), + (this.sizeInfo.selectOffsetBot = + d.height() - + this.sizeInfo.selectOffsetTop - + this.sizeInfo.selectHeight - + b.top - + g[2]), + (this.sizeInfo.selectOffsetLeft = e.left - b.left - d.scrollLeft()), + (this.sizeInfo.selectOffsetRight = + d.width() - + this.sizeInfo.selectOffsetLeft - + this.sizeInfo.selectWidth - + b.left - + g[1]), + (this.sizeInfo.selectOffsetTop -= g[0]), + (this.sizeInfo.selectOffsetLeft -= g[3]); + }, + setMenuSize: function (a) { + this.getSelectPosition(); + var b, + c, + d, + e, + f, + g, + h, + i = this.sizeInfo.selectWidth, + j = this.sizeInfo.liHeight, + k = this.sizeInfo.headerHeight, + l = this.sizeInfo.searchHeight, + m = this.sizeInfo.actionsHeight, + n = this.sizeInfo.doneButtonHeight, + o = this.sizeInfo.dividerHeight, + p = this.sizeInfo.menuPadding, + q = 0; + if ( + (this.options.dropupAuto && + ((h = j * this.selectpicker.current.elements.length + p.vert), + this.$newElement.toggleClass( + u.DROPUP, + this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > + this.sizeInfo.menuExtras.vert && + h + this.sizeInfo.menuExtras.vert + 50 > + this.sizeInfo.selectOffsetBot, + )), + 'auto' === this.options.size) + ) + (e = + this.selectpicker.current.elements.length > 3 + ? 3 * this.sizeInfo.liHeight + this.sizeInfo.menuExtras.vert - 2 + : 0), + (c = + this.sizeInfo.selectOffsetBot - this.sizeInfo.menuExtras.vert), + (d = e + k + l + m + n), + (g = Math.max(e - p.vert, 0)), + this.$newElement.hasClass(u.DROPUP) && + (c = + this.sizeInfo.selectOffsetTop - + this.sizeInfo.menuExtras.vert), + (f = c), + (b = c - k - l - m - n - p.vert); + else if ( + this.options.size && + 'auto' != this.options.size && + this.selectpicker.current.elements.length > this.options.size + ) { + for (var r = 0; r < this.options.size; r++) + 'divider' === this.selectpicker.current.data[r].type && q++; + (c = j * this.options.size + q * o + p.vert), + (b = c - p.vert), + (f = c + k + l + m + n), + (d = g = ''); + } + 'auto' === this.options.dropdownAlignRight && + this.$menu.toggleClass( + u.MENURIGHT, + this.sizeInfo.selectOffsetLeft > + this.sizeInfo.selectOffsetRight && + this.sizeInfo.selectOffsetRight < this.$menu[0].offsetWidth - i, + ), + this.$menu.css({ + 'max-height': f + 'px', + overflow: 'hidden', + 'min-height': d + 'px', + }), + this.$menuInner.css({ + 'max-height': b + 'px', + 'overflow-y': 'auto', + 'min-height': g + 'px', + }), + (this.sizeInfo.menuInnerHeight = b), + this.selectpicker.current.data.length && + this.selectpicker.current.data[ + this.selectpicker.current.data.length - 1 + ].position > this.sizeInfo.menuInnerHeight && + ((this.sizeInfo.hasScrollBar = !0), + (this.sizeInfo.totalMenuWidth = + this.sizeInfo.menuWidth + this.sizeInfo.scrollBarWidth), + this.$menu.css('min-width', this.sizeInfo.totalMenuWidth)), + this.dropdown && + this.dropdown._popper && + this.dropdown._popper.update(); + }, + setSize: function (b) { + if ( + (this.liHeight(b), + this.options.header && this.$menu.css('padding-top', 0), + !1 !== this.options.size) + ) { + var c, + d = this, + e = a(window), + f = 0; + this.setMenuSize(), + 'auto' === this.options.size + ? (this.$searchbox + .off('input.setMenuSize propertychange.setMenuSize') + .on( + 'input.setMenuSize propertychange.setMenuSize', + function () { + return d.setMenuSize(); + }, + ), + e + .off('resize.setMenuSize scroll.setMenuSize') + .on('resize.setMenuSize scroll.setMenuSize', function () { + return d.setMenuSize(); + })) + : this.options.size && + 'auto' != this.options.size && + this.selectpicker.current.elements.length > + this.options.size && + (this.$searchbox.off( + 'input.setMenuSize propertychange.setMenuSize', + ), + e.off('resize.setMenuSize scroll.setMenuSize')), + b + ? (f = this.$menuInner[0].scrollTop) + : d.multiple || + ('number' == + typeof (c = + d.selectpicker.main.map.newIndex[ + d.$element[0].selectedIndex + ]) && + !1 !== d.options.size && + ((f = d.sizeInfo.liHeight * c), + (f = + f - + d.sizeInfo.menuInnerHeight / 2 + + d.sizeInfo.liHeight / 2))), + d.createView(!1, f); + } + }, + setWidth: function () { + var a = this; + 'auto' === this.options.width + ? requestAnimationFrame(function () { + a.$menu.css('min-width', '0'), a.liHeight(), a.setMenuSize(); + var b = a.$newElement.clone().appendTo('body'), + c = b.css('width', 'auto').children('button').outerWidth(); + b.remove(), + (a.sizeInfo.selectWidth = Math.max( + a.sizeInfo.totalMenuWidth, + c, + )), + a.$newElement.css('width', a.sizeInfo.selectWidth + 'px'); + }) + : 'fit' === this.options.width + ? (this.$menu.css('min-width', ''), + this.$newElement.css('width', '').addClass('fit-width')) + : this.options.width + ? (this.$menu.css('min-width', ''), + this.$newElement.css('width', this.options.width)) + : (this.$menu.css('min-width', ''), + this.$newElement.css('width', '')), + this.$newElement.hasClass('fit-width') && + 'fit' !== this.options.width && + this.$newElement.removeClass('fit-width'); + }, + selectPosition: function () { + this.$bsContainer = a('
'); + var b, + c, + d, + e = this, + f = a(this.options.container), + g = function (a) { + var g = {}; + e.$bsContainer + .addClass( + a.attr('class').replace(/form-control|fit-width/gi, ''), + ) + .toggleClass(u.DROPUP, a.hasClass(u.DROPUP)), + (b = a.offset()), + f.is('body') + ? (c = { top: 0, left: 0 }) + : ((c = f.offset()), + (c.top += + parseInt(f.css('borderTopWidth')) - f.scrollTop()), + (c.left += + parseInt(f.css('borderLeftWidth')) - f.scrollLeft())), + (d = a.hasClass(u.DROPUP) ? 0 : a[0].offsetHeight), + t.major < 4 && + ((g.top = b.top - c.top + d), (g.left = b.left - c.left)), + (g.width = a[0].offsetWidth), + e.$bsContainer.css(g); + }; + this.$button.on('click.bs.dropdown.data-api', function () { + e.isDisabled() || + (g(e.$newElement), + e.$bsContainer + .appendTo(e.options.container) + .toggleClass(u.SHOW, !e.$button.hasClass(u.SHOW)) + .append(e.$menu)); + }), + a(window).on('resize scroll', function () { + g(e.$newElement); + }), + this.$element.on('hide.bs.select', function () { + e.$menu.data('height', e.$menu.height()), e.$bsContainer.detach(); + }); + }, + setOptionStatus: function () { + var a = this, + b = this.$element.find('option'); + if ( + ((a.noScroll = !1), + a.selectpicker.view.visibleElements && + a.selectpicker.view.visibleElements.length) + ) + for ( + var c = 0; + c < a.selectpicker.view.visibleElements.length; + c++ + ) { + var d = + a.selectpicker.current.map.originalIndex[ + c + a.selectpicker.view.position0 + ], + e = b[d]; + if (e) { + var f = this.selectpicker.main.map.newIndex[d], + g = this.selectpicker.main.elements[f]; + a.setDisabled( + d, + e.disabled || + ('OPTGROUP' === e.parentNode.tagName && + e.parentNode.disabled), + f, + g, + ), + a.setSelected(d, e.selected, f, g); + } + } + }, + setSelected: function (a, b, c, d) { + var e, + f, + g, + h = void 0 !== this.activeIndex, + i = this.activeIndex === a, + j = i || (b && !this.multiple && !h); + c || (c = this.selectpicker.main.map.newIndex[a]), + d || (d = this.selectpicker.main.elements[c]), + (g = d.firstChild), + b && (this.selectedIndex = a), + d.classList.toggle('selected', b), + d.classList.toggle('active', j), + j && + ((this.selectpicker.view.currentActive = d), + (this.activeIndex = a)), + g && + (g.classList.toggle('selected', b), + g.classList.toggle('active', j), + g.setAttribute('aria-selected', b)), + j || + (!h && + b && + void 0 !== this.prevActiveIndex && + ((e = + this.selectpicker.main.map.newIndex[this.prevActiveIndex]), + (f = this.selectpicker.main.elements[e]), + f.classList.remove('selected'), + f.classList.remove('active'), + f.firstChild && + (f.firstChild.classList.remove('selected'), + f.firstChild.classList.remove('active')))); + }, + setDisabled: function (a, b, c, d) { + var e; + c || (c = this.selectpicker.main.map.newIndex[a]), + d || (d = this.selectpicker.main.elements[c]), + (e = d.firstChild), + d.classList.toggle(u.DISABLED, b), + e && + ('4' === t.major && e.classList.toggle(u.DISABLED, b), + e.setAttribute('aria-disabled', b), + b + ? e.setAttribute('tabindex', -1) + : e.setAttribute('tabindex', 0)); + }, + isDisabled: function () { + return this.$element[0].disabled; + }, + checkDisabled: function () { + var a = this; + this.isDisabled() + ? (this.$newElement.addClass(u.DISABLED), + this.$button + .addClass(u.DISABLED) + .attr('tabindex', -1) + .attr('aria-disabled', !0)) + : (this.$button.hasClass(u.DISABLED) && + (this.$newElement.removeClass(u.DISABLED), + this.$button.removeClass(u.DISABLED).attr('aria-disabled', !1)), + -1 != this.$button.attr('tabindex') || + this.$element.data('tabindex') || + this.$button.removeAttr('tabindex')), + this.$button.click(function () { + return !a.isDisabled(); + }); + }, + togglePlaceholder: function () { + var a = this.$element[0], + b = a.selectedIndex, + c = -1 === b; + c || a.options[b].value || (c = !0), + this.$button.toggleClass('bs-placeholder', c); + }, + tabIndex: function () { + this.$element.data('tabindex') !== this.$element.attr('tabindex') && + -98 !== this.$element.attr('tabindex') && + '-98' !== this.$element.attr('tabindex') && + (this.$element.data('tabindex', this.$element.attr('tabindex')), + this.$button.attr('tabindex', this.$element.data('tabindex'))), + this.$element.attr('tabindex', -98); + }, + clickListener: function () { + var b = this, + d = a(document); + d.data('spaceSelect', !1), + this.$button.on('keyup', function (a) { + /(32)/.test(a.keyCode.toString(10)) && + d.data('spaceSelect') && + (a.preventDefault(), d.data('spaceSelect', !1)); + }), + this.$newElement.on('show.bs.dropdown', function () { + t.major > 3 && + !b.dropdown && + ((b.dropdown = b.$button.data('bs.dropdown')), + (b.dropdown._menu = b.$menu[0])); + }), + this.$button.on('click.bs.dropdown.data-api', function () { + b.$newElement.hasClass(u.SHOW) || b.setSize(); + }), + this.$element.on('shown.bs.select', function () { + b.$menuInner[0].scrollTop !== b.selectpicker.view.scrollTop && + (b.$menuInner[0].scrollTop = b.selectpicker.view.scrollTop), + b.options.liveSearch + ? b.$searchbox.focus() + : b.$menuInner.focus(); + }), + this.$menuInner.on('click', 'li a', function (d, e) { + var f = a(this), + g = b.isVirtual() ? b.selectpicker.view.position0 : 0, + h = + b.selectpicker.current.map.originalIndex[ + f.parent().index() + g + ], + i = c(b.$element[0]), + j = b.$element.prop('selectedIndex'), + l = !0; + if ( + (b.multiple && + 1 !== b.options.maxOptions && + d.stopPropagation(), + d.preventDefault(), + !b.isDisabled() && !f.parent().hasClass(u.DISABLED)) + ) { + var m = b.$element.find('option'), + n = m.eq(h), + o = n.prop('selected'), + p = n.parent('optgroup'), + q = b.options.maxOptions, + r = p.data('maxOptions') || !1; + if ( + (h === b.activeIndex && (e = !0), + e || + ((b.prevActiveIndex = b.activeIndex), + (b.activeIndex = void 0)), + b.multiple) + ) { + if ( + (n.prop('selected', !o), + b.setSelected(h, !o), + f.blur(), + !1 !== q || !1 !== r) + ) { + var s = q < m.filter(':selected').length, + t = r < p.find('option:selected').length; + if ((q && s) || (r && t)) + if (q && 1 == q) + m.prop('selected', !1), + n.prop('selected', !0), + b.$menuInner + .find('.selected') + .removeClass('selected'), + b.setSelected(h, !0); + else if (r && 1 == r) { + p.find('option:selected').prop('selected', !1), + n.prop('selected', !0); + var v = + b.selectpicker.current.data[ + f.parent().index() + b.selectpicker.view.position0 + ].optID; + b.$menuInner + .find('.optgroup-' + v) + .removeClass('selected'), + b.setSelected(h, !0); + } else { + var w = + 'string' == typeof b.options.maxOptionsText + ? [ + b.options.maxOptionsText, + b.options.maxOptionsText, + ] + : b.options.maxOptionsText, + x = 'function' == typeof w ? w(q, r) : w, + y = x[0].replace('{n}', q), + z = x[1].replace('{n}', r), + A = a('
'); + x[2] && + ((y = y.replace('{var}', x[2][q > 1 ? 0 : 1])), + (z = z.replace('{var}', x[2][r > 1 ? 0 : 1]))), + n.prop('selected', !1), + b.$menu.append(A), + q && + s && + (A.append(a('
' + y + '
')), + (l = !1), + b.$element.trigger('maxReached.bs.select')), + r && + t && + (A.append(a('
' + z + '
')), + (l = !1), + b.$element.trigger('maxReachedGrp.bs.select')), + setTimeout(function () { + b.setSelected(h, !1); + }, 10), + A.delay(750).fadeOut(300, function () { + a(this).remove(); + }); + } + } + } else + m.prop('selected', !1), + n.prop('selected', !0), + b.setSelected(h, !0); + !b.multiple || (b.multiple && 1 === b.options.maxOptions) + ? b.$button.focus() + : b.options.liveSearch && b.$searchbox.focus(), + l && + ((i != c(b.$element[0]) && b.multiple) || + (j != b.$element.prop('selectedIndex') && !b.multiple)) && + ((k = [h, n.prop('selected'), i]), + b.$element.triggerNative('change')); + } + }), + this.$menu.on( + 'click', + 'li.' + + u.DISABLED + + ' a, .' + + u.POPOVERHEADER + + ', .' + + u.POPOVERHEADER + + ' :not(.close)', + function (c) { + c.currentTarget == this && + (c.preventDefault(), + c.stopPropagation(), + b.options.liveSearch && !a(c.target).hasClass('close') + ? b.$searchbox.focus() + : b.$button.focus()); + }, + ), + this.$menuInner.on( + 'click', + '.divider, .dropdown-header', + function (a) { + a.preventDefault(), + a.stopPropagation(), + b.options.liveSearch + ? b.$searchbox.focus() + : b.$button.focus(); + }, + ), + this.$menu.on( + 'click', + '.' + u.POPOVERHEADER + ' .close', + function () { + b.$button.click(); + }, + ), + this.$searchbox.on('click', function (a) { + a.stopPropagation(); + }), + this.$menu.on('click', '.actions-btn', function (c) { + b.options.liveSearch ? b.$searchbox.focus() : b.$button.focus(), + c.preventDefault(), + c.stopPropagation(), + a(this).hasClass('bs-select-all') + ? b.selectAll() + : b.deselectAll(); + }), + this.$element.on({ + change: function () { + b.render(), + b.$element.trigger('changed.bs.select', k), + (k = null); + }, + focus: function () { + b.$button.focus(); + }, + }); + }, + liveSearchListener: function () { + var a = this, + b = document.createElement('li'); + this.$button.on('click.bs.dropdown.data-api', function () { + a.$searchbox.val() && a.$searchbox.val(''); + }), + this.$searchbox.on( + 'click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api', + function (a) { + a.stopPropagation(); + }, + ), + this.$searchbox.on('input propertychange', function () { + var c = a.$searchbox.val(); + if ( + ((a.selectpicker.search.map.newIndex = {}), + (a.selectpicker.search.map.originalIndex = {}), + (a.selectpicker.search.elements = []), + (a.selectpicker.search.data = []), + c) + ) { + var e, + f = [], + g = c.toUpperCase(), + h = {}, + i = [], + j = a._searchStyle(), + k = a.options.liveSearchNormalize; + a._$lisSelected = a.$menuInner.find('.selected'); + for (var e = 0; e < a.selectpicker.main.data.length; e++) { + var l = a.selectpicker.main.data[e]; + h[e] || (h[e] = d(l, g, j, k)), + h[e] && + void 0 !== l.headerIndex && + -1 === i.indexOf(l.headerIndex) && + (l.headerIndex > 0 && + ((h[l.headerIndex - 1] = !0), + i.push(l.headerIndex - 1)), + (h[l.headerIndex] = !0), + i.push(l.headerIndex), + (h[l.lastIndex + 1] = !0)), + h[e] && 'optgroup-label' !== l.type && i.push(e); + } + for (var e = 0, m = i.length; e < m; e++) { + var n = i[e], + o = i[e - 1], + l = a.selectpicker.main.data[n], + q = a.selectpicker.main.data[o]; + ('divider' !== l.type || + ('divider' === l.type && + q && + 'divider' !== q.type && + m - 1 !== e)) && + (a.selectpicker.search.data.push(l), + f.push(a.selectpicker.main.elements[n]), + (a.selectpicker.search.map.newIndex[l.originalIndex] = + f.length - 1), + (a.selectpicker.search.map.originalIndex[f.length - 1] = + l.originalIndex)); + } + (a.activeIndex = void 0), + (a.noScroll = !0), + a.$menuInner.scrollTop(0), + (a.selectpicker.search.elements = f), + a.createView(!0), + f.length || + ((b.className = 'no-results'), + (b.innerHTML = a.options.noneResultsText.replace( + '{0}', + '"' + p(c) + '"', + )), + a.$menuInner[0].firstChild.appendChild(b)); + } else a.$menuInner.scrollTop(0), a.createView(!1); + }); + }, + _searchStyle: function () { + return this.options.liveSearchStyle || 'contains'; + }, + val: function (a) { + return void 0 !== a + ? (this.$element.val(a).triggerNative('change'), this.$element) + : this.$element.val(); + }, + changeAll: function (a) { + if (this.multiple) { + void 0 === a && (a = !0); + var b = this.$element.find('option'), + d = 0, + e = 0, + f = c(this.$element[0]); + this.$element.addClass('bs-select-hidden'); + for ( + var g = 0; + g < this.selectpicker.current.elements.length; + g++ + ) { + var h = this.selectpicker.current.map.originalIndex[g], + i = b[h]; + i && (i.selected && d++, (i.selected = a), i.selected && e++); + } + this.$element.removeClass('bs-select-hidden'), + d !== e && + (this.setOptionStatus(), + this.togglePlaceholder(), + (k = [null, null, f]), + this.$element.triggerNative('change')); + } + }, + selectAll: function () { + return this.changeAll(!0); + }, + deselectAll: function () { + return this.changeAll(!1); + }, + toggle: function (a) { + (a = a || window.event), + a && a.stopPropagation(), + this.$button.trigger('click.bs.dropdown.data-api'); + }, + keydown: function (b) { + var c, + e, + f, + g, + h, + i = a(this), + j = i.is('input') ? i.parent().parent() : i.parent(), + k = j.data('this'), + l = k.findLis(), + m = !1, + n = + b.which === s.TAB && + !i.hasClass('dropdown-toggle') && + !k.options.selectOnTab, + o = v.test(b.which) || n, + p = k.$menuInner[0].scrollTop, + q = k.isVirtual(), + t = !0 === q ? k.selectpicker.view.position0 : 0; + if ( + ((e = k.$newElement.hasClass(u.SHOW)), + !e && + (o || + (b.which >= 48 && b.which <= 57) || + (b.which >= 96 && b.which <= 105) || + (b.which >= 65 && b.which <= 90)) && + k.$button.trigger('click.bs.dropdown.data-api'), + b.which === s.ESCAPE && + e && + (b.preventDefault(), + k.$button.trigger('click.bs.dropdown.data-api').focus()), + o) + ) { + if (!l.length) return; + (c = + !0 === q + ? l.index(l.filter('.active')) + : k.selectpicker.current.map.newIndex[k.activeIndex]), + void 0 === c && (c = -1), + -1 !== c && + ((f = k.selectpicker.current.elements[c + t]), + f.classList.remove('active'), + f.firstChild && f.firstChild.classList.remove('active')), + b.which === s.ARROW_UP + ? (-1 !== c && c--, + c + t < 0 && (c += l.length), + k.selectpicker.view.canHighlight[c + t] || + (-1 === + (c = + k.selectpicker.view.canHighlight + .slice(0, c + t) + .lastIndexOf(!0) - t) && + (c = l.length - 1))) + : (b.which === s.ARROW_DOWN || n) && + (c++, + c + t >= k.selectpicker.view.canHighlight.length && (c = 0), + k.selectpicker.view.canHighlight[c + t] || + (c = + c + + 1 + + k.selectpicker.view.canHighlight + .slice(c + t + 1) + .indexOf(!0))), + b.preventDefault(); + var x = t + c; + b.which === s.ARROW_UP + ? 0 === t && c === l.length - 1 + ? ((k.$menuInner[0].scrollTop = k.$menuInner[0].scrollHeight), + (x = k.selectpicker.current.elements.length - 1)) + : ((g = k.selectpicker.current.data[x]), + (h = g.position - g.height), + (m = h < p)) + : (b.which === s.ARROW_DOWN || n) && + (0 !== t && 0 === c + ? ((k.$menuInner[0].scrollTop = 0), (x = 0)) + : ((g = k.selectpicker.current.data[x]), + (h = g.position - k.sizeInfo.menuInnerHeight), + (m = h > p))), + (f = k.selectpicker.current.elements[x]), + f.classList.add('active'), + f.firstChild && f.firstChild.classList.add('active'), + (k.activeIndex = k.selectpicker.current.map.originalIndex[x]), + (k.selectpicker.view.currentActive = f), + m && (k.$menuInner[0].scrollTop = h), + k.options.liveSearch ? k.$searchbox.focus() : i.focus(); + } else if ( + (!i.is('input') && !w.test(b.which)) || + (b.which === s.SPACE && k.selectpicker.keydown.keyHistory) + ) { + var y, + z, + A = []; + b.preventDefault(), + (k.selectpicker.keydown.keyHistory += r[b.which]), + k.selectpicker.keydown.resetKeyHistory.cancel && + clearTimeout(k.selectpicker.keydown.resetKeyHistory.cancel), + (k.selectpicker.keydown.resetKeyHistory.cancel = + k.selectpicker.keydown.resetKeyHistory.start()), + (z = k.selectpicker.keydown.keyHistory), + /^(.)\1+$/.test(z) && (z = z.charAt(0)); + for (var B = 0; B < k.selectpicker.current.data.length; B++) { + var C, + D = k.selectpicker.current.data[B]; + (C = d(D, z, 'startsWith', !0)), + C && + k.selectpicker.view.canHighlight[B] && + ((D.index = B), A.push(D.originalIndex)); + } + if (A.length) { + var E = 0; + l.removeClass('active').find('a').removeClass('active'), + 1 === z.length && + ((E = A.indexOf(k.activeIndex)), + -1 === E || E === A.length - 1 ? (E = 0) : E++), + (y = k.selectpicker.current.map.newIndex[A[E]]), + (g = k.selectpicker.current.data[y]), + p - g.position > 0 + ? ((h = g.position - g.height), (m = !0)) + : ((h = g.position - k.sizeInfo.menuInnerHeight), + (m = g.position > p + k.sizeInfo.menuInnerHeight)), + (f = k.selectpicker.current.elements[y]), + f.classList.add('active'), + f.firstChild && f.firstChild.classList.add('active'), + (k.activeIndex = A[E]), + f.firstChild.focus(), + m && (k.$menuInner[0].scrollTop = h), + i.focus(); + } + } + e && + ((b.which === s.SPACE && !k.selectpicker.keydown.keyHistory) || + b.which === s.ENTER || + (b.which === s.TAB && k.options.selectOnTab)) && + (b.which !== s.SPACE && b.preventDefault(), + (k.options.liveSearch && b.which === s.SPACE) || + (k.$menuInner.find('.active a').trigger('click', !0), + i.focus(), + k.options.liveSearch || + (b.preventDefault(), a(document).data('spaceSelect', !0)))); + }, + mobile: function () { + this.$element.addClass('mobile-device'); + }, + refresh: function () { + var b = a.extend({}, this.options, this.$element.data()); + (this.options = b), + (this.selectpicker.main.map.newIndex = {}), + (this.selectpicker.main.map.originalIndex = {}), + this.createLi(), + this.checkDisabled(), + this.render(), + this.setStyle(), + this.setWidth(), + this.setSize(!0), + this.$element.trigger('refreshed.bs.select'); + }, + hide: function () { + this.$newElement.hide(); + }, + show: function () { + this.$newElement.show(); + }, + remove: function () { + this.$newElement.remove(), this.$element.remove(); + }, + destroy: function () { + this.$newElement.before(this.$element).remove(), + this.$bsContainer + ? this.$bsContainer.remove() + : this.$menu.remove(), + this.$element + .off('.bs.select') + .removeData('selectpicker') + .removeClass('bs-select-hidden selectpicker'); + }, + }); + var y = a.fn.selectpicker; + (a.fn.selectpicker = g), + (a.fn.selectpicker.Constructor = x), + (a.fn.selectpicker.noConflict = function () { + return (a.fn.selectpicker = y), this; + }), + a(document) + .off('keydown.bs.dropdown.data-api') + .on( + 'keydown.bs.select', + '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input', + x.prototype.keydown, + ) + .on( + 'focusin.modal', + '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input', + function (a) { + a.stopPropagation(); + }, + ), + a(window).on('load.bs.select.data-api', function () { + a('.selectpicker').each(function () { + var b = a(this); + g.call(b, b.data()); + }); + }); + })(a); +}); +//# sourceMappingURL=bootstrap-select.js.map diff --git a/test_web/static/js/html-sanitize-nonbrowser.js b/test_web/static/js/html-sanitize-nonbrowser.js index 4d5b9fb..9549da7 100644 --- a/test_web/static/js/html-sanitize-nonbrowser.js +++ b/test_web/static/js/html-sanitize-nonbrowser.js @@ -1,59 +1,57 @@ -// This is an un-browserified version of the script! If you make any changes to this file you need to run -// npx browserify html-sanitize-nonbrowser.js -o html-sanitize.js -// for your changes to be visible inside of the browser! -const sanitizeHtml = require("sanitize-html"); - -/** @typedef {{removeExtraParagraphSpacing?: boolean}} Options */ - -const loadingGif = - "https://raw.githubusercontent.com/LNReader/lnreader/plugins/android/app/src/main/assets/images/loading.gif"; - -/** - * - * @param {string} html - * @param {Options?} options - */ -window.sanitizeChapterText = (html, options) => { - if (html) { - html = html.replace(/</g, "<").replace(/>/g, ">"); - } - let text = sanitizeHtml(html, { - allowedTags: sanitizeHtml.defaults.allowedTags.concat([ - "img", - "i", - "em", - "b", - "a", - "div", - ]), - allowedAttributes: { - img: ["src"], - a: ["href"], - }, - allowedSchemes: ["data", "http", "https", "file"], - }); - if (text) { - if (options?.removeExtraParagraphSpacing) { - text = text - .replace(/<\s*br[^>]*>/gi, "\n") - .replace(/\n{2,}/g, "\n\n"); - } - const imgHandlerRegex = /]*src="[^"]+"[^>]*)>/g; - const ttsHandlerRegex = - /(< *\w+ *>[^<.?!]+< *\/\w+ *>|< *a *href *= *"[^"]+" *>\s*(]+>|[^<]+|(< *\w+ *>[^]+< *\/\w+ *>)*)\s*< *\/a *>|< *img[^>]+>|[^<>.?!]+[.?!])/g; - // first -> match whole tags which dont have attribute and [.?!] inside - // second -> match a tags and its childs - // third -> match img tags - // match sentence split by [.?!] - - text = text - .replace( - imgHandlerRegex, - `Plugin can't fetch this img` - ) - .replace(ttsHandlerRegex, "$1"); - } else { - text = "HTML is empty"; - } - return text; -}; +// This is an un-browserified version of the script! If you make any changes to this file you need to run +// npx browserify html-sanitize-nonbrowser.js -o html-sanitize.js +// for your changes to be visible inside of the browser! +const sanitizeHtml = require('sanitize-html'); + +/** @typedef {{removeExtraParagraphSpacing?: boolean}} Options */ + +const loadingGif = + 'https://raw.githubusercontent.com/LNReader/lnreader/plugins/android/app/src/main/assets/images/loading.gif'; + +/** + * + * @param {string} html + * @param {Options?} options + */ +window.sanitizeChapterText = (html, options) => { + if (html) { + html = html.replace(/</g, '<').replace(/>/g, '>'); + } + let text = sanitizeHtml(html, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat([ + 'img', + 'i', + 'em', + 'b', + 'a', + 'div', + ]), + allowedAttributes: { + img: ['src'], + a: ['href'], + }, + allowedSchemes: ['data', 'http', 'https', 'file'], + }); + if (text) { + if (options?.removeExtraParagraphSpacing) { + text = text.replace(/<\s*br[^>]*>/gi, '\n').replace(/\n{2,}/g, '\n\n'); + } + const imgHandlerRegex = /]*src="[^"]+"[^>]*)>/g; + const ttsHandlerRegex = + /(< *\w+ *>[^<.?!]+< *\/\w+ *>|< *a *href *= *"[^"]+" *>\s*(]+>|[^<]+|(< *\w+ *>[^]+< *\/\w+ *>)*)\s*< *\/a *>|< *img[^>]+>|[^<>.?!]+[.?!])/g; + // first -> match whole tags which dont have attribute and [.?!] inside + // second -> match a tags and its childs + // third -> match img tags + // match sentence split by [.?!] + + text = text + .replace( + imgHandlerRegex, + `Plugin can't fetch this img`, + ) + .replace(ttsHandlerRegex, '$1'); + } else { + text = 'HTML is empty'; + } + return text; +}; diff --git a/test_web/static/js/html-sanitize.js b/test_web/static/js/html-sanitize.js index 8f9882f..3568dae 100644 --- a/test_web/static/js/html-sanitize.js +++ b/test_web/static/js/html-sanitize.js @@ -1,6277 +1,7547 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0) { - throw new Error('Invalid string. Length must be a multiple of 4') - } - - // Trim off extra bytes after placeholder bytes are found - // See: https://github.com/beatgammit/base64-js/issues/42 - var validLen = b64.indexOf('=') - if (validLen === -1) validLen = len - - var placeHoldersLen = validLen === len - ? 0 - : 4 - (validLen % 4) - - return [validLen, placeHoldersLen] -} - -// base64 is 4/3 + up to two characters of the original data -function byteLength (b64) { - var lens = getLens(b64) - var validLen = lens[0] - var placeHoldersLen = lens[1] - return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen -} - -function _byteLength (b64, validLen, placeHoldersLen) { - return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen -} - -function toByteArray (b64) { - var tmp - var lens = getLens(b64) - var validLen = lens[0] - var placeHoldersLen = lens[1] - - var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)) - - var curByte = 0 - - // if there are placeholders, only get up to the last complete 4 chars - var len = placeHoldersLen > 0 - ? validLen - 4 - : validLen - - var i - for (i = 0; i < len; i += 4) { - tmp = - (revLookup[b64.charCodeAt(i)] << 18) | - (revLookup[b64.charCodeAt(i + 1)] << 12) | - (revLookup[b64.charCodeAt(i + 2)] << 6) | - revLookup[b64.charCodeAt(i + 3)] - arr[curByte++] = (tmp >> 16) & 0xFF - arr[curByte++] = (tmp >> 8) & 0xFF - arr[curByte++] = tmp & 0xFF - } - - if (placeHoldersLen === 2) { - tmp = - (revLookup[b64.charCodeAt(i)] << 2) | - (revLookup[b64.charCodeAt(i + 1)] >> 4) - arr[curByte++] = tmp & 0xFF - } - - if (placeHoldersLen === 1) { - tmp = - (revLookup[b64.charCodeAt(i)] << 10) | - (revLookup[b64.charCodeAt(i + 1)] << 4) | - (revLookup[b64.charCodeAt(i + 2)] >> 2) - arr[curByte++] = (tmp >> 8) & 0xFF - arr[curByte++] = tmp & 0xFF - } - - return arr -} - -function tripletToBase64 (num) { - return lookup[num >> 18 & 0x3F] + - lookup[num >> 12 & 0x3F] + - lookup[num >> 6 & 0x3F] + - lookup[num & 0x3F] -} - -function encodeChunk (uint8, start, end) { - var tmp - var output = [] - for (var i = start; i < end; i += 3) { - tmp = - ((uint8[i] << 16) & 0xFF0000) + - ((uint8[i + 1] << 8) & 0xFF00) + - (uint8[i + 2] & 0xFF) - output.push(tripletToBase64(tmp)) - } - return output.join('') -} - -function fromByteArray (uint8) { - var tmp - var len = uint8.length - var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes - var parts = [] - var maxChunkLength = 16383 // must be multiple of 3 - - // go through the array every three bytes, we'll deal with trailing stuff later - for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { - parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))) - } - - // pad the end with zeros, but make sure to not forget the extra bytes - if (extraBytes === 1) { - tmp = uint8[len - 1] - parts.push( - lookup[tmp >> 2] + - lookup[(tmp << 4) & 0x3F] + - '==' - ) - } else if (extraBytes === 2) { - tmp = (uint8[len - 2] << 8) + uint8[len - 1] - parts.push( - lookup[tmp >> 10] + - lookup[(tmp >> 4) & 0x3F] + - lookup[(tmp << 2) & 0x3F] + - '=' - ) - } - - return parts.join('') -} - -},{}],2:[function(require,module,exports){ - -},{}],3:[function(require,module,exports){ -(function (Buffer){(function (){ -/*! - * The buffer module from node.js, for the browser. - * - * @author Feross Aboukhadijeh - * @license MIT - */ -/* eslint-disable no-proto */ - -'use strict' - -var base64 = require('base64-js') -var ieee754 = require('ieee754') - -exports.Buffer = Buffer -exports.SlowBuffer = SlowBuffer -exports.INSPECT_MAX_BYTES = 50 - -var K_MAX_LENGTH = 0x7fffffff -exports.kMaxLength = K_MAX_LENGTH - -/** - * If `Buffer.TYPED_ARRAY_SUPPORT`: - * === true Use Uint8Array implementation (fastest) - * === false Print warning and recommend using `buffer` v4.x which has an Object - * implementation (most compatible, even IE6) - * - * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+, - * Opera 11.6+, iOS 4.2+. - * - * We report that the browser does not support typed arrays if the are not subclassable - * using __proto__. Firefox 4-29 lacks support for adding new properties to `Uint8Array` - * (See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438). IE 10 lacks support - * for __proto__ and has a buggy typed array implementation. - */ -Buffer.TYPED_ARRAY_SUPPORT = typedArraySupport() - -if (!Buffer.TYPED_ARRAY_SUPPORT && typeof console !== 'undefined' && - typeof console.error === 'function') { - console.error( - 'This browser lacks typed array (Uint8Array) support which is required by ' + - '`buffer` v5.x. Use `buffer` v4.x if you require old browser support.' - ) -} - -function typedArraySupport () { - // Can typed array instances can be augmented? - try { - var arr = new Uint8Array(1) - arr.__proto__ = { __proto__: Uint8Array.prototype, foo: function () { return 42 } } - return arr.foo() === 42 - } catch (e) { - return false - } -} - -Object.defineProperty(Buffer.prototype, 'parent', { - enumerable: true, - get: function () { - if (!Buffer.isBuffer(this)) return undefined - return this.buffer - } -}) - -Object.defineProperty(Buffer.prototype, 'offset', { - enumerable: true, - get: function () { - if (!Buffer.isBuffer(this)) return undefined - return this.byteOffset - } -}) - -function createBuffer (length) { - if (length > K_MAX_LENGTH) { - throw new RangeError('The value "' + length + '" is invalid for option "size"') - } - // Return an augmented `Uint8Array` instance - var buf = new Uint8Array(length) - buf.__proto__ = Buffer.prototype - return buf -} - -/** - * The Buffer constructor returns instances of `Uint8Array` that have their - * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of - * `Uint8Array`, so the returned instances will have all the node `Buffer` methods - * and the `Uint8Array` methods. Square bracket notation works as expected -- it - * returns a single octet. - * - * The `Uint8Array` prototype remains unmodified. - */ - -function Buffer (arg, encodingOrOffset, length) { - // Common case. - if (typeof arg === 'number') { - if (typeof encodingOrOffset === 'string') { - throw new TypeError( - 'The "string" argument must be of type string. Received type number' - ) - } - return allocUnsafe(arg) - } - return from(arg, encodingOrOffset, length) -} - -// Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97 -if (typeof Symbol !== 'undefined' && Symbol.species != null && - Buffer[Symbol.species] === Buffer) { - Object.defineProperty(Buffer, Symbol.species, { - value: null, - configurable: true, - enumerable: false, - writable: false - }) -} - -Buffer.poolSize = 8192 // not used by this implementation - -function from (value, encodingOrOffset, length) { - if (typeof value === 'string') { - return fromString(value, encodingOrOffset) - } - - if (ArrayBuffer.isView(value)) { - return fromArrayLike(value) - } - - if (value == null) { - throw TypeError( - 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + - 'or Array-like Object. Received type ' + (typeof value) - ) - } - - if (isInstance(value, ArrayBuffer) || - (value && isInstance(value.buffer, ArrayBuffer))) { - return fromArrayBuffer(value, encodingOrOffset, length) - } - - if (typeof value === 'number') { - throw new TypeError( - 'The "value" argument must not be of type number. Received type number' - ) - } - - var valueOf = value.valueOf && value.valueOf() - if (valueOf != null && valueOf !== value) { - return Buffer.from(valueOf, encodingOrOffset, length) - } - - var b = fromObject(value) - if (b) return b - - if (typeof Symbol !== 'undefined' && Symbol.toPrimitive != null && - typeof value[Symbol.toPrimitive] === 'function') { - return Buffer.from( - value[Symbol.toPrimitive]('string'), encodingOrOffset, length - ) - } - - throw new TypeError( - 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + - 'or Array-like Object. Received type ' + (typeof value) - ) -} - -/** - * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError - * if value is a number. - * Buffer.from(str[, encoding]) - * Buffer.from(array) - * Buffer.from(buffer) - * Buffer.from(arrayBuffer[, byteOffset[, length]]) - **/ -Buffer.from = function (value, encodingOrOffset, length) { - return from(value, encodingOrOffset, length) -} - -// Note: Change prototype *after* Buffer.from is defined to workaround Chrome bug: -// https://github.com/feross/buffer/pull/148 -Buffer.prototype.__proto__ = Uint8Array.prototype -Buffer.__proto__ = Uint8Array - -function assertSize (size) { - if (typeof size !== 'number') { - throw new TypeError('"size" argument must be of type number') - } else if (size < 0) { - throw new RangeError('The value "' + size + '" is invalid for option "size"') - } -} - -function alloc (size, fill, encoding) { - assertSize(size) - if (size <= 0) { - return createBuffer(size) - } - if (fill !== undefined) { - // Only pay attention to encoding if it's a string. This - // prevents accidentally sending in a number that would - // be interpretted as a start offset. - return typeof encoding === 'string' - ? createBuffer(size).fill(fill, encoding) - : createBuffer(size).fill(fill) - } - return createBuffer(size) -} - -/** - * Creates a new filled Buffer instance. - * alloc(size[, fill[, encoding]]) - **/ -Buffer.alloc = function (size, fill, encoding) { - return alloc(size, fill, encoding) -} - -function allocUnsafe (size) { - assertSize(size) - return createBuffer(size < 0 ? 0 : checked(size) | 0) -} - -/** - * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance. - * */ -Buffer.allocUnsafe = function (size) { - return allocUnsafe(size) -} -/** - * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance. - */ -Buffer.allocUnsafeSlow = function (size) { - return allocUnsafe(size) -} - -function fromString (string, encoding) { - if (typeof encoding !== 'string' || encoding === '') { - encoding = 'utf8' - } - - if (!Buffer.isEncoding(encoding)) { - throw new TypeError('Unknown encoding: ' + encoding) - } - - var length = byteLength(string, encoding) | 0 - var buf = createBuffer(length) - - var actual = buf.write(string, encoding) - - if (actual !== length) { - // Writing a hex string, for example, that contains invalid characters will - // cause everything after the first invalid character to be ignored. (e.g. - // 'abxxcd' will be treated as 'ab') - buf = buf.slice(0, actual) - } - - return buf -} - -function fromArrayLike (array) { - var length = array.length < 0 ? 0 : checked(array.length) | 0 - var buf = createBuffer(length) - for (var i = 0; i < length; i += 1) { - buf[i] = array[i] & 255 - } - return buf -} - -function fromArrayBuffer (array, byteOffset, length) { - if (byteOffset < 0 || array.byteLength < byteOffset) { - throw new RangeError('"offset" is outside of buffer bounds') - } - - if (array.byteLength < byteOffset + (length || 0)) { - throw new RangeError('"length" is outside of buffer bounds') - } - - var buf - if (byteOffset === undefined && length === undefined) { - buf = new Uint8Array(array) - } else if (length === undefined) { - buf = new Uint8Array(array, byteOffset) - } else { - buf = new Uint8Array(array, byteOffset, length) - } - - // Return an augmented `Uint8Array` instance - buf.__proto__ = Buffer.prototype - return buf -} - -function fromObject (obj) { - if (Buffer.isBuffer(obj)) { - var len = checked(obj.length) | 0 - var buf = createBuffer(len) - - if (buf.length === 0) { - return buf - } - - obj.copy(buf, 0, 0, len) - return buf - } - - if (obj.length !== undefined) { - if (typeof obj.length !== 'number' || numberIsNaN(obj.length)) { - return createBuffer(0) - } - return fromArrayLike(obj) - } - - if (obj.type === 'Buffer' && Array.isArray(obj.data)) { - return fromArrayLike(obj.data) - } -} - -function checked (length) { - // Note: cannot use `length < K_MAX_LENGTH` here because that fails when - // length is NaN (which is otherwise coerced to zero.) - if (length >= K_MAX_LENGTH) { - throw new RangeError('Attempt to allocate Buffer larger than maximum ' + - 'size: 0x' + K_MAX_LENGTH.toString(16) + ' bytes') - } - return length | 0 -} - -function SlowBuffer (length) { - if (+length != length) { // eslint-disable-line eqeqeq - length = 0 - } - return Buffer.alloc(+length) -} - -Buffer.isBuffer = function isBuffer (b) { - return b != null && b._isBuffer === true && - b !== Buffer.prototype // so Buffer.isBuffer(Buffer.prototype) will be false -} - -Buffer.compare = function compare (a, b) { - if (isInstance(a, Uint8Array)) a = Buffer.from(a, a.offset, a.byteLength) - if (isInstance(b, Uint8Array)) b = Buffer.from(b, b.offset, b.byteLength) - if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { - throw new TypeError( - 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' - ) - } - - if (a === b) return 0 - - var x = a.length - var y = b.length - - for (var i = 0, len = Math.min(x, y); i < len; ++i) { - if (a[i] !== b[i]) { - x = a[i] - y = b[i] - break - } - } - - if (x < y) return -1 - if (y < x) return 1 - return 0 -} - -Buffer.isEncoding = function isEncoding (encoding) { - switch (String(encoding).toLowerCase()) { - case 'hex': - case 'utf8': - case 'utf-8': - case 'ascii': - case 'latin1': - case 'binary': - case 'base64': - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return true - default: - return false - } -} - -Buffer.concat = function concat (list, length) { - if (!Array.isArray(list)) { - throw new TypeError('"list" argument must be an Array of Buffers') - } - - if (list.length === 0) { - return Buffer.alloc(0) - } - - var i - if (length === undefined) { - length = 0 - for (i = 0; i < list.length; ++i) { - length += list[i].length - } - } - - var buffer = Buffer.allocUnsafe(length) - var pos = 0 - for (i = 0; i < list.length; ++i) { - var buf = list[i] - if (isInstance(buf, Uint8Array)) { - buf = Buffer.from(buf) - } - if (!Buffer.isBuffer(buf)) { - throw new TypeError('"list" argument must be an Array of Buffers') - } - buf.copy(buffer, pos) - pos += buf.length - } - return buffer -} - -function byteLength (string, encoding) { - if (Buffer.isBuffer(string)) { - return string.length - } - if (ArrayBuffer.isView(string) || isInstance(string, ArrayBuffer)) { - return string.byteLength - } - if (typeof string !== 'string') { - throw new TypeError( - 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. ' + - 'Received type ' + typeof string - ) - } - - var len = string.length - var mustMatch = (arguments.length > 2 && arguments[2] === true) - if (!mustMatch && len === 0) return 0 - - // Use a for loop to avoid recursion - var loweredCase = false - for (;;) { - switch (encoding) { - case 'ascii': - case 'latin1': - case 'binary': - return len - case 'utf8': - case 'utf-8': - return utf8ToBytes(string).length - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return len * 2 - case 'hex': - return len >>> 1 - case 'base64': - return base64ToBytes(string).length - default: - if (loweredCase) { - return mustMatch ? -1 : utf8ToBytes(string).length // assume utf8 +(function () { + function r(e, n, t) { + function o(i, f) { + if (!n[i]) { + if (!e[i]) { + var c = 'function' == typeof require && require; + if (!f && c) return c(i, !0); + if (u) return u(i, !0); + var a = new Error("Cannot find module '" + i + "'"); + throw ((a.code = 'MODULE_NOT_FOUND'), a); } - encoding = ('' + encoding).toLowerCase() - loweredCase = true + var p = (n[i] = { exports: {} }); + e[i][0].call( + p.exports, + function (r) { + var n = e[i][1][r]; + return o(n || r); + }, + p, + p.exports, + r, + e, + n, + t, + ); + } + return n[i].exports; } - } -} -Buffer.byteLength = byteLength - -function slowToString (encoding, start, end) { - var loweredCase = false - - // No need to verify that "this.length <= MAX_UINT32" since it's a read-only - // property of a typed array. - - // This behaves neither like String nor Uint8Array in that we set start/end - // to their upper/lower bounds if the value passed is out of range. - // undefined is handled specially as per ECMA-262 6th Edition, - // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization. - if (start === undefined || start < 0) { - start = 0 - } - // Return early if start > this.length. Done here to prevent potential uint32 - // coercion fail below. - if (start > this.length) { - return '' - } - - if (end === undefined || end > this.length) { - end = this.length - } - - if (end <= 0) { - return '' - } - - // Force coersion to uint32. This will also coerce falsey/NaN values to 0. - end >>>= 0 - start >>>= 0 - - if (end <= start) { - return '' - } - - if (!encoding) encoding = 'utf8' - - while (true) { - switch (encoding) { - case 'hex': - return hexSlice(this, start, end) - - case 'utf8': - case 'utf-8': - return utf8Slice(this, start, end) - - case 'ascii': - return asciiSlice(this, start, end) - - case 'latin1': - case 'binary': - return latin1Slice(this, start, end) - - case 'base64': - return base64Slice(this, start, end) - - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return utf16leSlice(this, start, end) - - default: - if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) - encoding = (encoding + '').toLowerCase() - loweredCase = true - } - } -} - -// This property is used by `Buffer.isBuffer` (and the `is-buffer` npm package) -// to detect a Buffer instance. It's not possible to use `instanceof Buffer` -// reliably in a browserify context because there could be multiple different -// copies of the 'buffer' package in use. This method works even for Buffer -// instances that were created from another copy of the `buffer` package. -// See: https://github.com/feross/buffer/issues/154 -Buffer.prototype._isBuffer = true - -function swap (b, n, m) { - var i = b[n] - b[n] = b[m] - b[m] = i -} - -Buffer.prototype.swap16 = function swap16 () { - var len = this.length - if (len % 2 !== 0) { - throw new RangeError('Buffer size must be a multiple of 16-bits') - } - for (var i = 0; i < len; i += 2) { - swap(this, i, i + 1) - } - return this -} - -Buffer.prototype.swap32 = function swap32 () { - var len = this.length - if (len % 4 !== 0) { - throw new RangeError('Buffer size must be a multiple of 32-bits') - } - for (var i = 0; i < len; i += 4) { - swap(this, i, i + 3) - swap(this, i + 1, i + 2) - } - return this -} - -Buffer.prototype.swap64 = function swap64 () { - var len = this.length - if (len % 8 !== 0) { - throw new RangeError('Buffer size must be a multiple of 64-bits') - } - for (var i = 0; i < len; i += 8) { - swap(this, i, i + 7) - swap(this, i + 1, i + 6) - swap(this, i + 2, i + 5) - swap(this, i + 3, i + 4) - } - return this -} - -Buffer.prototype.toString = function toString () { - var length = this.length - if (length === 0) return '' - if (arguments.length === 0) return utf8Slice(this, 0, length) - return slowToString.apply(this, arguments) -} - -Buffer.prototype.toLocaleString = Buffer.prototype.toString - -Buffer.prototype.equals = function equals (b) { - if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer') - if (this === b) return true - return Buffer.compare(this, b) === 0 -} - -Buffer.prototype.inspect = function inspect () { - var str = '' - var max = exports.INSPECT_MAX_BYTES - str = this.toString('hex', 0, max).replace(/(.{2})/g, '$1 ').trim() - if (this.length > max) str += ' ... ' - return '' -} - -Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) { - if (isInstance(target, Uint8Array)) { - target = Buffer.from(target, target.offset, target.byteLength) - } - if (!Buffer.isBuffer(target)) { - throw new TypeError( - 'The "target" argument must be one of type Buffer or Uint8Array. ' + - 'Received type ' + (typeof target) + for ( + var u = 'function' == typeof require && require, i = 0; + i < t.length; + i++ ) + o(t[i]); + return o; } + return r; +})()( + { + 1: [ + function (require, module, exports) { + 'use strict'; - if (start === undefined) { - start = 0 - } - if (end === undefined) { - end = target ? target.length : 0 - } - if (thisStart === undefined) { - thisStart = 0 - } - if (thisEnd === undefined) { - thisEnd = this.length - } + exports.byteLength = byteLength; + exports.toByteArray = toByteArray; + exports.fromByteArray = fromByteArray; - if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { - throw new RangeError('out of range index') - } + var lookup = []; + var revLookup = []; + var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array; - if (thisStart >= thisEnd && start >= end) { - return 0 - } - if (thisStart >= thisEnd) { - return -1 - } - if (start >= end) { - return 1 - } - - start >>>= 0 - end >>>= 0 - thisStart >>>= 0 - thisEnd >>>= 0 - - if (this === target) return 0 - - var x = thisEnd - thisStart - var y = end - start - var len = Math.min(x, y) - - var thisCopy = this.slice(thisStart, thisEnd) - var targetCopy = target.slice(start, end) - - for (var i = 0; i < len; ++i) { - if (thisCopy[i] !== targetCopy[i]) { - x = thisCopy[i] - y = targetCopy[i] - break - } - } - - if (x < y) return -1 - if (y < x) return 1 - return 0 -} - -// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`, -// OR the last index of `val` in `buffer` at offset <= `byteOffset`. -// -// Arguments: -// - buffer - a Buffer to search -// - val - a string, Buffer, or number -// - byteOffset - an index into `buffer`; will be clamped to an int32 -// - encoding - an optional encoding, relevant is val is a string -// - dir - true for indexOf, false for lastIndexOf -function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) { - // Empty buffer means no match - if (buffer.length === 0) return -1 - - // Normalize byteOffset - if (typeof byteOffset === 'string') { - encoding = byteOffset - byteOffset = 0 - } else if (byteOffset > 0x7fffffff) { - byteOffset = 0x7fffffff - } else if (byteOffset < -0x80000000) { - byteOffset = -0x80000000 - } - byteOffset = +byteOffset // Coerce to Number. - if (numberIsNaN(byteOffset)) { - // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer - byteOffset = dir ? 0 : (buffer.length - 1) - } - - // Normalize byteOffset: negative offsets start from the end of the buffer - if (byteOffset < 0) byteOffset = buffer.length + byteOffset - if (byteOffset >= buffer.length) { - if (dir) return -1 - else byteOffset = buffer.length - 1 - } else if (byteOffset < 0) { - if (dir) byteOffset = 0 - else return -1 - } - - // Normalize val - if (typeof val === 'string') { - val = Buffer.from(val, encoding) - } - - // Finally, search either indexOf (if dir is true) or lastIndexOf - if (Buffer.isBuffer(val)) { - // Special case: looking for empty string/buffer always fails - if (val.length === 0) { - return -1 - } - return arrayIndexOf(buffer, val, byteOffset, encoding, dir) - } else if (typeof val === 'number') { - val = val & 0xFF // Search for a byte value [0-255] - if (typeof Uint8Array.prototype.indexOf === 'function') { - if (dir) { - return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset) - } else { - return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset) - } - } - return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir) - } - - throw new TypeError('val must be string, number or Buffer') -} - -function arrayIndexOf (arr, val, byteOffset, encoding, dir) { - var indexSize = 1 - var arrLength = arr.length - var valLength = val.length - - if (encoding !== undefined) { - encoding = String(encoding).toLowerCase() - if (encoding === 'ucs2' || encoding === 'ucs-2' || - encoding === 'utf16le' || encoding === 'utf-16le') { - if (arr.length < 2 || val.length < 2) { - return -1 - } - indexSize = 2 - arrLength /= 2 - valLength /= 2 - byteOffset /= 2 - } - } - - function read (buf, i) { - if (indexSize === 1) { - return buf[i] - } else { - return buf.readUInt16BE(i * indexSize) - } - } - - var i - if (dir) { - var foundIndex = -1 - for (i = byteOffset; i < arrLength; i++) { - if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { - if (foundIndex === -1) foundIndex = i - if (i - foundIndex + 1 === valLength) return foundIndex * indexSize - } else { - if (foundIndex !== -1) i -= i - foundIndex - foundIndex = -1 - } - } - } else { - if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength - for (i = byteOffset; i >= 0; i--) { - var found = true - for (var j = 0; j < valLength; j++) { - if (read(arr, i + j) !== read(val, j)) { - found = false - break + var code = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + for (var i = 0, len = code.length; i < len; ++i) { + lookup[i] = code[i]; + revLookup[code.charCodeAt(i)] = i; } - } - if (found) return i - } - } - return -1 -} + // Support decoding URL-safe base64 strings, as Node.js does. + // See: https://en.wikipedia.org/wiki/Base64#URL_applications + revLookup['-'.charCodeAt(0)] = 62; + revLookup['_'.charCodeAt(0)] = 63; -Buffer.prototype.includes = function includes (val, byteOffset, encoding) { - return this.indexOf(val, byteOffset, encoding) !== -1 -} + function getLens(b64) { + var len = b64.length; -Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, true) -} - -Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, false) -} - -function hexWrite (buf, string, offset, length) { - offset = Number(offset) || 0 - var remaining = buf.length - offset - if (!length) { - length = remaining - } else { - length = Number(length) - if (length > remaining) { - length = remaining - } - } - - var strLen = string.length - - if (length > strLen / 2) { - length = strLen / 2 - } - for (var i = 0; i < length; ++i) { - var parsed = parseInt(string.substr(i * 2, 2), 16) - if (numberIsNaN(parsed)) return i - buf[offset + i] = parsed - } - return i -} - -function utf8Write (buf, string, offset, length) { - return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length) -} - -function asciiWrite (buf, string, offset, length) { - return blitBuffer(asciiToBytes(string), buf, offset, length) -} - -function latin1Write (buf, string, offset, length) { - return asciiWrite(buf, string, offset, length) -} - -function base64Write (buf, string, offset, length) { - return blitBuffer(base64ToBytes(string), buf, offset, length) -} - -function ucs2Write (buf, string, offset, length) { - return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length) -} - -Buffer.prototype.write = function write (string, offset, length, encoding) { - // Buffer#write(string) - if (offset === undefined) { - encoding = 'utf8' - length = this.length - offset = 0 - // Buffer#write(string, encoding) - } else if (length === undefined && typeof offset === 'string') { - encoding = offset - length = this.length - offset = 0 - // Buffer#write(string, offset[, length][, encoding]) - } else if (isFinite(offset)) { - offset = offset >>> 0 - if (isFinite(length)) { - length = length >>> 0 - if (encoding === undefined) encoding = 'utf8' - } else { - encoding = length - length = undefined - } - } else { - throw new Error( - 'Buffer.write(string, encoding, offset[, length]) is no longer supported' - ) - } - - var remaining = this.length - offset - if (length === undefined || length > remaining) length = remaining - - if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) { - throw new RangeError('Attempt to write outside buffer bounds') - } - - if (!encoding) encoding = 'utf8' - - var loweredCase = false - for (;;) { - switch (encoding) { - case 'hex': - return hexWrite(this, string, offset, length) - - case 'utf8': - case 'utf-8': - return utf8Write(this, string, offset, length) - - case 'ascii': - return asciiWrite(this, string, offset, length) - - case 'latin1': - case 'binary': - return latin1Write(this, string, offset, length) - - case 'base64': - // Warning: maxLength not taken into account in base64Write - return base64Write(this, string, offset, length) - - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return ucs2Write(this, string, offset, length) - - default: - if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) - encoding = ('' + encoding).toLowerCase() - loweredCase = true - } - } -} - -Buffer.prototype.toJSON = function toJSON () { - return { - type: 'Buffer', - data: Array.prototype.slice.call(this._arr || this, 0) - } -} - -function base64Slice (buf, start, end) { - if (start === 0 && end === buf.length) { - return base64.fromByteArray(buf) - } else { - return base64.fromByteArray(buf.slice(start, end)) - } -} - -function utf8Slice (buf, start, end) { - end = Math.min(buf.length, end) - var res = [] - - var i = start - while (i < end) { - var firstByte = buf[i] - var codePoint = null - var bytesPerSequence = (firstByte > 0xEF) ? 4 - : (firstByte > 0xDF) ? 3 - : (firstByte > 0xBF) ? 2 - : 1 - - if (i + bytesPerSequence <= end) { - var secondByte, thirdByte, fourthByte, tempCodePoint - - switch (bytesPerSequence) { - case 1: - if (firstByte < 0x80) { - codePoint = firstByte + if (len % 4 > 0) { + throw new Error('Invalid string. Length must be a multiple of 4'); } - break - case 2: - secondByte = buf[i + 1] - if ((secondByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F) - if (tempCodePoint > 0x7F) { - codePoint = tempCodePoint + + // Trim off extra bytes after placeholder bytes are found + // See: https://github.com/beatgammit/base64-js/issues/42 + var validLen = b64.indexOf('='); + if (validLen === -1) validLen = len; + + var placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4); + + return [validLen, placeHoldersLen]; + } + + // base64 is 4/3 + up to two characters of the original data + function byteLength(b64) { + var lens = getLens(b64); + var validLen = lens[0]; + var placeHoldersLen = lens[1]; + return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; + } + + function _byteLength(b64, validLen, placeHoldersLen) { + return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; + } + + function toByteArray(b64) { + var tmp; + var lens = getLens(b64); + var validLen = lens[0]; + var placeHoldersLen = lens[1]; + + var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)); + + var curByte = 0; + + // if there are placeholders, only get up to the last complete 4 chars + var len = placeHoldersLen > 0 ? validLen - 4 : validLen; + + var i; + for (i = 0; i < len; i += 4) { + tmp = + (revLookup[b64.charCodeAt(i)] << 18) | + (revLookup[b64.charCodeAt(i + 1)] << 12) | + (revLookup[b64.charCodeAt(i + 2)] << 6) | + revLookup[b64.charCodeAt(i + 3)]; + arr[curByte++] = (tmp >> 16) & 0xff; + arr[curByte++] = (tmp >> 8) & 0xff; + arr[curByte++] = tmp & 0xff; + } + + if (placeHoldersLen === 2) { + tmp = + (revLookup[b64.charCodeAt(i)] << 2) | + (revLookup[b64.charCodeAt(i + 1)] >> 4); + arr[curByte++] = tmp & 0xff; + } + + if (placeHoldersLen === 1) { + tmp = + (revLookup[b64.charCodeAt(i)] << 10) | + (revLookup[b64.charCodeAt(i + 1)] << 4) | + (revLookup[b64.charCodeAt(i + 2)] >> 2); + arr[curByte++] = (tmp >> 8) & 0xff; + arr[curByte++] = tmp & 0xff; + } + + return arr; + } + + function tripletToBase64(num) { + return ( + lookup[(num >> 18) & 0x3f] + + lookup[(num >> 12) & 0x3f] + + lookup[(num >> 6) & 0x3f] + + lookup[num & 0x3f] + ); + } + + function encodeChunk(uint8, start, end) { + var tmp; + var output = []; + for (var i = start; i < end; i += 3) { + tmp = + ((uint8[i] << 16) & 0xff0000) + + ((uint8[i + 1] << 8) & 0xff00) + + (uint8[i + 2] & 0xff); + output.push(tripletToBase64(tmp)); + } + return output.join(''); + } + + function fromByteArray(uint8) { + var tmp; + var len = uint8.length; + var extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes + var parts = []; + var maxChunkLength = 16383; // must be multiple of 3 + + // go through the array every three bytes, we'll deal with trailing stuff later + for ( + var i = 0, len2 = len - extraBytes; + i < len2; + i += maxChunkLength + ) { + parts.push( + encodeChunk( + uint8, + i, + i + maxChunkLength > len2 ? len2 : i + maxChunkLength, + ), + ); + } + + // pad the end with zeros, but make sure to not forget the extra bytes + if (extraBytes === 1) { + tmp = uint8[len - 1]; + parts.push(lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f] + '=='); + } else if (extraBytes === 2) { + tmp = (uint8[len - 2] << 8) + uint8[len - 1]; + parts.push( + lookup[tmp >> 10] + + lookup[(tmp >> 4) & 0x3f] + + lookup[(tmp << 2) & 0x3f] + + '=', + ); + } + + return parts.join(''); + } + }, + {}, + ], + 2: [function (require, module, exports) {}, {}], + 3: [ + function (require, module, exports) { + (function (Buffer) { + (function () { + /*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh + * @license MIT + */ + /* eslint-disable no-proto */ + + 'use strict'; + + var base64 = require('base64-js'); + var ieee754 = require('ieee754'); + + exports.Buffer = Buffer; + exports.SlowBuffer = SlowBuffer; + exports.INSPECT_MAX_BYTES = 50; + + var K_MAX_LENGTH = 0x7fffffff; + exports.kMaxLength = K_MAX_LENGTH; + + /** + * If `Buffer.TYPED_ARRAY_SUPPORT`: + * === true Use Uint8Array implementation (fastest) + * === false Print warning and recommend using `buffer` v4.x which has an Object + * implementation (most compatible, even IE6) + * + * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+, + * Opera 11.6+, iOS 4.2+. + * + * We report that the browser does not support typed arrays if the are not subclassable + * using __proto__. Firefox 4-29 lacks support for adding new properties to `Uint8Array` + * (See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438). IE 10 lacks support + * for __proto__ and has a buggy typed array implementation. + */ + Buffer.TYPED_ARRAY_SUPPORT = typedArraySupport(); + + if ( + !Buffer.TYPED_ARRAY_SUPPORT && + typeof console !== 'undefined' && + typeof console.error === 'function' + ) { + console.error( + 'This browser lacks typed array (Uint8Array) support which is required by ' + + '`buffer` v5.x. Use `buffer` v4.x if you require old browser support.', + ); + } + + function typedArraySupport() { + // Can typed array instances can be augmented? + try { + var arr = new Uint8Array(1); + arr.__proto__ = { + __proto__: Uint8Array.prototype, + foo: function () { + return 42; + }, + }; + return arr.foo() === 42; + } catch (e) { + return false; + } + } + + Object.defineProperty(Buffer.prototype, 'parent', { + enumerable: true, + get: function () { + if (!Buffer.isBuffer(this)) return undefined; + return this.buffer; + }, + }); + + Object.defineProperty(Buffer.prototype, 'offset', { + enumerable: true, + get: function () { + if (!Buffer.isBuffer(this)) return undefined; + return this.byteOffset; + }, + }); + + function createBuffer(length) { + if (length > K_MAX_LENGTH) { + throw new RangeError( + 'The value "' + length + '" is invalid for option "size"', + ); + } + // Return an augmented `Uint8Array` instance + var buf = new Uint8Array(length); + buf.__proto__ = Buffer.prototype; + return buf; + } + + /** + * The Buffer constructor returns instances of `Uint8Array` that have their + * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of + * `Uint8Array`, so the returned instances will have all the node `Buffer` methods + * and the `Uint8Array` methods. Square bracket notation works as expected -- it + * returns a single octet. + * + * The `Uint8Array` prototype remains unmodified. + */ + + function Buffer(arg, encodingOrOffset, length) { + // Common case. + if (typeof arg === 'number') { + if (typeof encodingOrOffset === 'string') { + throw new TypeError( + 'The "string" argument must be of type string. Received type number', + ); + } + return allocUnsafe(arg); + } + return from(arg, encodingOrOffset, length); + } + + // Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97 + if ( + typeof Symbol !== 'undefined' && + Symbol.species != null && + Buffer[Symbol.species] === Buffer + ) { + Object.defineProperty(Buffer, Symbol.species, { + value: null, + configurable: true, + enumerable: false, + writable: false, + }); + } + + Buffer.poolSize = 8192; // not used by this implementation + + function from(value, encodingOrOffset, length) { + if (typeof value === 'string') { + return fromString(value, encodingOrOffset); + } + + if (ArrayBuffer.isView(value)) { + return fromArrayLike(value); + } + + if (value == null) { + throw TypeError( + 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + + 'or Array-like Object. Received type ' + + typeof value, + ); + } + + if ( + isInstance(value, ArrayBuffer) || + (value && isInstance(value.buffer, ArrayBuffer)) + ) { + return fromArrayBuffer(value, encodingOrOffset, length); + } + + if (typeof value === 'number') { + throw new TypeError( + 'The "value" argument must not be of type number. Received type number', + ); + } + + var valueOf = value.valueOf && value.valueOf(); + if (valueOf != null && valueOf !== value) { + return Buffer.from(valueOf, encodingOrOffset, length); + } + + var b = fromObject(value); + if (b) return b; + + if ( + typeof Symbol !== 'undefined' && + Symbol.toPrimitive != null && + typeof value[Symbol.toPrimitive] === 'function' + ) { + return Buffer.from( + value[Symbol.toPrimitive]('string'), + encodingOrOffset, + length, + ); + } + + throw new TypeError( + 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + + 'or Array-like Object. Received type ' + + typeof value, + ); + } + + /** + * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError + * if value is a number. + * Buffer.from(str[, encoding]) + * Buffer.from(array) + * Buffer.from(buffer) + * Buffer.from(arrayBuffer[, byteOffset[, length]]) + **/ + Buffer.from = function (value, encodingOrOffset, length) { + return from(value, encodingOrOffset, length); + }; + + // Note: Change prototype *after* Buffer.from is defined to workaround Chrome bug: + // https://github.com/feross/buffer/pull/148 + Buffer.prototype.__proto__ = Uint8Array.prototype; + Buffer.__proto__ = Uint8Array; + + function assertSize(size) { + if (typeof size !== 'number') { + throw new TypeError('"size" argument must be of type number'); + } else if (size < 0) { + throw new RangeError( + 'The value "' + size + '" is invalid for option "size"', + ); + } + } + + function alloc(size, fill, encoding) { + assertSize(size); + if (size <= 0) { + return createBuffer(size); + } + if (fill !== undefined) { + // Only pay attention to encoding if it's a string. This + // prevents accidentally sending in a number that would + // be interpretted as a start offset. + return typeof encoding === 'string' + ? createBuffer(size).fill(fill, encoding) + : createBuffer(size).fill(fill); + } + return createBuffer(size); + } + + /** + * Creates a new filled Buffer instance. + * alloc(size[, fill[, encoding]]) + **/ + Buffer.alloc = function (size, fill, encoding) { + return alloc(size, fill, encoding); + }; + + function allocUnsafe(size) { + assertSize(size); + return createBuffer(size < 0 ? 0 : checked(size) | 0); + } + + /** + * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance. + * */ + Buffer.allocUnsafe = function (size) { + return allocUnsafe(size); + }; + /** + * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance. + */ + Buffer.allocUnsafeSlow = function (size) { + return allocUnsafe(size); + }; + + function fromString(string, encoding) { + if (typeof encoding !== 'string' || encoding === '') { + encoding = 'utf8'; + } + + if (!Buffer.isEncoding(encoding)) { + throw new TypeError('Unknown encoding: ' + encoding); + } + + var length = byteLength(string, encoding) | 0; + var buf = createBuffer(length); + + var actual = buf.write(string, encoding); + + if (actual !== length) { + // Writing a hex string, for example, that contains invalid characters will + // cause everything after the first invalid character to be ignored. (e.g. + // 'abxxcd' will be treated as 'ab') + buf = buf.slice(0, actual); + } + + return buf; + } + + function fromArrayLike(array) { + var length = array.length < 0 ? 0 : checked(array.length) | 0; + var buf = createBuffer(length); + for (var i = 0; i < length; i += 1) { + buf[i] = array[i] & 255; + } + return buf; + } + + function fromArrayBuffer(array, byteOffset, length) { + if (byteOffset < 0 || array.byteLength < byteOffset) { + throw new RangeError('"offset" is outside of buffer bounds'); + } + + if (array.byteLength < byteOffset + (length || 0)) { + throw new RangeError('"length" is outside of buffer bounds'); + } + + var buf; + if (byteOffset === undefined && length === undefined) { + buf = new Uint8Array(array); + } else if (length === undefined) { + buf = new Uint8Array(array, byteOffset); + } else { + buf = new Uint8Array(array, byteOffset, length); + } + + // Return an augmented `Uint8Array` instance + buf.__proto__ = Buffer.prototype; + return buf; + } + + function fromObject(obj) { + if (Buffer.isBuffer(obj)) { + var len = checked(obj.length) | 0; + var buf = createBuffer(len); + + if (buf.length === 0) { + return buf; + } + + obj.copy(buf, 0, 0, len); + return buf; + } + + if (obj.length !== undefined) { + if (typeof obj.length !== 'number' || numberIsNaN(obj.length)) { + return createBuffer(0); + } + return fromArrayLike(obj); + } + + if (obj.type === 'Buffer' && Array.isArray(obj.data)) { + return fromArrayLike(obj.data); + } + } + + function checked(length) { + // Note: cannot use `length < K_MAX_LENGTH` here because that fails when + // length is NaN (which is otherwise coerced to zero.) + if (length >= K_MAX_LENGTH) { + throw new RangeError( + 'Attempt to allocate Buffer larger than maximum ' + + 'size: 0x' + + K_MAX_LENGTH.toString(16) + + ' bytes', + ); + } + return length | 0; + } + + function SlowBuffer(length) { + if (+length != length) { + // eslint-disable-line eqeqeq + length = 0; + } + return Buffer.alloc(+length); + } + + Buffer.isBuffer = function isBuffer(b) { + return ( + b != null && b._isBuffer === true && b !== Buffer.prototype + ); // so Buffer.isBuffer(Buffer.prototype) will be false + }; + + Buffer.compare = function compare(a, b) { + if (isInstance(a, Uint8Array)) + a = Buffer.from(a, a.offset, a.byteLength); + if (isInstance(b, Uint8Array)) + b = Buffer.from(b, b.offset, b.byteLength); + if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { + throw new TypeError( + 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array', + ); + } + + if (a === b) return 0; + + var x = a.length; + var y = b.length; + + for (var i = 0, len = Math.min(x, y); i < len; ++i) { + if (a[i] !== b[i]) { + x = a[i]; + y = b[i]; + break; + } + } + + if (x < y) return -1; + if (y < x) return 1; + return 0; + }; + + Buffer.isEncoding = function isEncoding(encoding) { + switch (String(encoding).toLowerCase()) { + case 'hex': + case 'utf8': + case 'utf-8': + case 'ascii': + case 'latin1': + case 'binary': + case 'base64': + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return true; + default: + return false; + } + }; + + Buffer.concat = function concat(list, length) { + if (!Array.isArray(list)) { + throw new TypeError( + '"list" argument must be an Array of Buffers', + ); + } + + if (list.length === 0) { + return Buffer.alloc(0); + } + + var i; + if (length === undefined) { + length = 0; + for (i = 0; i < list.length; ++i) { + length += list[i].length; + } + } + + var buffer = Buffer.allocUnsafe(length); + var pos = 0; + for (i = 0; i < list.length; ++i) { + var buf = list[i]; + if (isInstance(buf, Uint8Array)) { + buf = Buffer.from(buf); + } + if (!Buffer.isBuffer(buf)) { + throw new TypeError( + '"list" argument must be an Array of Buffers', + ); + } + buf.copy(buffer, pos); + pos += buf.length; + } + return buffer; + }; + + function byteLength(string, encoding) { + if (Buffer.isBuffer(string)) { + return string.length; + } + if ( + ArrayBuffer.isView(string) || + isInstance(string, ArrayBuffer) + ) { + return string.byteLength; + } + if (typeof string !== 'string') { + throw new TypeError( + 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. ' + + 'Received type ' + + typeof string, + ); + } + + var len = string.length; + var mustMatch = arguments.length > 2 && arguments[2] === true; + if (!mustMatch && len === 0) return 0; + + // Use a for loop to avoid recursion + var loweredCase = false; + for (;;) { + switch (encoding) { + case 'ascii': + case 'latin1': + case 'binary': + return len; + case 'utf8': + case 'utf-8': + return utf8ToBytes(string).length; + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return len * 2; + case 'hex': + return len >>> 1; + case 'base64': + return base64ToBytes(string).length; + default: + if (loweredCase) { + return mustMatch ? -1 : utf8ToBytes(string).length; // assume utf8 + } + encoding = ('' + encoding).toLowerCase(); + loweredCase = true; + } + } + } + Buffer.byteLength = byteLength; + + function slowToString(encoding, start, end) { + var loweredCase = false; + + // No need to verify that "this.length <= MAX_UINT32" since it's a read-only + // property of a typed array. + + // This behaves neither like String nor Uint8Array in that we set start/end + // to their upper/lower bounds if the value passed is out of range. + // undefined is handled specially as per ECMA-262 6th Edition, + // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization. + if (start === undefined || start < 0) { + start = 0; + } + // Return early if start > this.length. Done here to prevent potential uint32 + // coercion fail below. + if (start > this.length) { + return ''; + } + + if (end === undefined || end > this.length) { + end = this.length; + } + + if (end <= 0) { + return ''; + } + + // Force coersion to uint32. This will also coerce falsey/NaN values to 0. + end >>>= 0; + start >>>= 0; + + if (end <= start) { + return ''; + } + + if (!encoding) encoding = 'utf8'; + + while (true) { + switch (encoding) { + case 'hex': + return hexSlice(this, start, end); + + case 'utf8': + case 'utf-8': + return utf8Slice(this, start, end); + + case 'ascii': + return asciiSlice(this, start, end); + + case 'latin1': + case 'binary': + return latin1Slice(this, start, end); + + case 'base64': + return base64Slice(this, start, end); + + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return utf16leSlice(this, start, end); + + default: + if (loweredCase) + throw new TypeError('Unknown encoding: ' + encoding); + encoding = (encoding + '').toLowerCase(); + loweredCase = true; + } + } + } + + // This property is used by `Buffer.isBuffer` (and the `is-buffer` npm package) + // to detect a Buffer instance. It's not possible to use `instanceof Buffer` + // reliably in a browserify context because there could be multiple different + // copies of the 'buffer' package in use. This method works even for Buffer + // instances that were created from another copy of the `buffer` package. + // See: https://github.com/feross/buffer/issues/154 + Buffer.prototype._isBuffer = true; + + function swap(b, n, m) { + var i = b[n]; + b[n] = b[m]; + b[m] = i; + } + + Buffer.prototype.swap16 = function swap16() { + var len = this.length; + if (len % 2 !== 0) { + throw new RangeError( + 'Buffer size must be a multiple of 16-bits', + ); + } + for (var i = 0; i < len; i += 2) { + swap(this, i, i + 1); + } + return this; + }; + + Buffer.prototype.swap32 = function swap32() { + var len = this.length; + if (len % 4 !== 0) { + throw new RangeError( + 'Buffer size must be a multiple of 32-bits', + ); + } + for (var i = 0; i < len; i += 4) { + swap(this, i, i + 3); + swap(this, i + 1, i + 2); + } + return this; + }; + + Buffer.prototype.swap64 = function swap64() { + var len = this.length; + if (len % 8 !== 0) { + throw new RangeError( + 'Buffer size must be a multiple of 64-bits', + ); + } + for (var i = 0; i < len; i += 8) { + swap(this, i, i + 7); + swap(this, i + 1, i + 6); + swap(this, i + 2, i + 5); + swap(this, i + 3, i + 4); + } + return this; + }; + + Buffer.prototype.toString = function toString() { + var length = this.length; + if (length === 0) return ''; + if (arguments.length === 0) return utf8Slice(this, 0, length); + return slowToString.apply(this, arguments); + }; + + Buffer.prototype.toLocaleString = Buffer.prototype.toString; + + Buffer.prototype.equals = function equals(b) { + if (!Buffer.isBuffer(b)) + throw new TypeError('Argument must be a Buffer'); + if (this === b) return true; + return Buffer.compare(this, b) === 0; + }; + + Buffer.prototype.inspect = function inspect() { + var str = ''; + var max = exports.INSPECT_MAX_BYTES; + str = this.toString('hex', 0, max) + .replace(/(.{2})/g, '$1 ') + .trim(); + if (this.length > max) str += ' ... '; + return ''; + }; + + Buffer.prototype.compare = function compare( + target, + start, + end, + thisStart, + thisEnd, + ) { + if (isInstance(target, Uint8Array)) { + target = Buffer.from(target, target.offset, target.byteLength); + } + if (!Buffer.isBuffer(target)) { + throw new TypeError( + 'The "target" argument must be one of type Buffer or Uint8Array. ' + + 'Received type ' + + typeof target, + ); + } + + if (start === undefined) { + start = 0; + } + if (end === undefined) { + end = target ? target.length : 0; + } + if (thisStart === undefined) { + thisStart = 0; + } + if (thisEnd === undefined) { + thisEnd = this.length; + } + + if ( + start < 0 || + end > target.length || + thisStart < 0 || + thisEnd > this.length + ) { + throw new RangeError('out of range index'); + } + + if (thisStart >= thisEnd && start >= end) { + return 0; + } + if (thisStart >= thisEnd) { + return -1; + } + if (start >= end) { + return 1; + } + + start >>>= 0; + end >>>= 0; + thisStart >>>= 0; + thisEnd >>>= 0; + + if (this === target) return 0; + + var x = thisEnd - thisStart; + var y = end - start; + var len = Math.min(x, y); + + var thisCopy = this.slice(thisStart, thisEnd); + var targetCopy = target.slice(start, end); + + for (var i = 0; i < len; ++i) { + if (thisCopy[i] !== targetCopy[i]) { + x = thisCopy[i]; + y = targetCopy[i]; + break; + } + } + + if (x < y) return -1; + if (y < x) return 1; + return 0; + }; + + // Finds either the first index of `val` in `buffer` at offset >= `byteOffset`, + // OR the last index of `val` in `buffer` at offset <= `byteOffset`. + // + // Arguments: + // - buffer - a Buffer to search + // - val - a string, Buffer, or number + // - byteOffset - an index into `buffer`; will be clamped to an int32 + // - encoding - an optional encoding, relevant is val is a string + // - dir - true for indexOf, false for lastIndexOf + function bidirectionalIndexOf( + buffer, + val, + byteOffset, + encoding, + dir, + ) { + // Empty buffer means no match + if (buffer.length === 0) return -1; + + // Normalize byteOffset + if (typeof byteOffset === 'string') { + encoding = byteOffset; + byteOffset = 0; + } else if (byteOffset > 0x7fffffff) { + byteOffset = 0x7fffffff; + } else if (byteOffset < -0x80000000) { + byteOffset = -0x80000000; + } + byteOffset = +byteOffset; // Coerce to Number. + if (numberIsNaN(byteOffset)) { + // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer + byteOffset = dir ? 0 : buffer.length - 1; + } + + // Normalize byteOffset: negative offsets start from the end of the buffer + if (byteOffset < 0) byteOffset = buffer.length + byteOffset; + if (byteOffset >= buffer.length) { + if (dir) return -1; + else byteOffset = buffer.length - 1; + } else if (byteOffset < 0) { + if (dir) byteOffset = 0; + else return -1; + } + + // Normalize val + if (typeof val === 'string') { + val = Buffer.from(val, encoding); + } + + // Finally, search either indexOf (if dir is true) or lastIndexOf + if (Buffer.isBuffer(val)) { + // Special case: looking for empty string/buffer always fails + if (val.length === 0) { + return -1; + } + return arrayIndexOf(buffer, val, byteOffset, encoding, dir); + } else if (typeof val === 'number') { + val = val & 0xff; // Search for a byte value [0-255] + if (typeof Uint8Array.prototype.indexOf === 'function') { + if (dir) { + return Uint8Array.prototype.indexOf.call( + buffer, + val, + byteOffset, + ); + } else { + return Uint8Array.prototype.lastIndexOf.call( + buffer, + val, + byteOffset, + ); + } + } + return arrayIndexOf(buffer, [val], byteOffset, encoding, dir); + } + + throw new TypeError('val must be string, number or Buffer'); + } + + function arrayIndexOf(arr, val, byteOffset, encoding, dir) { + var indexSize = 1; + var arrLength = arr.length; + var valLength = val.length; + + if (encoding !== undefined) { + encoding = String(encoding).toLowerCase(); + if ( + encoding === 'ucs2' || + encoding === 'ucs-2' || + encoding === 'utf16le' || + encoding === 'utf-16le' + ) { + if (arr.length < 2 || val.length < 2) { + return -1; + } + indexSize = 2; + arrLength /= 2; + valLength /= 2; + byteOffset /= 2; + } + } + + function read(buf, i) { + if (indexSize === 1) { + return buf[i]; + } else { + return buf.readUInt16BE(i * indexSize); + } + } + + var i; + if (dir) { + var foundIndex = -1; + for (i = byteOffset; i < arrLength; i++) { + if ( + read(arr, i) === + read(val, foundIndex === -1 ? 0 : i - foundIndex) + ) { + if (foundIndex === -1) foundIndex = i; + if (i - foundIndex + 1 === valLength) + return foundIndex * indexSize; + } else { + if (foundIndex !== -1) i -= i - foundIndex; + foundIndex = -1; + } + } + } else { + if (byteOffset + valLength > arrLength) + byteOffset = arrLength - valLength; + for (i = byteOffset; i >= 0; i--) { + var found = true; + for (var j = 0; j < valLength; j++) { + if (read(arr, i + j) !== read(val, j)) { + found = false; + break; + } + } + if (found) return i; + } + } + + return -1; + } + + Buffer.prototype.includes = function includes( + val, + byteOffset, + encoding, + ) { + return this.indexOf(val, byteOffset, encoding) !== -1; + }; + + Buffer.prototype.indexOf = function indexOf( + val, + byteOffset, + encoding, + ) { + return bidirectionalIndexOf( + this, + val, + byteOffset, + encoding, + true, + ); + }; + + Buffer.prototype.lastIndexOf = function lastIndexOf( + val, + byteOffset, + encoding, + ) { + return bidirectionalIndexOf( + this, + val, + byteOffset, + encoding, + false, + ); + }; + + function hexWrite(buf, string, offset, length) { + offset = Number(offset) || 0; + var remaining = buf.length - offset; + if (!length) { + length = remaining; + } else { + length = Number(length); + if (length > remaining) { + length = remaining; + } + } + + var strLen = string.length; + + if (length > strLen / 2) { + length = strLen / 2; + } + for (var i = 0; i < length; ++i) { + var parsed = parseInt(string.substr(i * 2, 2), 16); + if (numberIsNaN(parsed)) return i; + buf[offset + i] = parsed; + } + return i; + } + + function utf8Write(buf, string, offset, length) { + return blitBuffer( + utf8ToBytes(string, buf.length - offset), + buf, + offset, + length, + ); + } + + function asciiWrite(buf, string, offset, length) { + return blitBuffer(asciiToBytes(string), buf, offset, length); + } + + function latin1Write(buf, string, offset, length) { + return asciiWrite(buf, string, offset, length); + } + + function base64Write(buf, string, offset, length) { + return blitBuffer(base64ToBytes(string), buf, offset, length); + } + + function ucs2Write(buf, string, offset, length) { + return blitBuffer( + utf16leToBytes(string, buf.length - offset), + buf, + offset, + length, + ); + } + + Buffer.prototype.write = function write( + string, + offset, + length, + encoding, + ) { + // Buffer#write(string) + if (offset === undefined) { + encoding = 'utf8'; + length = this.length; + offset = 0; + // Buffer#write(string, encoding) + } else if (length === undefined && typeof offset === 'string') { + encoding = offset; + length = this.length; + offset = 0; + // Buffer#write(string, offset[, length][, encoding]) + } else if (isFinite(offset)) { + offset = offset >>> 0; + if (isFinite(length)) { + length = length >>> 0; + if (encoding === undefined) encoding = 'utf8'; + } else { + encoding = length; + length = undefined; + } + } else { + throw new Error( + 'Buffer.write(string, encoding, offset[, length]) is no longer supported', + ); + } + + var remaining = this.length - offset; + if (length === undefined || length > remaining) + length = remaining; + + if ( + (string.length > 0 && (length < 0 || offset < 0)) || + offset > this.length + ) { + throw new RangeError('Attempt to write outside buffer bounds'); + } + + if (!encoding) encoding = 'utf8'; + + var loweredCase = false; + for (;;) { + switch (encoding) { + case 'hex': + return hexWrite(this, string, offset, length); + + case 'utf8': + case 'utf-8': + return utf8Write(this, string, offset, length); + + case 'ascii': + return asciiWrite(this, string, offset, length); + + case 'latin1': + case 'binary': + return latin1Write(this, string, offset, length); + + case 'base64': + // Warning: maxLength not taken into account in base64Write + return base64Write(this, string, offset, length); + + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return ucs2Write(this, string, offset, length); + + default: + if (loweredCase) + throw new TypeError('Unknown encoding: ' + encoding); + encoding = ('' + encoding).toLowerCase(); + loweredCase = true; + } + } + }; + + Buffer.prototype.toJSON = function toJSON() { + return { + type: 'Buffer', + data: Array.prototype.slice.call(this._arr || this, 0), + }; + }; + + function base64Slice(buf, start, end) { + if (start === 0 && end === buf.length) { + return base64.fromByteArray(buf); + } else { + return base64.fromByteArray(buf.slice(start, end)); + } + } + + function utf8Slice(buf, start, end) { + end = Math.min(buf.length, end); + var res = []; + + var i = start; + while (i < end) { + var firstByte = buf[i]; + var codePoint = null; + var bytesPerSequence = + firstByte > 0xef + ? 4 + : firstByte > 0xdf + ? 3 + : firstByte > 0xbf + ? 2 + : 1; + + if (i + bytesPerSequence <= end) { + var secondByte, thirdByte, fourthByte, tempCodePoint; + + switch (bytesPerSequence) { + case 1: + if (firstByte < 0x80) { + codePoint = firstByte; + } + break; + case 2: + secondByte = buf[i + 1]; + if ((secondByte & 0xc0) === 0x80) { + tempCodePoint = + ((firstByte & 0x1f) << 0x6) | (secondByte & 0x3f); + if (tempCodePoint > 0x7f) { + codePoint = tempCodePoint; + } + } + break; + case 3: + secondByte = buf[i + 1]; + thirdByte = buf[i + 2]; + if ( + (secondByte & 0xc0) === 0x80 && + (thirdByte & 0xc0) === 0x80 + ) { + tempCodePoint = + ((firstByte & 0xf) << 0xc) | + ((secondByte & 0x3f) << 0x6) | + (thirdByte & 0x3f); + if ( + tempCodePoint > 0x7ff && + (tempCodePoint < 0xd800 || tempCodePoint > 0xdfff) + ) { + codePoint = tempCodePoint; + } + } + break; + case 4: + secondByte = buf[i + 1]; + thirdByte = buf[i + 2]; + fourthByte = buf[i + 3]; + if ( + (secondByte & 0xc0) === 0x80 && + (thirdByte & 0xc0) === 0x80 && + (fourthByte & 0xc0) === 0x80 + ) { + tempCodePoint = + ((firstByte & 0xf) << 0x12) | + ((secondByte & 0x3f) << 0xc) | + ((thirdByte & 0x3f) << 0x6) | + (fourthByte & 0x3f); + if ( + tempCodePoint > 0xffff && + tempCodePoint < 0x110000 + ) { + codePoint = tempCodePoint; + } + } + } + } + + if (codePoint === null) { + // we did not generate a valid codePoint so insert a + // replacement char (U+FFFD) and advance only 1 byte + codePoint = 0xfffd; + bytesPerSequence = 1; + } else if (codePoint > 0xffff) { + // encode to utf16 (surrogate pair dance) + codePoint -= 0x10000; + res.push(((codePoint >>> 10) & 0x3ff) | 0xd800); + codePoint = 0xdc00 | (codePoint & 0x3ff); + } + + res.push(codePoint); + i += bytesPerSequence; + } + + return decodeCodePointsArray(res); + } + + // Based on http://stackoverflow.com/a/22747272/680742, the browser with + // the lowest limit is Chrome, with 0x10000 args. + // We go 1 magnitude less, for safety + var MAX_ARGUMENTS_LENGTH = 0x1000; + + function decodeCodePointsArray(codePoints) { + var len = codePoints.length; + if (len <= MAX_ARGUMENTS_LENGTH) { + return String.fromCharCode.apply(String, codePoints); // avoid extra slice() + } + + // Decode in chunks to avoid "call stack size exceeded". + var res = ''; + var i = 0; + while (i < len) { + res += String.fromCharCode.apply( + String, + codePoints.slice(i, (i += MAX_ARGUMENTS_LENGTH)), + ); + } + return res; + } + + function asciiSlice(buf, start, end) { + var ret = ''; + end = Math.min(buf.length, end); + + for (var i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i] & 0x7f); + } + return ret; + } + + function latin1Slice(buf, start, end) { + var ret = ''; + end = Math.min(buf.length, end); + + for (var i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i]); + } + return ret; + } + + function hexSlice(buf, start, end) { + var len = buf.length; + + if (!start || start < 0) start = 0; + if (!end || end < 0 || end > len) end = len; + + var out = ''; + for (var i = start; i < end; ++i) { + out += toHex(buf[i]); + } + return out; + } + + function utf16leSlice(buf, start, end) { + var bytes = buf.slice(start, end); + var res = ''; + for (var i = 0; i < bytes.length; i += 2) { + res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256); + } + return res; + } + + Buffer.prototype.slice = function slice(start, end) { + var len = this.length; + start = ~~start; + end = end === undefined ? len : ~~end; + + if (start < 0) { + start += len; + if (start < 0) start = 0; + } else if (start > len) { + start = len; + } + + if (end < 0) { + end += len; + if (end < 0) end = 0; + } else if (end > len) { + end = len; + } + + if (end < start) end = start; + + var newBuf = this.subarray(start, end); + // Return an augmented `Uint8Array` instance + newBuf.__proto__ = Buffer.prototype; + return newBuf; + }; + + /* + * Need to make sure that buffer isn't trying to write out of bounds. + */ + function checkOffset(offset, ext, length) { + if (offset % 1 !== 0 || offset < 0) + throw new RangeError('offset is not uint'); + if (offset + ext > length) + throw new RangeError('Trying to access beyond buffer length'); + } + + Buffer.prototype.readUIntLE = function readUIntLE( + offset, + byteLength, + noAssert, + ) { + offset = offset >>> 0; + byteLength = byteLength >>> 0; + if (!noAssert) checkOffset(offset, byteLength, this.length); + + var val = this[offset]; + var mul = 1; + var i = 0; + while (++i < byteLength && (mul *= 0x100)) { + val += this[offset + i] * mul; + } + + return val; + }; + + Buffer.prototype.readUIntBE = function readUIntBE( + offset, + byteLength, + noAssert, + ) { + offset = offset >>> 0; + byteLength = byteLength >>> 0; + if (!noAssert) { + checkOffset(offset, byteLength, this.length); + } + + var val = this[offset + --byteLength]; + var mul = 1; + while (byteLength > 0 && (mul *= 0x100)) { + val += this[offset + --byteLength] * mul; + } + + return val; + }; + + Buffer.prototype.readUInt8 = function readUInt8(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 1, this.length); + return this[offset]; + }; + + Buffer.prototype.readUInt16LE = function readUInt16LE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 2, this.length); + return this[offset] | (this[offset + 1] << 8); + }; + + Buffer.prototype.readUInt16BE = function readUInt16BE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 2, this.length); + return (this[offset] << 8) | this[offset + 1]; + }; + + Buffer.prototype.readUInt32LE = function readUInt32LE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + + return ( + (this[offset] | + (this[offset + 1] << 8) | + (this[offset + 2] << 16)) + + this[offset + 3] * 0x1000000 + ); + }; + + Buffer.prototype.readUInt32BE = function readUInt32BE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + + return ( + this[offset] * 0x1000000 + + ((this[offset + 1] << 16) | + (this[offset + 2] << 8) | + this[offset + 3]) + ); + }; + + Buffer.prototype.readIntLE = function readIntLE( + offset, + byteLength, + noAssert, + ) { + offset = offset >>> 0; + byteLength = byteLength >>> 0; + if (!noAssert) checkOffset(offset, byteLength, this.length); + + var val = this[offset]; + var mul = 1; + var i = 0; + while (++i < byteLength && (mul *= 0x100)) { + val += this[offset + i] * mul; + } + mul *= 0x80; + + if (val >= mul) val -= Math.pow(2, 8 * byteLength); + + return val; + }; + + Buffer.prototype.readIntBE = function readIntBE( + offset, + byteLength, + noAssert, + ) { + offset = offset >>> 0; + byteLength = byteLength >>> 0; + if (!noAssert) checkOffset(offset, byteLength, this.length); + + var i = byteLength; + var mul = 1; + var val = this[offset + --i]; + while (i > 0 && (mul *= 0x100)) { + val += this[offset + --i] * mul; + } + mul *= 0x80; + + if (val >= mul) val -= Math.pow(2, 8 * byteLength); + + return val; + }; + + Buffer.prototype.readInt8 = function readInt8(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 1, this.length); + if (!(this[offset] & 0x80)) return this[offset]; + return (0xff - this[offset] + 1) * -1; + }; + + Buffer.prototype.readInt16LE = function readInt16LE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 2, this.length); + var val = this[offset] | (this[offset + 1] << 8); + return val & 0x8000 ? val | 0xffff0000 : val; + }; + + Buffer.prototype.readInt16BE = function readInt16BE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 2, this.length); + var val = this[offset + 1] | (this[offset] << 8); + return val & 0x8000 ? val | 0xffff0000 : val; + }; + + Buffer.prototype.readInt32LE = function readInt32LE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + + return ( + this[offset] | + (this[offset + 1] << 8) | + (this[offset + 2] << 16) | + (this[offset + 3] << 24) + ); + }; + + Buffer.prototype.readInt32BE = function readInt32BE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + + return ( + (this[offset] << 24) | + (this[offset + 1] << 16) | + (this[offset + 2] << 8) | + this[offset + 3] + ); + }; + + Buffer.prototype.readFloatLE = function readFloatLE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + return ieee754.read(this, offset, true, 23, 4); + }; + + Buffer.prototype.readFloatBE = function readFloatBE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + return ieee754.read(this, offset, false, 23, 4); + }; + + Buffer.prototype.readDoubleLE = function readDoubleLE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 8, this.length); + return ieee754.read(this, offset, true, 52, 8); + }; + + Buffer.prototype.readDoubleBE = function readDoubleBE( + offset, + noAssert, + ) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 8, this.length); + return ieee754.read(this, offset, false, 52, 8); + }; + + function checkInt(buf, value, offset, ext, max, min) { + if (!Buffer.isBuffer(buf)) + throw new TypeError( + '"buffer" argument must be a Buffer instance', + ); + if (value > max || value < min) + throw new RangeError('"value" argument is out of bounds'); + if (offset + ext > buf.length) + throw new RangeError('Index out of range'); + } + + Buffer.prototype.writeUIntLE = function writeUIntLE( + value, + offset, + byteLength, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + byteLength = byteLength >>> 0; + if (!noAssert) { + var maxBytes = Math.pow(2, 8 * byteLength) - 1; + checkInt(this, value, offset, byteLength, maxBytes, 0); + } + + var mul = 1; + var i = 0; + this[offset] = value & 0xff; + while (++i < byteLength && (mul *= 0x100)) { + this[offset + i] = (value / mul) & 0xff; + } + + return offset + byteLength; + }; + + Buffer.prototype.writeUIntBE = function writeUIntBE( + value, + offset, + byteLength, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + byteLength = byteLength >>> 0; + if (!noAssert) { + var maxBytes = Math.pow(2, 8 * byteLength) - 1; + checkInt(this, value, offset, byteLength, maxBytes, 0); + } + + var i = byteLength - 1; + var mul = 1; + this[offset + i] = value & 0xff; + while (--i >= 0 && (mul *= 0x100)) { + this[offset + i] = (value / mul) & 0xff; + } + + return offset + byteLength; + }; + + Buffer.prototype.writeUInt8 = function writeUInt8( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0); + this[offset] = value & 0xff; + return offset + 1; + }; + + Buffer.prototype.writeUInt16LE = function writeUInt16LE( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0); + this[offset] = value & 0xff; + this[offset + 1] = value >>> 8; + return offset + 2; + }; + + Buffer.prototype.writeUInt16BE = function writeUInt16BE( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0); + this[offset] = value >>> 8; + this[offset + 1] = value & 0xff; + return offset + 2; + }; + + Buffer.prototype.writeUInt32LE = function writeUInt32LE( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0); + this[offset + 3] = value >>> 24; + this[offset + 2] = value >>> 16; + this[offset + 1] = value >>> 8; + this[offset] = value & 0xff; + return offset + 4; + }; + + Buffer.prototype.writeUInt32BE = function writeUInt32BE( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0); + this[offset] = value >>> 24; + this[offset + 1] = value >>> 16; + this[offset + 2] = value >>> 8; + this[offset + 3] = value & 0xff; + return offset + 4; + }; + + Buffer.prototype.writeIntLE = function writeIntLE( + value, + offset, + byteLength, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) { + var limit = Math.pow(2, 8 * byteLength - 1); + + checkInt(this, value, offset, byteLength, limit - 1, -limit); + } + + var i = 0; + var mul = 1; + var sub = 0; + this[offset] = value & 0xff; + while (++i < byteLength && (mul *= 0x100)) { + if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { + sub = 1; + } + this[offset + i] = (((value / mul) >> 0) - sub) & 0xff; + } + + return offset + byteLength; + }; + + Buffer.prototype.writeIntBE = function writeIntBE( + value, + offset, + byteLength, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) { + var limit = Math.pow(2, 8 * byteLength - 1); + + checkInt(this, value, offset, byteLength, limit - 1, -limit); + } + + var i = byteLength - 1; + var mul = 1; + var sub = 0; + this[offset + i] = value & 0xff; + while (--i >= 0 && (mul *= 0x100)) { + if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { + sub = 1; + } + this[offset + i] = (((value / mul) >> 0) - sub) & 0xff; + } + + return offset + byteLength; + }; + + Buffer.prototype.writeInt8 = function writeInt8( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80); + if (value < 0) value = 0xff + value + 1; + this[offset] = value & 0xff; + return offset + 1; + }; + + Buffer.prototype.writeInt16LE = function writeInt16LE( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000); + this[offset] = value & 0xff; + this[offset + 1] = value >>> 8; + return offset + 2; + }; + + Buffer.prototype.writeInt16BE = function writeInt16BE( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000); + this[offset] = value >>> 8; + this[offset + 1] = value & 0xff; + return offset + 2; + }; + + Buffer.prototype.writeInt32LE = function writeInt32LE( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) + checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000); + this[offset] = value & 0xff; + this[offset + 1] = value >>> 8; + this[offset + 2] = value >>> 16; + this[offset + 3] = value >>> 24; + return offset + 4; + }; + + Buffer.prototype.writeInt32BE = function writeInt32BE( + value, + offset, + noAssert, + ) { + value = +value; + offset = offset >>> 0; + if (!noAssert) + checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000); + if (value < 0) value = 0xffffffff + value + 1; + this[offset] = value >>> 24; + this[offset + 1] = value >>> 16; + this[offset + 2] = value >>> 8; + this[offset + 3] = value & 0xff; + return offset + 4; + }; + + function checkIEEE754(buf, value, offset, ext, max, min) { + if (offset + ext > buf.length) + throw new RangeError('Index out of range'); + if (offset < 0) throw new RangeError('Index out of range'); + } + + function writeFloat(buf, value, offset, littleEndian, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) { + checkIEEE754( + buf, + value, + offset, + 4, + 3.4028234663852886e38, + -3.4028234663852886e38, + ); + } + ieee754.write(buf, value, offset, littleEndian, 23, 4); + return offset + 4; + } + + Buffer.prototype.writeFloatLE = function writeFloatLE( + value, + offset, + noAssert, + ) { + return writeFloat(this, value, offset, true, noAssert); + }; + + Buffer.prototype.writeFloatBE = function writeFloatBE( + value, + offset, + noAssert, + ) { + return writeFloat(this, value, offset, false, noAssert); + }; + + function writeDouble(buf, value, offset, littleEndian, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) { + checkIEEE754( + buf, + value, + offset, + 8, + 1.7976931348623157e308, + -1.7976931348623157e308, + ); + } + ieee754.write(buf, value, offset, littleEndian, 52, 8); + return offset + 8; + } + + Buffer.prototype.writeDoubleLE = function writeDoubleLE( + value, + offset, + noAssert, + ) { + return writeDouble(this, value, offset, true, noAssert); + }; + + Buffer.prototype.writeDoubleBE = function writeDoubleBE( + value, + offset, + noAssert, + ) { + return writeDouble(this, value, offset, false, noAssert); + }; + + // copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) + Buffer.prototype.copy = function copy( + target, + targetStart, + start, + end, + ) { + if (!Buffer.isBuffer(target)) + throw new TypeError('argument should be a Buffer'); + if (!start) start = 0; + if (!end && end !== 0) end = this.length; + if (targetStart >= target.length) targetStart = target.length; + if (!targetStart) targetStart = 0; + if (end > 0 && end < start) end = start; + + // Copy 0 bytes; we're done + if (end === start) return 0; + if (target.length === 0 || this.length === 0) return 0; + + // Fatal error conditions + if (targetStart < 0) { + throw new RangeError('targetStart out of bounds'); + } + if (start < 0 || start >= this.length) + throw new RangeError('Index out of range'); + if (end < 0) throw new RangeError('sourceEnd out of bounds'); + + // Are we oob? + if (end > this.length) end = this.length; + if (target.length - targetStart < end - start) { + end = target.length - targetStart + start; + } + + var len = end - start; + + if ( + this === target && + typeof Uint8Array.prototype.copyWithin === 'function' + ) { + // Use built-in when available, missing from IE11 + this.copyWithin(targetStart, start, end); + } else if ( + this === target && + start < targetStart && + targetStart < end + ) { + // descending copy from end + for (var i = len - 1; i >= 0; --i) { + target[i + targetStart] = this[i + start]; + } + } else { + Uint8Array.prototype.set.call( + target, + this.subarray(start, end), + targetStart, + ); + } + + return len; + }; + + // Usage: + // buffer.fill(number[, offset[, end]]) + // buffer.fill(buffer[, offset[, end]]) + // buffer.fill(string[, offset[, end]][, encoding]) + Buffer.prototype.fill = function fill(val, start, end, encoding) { + // Handle string cases: + if (typeof val === 'string') { + if (typeof start === 'string') { + encoding = start; + start = 0; + end = this.length; + } else if (typeof end === 'string') { + encoding = end; + end = this.length; + } + if (encoding !== undefined && typeof encoding !== 'string') { + throw new TypeError('encoding must be a string'); + } + if ( + typeof encoding === 'string' && + !Buffer.isEncoding(encoding) + ) { + throw new TypeError('Unknown encoding: ' + encoding); + } + if (val.length === 1) { + var code = val.charCodeAt(0); + if ( + (encoding === 'utf8' && code < 128) || + encoding === 'latin1' + ) { + // Fast path: If `val` fits into a single byte, use that numeric value. + val = code; + } + } + } else if (typeof val === 'number') { + val = val & 255; + } + + // Invalid ranges are not set to a default, so can range check early. + if (start < 0 || this.length < start || this.length < end) { + throw new RangeError('Out of range index'); + } + + if (end <= start) { + return this; + } + + start = start >>> 0; + end = end === undefined ? this.length : end >>> 0; + + if (!val) val = 0; + + var i; + if (typeof val === 'number') { + for (i = start; i < end; ++i) { + this[i] = val; + } + } else { + var bytes = Buffer.isBuffer(val) + ? val + : Buffer.from(val, encoding); + var len = bytes.length; + if (len === 0) { + throw new TypeError( + 'The value "' + val + '" is invalid for argument "value"', + ); + } + for (i = 0; i < end - start; ++i) { + this[i + start] = bytes[i % len]; + } + } + + return this; + }; + + // HELPER FUNCTIONS + // ================ + + var INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g; + + function base64clean(str) { + // Node takes equal signs as end of the Base64 encoding + str = str.split('=')[0]; + // Node strips out invalid characters like \n and \t from the string, base64-js does not + str = str.trim().replace(INVALID_BASE64_RE, ''); + // Node converts strings with length < 2 to '' + if (str.length < 2) return ''; + // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not + while (str.length % 4 !== 0) { + str = str + '='; + } + return str; + } + + function toHex(n) { + if (n < 16) return '0' + n.toString(16); + return n.toString(16); + } + + function utf8ToBytes(string, units) { + units = units || Infinity; + var codePoint; + var length = string.length; + var leadSurrogate = null; + var bytes = []; + + for (var i = 0; i < length; ++i) { + codePoint = string.charCodeAt(i); + + // is surrogate component + if (codePoint > 0xd7ff && codePoint < 0xe000) { + // last char was a lead + if (!leadSurrogate) { + // no lead yet + if (codePoint > 0xdbff) { + // unexpected trail + if ((units -= 3) > -1) bytes.push(0xef, 0xbf, 0xbd); + continue; + } else if (i + 1 === length) { + // unpaired lead + if ((units -= 3) > -1) bytes.push(0xef, 0xbf, 0xbd); + continue; + } + + // valid lead + leadSurrogate = codePoint; + + continue; + } + + // 2 leads in a row + if (codePoint < 0xdc00) { + if ((units -= 3) > -1) bytes.push(0xef, 0xbf, 0xbd); + leadSurrogate = codePoint; + continue; + } + + // valid surrogate pair + codePoint = + (((leadSurrogate - 0xd800) << 10) | (codePoint - 0xdc00)) + + 0x10000; + } else if (leadSurrogate) { + // valid bmp char, but last char was a lead + if ((units -= 3) > -1) bytes.push(0xef, 0xbf, 0xbd); + } + + leadSurrogate = null; + + // encode utf8 + if (codePoint < 0x80) { + if ((units -= 1) < 0) break; + bytes.push(codePoint); + } else if (codePoint < 0x800) { + if ((units -= 2) < 0) break; + bytes.push( + (codePoint >> 0x6) | 0xc0, + (codePoint & 0x3f) | 0x80, + ); + } else if (codePoint < 0x10000) { + if ((units -= 3) < 0) break; + bytes.push( + (codePoint >> 0xc) | 0xe0, + ((codePoint >> 0x6) & 0x3f) | 0x80, + (codePoint & 0x3f) | 0x80, + ); + } else if (codePoint < 0x110000) { + if ((units -= 4) < 0) break; + bytes.push( + (codePoint >> 0x12) | 0xf0, + ((codePoint >> 0xc) & 0x3f) | 0x80, + ((codePoint >> 0x6) & 0x3f) | 0x80, + (codePoint & 0x3f) | 0x80, + ); + } else { + throw new Error('Invalid code point'); + } + } + + return bytes; + } + + function asciiToBytes(str) { + var byteArray = []; + for (var i = 0; i < str.length; ++i) { + // Node's code seems to be doing this and not & 0x7F.. + byteArray.push(str.charCodeAt(i) & 0xff); + } + return byteArray; + } + + function utf16leToBytes(str, units) { + var c, hi, lo; + var byteArray = []; + for (var i = 0; i < str.length; ++i) { + if ((units -= 2) < 0) break; + + c = str.charCodeAt(i); + hi = c >> 8; + lo = c % 256; + byteArray.push(lo); + byteArray.push(hi); + } + + return byteArray; + } + + function base64ToBytes(str) { + return base64.toByteArray(base64clean(str)); + } + + function blitBuffer(src, dst, offset, length) { + for (var i = 0; i < length; ++i) { + if (i + offset >= dst.length || i >= src.length) break; + dst[i + offset] = src[i]; + } + return i; + } + + // ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass + // the `instanceof` check but they should be treated as of that type. + // See: https://github.com/feross/buffer/issues/166 + function isInstance(obj, type) { + return ( + obj instanceof type || + (obj != null && + obj.constructor != null && + obj.constructor.name != null && + obj.constructor.name === type.name) + ); + } + function numberIsNaN(obj) { + // For IE11 support + return obj !== obj; // eslint-disable-line no-self-compare + } + }).call(this); + }).call(this, require('buffer').Buffer); + }, + { 'base64-js': 1, 'buffer': 3, 'ieee754': 30 }, + ], + 4: [ + function (require, module, exports) { + 'use strict'; + + var isMergeableObject = function isMergeableObject(value) { + return isNonNullObject(value) && !isSpecial(value); + }; + + function isNonNullObject(value) { + return !!value && typeof value === 'object'; + } + + function isSpecial(value) { + var stringValue = Object.prototype.toString.call(value); + + return ( + stringValue === '[object RegExp]' || + stringValue === '[object Date]' || + isReactElement(value) + ); + } + + // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 + var canUseSymbol = typeof Symbol === 'function' && Symbol.for; + var REACT_ELEMENT_TYPE = canUseSymbol + ? Symbol.for('react.element') + : 0xeac7; + + function isReactElement(value) { + return value.$$typeof === REACT_ELEMENT_TYPE; + } + + function emptyTarget(val) { + return Array.isArray(val) ? [] : {}; + } + + function cloneUnlessOtherwiseSpecified(value, options) { + return options.clone !== false && options.isMergeableObject(value) + ? deepmerge(emptyTarget(value), value, options) + : value; + } + + function defaultArrayMerge(target, source, options) { + return target.concat(source).map(function (element) { + return cloneUnlessOtherwiseSpecified(element, options); + }); + } + + function getMergeFunction(key, options) { + if (!options.customMerge) { + return deepmerge; + } + var customMerge = options.customMerge(key); + return typeof customMerge === 'function' ? customMerge : deepmerge; + } + + function getEnumerableOwnPropertySymbols(target) { + return Object.getOwnPropertySymbols + ? Object.getOwnPropertySymbols(target).filter(function (symbol) { + return Object.propertyIsEnumerable.call(target, symbol); + }) + : []; + } + + function getKeys(target) { + return Object.keys(target).concat( + getEnumerableOwnPropertySymbols(target), + ); + } + + function propertyIsOnObject(object, property) { + try { + return property in object; + } catch (_) { + return false; + } + } + + // Protects from prototype poisoning and unexpected merging up the prototype chain. + function propertyIsUnsafe(target, key) { + return ( + propertyIsOnObject(target, key) && // Properties are safe to merge if they don't exist in the target yet, + !( + Object.hasOwnProperty.call(target, key) && // unsafe if they exist up the prototype chain, + Object.propertyIsEnumerable.call(target, key) + ) + ); // and also unsafe if they're nonenumerable. + } + + function mergeObject(target, source, options) { + var destination = {}; + if (options.isMergeableObject(target)) { + getKeys(target).forEach(function (key) { + destination[key] = cloneUnlessOtherwiseSpecified( + target[key], + options, + ); + }); + } + getKeys(source).forEach(function (key) { + if (propertyIsUnsafe(target, key)) { + return; + } + + if ( + propertyIsOnObject(target, key) && + options.isMergeableObject(source[key]) + ) { + destination[key] = getMergeFunction(key, options)( + target[key], + source[key], + options, + ); + } else { + destination[key] = cloneUnlessOtherwiseSpecified( + source[key], + options, + ); + } + }); + return destination; + } + + function deepmerge(target, source, options) { + options = options || {}; + options.arrayMerge = options.arrayMerge || defaultArrayMerge; + options.isMergeableObject = + options.isMergeableObject || isMergeableObject; + // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() + // implementations can use it. The caller may not replace it. + options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; + + var sourceIsArray = Array.isArray(source); + var targetIsArray = Array.isArray(target); + var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; + + if (!sourceAndTargetTypesMatch) { + return cloneUnlessOtherwiseSpecified(source, options); + } else if (sourceIsArray) { + return options.arrayMerge(target, source, options); + } else { + return mergeObject(target, source, options); + } + } + + deepmerge.all = function deepmergeAll(array, options) { + if (!Array.isArray(array)) { + throw new Error('first argument should be an array'); + } + + return array.reduce(function (prev, next) { + return deepmerge(prev, next, options); + }, {}); + }; + + var deepmerge_1 = deepmerge; + + module.exports = deepmerge_1; + }, + {}, + ], + 5: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.attributeNames = exports.elementNames = void 0; + exports.elementNames = new Map( + [ + 'altGlyph', + 'altGlyphDef', + 'altGlyphItem', + 'animateColor', + 'animateMotion', + 'animateTransform', + 'clipPath', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'foreignObject', + 'glyphRef', + 'linearGradient', + 'radialGradient', + 'textPath', + ].map(function (val) { + return [val.toLowerCase(), val]; + }), + ); + exports.attributeNames = new Map( + [ + 'definitionURL', + 'attributeName', + 'attributeType', + 'baseFrequency', + 'baseProfile', + 'calcMode', + 'clipPathUnits', + 'diffuseConstant', + 'edgeMode', + 'filterUnits', + 'glyphRef', + 'gradientTransform', + 'gradientUnits', + 'kernelMatrix', + 'kernelUnitLength', + 'keyPoints', + 'keySplines', + 'keyTimes', + 'lengthAdjust', + 'limitingConeAngle', + 'markerHeight', + 'markerUnits', + 'markerWidth', + 'maskContentUnits', + 'maskUnits', + 'numOctaves', + 'pathLength', + 'patternContentUnits', + 'patternTransform', + 'patternUnits', + 'pointsAtX', + 'pointsAtY', + 'pointsAtZ', + 'preserveAlpha', + 'preserveAspectRatio', + 'primitiveUnits', + 'refX', + 'refY', + 'repeatCount', + 'repeatDur', + 'requiredExtensions', + 'requiredFeatures', + 'specularConstant', + 'specularExponent', + 'spreadMethod', + 'startOffset', + 'stdDeviation', + 'stitchTiles', + 'surfaceScale', + 'systemLanguage', + 'tableValues', + 'targetX', + 'targetY', + 'textLength', + 'viewBox', + 'viewTarget', + 'xChannelSelector', + 'yChannelSelector', + 'zoomAndPan', + ].map(function (val) { + return [val.toLowerCase(), val]; + }), + ); + }, + {}, + ], + 6: [ + function (require, module, exports) { + 'use strict'; + var __assign = + (this && this.__assign) || + function () { + __assign = + Object.assign || + function (t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) + if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if ( + !desc || + ('get' in desc + ? !m.__esModule + : desc.writable || desc.configurable) + ) { + desc = { + enumerable: true, + get: function () { + return m[k]; + }, + }; + } + Object.defineProperty(o, k2, desc); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { + enumerable: true, + value: v, + }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if ( + k !== 'default' && + Object.prototype.hasOwnProperty.call(mod, k) + ) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.render = void 0; + /* + * Module dependencies + */ + var ElementType = __importStar(require('domelementtype')); + var entities_1 = require('entities'); + /** + * Mixed-case SVG and MathML tags & attributes + * recognized by the HTML parser. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign + */ + var foreignNames_js_1 = require('./foreignNames.js'); + var unencodedElements = new Set([ + 'style', + 'script', + 'xmp', + 'iframe', + 'noembed', + 'noframes', + 'plaintext', + 'noscript', + ]); + function replaceQuotes(value) { + return value.replace(/"/g, '"'); + } + /** + * Format attributes + */ + function formatAttributes(attributes, opts) { + var _a; + if (!attributes) return; + var encode = + ((_a = opts.encodeEntities) !== null && _a !== void 0 + ? _a + : opts.decodeEntities) === false + ? replaceQuotes + : opts.xmlMode || opts.encodeEntities !== 'utf8' + ? entities_1.encodeXML + : entities_1.escapeAttribute; + return Object.keys(attributes) + .map(function (key) { + var _a, _b; + var value = + (_a = attributes[key]) !== null && _a !== void 0 ? _a : ''; + if (opts.xmlMode === 'foreign') { + /* Fix up mixed-case attribute names */ + key = + (_b = foreignNames_js_1.attributeNames.get(key)) !== null && + _b !== void 0 + ? _b + : key; + } + if (!opts.emptyAttrs && !opts.xmlMode && value === '') { + return key; + } + return ''.concat(key, '="').concat(encode(value), '"'); + }) + .join(' '); + } + /** + * Self-enclosing tags + */ + var singleTag = new Set([ + 'area', + 'base', + 'basefont', + 'br', + 'col', + 'command', + 'embed', + 'frame', + 'hr', + 'img', + 'input', + 'isindex', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + ]); + /** + * Renders a DOM node or an array of DOM nodes to a string. + * + * Can be thought of as the equivalent of the `outerHTML` of the passed node(s). + * + * @param node Node to be rendered. + * @param options Changes serialization behavior + */ + function render(node, options) { + if (options === void 0) { + options = {}; + } + var nodes = 'length' in node ? node : [node]; + var output = ''; + for (var i = 0; i < nodes.length; i++) { + output += renderNode(nodes[i], options); + } + return output; + } + exports.render = render; + exports.default = render; + function renderNode(node, options) { + switch (node.type) { + case ElementType.Root: + return render(node.children, options); + // @ts-expect-error We don't use `Doctype` yet + case ElementType.Doctype: + case ElementType.Directive: + return renderDirective(node); + case ElementType.Comment: + return renderComment(node); + case ElementType.CDATA: + return renderCdata(node); + case ElementType.Script: + case ElementType.Style: + case ElementType.Tag: + return renderTag(node, options); + case ElementType.Text: + return renderText(node, options); + } + } + var foreignModeIntegrationPoints = new Set([ + 'mi', + 'mo', + 'mn', + 'ms', + 'mtext', + 'annotation-xml', + 'foreignObject', + 'desc', + 'title', + ]); + var foreignElements = new Set(['svg', 'math']); + function renderTag(elem, opts) { + var _a; + // Handle SVG / MathML in HTML + if (opts.xmlMode === 'foreign') { + /* Fix up mixed-case element names */ + elem.name = + (_a = foreignNames_js_1.elementNames.get(elem.name)) !== null && + _a !== void 0 + ? _a + : elem.name; + /* Exit foreign mode at integration points */ + if ( + elem.parent && + foreignModeIntegrationPoints.has(elem.parent.name) + ) { + opts = __assign(__assign({}, opts), { xmlMode: false }); } } - break - case 3: - secondByte = buf[i + 1] - thirdByte = buf[i + 2] - if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F) - if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) { - codePoint = tempCodePoint - } + if (!opts.xmlMode && foreignElements.has(elem.name)) { + opts = __assign(__assign({}, opts), { xmlMode: 'foreign' }); } - break - case 4: - secondByte = buf[i + 1] - thirdByte = buf[i + 2] - fourthByte = buf[i + 3] - if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F) - if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) { - codePoint = tempCodePoint - } + var tag = '<'.concat(elem.name); + var attribs = formatAttributes(elem.attribs, opts); + if (attribs) { + tag += ' '.concat(attribs); } - } - } - - if (codePoint === null) { - // we did not generate a valid codePoint so insert a - // replacement char (U+FFFD) and advance only 1 byte - codePoint = 0xFFFD - bytesPerSequence = 1 - } else if (codePoint > 0xFFFF) { - // encode to utf16 (surrogate pair dance) - codePoint -= 0x10000 - res.push(codePoint >>> 10 & 0x3FF | 0xD800) - codePoint = 0xDC00 | codePoint & 0x3FF - } - - res.push(codePoint) - i += bytesPerSequence - } - - return decodeCodePointsArray(res) -} - -// Based on http://stackoverflow.com/a/22747272/680742, the browser with -// the lowest limit is Chrome, with 0x10000 args. -// We go 1 magnitude less, for safety -var MAX_ARGUMENTS_LENGTH = 0x1000 - -function decodeCodePointsArray (codePoints) { - var len = codePoints.length - if (len <= MAX_ARGUMENTS_LENGTH) { - return String.fromCharCode.apply(String, codePoints) // avoid extra slice() - } - - // Decode in chunks to avoid "call stack size exceeded". - var res = '' - var i = 0 - while (i < len) { - res += String.fromCharCode.apply( - String, - codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) - ) - } - return res -} - -function asciiSlice (buf, start, end) { - var ret = '' - end = Math.min(buf.length, end) - - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i] & 0x7F) - } - return ret -} - -function latin1Slice (buf, start, end) { - var ret = '' - end = Math.min(buf.length, end) - - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i]) - } - return ret -} - -function hexSlice (buf, start, end) { - var len = buf.length - - if (!start || start < 0) start = 0 - if (!end || end < 0 || end > len) end = len - - var out = '' - for (var i = start; i < end; ++i) { - out += toHex(buf[i]) - } - return out -} - -function utf16leSlice (buf, start, end) { - var bytes = buf.slice(start, end) - var res = '' - for (var i = 0; i < bytes.length; i += 2) { - res += String.fromCharCode(bytes[i] + (bytes[i + 1] * 256)) - } - return res -} - -Buffer.prototype.slice = function slice (start, end) { - var len = this.length - start = ~~start - end = end === undefined ? len : ~~end - - if (start < 0) { - start += len - if (start < 0) start = 0 - } else if (start > len) { - start = len - } - - if (end < 0) { - end += len - if (end < 0) end = 0 - } else if (end > len) { - end = len - } - - if (end < start) end = start - - var newBuf = this.subarray(start, end) - // Return an augmented `Uint8Array` instance - newBuf.__proto__ = Buffer.prototype - return newBuf -} - -/* - * Need to make sure that buffer isn't trying to write out of bounds. - */ -function checkOffset (offset, ext, length) { - if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint') - if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length') -} - -Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var val = this[offset] - var mul = 1 - var i = 0 - while (++i < byteLength && (mul *= 0x100)) { - val += this[offset + i] * mul - } - - return val -} - -Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - checkOffset(offset, byteLength, this.length) - } - - var val = this[offset + --byteLength] - var mul = 1 - while (byteLength > 0 && (mul *= 0x100)) { - val += this[offset + --byteLength] * mul - } - - return val -} - -Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 1, this.length) - return this[offset] -} - -Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - return this[offset] | (this[offset + 1] << 8) -} - -Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - return (this[offset] << 8) | this[offset + 1] -} - -Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return ((this[offset]) | - (this[offset + 1] << 8) | - (this[offset + 2] << 16)) + - (this[offset + 3] * 0x1000000) -} - -Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset] * 0x1000000) + - ((this[offset + 1] << 16) | - (this[offset + 2] << 8) | - this[offset + 3]) -} - -Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var val = this[offset] - var mul = 1 - var i = 0 - while (++i < byteLength && (mul *= 0x100)) { - val += this[offset + i] * mul - } - mul *= 0x80 - - if (val >= mul) val -= Math.pow(2, 8 * byteLength) - - return val -} - -Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var i = byteLength - var mul = 1 - var val = this[offset + --i] - while (i > 0 && (mul *= 0x100)) { - val += this[offset + --i] * mul - } - mul *= 0x80 - - if (val >= mul) val -= Math.pow(2, 8 * byteLength) - - return val -} - -Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 1, this.length) - if (!(this[offset] & 0x80)) return (this[offset]) - return ((0xff - this[offset] + 1) * -1) -} - -Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - var val = this[offset] | (this[offset + 1] << 8) - return (val & 0x8000) ? val | 0xFFFF0000 : val -} - -Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - var val = this[offset + 1] | (this[offset] << 8) - return (val & 0x8000) ? val | 0xFFFF0000 : val -} - -Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset]) | - (this[offset + 1] << 8) | - (this[offset + 2] << 16) | - (this[offset + 3] << 24) -} - -Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset] << 24) | - (this[offset + 1] << 16) | - (this[offset + 2] << 8) | - (this[offset + 3]) -} - -Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - return ieee754.read(this, offset, true, 23, 4) -} - -Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - return ieee754.read(this, offset, false, 23, 4) -} - -Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 8, this.length) - return ieee754.read(this, offset, true, 52, 8) -} - -Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 8, this.length) - return ieee754.read(this, offset, false, 52, 8) -} - -function checkInt (buf, value, offset, ext, max, min) { - if (!Buffer.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance') - if (value > max || value < min) throw new RangeError('"value" argument is out of bounds') - if (offset + ext > buf.length) throw new RangeError('Index out of range') -} - -Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength) - 1 - checkInt(this, value, offset, byteLength, maxBytes, 0) - } - - var mul = 1 - var i = 0 - this[offset] = value & 0xFF - while (++i < byteLength && (mul *= 0x100)) { - this[offset + i] = (value / mul) & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength) - 1 - checkInt(this, value, offset, byteLength, maxBytes, 0) - } - - var i = byteLength - 1 - var mul = 1 - this[offset + i] = value & 0xFF - while (--i >= 0 && (mul *= 0x100)) { - this[offset + i] = (value / mul) & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0) - this[offset] = (value & 0xff) - return offset + 1 -} - -Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - return offset + 2 -} - -Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) - this[offset] = (value >>> 8) - this[offset + 1] = (value & 0xff) - return offset + 2 -} - -Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) - this[offset + 3] = (value >>> 24) - this[offset + 2] = (value >>> 16) - this[offset + 1] = (value >>> 8) - this[offset] = (value & 0xff) - return offset + 4 -} - -Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) - this[offset] = (value >>> 24) - this[offset + 1] = (value >>> 16) - this[offset + 2] = (value >>> 8) - this[offset + 3] = (value & 0xff) - return offset + 4 -} - -Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - var limit = Math.pow(2, (8 * byteLength) - 1) - - checkInt(this, value, offset, byteLength, limit - 1, -limit) - } - - var i = 0 - var mul = 1 - var sub = 0 - this[offset] = value & 0xFF - while (++i < byteLength && (mul *= 0x100)) { - if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { - sub = 1 - } - this[offset + i] = ((value / mul) >> 0) - sub & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - var limit = Math.pow(2, (8 * byteLength) - 1) - - checkInt(this, value, offset, byteLength, limit - 1, -limit) - } - - var i = byteLength - 1 - var mul = 1 - var sub = 0 - this[offset + i] = value & 0xFF - while (--i >= 0 && (mul *= 0x100)) { - if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { - sub = 1 - } - this[offset + i] = ((value / mul) >> 0) - sub & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80) - if (value < 0) value = 0xff + value + 1 - this[offset] = (value & 0xff) - return offset + 1 -} - -Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - return offset + 2 -} - -Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) - this[offset] = (value >>> 8) - this[offset + 1] = (value & 0xff) - return offset + 2 -} - -Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - this[offset + 2] = (value >>> 16) - this[offset + 3] = (value >>> 24) - return offset + 4 -} - -Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) - if (value < 0) value = 0xffffffff + value + 1 - this[offset] = (value >>> 24) - this[offset + 1] = (value >>> 16) - this[offset + 2] = (value >>> 8) - this[offset + 3] = (value & 0xff) - return offset + 4 -} - -function checkIEEE754 (buf, value, offset, ext, max, min) { - if (offset + ext > buf.length) throw new RangeError('Index out of range') - if (offset < 0) throw new RangeError('Index out of range') -} - -function writeFloat (buf, value, offset, littleEndian, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38) - } - ieee754.write(buf, value, offset, littleEndian, 23, 4) - return offset + 4 -} - -Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) { - return writeFloat(this, value, offset, true, noAssert) -} - -Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) { - return writeFloat(this, value, offset, false, noAssert) -} - -function writeDouble (buf, value, offset, littleEndian, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308) - } - ieee754.write(buf, value, offset, littleEndian, 52, 8) - return offset + 8 -} - -Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) { - return writeDouble(this, value, offset, true, noAssert) -} - -Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) { - return writeDouble(this, value, offset, false, noAssert) -} - -// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) -Buffer.prototype.copy = function copy (target, targetStart, start, end) { - if (!Buffer.isBuffer(target)) throw new TypeError('argument should be a Buffer') - if (!start) start = 0 - if (!end && end !== 0) end = this.length - if (targetStart >= target.length) targetStart = target.length - if (!targetStart) targetStart = 0 - if (end > 0 && end < start) end = start - - // Copy 0 bytes; we're done - if (end === start) return 0 - if (target.length === 0 || this.length === 0) return 0 - - // Fatal error conditions - if (targetStart < 0) { - throw new RangeError('targetStart out of bounds') - } - if (start < 0 || start >= this.length) throw new RangeError('Index out of range') - if (end < 0) throw new RangeError('sourceEnd out of bounds') - - // Are we oob? - if (end > this.length) end = this.length - if (target.length - targetStart < end - start) { - end = target.length - targetStart + start - } - - var len = end - start - - if (this === target && typeof Uint8Array.prototype.copyWithin === 'function') { - // Use built-in when available, missing from IE11 - this.copyWithin(targetStart, start, end) - } else if (this === target && start < targetStart && targetStart < end) { - // descending copy from end - for (var i = len - 1; i >= 0; --i) { - target[i + targetStart] = this[i + start] - } - } else { - Uint8Array.prototype.set.call( - target, - this.subarray(start, end), - targetStart - ) - } - - return len -} - -// Usage: -// buffer.fill(number[, offset[, end]]) -// buffer.fill(buffer[, offset[, end]]) -// buffer.fill(string[, offset[, end]][, encoding]) -Buffer.prototype.fill = function fill (val, start, end, encoding) { - // Handle string cases: - if (typeof val === 'string') { - if (typeof start === 'string') { - encoding = start - start = 0 - end = this.length - } else if (typeof end === 'string') { - encoding = end - end = this.length - } - if (encoding !== undefined && typeof encoding !== 'string') { - throw new TypeError('encoding must be a string') - } - if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) { - throw new TypeError('Unknown encoding: ' + encoding) - } - if (val.length === 1) { - var code = val.charCodeAt(0) - if ((encoding === 'utf8' && code < 128) || - encoding === 'latin1') { - // Fast path: If `val` fits into a single byte, use that numeric value. - val = code - } - } - } else if (typeof val === 'number') { - val = val & 255 - } - - // Invalid ranges are not set to a default, so can range check early. - if (start < 0 || this.length < start || this.length < end) { - throw new RangeError('Out of range index') - } - - if (end <= start) { - return this - } - - start = start >>> 0 - end = end === undefined ? this.length : end >>> 0 - - if (!val) val = 0 - - var i - if (typeof val === 'number') { - for (i = start; i < end; ++i) { - this[i] = val - } - } else { - var bytes = Buffer.isBuffer(val) - ? val - : Buffer.from(val, encoding) - var len = bytes.length - if (len === 0) { - throw new TypeError('The value "' + val + - '" is invalid for argument "value"') - } - for (i = 0; i < end - start; ++i) { - this[i + start] = bytes[i % len] - } - } - - return this -} - -// HELPER FUNCTIONS -// ================ - -var INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g - -function base64clean (str) { - // Node takes equal signs as end of the Base64 encoding - str = str.split('=')[0] - // Node strips out invalid characters like \n and \t from the string, base64-js does not - str = str.trim().replace(INVALID_BASE64_RE, '') - // Node converts strings with length < 2 to '' - if (str.length < 2) return '' - // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not - while (str.length % 4 !== 0) { - str = str + '=' - } - return str -} - -function toHex (n) { - if (n < 16) return '0' + n.toString(16) - return n.toString(16) -} - -function utf8ToBytes (string, units) { - units = units || Infinity - var codePoint - var length = string.length - var leadSurrogate = null - var bytes = [] - - for (var i = 0; i < length; ++i) { - codePoint = string.charCodeAt(i) - - // is surrogate component - if (codePoint > 0xD7FF && codePoint < 0xE000) { - // last char was a lead - if (!leadSurrogate) { - // no lead yet - if (codePoint > 0xDBFF) { - // unexpected trail - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } else if (i + 1 === length) { - // unpaired lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } - - // valid lead - leadSurrogate = codePoint - - continue - } - - // 2 leads in a row - if (codePoint < 0xDC00) { - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - leadSurrogate = codePoint - continue - } - - // valid surrogate pair - codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000 - } else if (leadSurrogate) { - // valid bmp char, but last char was a lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - } - - leadSurrogate = null - - // encode utf8 - if (codePoint < 0x80) { - if ((units -= 1) < 0) break - bytes.push(codePoint) - } else if (codePoint < 0x800) { - if ((units -= 2) < 0) break - bytes.push( - codePoint >> 0x6 | 0xC0, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x10000) { - if ((units -= 3) < 0) break - bytes.push( - codePoint >> 0xC | 0xE0, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x110000) { - if ((units -= 4) < 0) break - bytes.push( - codePoint >> 0x12 | 0xF0, - codePoint >> 0xC & 0x3F | 0x80, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else { - throw new Error('Invalid code point') - } - } - - return bytes -} - -function asciiToBytes (str) { - var byteArray = [] - for (var i = 0; i < str.length; ++i) { - // Node's code seems to be doing this and not & 0x7F.. - byteArray.push(str.charCodeAt(i) & 0xFF) - } - return byteArray -} - -function utf16leToBytes (str, units) { - var c, hi, lo - var byteArray = [] - for (var i = 0; i < str.length; ++i) { - if ((units -= 2) < 0) break - - c = str.charCodeAt(i) - hi = c >> 8 - lo = c % 256 - byteArray.push(lo) - byteArray.push(hi) - } - - return byteArray -} - -function base64ToBytes (str) { - return base64.toByteArray(base64clean(str)) -} - -function blitBuffer (src, dst, offset, length) { - for (var i = 0; i < length; ++i) { - if ((i + offset >= dst.length) || (i >= src.length)) break - dst[i + offset] = src[i] - } - return i -} - -// ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass -// the `instanceof` check but they should be treated as of that type. -// See: https://github.com/feross/buffer/issues/166 -function isInstance (obj, type) { - return obj instanceof type || - (obj != null && obj.constructor != null && obj.constructor.name != null && - obj.constructor.name === type.name) -} -function numberIsNaN (obj) { - // For IE11 support - return obj !== obj // eslint-disable-line no-self-compare -} - -}).call(this)}).call(this,require("buffer").Buffer) -},{"base64-js":1,"buffer":3,"ieee754":30}],4:[function(require,module,exports){ -'use strict'; - -var isMergeableObject = function isMergeableObject(value) { - return isNonNullObject(value) - && !isSpecial(value) -}; - -function isNonNullObject(value) { - return !!value && typeof value === 'object' -} - -function isSpecial(value) { - var stringValue = Object.prototype.toString.call(value); - - return stringValue === '[object RegExp]' - || stringValue === '[object Date]' - || isReactElement(value) -} - -// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 -var canUseSymbol = typeof Symbol === 'function' && Symbol.for; -var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7; - -function isReactElement(value) { - return value.$$typeof === REACT_ELEMENT_TYPE -} - -function emptyTarget(val) { - return Array.isArray(val) ? [] : {} -} - -function cloneUnlessOtherwiseSpecified(value, options) { - return (options.clone !== false && options.isMergeableObject(value)) - ? deepmerge(emptyTarget(value), value, options) - : value -} - -function defaultArrayMerge(target, source, options) { - return target.concat(source).map(function(element) { - return cloneUnlessOtherwiseSpecified(element, options) - }) -} - -function getMergeFunction(key, options) { - if (!options.customMerge) { - return deepmerge - } - var customMerge = options.customMerge(key); - return typeof customMerge === 'function' ? customMerge : deepmerge -} - -function getEnumerableOwnPropertySymbols(target) { - return Object.getOwnPropertySymbols - ? Object.getOwnPropertySymbols(target).filter(function(symbol) { - return Object.propertyIsEnumerable.call(target, symbol) - }) - : [] -} - -function getKeys(target) { - return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) -} - -function propertyIsOnObject(object, property) { - try { - return property in object - } catch(_) { - return false - } -} - -// Protects from prototype poisoning and unexpected merging up the prototype chain. -function propertyIsUnsafe(target, key) { - return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet, - && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain, - && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable. -} - -function mergeObject(target, source, options) { - var destination = {}; - if (options.isMergeableObject(target)) { - getKeys(target).forEach(function(key) { - destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); - }); - } - getKeys(source).forEach(function(key) { - if (propertyIsUnsafe(target, key)) { - return - } - - if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) { - destination[key] = getMergeFunction(key, options)(target[key], source[key], options); - } else { - destination[key] = cloneUnlessOtherwiseSpecified(source[key], options); - } - }); - return destination -} - -function deepmerge(target, source, options) { - options = options || {}; - options.arrayMerge = options.arrayMerge || defaultArrayMerge; - options.isMergeableObject = options.isMergeableObject || isMergeableObject; - // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() - // implementations can use it. The caller may not replace it. - options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; - - var sourceIsArray = Array.isArray(source); - var targetIsArray = Array.isArray(target); - var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; - - if (!sourceAndTargetTypesMatch) { - return cloneUnlessOtherwiseSpecified(source, options) - } else if (sourceIsArray) { - return options.arrayMerge(target, source, options) - } else { - return mergeObject(target, source, options) - } -} - -deepmerge.all = function deepmergeAll(array, options) { - if (!Array.isArray(array)) { - throw new Error('first argument should be an array') - } - - return array.reduce(function(prev, next) { - return deepmerge(prev, next, options) - }, {}) -}; - -var deepmerge_1 = deepmerge; - -module.exports = deepmerge_1; - -},{}],5:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.attributeNames = exports.elementNames = void 0; -exports.elementNames = new Map([ - "altGlyph", - "altGlyphDef", - "altGlyphItem", - "animateColor", - "animateMotion", - "animateTransform", - "clipPath", - "feBlend", - "feColorMatrix", - "feComponentTransfer", - "feComposite", - "feConvolveMatrix", - "feDiffuseLighting", - "feDisplacementMap", - "feDistantLight", - "feDropShadow", - "feFlood", - "feFuncA", - "feFuncB", - "feFuncG", - "feFuncR", - "feGaussianBlur", - "feImage", - "feMerge", - "feMergeNode", - "feMorphology", - "feOffset", - "fePointLight", - "feSpecularLighting", - "feSpotLight", - "feTile", - "feTurbulence", - "foreignObject", - "glyphRef", - "linearGradient", - "radialGradient", - "textPath", -].map(function (val) { return [val.toLowerCase(), val]; })); -exports.attributeNames = new Map([ - "definitionURL", - "attributeName", - "attributeType", - "baseFrequency", - "baseProfile", - "calcMode", - "clipPathUnits", - "diffuseConstant", - "edgeMode", - "filterUnits", - "glyphRef", - "gradientTransform", - "gradientUnits", - "kernelMatrix", - "kernelUnitLength", - "keyPoints", - "keySplines", - "keyTimes", - "lengthAdjust", - "limitingConeAngle", - "markerHeight", - "markerUnits", - "markerWidth", - "maskContentUnits", - "maskUnits", - "numOctaves", - "pathLength", - "patternContentUnits", - "patternTransform", - "patternUnits", - "pointsAtX", - "pointsAtY", - "pointsAtZ", - "preserveAlpha", - "preserveAspectRatio", - "primitiveUnits", - "refX", - "refY", - "repeatCount", - "repeatDur", - "requiredExtensions", - "requiredFeatures", - "specularConstant", - "specularExponent", - "spreadMethod", - "startOffset", - "stdDeviation", - "stitchTiles", - "surfaceScale", - "systemLanguage", - "tableValues", - "targetX", - "targetY", - "textLength", - "viewBox", - "viewTarget", - "xChannelSelector", - "yChannelSelector", - "zoomAndPan", -].map(function (val) { return [val.toLowerCase(), val]; })); - -},{}],6:[function(require,module,exports){ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.render = void 0; -/* - * Module dependencies - */ -var ElementType = __importStar(require("domelementtype")); -var entities_1 = require("entities"); -/** - * Mixed-case SVG and MathML tags & attributes - * recognized by the HTML parser. - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign - */ -var foreignNames_js_1 = require("./foreignNames.js"); -var unencodedElements = new Set([ - "style", - "script", - "xmp", - "iframe", - "noembed", - "noframes", - "plaintext", - "noscript", -]); -function replaceQuotes(value) { - return value.replace(/"/g, """); -} -/** - * Format attributes - */ -function formatAttributes(attributes, opts) { - var _a; - if (!attributes) - return; - var encode = ((_a = opts.encodeEntities) !== null && _a !== void 0 ? _a : opts.decodeEntities) === false - ? replaceQuotes - : opts.xmlMode || opts.encodeEntities !== "utf8" - ? entities_1.encodeXML - : entities_1.escapeAttribute; - return Object.keys(attributes) - .map(function (key) { - var _a, _b; - var value = (_a = attributes[key]) !== null && _a !== void 0 ? _a : ""; - if (opts.xmlMode === "foreign") { - /* Fix up mixed-case attribute names */ - key = (_b = foreignNames_js_1.attributeNames.get(key)) !== null && _b !== void 0 ? _b : key; - } - if (!opts.emptyAttrs && !opts.xmlMode && value === "") { - return key; - } - return "".concat(key, "=\"").concat(encode(value), "\""); - }) - .join(" "); -} -/** - * Self-enclosing tags - */ -var singleTag = new Set([ - "area", - "base", - "basefont", - "br", - "col", - "command", - "embed", - "frame", - "hr", - "img", - "input", - "isindex", - "keygen", - "link", - "meta", - "param", - "source", - "track", - "wbr", -]); -/** - * Renders a DOM node or an array of DOM nodes to a string. - * - * Can be thought of as the equivalent of the `outerHTML` of the passed node(s). - * - * @param node Node to be rendered. - * @param options Changes serialization behavior - */ -function render(node, options) { - if (options === void 0) { options = {}; } - var nodes = "length" in node ? node : [node]; - var output = ""; - for (var i = 0; i < nodes.length; i++) { - output += renderNode(nodes[i], options); - } - return output; -} -exports.render = render; -exports.default = render; -function renderNode(node, options) { - switch (node.type) { - case ElementType.Root: - return render(node.children, options); - // @ts-expect-error We don't use `Doctype` yet - case ElementType.Doctype: - case ElementType.Directive: - return renderDirective(node); - case ElementType.Comment: - return renderComment(node); - case ElementType.CDATA: - return renderCdata(node); - case ElementType.Script: - case ElementType.Style: - case ElementType.Tag: - return renderTag(node, options); - case ElementType.Text: - return renderText(node, options); - } -} -var foreignModeIntegrationPoints = new Set([ - "mi", - "mo", - "mn", - "ms", - "mtext", - "annotation-xml", - "foreignObject", - "desc", - "title", -]); -var foreignElements = new Set(["svg", "math"]); -function renderTag(elem, opts) { - var _a; - // Handle SVG / MathML in HTML - if (opts.xmlMode === "foreign") { - /* Fix up mixed-case element names */ - elem.name = (_a = foreignNames_js_1.elementNames.get(elem.name)) !== null && _a !== void 0 ? _a : elem.name; - /* Exit foreign mode at integration points */ - if (elem.parent && - foreignModeIntegrationPoints.has(elem.parent.name)) { - opts = __assign(__assign({}, opts), { xmlMode: false }); - } - } - if (!opts.xmlMode && foreignElements.has(elem.name)) { - opts = __assign(__assign({}, opts), { xmlMode: "foreign" }); - } - var tag = "<".concat(elem.name); - var attribs = formatAttributes(elem.attribs, opts); - if (attribs) { - tag += " ".concat(attribs); - } - if (elem.children.length === 0 && - (opts.xmlMode - ? // In XML mode or foreign mode, and user hasn't explicitly turned off self-closing tags + if ( + elem.children.length === 0 && + (opts.xmlMode + ? // In XML mode or foreign mode, and user hasn't explicitly turned off self-closing tags opts.selfClosingTags !== false - : // User explicitly asked for self-closing tags, even in HTML mode - opts.selfClosingTags && singleTag.has(elem.name))) { - if (!opts.xmlMode) - tag += " "; - tag += "/>"; - } - else { - tag += ">"; - if (elem.children.length > 0) { - tag += render(elem.children, opts); + : // User explicitly asked for self-closing tags, even in HTML mode + opts.selfClosingTags && singleTag.has(elem.name)) + ) { + if (!opts.xmlMode) tag += ' '; + tag += '/>'; + } else { + tag += '>'; + if (elem.children.length > 0) { + tag += render(elem.children, opts); + } + if (opts.xmlMode || !singleTag.has(elem.name)) { + tag += ''); + } + } + return tag; } - if (opts.xmlMode || !singleTag.has(elem.name)) { - tag += ""); + function renderDirective(elem) { + return '<'.concat(elem.data, '>'); } - } - return tag; -} -function renderDirective(elem) { - return "<".concat(elem.data, ">"); -} -function renderText(elem, opts) { - var _a; - var data = elem.data || ""; - // If entities weren't decoded, no need to encode them back - if (((_a = opts.encodeEntities) !== null && _a !== void 0 ? _a : opts.decodeEntities) !== false && - !(!opts.xmlMode && - elem.parent && - unencodedElements.has(elem.parent.name))) { - data = - opts.xmlMode || opts.encodeEntities !== "utf8" + function renderText(elem, opts) { + var _a; + var data = elem.data || ''; + // If entities weren't decoded, no need to encode them back + if ( + ((_a = opts.encodeEntities) !== null && _a !== void 0 + ? _a + : opts.decodeEntities) !== false && + !( + !opts.xmlMode && + elem.parent && + unencodedElements.has(elem.parent.name) + ) + ) { + data = + opts.xmlMode || opts.encodeEntities !== 'utf8' ? (0, entities_1.encodeXML)(data) : (0, entities_1.escapeText)(data); - } - return data; -} -function renderCdata(elem) { - return ""); -} -function renderComment(elem) { - return ""); -} - -},{"./foreignNames.js":5,"domelementtype":7,"entities":25}],7:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Doctype = exports.CDATA = exports.Tag = exports.Style = exports.Script = exports.Comment = exports.Directive = exports.Text = exports.Root = exports.isTag = exports.ElementType = void 0; -/** Types of elements found in htmlparser2's DOM */ -var ElementType; -(function (ElementType) { - /** Type for the root element of a document */ - ElementType["Root"] = "root"; - /** Type for Text */ - ElementType["Text"] = "text"; - /** Type for */ - ElementType["Directive"] = "directive"; - /** Type for */ - ElementType["Comment"] = "comment"; - /** Type for `. + this.sequenceIndex = Number(c === CharCodes.Lt); } - } - else { - // If we see a `<`, set the sequence index to 1; useful for eg. `<`. - this.sequenceIndex = Number(c === CharCodes.Lt); - } - }; - Tokenizer.prototype.stateCDATASequence = function (c) { - if (c === Sequences.Cdata[this.sequenceIndex]) { - if (++this.sequenceIndex === Sequences.Cdata.length) { + }; + Tokenizer.prototype.stateCDATASequence = function (c) { + if (c === Sequences.Cdata[this.sequenceIndex]) { + if (++this.sequenceIndex === Sequences.Cdata.length) { this.state = State.InCommentLike; this.currentSequence = Sequences.CdataEnd; this.sequenceIndex = 0; this.sectionStart = this.index + 1; + } + } else { + this.sequenceIndex = 0; + this.state = State.InDeclaration; + this.stateInDeclaration(c); // Reconsume the character } - } - else { - this.sequenceIndex = 0; - this.state = State.InDeclaration; - this.stateInDeclaration(c); // Reconsume the character - } - }; - /** - * When we wait for one specific character, we can speed things up - * by skipping through the buffer until we find it. - * - * @returns Whether the character was found. - */ - Tokenizer.prototype.fastForwardTo = function (c) { - while (++this.index < this.buffer.length + this.offset) { - if (this.buffer.charCodeAt(this.index - this.offset) === c) { + }; + /** + * When we wait for one specific character, we can speed things up + * by skipping through the buffer until we find it. + * + * @returns Whether the character was found. + */ + Tokenizer.prototype.fastForwardTo = function (c) { + while (++this.index < this.buffer.length + this.offset) { + if (this.buffer.charCodeAt(this.index - this.offset) === c) { return true; + } } - } - /* - * We increment the index at the end of the `parse` loop, - * so set it to `buffer.length - 1` here. - * - * TODO: Refactor `parse` to increment index before calling states. - */ - this.index = this.buffer.length + this.offset - 1; - return false; - }; - /** - * Comments and CDATA end with `-->` and `]]>`. - * - * Their common qualities are: - * - Their end sequences have a distinct character they start with. - * - That character is then repeated, so we have to check multiple repeats. - * - All characters but the start character of the sequence can be skipped. - */ - Tokenizer.prototype.stateInCommentLike = function (c) { - if (c === this.currentSequence[this.sequenceIndex]) { - if (++this.sequenceIndex === this.currentSequence.length) { + /* + * We increment the index at the end of the `parse` loop, + * so set it to `buffer.length - 1` here. + * + * TODO: Refactor `parse` to increment index before calling states. + */ + this.index = this.buffer.length + this.offset - 1; + return false; + }; + /** + * Comments and CDATA end with `-->` and `]]>`. + * + * Their common qualities are: + * - Their end sequences have a distinct character they start with. + * - That character is then repeated, so we have to check multiple repeats. + * - All characters but the start character of the sequence can be skipped. + */ + Tokenizer.prototype.stateInCommentLike = function (c) { + if (c === this.currentSequence[this.sequenceIndex]) { + if (++this.sequenceIndex === this.currentSequence.length) { if (this.currentSequence === Sequences.CdataEnd) { - this.cbs.oncdata(this.sectionStart, this.index, 2); - } - else { - this.cbs.oncomment(this.sectionStart, this.index, 2); + this.cbs.oncdata(this.sectionStart, this.index, 2); + } else { + this.cbs.oncomment(this.sectionStart, this.index, 2); } this.sequenceIndex = 0; this.sectionStart = this.index + 1; this.state = State.Text; - } - } - else if (this.sequenceIndex === 0) { - // Fast-forward to the first character of the sequence - if (this.fastForwardTo(this.currentSequence[0])) { + } + } else if (this.sequenceIndex === 0) { + // Fast-forward to the first character of the sequence + if (this.fastForwardTo(this.currentSequence[0])) { this.sequenceIndex = 1; + } + } else if (c !== this.currentSequence[this.sequenceIndex - 1]) { + // Allow long sequences, eg. --->, ]]]> + this.sequenceIndex = 0; } - } - else if (c !== this.currentSequence[this.sequenceIndex - 1]) { - // Allow long sequences, eg. --->, ]]]> - this.sequenceIndex = 0; - } - }; - /** - * HTML only allows ASCII alpha characters (a-z and A-Z) at the beginning of a tag name. - * - * XML allows a lot more characters here (@see https://www.w3.org/TR/REC-xml/#NT-NameStartChar). - * We allow anything that wouldn't end the tag. - */ - Tokenizer.prototype.isTagStartChar = function (c) { - return this.xmlMode ? !isEndOfTagSection(c) : isASCIIAlpha(c); - }; - Tokenizer.prototype.startSpecial = function (sequence, offset) { - this.isSpecial = true; - this.currentSequence = sequence; - this.sequenceIndex = offset; - this.state = State.SpecialStartSequence; - }; - Tokenizer.prototype.stateBeforeTagName = function (c) { - if (c === CharCodes.ExclamationMark) { - this.state = State.BeforeDeclaration; - this.sectionStart = this.index + 1; - } - else if (c === CharCodes.Questionmark) { - this.state = State.InProcessingInstruction; - this.sectionStart = this.index + 1; - } - else if (this.isTagStartChar(c)) { - var lower = c | 0x20; - this.sectionStart = this.index; - if (!this.xmlMode && lower === Sequences.TitleEnd[2]) { + }; + /** + * HTML only allows ASCII alpha characters (a-z and A-Z) at the beginning of a tag name. + * + * XML allows a lot more characters here (@see https://www.w3.org/TR/REC-xml/#NT-NameStartChar). + * We allow anything that wouldn't end the tag. + */ + Tokenizer.prototype.isTagStartChar = function (c) { + return this.xmlMode ? !isEndOfTagSection(c) : isASCIIAlpha(c); + }; + Tokenizer.prototype.startSpecial = function (sequence, offset) { + this.isSpecial = true; + this.currentSequence = sequence; + this.sequenceIndex = offset; + this.state = State.SpecialStartSequence; + }; + Tokenizer.prototype.stateBeforeTagName = function (c) { + if (c === CharCodes.ExclamationMark) { + this.state = State.BeforeDeclaration; + this.sectionStart = this.index + 1; + } else if (c === CharCodes.Questionmark) { + this.state = State.InProcessingInstruction; + this.sectionStart = this.index + 1; + } else if (this.isTagStartChar(c)) { + var lower = c | 0x20; + this.sectionStart = this.index; + if (!this.xmlMode && lower === Sequences.TitleEnd[2]) { this.startSpecial(Sequences.TitleEnd, 3); - } - else { + } else { this.state = - !this.xmlMode && lower === Sequences.ScriptEnd[2] - ? State.BeforeSpecialS - : State.InTagName; + !this.xmlMode && lower === Sequences.ScriptEnd[2] + ? State.BeforeSpecialS + : State.InTagName; + } + } else if (c === CharCodes.Slash) { + this.state = State.BeforeClosingTagName; + } else { + this.state = State.Text; + this.stateText(c); } - } - else if (c === CharCodes.Slash) { - this.state = State.BeforeClosingTagName; - } - else { - this.state = State.Text; - this.stateText(c); - } - }; - Tokenizer.prototype.stateInTagName = function (c) { - if (isEndOfTagSection(c)) { - this.cbs.onopentagname(this.sectionStart, this.index); - this.sectionStart = -1; - this.state = State.BeforeAttributeName; - this.stateBeforeAttributeName(c); - } - }; - Tokenizer.prototype.stateBeforeClosingTagName = function (c) { - if (isWhitespace(c)) { - // Ignore - } - else if (c === CharCodes.Gt) { - this.state = State.Text; - } - else { - this.state = this.isTagStartChar(c) + }; + Tokenizer.prototype.stateInTagName = function (c) { + if (isEndOfTagSection(c)) { + this.cbs.onopentagname(this.sectionStart, this.index); + this.sectionStart = -1; + this.state = State.BeforeAttributeName; + this.stateBeforeAttributeName(c); + } + }; + Tokenizer.prototype.stateBeforeClosingTagName = function (c) { + if (isWhitespace(c)) { + // Ignore + } else if (c === CharCodes.Gt) { + this.state = State.Text; + } else { + this.state = this.isTagStartChar(c) ? State.InClosingTagName : State.InSpecialComment; - this.sectionStart = this.index; - } - }; - Tokenizer.prototype.stateInClosingTagName = function (c) { - if (c === CharCodes.Gt || isWhitespace(c)) { - this.cbs.onclosetag(this.sectionStart, this.index); - this.sectionStart = -1; - this.state = State.AfterClosingTagName; - this.stateAfterClosingTagName(c); - } - }; - Tokenizer.prototype.stateAfterClosingTagName = function (c) { - // Skip everything until ">" - if (c === CharCodes.Gt || this.fastForwardTo(CharCodes.Gt)) { - this.state = State.Text; - this.baseState = State.Text; - this.sectionStart = this.index + 1; - } - }; - Tokenizer.prototype.stateBeforeAttributeName = function (c) { - if (c === CharCodes.Gt) { - this.cbs.onopentagend(this.index); - if (this.isSpecial) { + this.sectionStart = this.index; + } + }; + Tokenizer.prototype.stateInClosingTagName = function (c) { + if (c === CharCodes.Gt || isWhitespace(c)) { + this.cbs.onclosetag(this.sectionStart, this.index); + this.sectionStart = -1; + this.state = State.AfterClosingTagName; + this.stateAfterClosingTagName(c); + } + }; + Tokenizer.prototype.stateAfterClosingTagName = function (c) { + // Skip everything until ">" + if (c === CharCodes.Gt || this.fastForwardTo(CharCodes.Gt)) { + this.state = State.Text; + this.baseState = State.Text; + this.sectionStart = this.index + 1; + } + }; + Tokenizer.prototype.stateBeforeAttributeName = function (c) { + if (c === CharCodes.Gt) { + this.cbs.onopentagend(this.index); + if (this.isSpecial) { this.state = State.InSpecialTag; this.sequenceIndex = 0; - } - else { + } else { this.state = State.Text; + } + this.baseState = this.state; + this.sectionStart = this.index + 1; + } else if (c === CharCodes.Slash) { + this.state = State.InSelfClosingTag; + } else if (!isWhitespace(c)) { + this.state = State.InAttributeName; + this.sectionStart = this.index; } - this.baseState = this.state; - this.sectionStart = this.index + 1; - } - else if (c === CharCodes.Slash) { - this.state = State.InSelfClosingTag; - } - else if (!isWhitespace(c)) { - this.state = State.InAttributeName; - this.sectionStart = this.index; - } - }; - Tokenizer.prototype.stateInSelfClosingTag = function (c) { - if (c === CharCodes.Gt) { - this.cbs.onselfclosingtag(this.index); - this.state = State.Text; - this.baseState = State.Text; - this.sectionStart = this.index + 1; - this.isSpecial = false; // Reset special state, in case of self-closing special tags - } - else if (!isWhitespace(c)) { - this.state = State.BeforeAttributeName; - this.stateBeforeAttributeName(c); - } - }; - Tokenizer.prototype.stateInAttributeName = function (c) { - if (c === CharCodes.Eq || isEndOfTagSection(c)) { - this.cbs.onattribname(this.sectionStart, this.index); - this.sectionStart = -1; - this.state = State.AfterAttributeName; - this.stateAfterAttributeName(c); - } - }; - Tokenizer.prototype.stateAfterAttributeName = function (c) { - if (c === CharCodes.Eq) { - this.state = State.BeforeAttributeValue; - } - else if (c === CharCodes.Slash || c === CharCodes.Gt) { - this.cbs.onattribend(QuoteType.NoValue, this.index); - this.state = State.BeforeAttributeName; - this.stateBeforeAttributeName(c); - } - else if (!isWhitespace(c)) { - this.cbs.onattribend(QuoteType.NoValue, this.index); - this.state = State.InAttributeName; - this.sectionStart = this.index; - } - }; - Tokenizer.prototype.stateBeforeAttributeValue = function (c) { - if (c === CharCodes.DoubleQuote) { - this.state = State.InAttributeValueDq; - this.sectionStart = this.index + 1; - } - else if (c === CharCodes.SingleQuote) { - this.state = State.InAttributeValueSq; - this.sectionStart = this.index + 1; - } - else if (!isWhitespace(c)) { - this.sectionStart = this.index; - this.state = State.InAttributeValueNq; - this.stateInAttributeValueNoQuotes(c); // Reconsume token - } - }; - Tokenizer.prototype.handleInAttributeValue = function (c, quote) { - if (c === quote || - (!this.decodeEntities && this.fastForwardTo(quote))) { - this.cbs.onattribdata(this.sectionStart, this.index); - this.sectionStart = -1; - this.cbs.onattribend(quote === CharCodes.DoubleQuote - ? QuoteType.Double - : QuoteType.Single, this.index); - this.state = State.BeforeAttributeName; - } - else if (this.decodeEntities && c === CharCodes.Amp) { - this.baseState = this.state; - this.state = State.BeforeEntity; - } - }; - Tokenizer.prototype.stateInAttributeValueDoubleQuotes = function (c) { - this.handleInAttributeValue(c, CharCodes.DoubleQuote); - }; - Tokenizer.prototype.stateInAttributeValueSingleQuotes = function (c) { - this.handleInAttributeValue(c, CharCodes.SingleQuote); - }; - Tokenizer.prototype.stateInAttributeValueNoQuotes = function (c) { - if (isWhitespace(c) || c === CharCodes.Gt) { - this.cbs.onattribdata(this.sectionStart, this.index); - this.sectionStart = -1; - this.cbs.onattribend(QuoteType.Unquoted, this.index); - this.state = State.BeforeAttributeName; - this.stateBeforeAttributeName(c); - } - else if (this.decodeEntities && c === CharCodes.Amp) { - this.baseState = this.state; - this.state = State.BeforeEntity; - } - }; - Tokenizer.prototype.stateBeforeDeclaration = function (c) { - if (c === CharCodes.OpeningSquareBracket) { - this.state = State.CDATASequence; - this.sequenceIndex = 0; - } - else { - this.state = + }; + Tokenizer.prototype.stateInSelfClosingTag = function (c) { + if (c === CharCodes.Gt) { + this.cbs.onselfclosingtag(this.index); + this.state = State.Text; + this.baseState = State.Text; + this.sectionStart = this.index + 1; + this.isSpecial = false; // Reset special state, in case of self-closing special tags + } else if (!isWhitespace(c)) { + this.state = State.BeforeAttributeName; + this.stateBeforeAttributeName(c); + } + }; + Tokenizer.prototype.stateInAttributeName = function (c) { + if (c === CharCodes.Eq || isEndOfTagSection(c)) { + this.cbs.onattribname(this.sectionStart, this.index); + this.sectionStart = -1; + this.state = State.AfterAttributeName; + this.stateAfterAttributeName(c); + } + }; + Tokenizer.prototype.stateAfterAttributeName = function (c) { + if (c === CharCodes.Eq) { + this.state = State.BeforeAttributeValue; + } else if (c === CharCodes.Slash || c === CharCodes.Gt) { + this.cbs.onattribend(QuoteType.NoValue, this.index); + this.state = State.BeforeAttributeName; + this.stateBeforeAttributeName(c); + } else if (!isWhitespace(c)) { + this.cbs.onattribend(QuoteType.NoValue, this.index); + this.state = State.InAttributeName; + this.sectionStart = this.index; + } + }; + Tokenizer.prototype.stateBeforeAttributeValue = function (c) { + if (c === CharCodes.DoubleQuote) { + this.state = State.InAttributeValueDq; + this.sectionStart = this.index + 1; + } else if (c === CharCodes.SingleQuote) { + this.state = State.InAttributeValueSq; + this.sectionStart = this.index + 1; + } else if (!isWhitespace(c)) { + this.sectionStart = this.index; + this.state = State.InAttributeValueNq; + this.stateInAttributeValueNoQuotes(c); // Reconsume token + } + }; + Tokenizer.prototype.handleInAttributeValue = function (c, quote) { + if ( + c === quote || + (!this.decodeEntities && this.fastForwardTo(quote)) + ) { + this.cbs.onattribdata(this.sectionStart, this.index); + this.sectionStart = -1; + this.cbs.onattribend( + quote === CharCodes.DoubleQuote + ? QuoteType.Double + : QuoteType.Single, + this.index, + ); + this.state = State.BeforeAttributeName; + } else if (this.decodeEntities && c === CharCodes.Amp) { + this.baseState = this.state; + this.state = State.BeforeEntity; + } + }; + Tokenizer.prototype.stateInAttributeValueDoubleQuotes = function (c) { + this.handleInAttributeValue(c, CharCodes.DoubleQuote); + }; + Tokenizer.prototype.stateInAttributeValueSingleQuotes = function (c) { + this.handleInAttributeValue(c, CharCodes.SingleQuote); + }; + Tokenizer.prototype.stateInAttributeValueNoQuotes = function (c) { + if (isWhitespace(c) || c === CharCodes.Gt) { + this.cbs.onattribdata(this.sectionStart, this.index); + this.sectionStart = -1; + this.cbs.onattribend(QuoteType.Unquoted, this.index); + this.state = State.BeforeAttributeName; + this.stateBeforeAttributeName(c); + } else if (this.decodeEntities && c === CharCodes.Amp) { + this.baseState = this.state; + this.state = State.BeforeEntity; + } + }; + Tokenizer.prototype.stateBeforeDeclaration = function (c) { + if (c === CharCodes.OpeningSquareBracket) { + this.state = State.CDATASequence; + this.sequenceIndex = 0; + } else { + this.state = c === CharCodes.Dash - ? State.BeforeComment - : State.InDeclaration; - } - }; - Tokenizer.prototype.stateInDeclaration = function (c) { - if (c === CharCodes.Gt || this.fastForwardTo(CharCodes.Gt)) { - this.cbs.ondeclaration(this.sectionStart, this.index); - this.state = State.Text; - this.sectionStart = this.index + 1; - } - }; - Tokenizer.prototype.stateInProcessingInstruction = function (c) { - if (c === CharCodes.Gt || this.fastForwardTo(CharCodes.Gt)) { - this.cbs.onprocessinginstruction(this.sectionStart, this.index); - this.state = State.Text; - this.sectionStart = this.index + 1; - } - }; - Tokenizer.prototype.stateBeforeComment = function (c) { - if (c === CharCodes.Dash) { - this.state = State.InCommentLike; - this.currentSequence = Sequences.CommentEnd; - // Allow short comments (eg. ) - this.sequenceIndex = 2; - this.sectionStart = this.index + 1; - } - else { - this.state = State.InDeclaration; - } - }; - Tokenizer.prototype.stateInSpecialComment = function (c) { - if (c === CharCodes.Gt || this.fastForwardTo(CharCodes.Gt)) { - this.cbs.oncomment(this.sectionStart, this.index, 0); - this.state = State.Text; - this.sectionStart = this.index + 1; - } - }; - Tokenizer.prototype.stateBeforeSpecialS = function (c) { - var lower = c | 0x20; - if (lower === Sequences.ScriptEnd[3]) { - this.startSpecial(Sequences.ScriptEnd, 4); - } - else if (lower === Sequences.StyleEnd[3]) { - this.startSpecial(Sequences.StyleEnd, 4); - } - else { - this.state = State.InTagName; - this.stateInTagName(c); // Consume the token again - } - }; - Tokenizer.prototype.stateBeforeEntity = function (c) { - // Start excess with 1 to include the '&' - this.entityExcess = 1; - this.entityResult = 0; - if (c === CharCodes.Number) { - this.state = State.BeforeNumericEntity; - } - else if (c === CharCodes.Amp) { - // We have two `&` characters in a row. Stay in the current state. - } - else { - this.trieIndex = 0; - this.trieCurrent = this.entityTrie[0]; - this.state = State.InNamedEntity; - this.stateInNamedEntity(c); - } - }; - Tokenizer.prototype.stateInNamedEntity = function (c) { - this.entityExcess += 1; - this.trieIndex = (0, decode_js_1.determineBranch)(this.entityTrie, this.trieCurrent, this.trieIndex + 1, c); - if (this.trieIndex < 0) { - this.emitNamedEntity(); - this.index--; - return; - } - this.trieCurrent = this.entityTrie[this.trieIndex]; - var masked = this.trieCurrent & decode_js_1.BinTrieFlags.VALUE_LENGTH; - // If the branch is a value, store it and continue - if (masked) { - // The mask is the number of bytes of the value, including the current byte. - var valueLength = (masked >> 14) - 1; - // If we have a legacy entity while parsing strictly, just skip the number of bytes - if (!this.allowLegacyEntity() && c !== CharCodes.Semi) { - this.trieIndex += valueLength; + ? State.BeforeComment + : State.InDeclaration; } - else { + }; + Tokenizer.prototype.stateInDeclaration = function (c) { + if (c === CharCodes.Gt || this.fastForwardTo(CharCodes.Gt)) { + this.cbs.ondeclaration(this.sectionStart, this.index); + this.state = State.Text; + this.sectionStart = this.index + 1; + } + }; + Tokenizer.prototype.stateInProcessingInstruction = function (c) { + if (c === CharCodes.Gt || this.fastForwardTo(CharCodes.Gt)) { + this.cbs.onprocessinginstruction(this.sectionStart, this.index); + this.state = State.Text; + this.sectionStart = this.index + 1; + } + }; + Tokenizer.prototype.stateBeforeComment = function (c) { + if (c === CharCodes.Dash) { + this.state = State.InCommentLike; + this.currentSequence = Sequences.CommentEnd; + // Allow short comments (eg. ) + this.sequenceIndex = 2; + this.sectionStart = this.index + 1; + } else { + this.state = State.InDeclaration; + } + }; + Tokenizer.prototype.stateInSpecialComment = function (c) { + if (c === CharCodes.Gt || this.fastForwardTo(CharCodes.Gt)) { + this.cbs.oncomment(this.sectionStart, this.index, 0); + this.state = State.Text; + this.sectionStart = this.index + 1; + } + }; + Tokenizer.prototype.stateBeforeSpecialS = function (c) { + var lower = c | 0x20; + if (lower === Sequences.ScriptEnd[3]) { + this.startSpecial(Sequences.ScriptEnd, 4); + } else if (lower === Sequences.StyleEnd[3]) { + this.startSpecial(Sequences.StyleEnd, 4); + } else { + this.state = State.InTagName; + this.stateInTagName(c); // Consume the token again + } + }; + Tokenizer.prototype.stateBeforeEntity = function (c) { + // Start excess with 1 to include the '&' + this.entityExcess = 1; + this.entityResult = 0; + if (c === CharCodes.Number) { + this.state = State.BeforeNumericEntity; + } else if (c === CharCodes.Amp) { + // We have two `&` characters in a row. Stay in the current state. + } else { + this.trieIndex = 0; + this.trieCurrent = this.entityTrie[0]; + this.state = State.InNamedEntity; + this.stateInNamedEntity(c); + } + }; + Tokenizer.prototype.stateInNamedEntity = function (c) { + this.entityExcess += 1; + this.trieIndex = (0, decode_js_1.determineBranch)( + this.entityTrie, + this.trieCurrent, + this.trieIndex + 1, + c, + ); + if (this.trieIndex < 0) { + this.emitNamedEntity(); + this.index--; + return; + } + this.trieCurrent = this.entityTrie[this.trieIndex]; + var masked = + this.trieCurrent & decode_js_1.BinTrieFlags.VALUE_LENGTH; + // If the branch is a value, store it and continue + if (masked) { + // The mask is the number of bytes of the value, including the current byte. + var valueLength = (masked >> 14) - 1; + // If we have a legacy entity while parsing strictly, just skip the number of bytes + if (!this.allowLegacyEntity() && c !== CharCodes.Semi) { + this.trieIndex += valueLength; + } else { // Add 1 as we have already incremented the excess var entityStart = this.index - this.entityExcess + 1; if (entityStart > this.sectionStart) { - this.emitPartial(this.sectionStart, entityStart); + this.emitPartial(this.sectionStart, entityStart); } // If this is a surrogate pair, consume the next two bytes this.entityResult = this.trieIndex; @@ -6279,6332 +7549,7207 @@ var Tokenizer = /** @class */ (function () { this.entityExcess = 0; this.sectionStart = this.index + 1; if (valueLength === 0) { - this.emitNamedEntity(); + this.emitNamedEntity(); } + } } - } - }; - Tokenizer.prototype.emitNamedEntity = function () { - this.state = this.baseState; - if (this.entityResult === 0) { - return; - } - var valueLength = (this.entityTrie[this.entityResult] & decode_js_1.BinTrieFlags.VALUE_LENGTH) >> - 14; - switch (valueLength) { - case 1: { - this.emitCodePoint(this.entityTrie[this.entityResult] & - ~decode_js_1.BinTrieFlags.VALUE_LENGTH); + }; + Tokenizer.prototype.emitNamedEntity = function () { + this.state = this.baseState; + if (this.entityResult === 0) { + return; + } + var valueLength = + (this.entityTrie[this.entityResult] & + decode_js_1.BinTrieFlags.VALUE_LENGTH) >> + 14; + switch (valueLength) { + case 1: { + this.emitCodePoint( + this.entityTrie[this.entityResult] & + ~decode_js_1.BinTrieFlags.VALUE_LENGTH, + ); break; - } - case 2: { + } + case 2: { this.emitCodePoint(this.entityTrie[this.entityResult + 1]); break; - } - case 3: { + } + case 3: { this.emitCodePoint(this.entityTrie[this.entityResult + 1]); this.emitCodePoint(this.entityTrie[this.entityResult + 2]); + } } - } - }; - Tokenizer.prototype.stateBeforeNumericEntity = function (c) { - if ((c | 0x20) === CharCodes.LowerX) { - this.entityExcess++; - this.state = State.InHexEntity; - } - else { - this.state = State.InNumericEntity; - this.stateInNumericEntity(c); - } - }; - Tokenizer.prototype.emitNumericEntity = function (strict) { - var entityStart = this.index - this.entityExcess - 1; - var numberStart = entityStart + 2 + Number(this.state === State.InHexEntity); - if (numberStart !== this.index) { - // Emit leading data if any - if (entityStart > this.sectionStart) { + }; + Tokenizer.prototype.stateBeforeNumericEntity = function (c) { + if ((c | 0x20) === CharCodes.LowerX) { + this.entityExcess++; + this.state = State.InHexEntity; + } else { + this.state = State.InNumericEntity; + this.stateInNumericEntity(c); + } + }; + Tokenizer.prototype.emitNumericEntity = function (strict) { + var entityStart = this.index - this.entityExcess - 1; + var numberStart = + entityStart + 2 + Number(this.state === State.InHexEntity); + if (numberStart !== this.index) { + // Emit leading data if any + if (entityStart > this.sectionStart) { this.emitPartial(this.sectionStart, entityStart); + } + this.sectionStart = this.index + Number(strict); + this.emitCodePoint( + (0, decode_js_1.replaceCodePoint)(this.entityResult), + ); } - this.sectionStart = this.index + Number(strict); - this.emitCodePoint((0, decode_js_1.replaceCodePoint)(this.entityResult)); - } - this.state = this.baseState; - }; - Tokenizer.prototype.stateInNumericEntity = function (c) { - if (c === CharCodes.Semi) { - this.emitNumericEntity(true); - } - else if (isNumber(c)) { - this.entityResult = this.entityResult * 10 + (c - CharCodes.Zero); - this.entityExcess++; - } - else { - if (this.allowLegacyEntity()) { + this.state = this.baseState; + }; + Tokenizer.prototype.stateInNumericEntity = function (c) { + if (c === CharCodes.Semi) { + this.emitNumericEntity(true); + } else if (isNumber(c)) { + this.entityResult = this.entityResult * 10 + (c - CharCodes.Zero); + this.entityExcess++; + } else { + if (this.allowLegacyEntity()) { this.emitNumericEntity(false); - } - else { + } else { this.state = this.baseState; + } + this.index--; } - this.index--; - } - }; - Tokenizer.prototype.stateInHexEntity = function (c) { - if (c === CharCodes.Semi) { - this.emitNumericEntity(true); - } - else if (isNumber(c)) { - this.entityResult = this.entityResult * 16 + (c - CharCodes.Zero); - this.entityExcess++; - } - else if (isHexDigit(c)) { - this.entityResult = + }; + Tokenizer.prototype.stateInHexEntity = function (c) { + if (c === CharCodes.Semi) { + this.emitNumericEntity(true); + } else if (isNumber(c)) { + this.entityResult = this.entityResult * 16 + (c - CharCodes.Zero); + this.entityExcess++; + } else if (isHexDigit(c)) { + this.entityResult = this.entityResult * 16 + ((c | 0x20) - CharCodes.LowerA + 10); - this.entityExcess++; - } - else { - if (this.allowLegacyEntity()) { + this.entityExcess++; + } else { + if (this.allowLegacyEntity()) { this.emitNumericEntity(false); - } - else { + } else { this.state = this.baseState; + } + this.index--; } - this.index--; - } - }; - Tokenizer.prototype.allowLegacyEntity = function () { - return (!this.xmlMode && - (this.baseState === State.Text || - this.baseState === State.InSpecialTag)); - }; - /** - * Remove data that has already been consumed from the buffer. - */ - Tokenizer.prototype.cleanup = function () { - // If we are inside of text or attributes, emit what we already have. - if (this.running && this.sectionStart !== this.index) { - if (this.state === State.Text || - (this.state === State.InSpecialTag && this.sequenceIndex === 0)) { + }; + Tokenizer.prototype.allowLegacyEntity = function () { + return ( + !this.xmlMode && + (this.baseState === State.Text || + this.baseState === State.InSpecialTag) + ); + }; + /** + * Remove data that has already been consumed from the buffer. + */ + Tokenizer.prototype.cleanup = function () { + // If we are inside of text or attributes, emit what we already have. + if (this.running && this.sectionStart !== this.index) { + if ( + this.state === State.Text || + (this.state === State.InSpecialTag && this.sequenceIndex === 0) + ) { this.cbs.ontext(this.sectionStart, this.index); this.sectionStart = this.index; - } - else if (this.state === State.InAttributeValueDq || + } else if ( + this.state === State.InAttributeValueDq || this.state === State.InAttributeValueSq || - this.state === State.InAttributeValueNq) { + this.state === State.InAttributeValueNq + ) { this.cbs.onattribdata(this.sectionStart, this.index); this.sectionStart = this.index; + } } - } - }; - Tokenizer.prototype.shouldContinue = function () { - return this.index < this.buffer.length + this.offset && this.running; - }; - /** - * Iterates through the buffer, calling the function corresponding to the current state. - * - * States that are more likely to be hit are higher up, as a performance improvement. - */ - Tokenizer.prototype.parse = function () { - while (this.shouldContinue()) { - var c = this.buffer.charCodeAt(this.index - this.offset); - switch (this.state) { + }; + Tokenizer.prototype.shouldContinue = function () { + return ( + this.index < this.buffer.length + this.offset && this.running + ); + }; + /** + * Iterates through the buffer, calling the function corresponding to the current state. + * + * States that are more likely to be hit are higher up, as a performance improvement. + */ + Tokenizer.prototype.parse = function () { + while (this.shouldContinue()) { + var c = this.buffer.charCodeAt(this.index - this.offset); + switch (this.state) { case State.Text: { - this.stateText(c); - break; + this.stateText(c); + break; } case State.SpecialStartSequence: { - this.stateSpecialStartSequence(c); - break; + this.stateSpecialStartSequence(c); + break; } case State.InSpecialTag: { - this.stateInSpecialTag(c); - break; + this.stateInSpecialTag(c); + break; } case State.CDATASequence: { - this.stateCDATASequence(c); - break; + this.stateCDATASequence(c); + break; } case State.InAttributeValueDq: { - this.stateInAttributeValueDoubleQuotes(c); - break; + this.stateInAttributeValueDoubleQuotes(c); + break; } case State.InAttributeName: { - this.stateInAttributeName(c); - break; + this.stateInAttributeName(c); + break; } case State.InCommentLike: { - this.stateInCommentLike(c); - break; + this.stateInCommentLike(c); + break; } case State.InSpecialComment: { - this.stateInSpecialComment(c); - break; + this.stateInSpecialComment(c); + break; } case State.BeforeAttributeName: { - this.stateBeforeAttributeName(c); - break; + this.stateBeforeAttributeName(c); + break; } case State.InTagName: { - this.stateInTagName(c); - break; + this.stateInTagName(c); + break; } case State.InClosingTagName: { - this.stateInClosingTagName(c); - break; + this.stateInClosingTagName(c); + break; } case State.BeforeTagName: { - this.stateBeforeTagName(c); - break; + this.stateBeforeTagName(c); + break; } case State.AfterAttributeName: { - this.stateAfterAttributeName(c); - break; + this.stateAfterAttributeName(c); + break; } case State.InAttributeValueSq: { - this.stateInAttributeValueSingleQuotes(c); - break; + this.stateInAttributeValueSingleQuotes(c); + break; } case State.BeforeAttributeValue: { - this.stateBeforeAttributeValue(c); - break; + this.stateBeforeAttributeValue(c); + break; } case State.BeforeClosingTagName: { - this.stateBeforeClosingTagName(c); - break; + this.stateBeforeClosingTagName(c); + break; } case State.AfterClosingTagName: { - this.stateAfterClosingTagName(c); - break; + this.stateAfterClosingTagName(c); + break; } case State.BeforeSpecialS: { - this.stateBeforeSpecialS(c); - break; + this.stateBeforeSpecialS(c); + break; } case State.InAttributeValueNq: { - this.stateInAttributeValueNoQuotes(c); - break; + this.stateInAttributeValueNoQuotes(c); + break; } case State.InSelfClosingTag: { - this.stateInSelfClosingTag(c); - break; + this.stateInSelfClosingTag(c); + break; } case State.InDeclaration: { - this.stateInDeclaration(c); - break; + this.stateInDeclaration(c); + break; } case State.BeforeDeclaration: { - this.stateBeforeDeclaration(c); - break; + this.stateBeforeDeclaration(c); + break; } case State.BeforeComment: { - this.stateBeforeComment(c); - break; + this.stateBeforeComment(c); + break; } case State.InProcessingInstruction: { - this.stateInProcessingInstruction(c); - break; + this.stateInProcessingInstruction(c); + break; } case State.InNamedEntity: { - this.stateInNamedEntity(c); - break; + this.stateInNamedEntity(c); + break; } case State.BeforeEntity: { - this.stateBeforeEntity(c); - break; + this.stateBeforeEntity(c); + break; } case State.InHexEntity: { - this.stateInHexEntity(c); - break; + this.stateInHexEntity(c); + break; } case State.InNumericEntity: { - this.stateInNumericEntity(c); - break; + this.stateInNumericEntity(c); + break; } default: { - // `this._state === State.BeforeNumericEntity` - this.stateBeforeNumericEntity(c); + // `this._state === State.BeforeNumericEntity` + this.stateBeforeNumericEntity(c); } + } + this.index++; } - this.index++; - } - this.cleanup(); - }; - Tokenizer.prototype.finish = function () { - if (this.state === State.InNamedEntity) { - this.emitNamedEntity(); - } - // If there is remaining data, emit it in a reasonable way - if (this.sectionStart < this.index) { - this.handleTrailingData(); - } - this.cbs.onend(); - }; - /** Handle any trailing data. */ - Tokenizer.prototype.handleTrailingData = function () { - var endIndex = this.buffer.length + this.offset; - if (this.state === State.InCommentLike) { - if (this.currentSequence === Sequences.CdataEnd) { + this.cleanup(); + }; + Tokenizer.prototype.finish = function () { + if (this.state === State.InNamedEntity) { + this.emitNamedEntity(); + } + // If there is remaining data, emit it in a reasonable way + if (this.sectionStart < this.index) { + this.handleTrailingData(); + } + this.cbs.onend(); + }; + /** Handle any trailing data. */ + Tokenizer.prototype.handleTrailingData = function () { + var endIndex = this.buffer.length + this.offset; + if (this.state === State.InCommentLike) { + if (this.currentSequence === Sequences.CdataEnd) { this.cbs.oncdata(this.sectionStart, endIndex, 0); - } - else { - this.cbs.oncomment(this.sectionStart, endIndex, 0); - } - } - else if (this.state === State.InNumericEntity && - this.allowLegacyEntity()) { - this.emitNumericEntity(false); - // All trailing data will have been consumed - } - else if (this.state === State.InHexEntity && - this.allowLegacyEntity()) { - this.emitNumericEntity(false); - // All trailing data will have been consumed - } - else if (this.state === State.InTagName || - this.state === State.BeforeAttributeName || - this.state === State.BeforeAttributeValue || - this.state === State.AfterAttributeName || - this.state === State.InAttributeName || - this.state === State.InAttributeValueSq || - this.state === State.InAttributeValueDq || - this.state === State.InAttributeValueNq || - this.state === State.InClosingTagName) { - /* - * If we are currently in an opening or closing tag, us not calling the - * respective callback signals that the tag should be ignored. - */ - } - else { - this.cbs.ontext(this.sectionStart, endIndex); - } - }; - Tokenizer.prototype.emitPartial = function (start, endIndex) { - if (this.baseState !== State.Text && - this.baseState !== State.InSpecialTag) { - this.cbs.onattribdata(start, endIndex); - } - else { - this.cbs.ontext(start, endIndex); - } - }; - Tokenizer.prototype.emitCodePoint = function (cp) { - if (this.baseState !== State.Text && - this.baseState !== State.InSpecialTag) { - this.cbs.onattribentity(cp); - } - else { - this.cbs.ontextentity(cp); - } - }; - return Tokenizer; -}()); -exports.default = Tokenizer; - -},{"entities/lib/decode.js":18}],29:[function(require,module,exports){ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DomUtils = exports.parseFeed = exports.getFeed = exports.ElementType = exports.Tokenizer = exports.createDomStream = exports.parseDOM = exports.parseDocument = exports.DefaultHandler = exports.DomHandler = exports.Parser = void 0; -var Parser_js_1 = require("./Parser.js"); -var Parser_js_2 = require("./Parser.js"); -Object.defineProperty(exports, "Parser", { enumerable: true, get: function () { return Parser_js_2.Parser; } }); -var domhandler_1 = require("domhandler"); -var domhandler_2 = require("domhandler"); -Object.defineProperty(exports, "DomHandler", { enumerable: true, get: function () { return domhandler_2.DomHandler; } }); -// Old name for DomHandler -Object.defineProperty(exports, "DefaultHandler", { enumerable: true, get: function () { return domhandler_2.DomHandler; } }); -// Helper methods -/** - * Parses the data, returns the resulting document. - * - * @param data The data that should be parsed. - * @param options Optional options for the parser and DOM builder. - */ -function parseDocument(data, options) { - var handler = new domhandler_1.DomHandler(undefined, options); - new Parser_js_1.Parser(handler, options).end(data); - return handler.root; -} -exports.parseDocument = parseDocument; -/** - * Parses data, returns an array of the root nodes. - * - * Note that the root nodes still have a `Document` node as their parent. - * Use `parseDocument` to get the `Document` node instead. - * - * @param data The data that should be parsed. - * @param options Optional options for the parser and DOM builder. - * @deprecated Use `parseDocument` instead. - */ -function parseDOM(data, options) { - return parseDocument(data, options).children; -} -exports.parseDOM = parseDOM; -/** - * Creates a parser instance, with an attached DOM handler. - * - * @param callback A callback that will be called once parsing has been completed. - * @param options Optional options for the parser and DOM builder. - * @param elementCallback An optional callback that will be called every time a tag has been completed inside of the DOM. - */ -function createDomStream(callback, options, elementCallback) { - var handler = new domhandler_1.DomHandler(callback, options, elementCallback); - return new Parser_js_1.Parser(handler, options); -} -exports.createDomStream = createDomStream; -var Tokenizer_js_1 = require("./Tokenizer.js"); -Object.defineProperty(exports, "Tokenizer", { enumerable: true, get: function () { return __importDefault(Tokenizer_js_1).default; } }); -/* - * All of the following exports exist for backwards-compatibility. - * They should probably be removed eventually. - */ -exports.ElementType = __importStar(require("domelementtype")); -var domutils_1 = require("domutils"); -var domutils_2 = require("domutils"); -Object.defineProperty(exports, "getFeed", { enumerable: true, get: function () { return domutils_2.getFeed; } }); -var parseFeedDefaultOptions = { xmlMode: true }; -/** - * Parse a feed. - * - * @param feed The feed that should be parsed, as a string. - * @param options Optionally, options for parsing. When using this, you should set `xmlMode` to `true`. - */ -function parseFeed(feed, options) { - if (options === void 0) { options = parseFeedDefaultOptions; } - return (0, domutils_1.getFeed)(parseDOM(feed, options)); -} -exports.parseFeed = parseFeed; -exports.DomUtils = __importStar(require("domutils")); - -},{"./Parser.js":27,"./Tokenizer.js":28,"domelementtype":7,"domhandler":8,"domutils":12}],30:[function(require,module,exports){ -/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ -exports.read = function (buffer, offset, isLE, mLen, nBytes) { - var e, m - var eLen = (nBytes * 8) - mLen - 1 - var eMax = (1 << eLen) - 1 - var eBias = eMax >> 1 - var nBits = -7 - var i = isLE ? (nBytes - 1) : 0 - var d = isLE ? -1 : 1 - var s = buffer[offset + i] - - i += d - - e = s & ((1 << (-nBits)) - 1) - s >>= (-nBits) - nBits += eLen - for (; nBits > 0; e = (e * 256) + buffer[offset + i], i += d, nBits -= 8) {} - - m = e & ((1 << (-nBits)) - 1) - e >>= (-nBits) - nBits += mLen - for (; nBits > 0; m = (m * 256) + buffer[offset + i], i += d, nBits -= 8) {} - - if (e === 0) { - e = 1 - eBias - } else if (e === eMax) { - return m ? NaN : ((s ? -1 : 1) * Infinity) - } else { - m = m + Math.pow(2, mLen) - e = e - eBias - } - return (s ? -1 : 1) * m * Math.pow(2, e - mLen) -} - -exports.write = function (buffer, value, offset, isLE, mLen, nBytes) { - var e, m, c - var eLen = (nBytes * 8) - mLen - 1 - var eMax = (1 << eLen) - 1 - var eBias = eMax >> 1 - var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0) - var i = isLE ? 0 : (nBytes - 1) - var d = isLE ? 1 : -1 - var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0 - - value = Math.abs(value) - - if (isNaN(value) || value === Infinity) { - m = isNaN(value) ? 1 : 0 - e = eMax - } else { - e = Math.floor(Math.log(value) / Math.LN2) - if (value * (c = Math.pow(2, -e)) < 1) { - e-- - c *= 2 - } - if (e + eBias >= 1) { - value += rt / c - } else { - value += rt * Math.pow(2, 1 - eBias) - } - if (value * c >= 2) { - e++ - c /= 2 - } - - if (e + eBias >= eMax) { - m = 0 - e = eMax - } else if (e + eBias >= 1) { - m = ((value * c) - 1) * Math.pow(2, mLen) - e = e + eBias - } else { - m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen) - e = 0 - } - } - - for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {} - - e = (e << mLen) | m - eLen += mLen - for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {} - - buffer[offset + i - d] |= s * 128 -} - -},{}],31:[function(require,module,exports){ -'use strict'; - -Object.defineProperty(exports, '__esModule', { value: true }); - -/*! - * is-plain-object - * - * Copyright (c) 2014-2017, Jon Schlinkert. - * Released under the MIT License. - */ - -function isObject(o) { - return Object.prototype.toString.call(o) === '[object Object]'; -} - -function isPlainObject(o) { - var ctor,prot; - - if (isObject(o) === false) return false; - - // If has modified constructor - ctor = o.constructor; - if (ctor === undefined) return true; - - // If has modified prototype - prot = ctor.prototype; - if (isObject(prot) === false) return false; - - // If constructor does not have an Object-specific method - if (prot.hasOwnProperty('isPrototypeOf') === false) { - return false; - } - - // Most likely a plain Object - return true; -} - -exports.isPlainObject = isPlainObject; - -},{}],32:[function(require,module,exports){ -let urlAlphabet = - 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' -let customAlphabet = (alphabet, defaultSize = 21) => { - return (size = defaultSize) => { - let id = '' - let i = size - while (i--) { - id += alphabet[(Math.random() * alphabet.length) | 0] - } - return id - } -} -let nanoid = (size = 21) => { - let id = '' - let i = size - while (i--) { - id += urlAlphabet[(Math.random() * 64) | 0] - } - return id -} -module.exports = { nanoid, customAlphabet } - -},{}],33:[function(require,module,exports){ -/** - * Srcset Parser - * - * By Alex Bell | MIT License - * - * JS Parser for the string value that appears in markup - * - * @returns Array [{url: _, d: _, w: _, h:_}, ...] - * - * Based super duper closely on the reference algorithm at: - * https://html.spec.whatwg.org/multipage/embedded-content.html#parse-a-srcset-attribute - * - * Most comments are copied in directly from the spec - * (except for comments in parens). - */ - -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define([], factory); - } else if (typeof module === 'object' && module.exports) { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - root.parseSrcset = factory(); - } -}(this, function () { - - // 1. Let input be the value passed to this algorithm. - return function (input) { - - // UTILITY FUNCTIONS - - // Manual is faster than RegEx - // http://bjorn.tipling.com/state-and-regular-expressions-in-javascript - // http://jsperf.com/whitespace-character/5 - function isSpace(c) { - return (c === "\u0020" || // space - c === "\u0009" || // horizontal tab - c === "\u000A" || // new line - c === "\u000C" || // form feed - c === "\u000D"); // carriage return - } - - function collectCharacters(regEx) { - var chars, - match = regEx.exec(input.substring(pos)); - if (match) { - chars = match[ 0 ]; - pos += chars.length; - return chars; - } - } - - var inputLength = input.length, - - // (Don't use \s, to avoid matching non-breaking space) - regexLeadingSpaces = /^[ \t\n\r\u000c]+/, - regexLeadingCommasOrSpaces = /^[, \t\n\r\u000c]+/, - regexLeadingNotSpaces = /^[^ \t\n\r\u000c]+/, - regexTrailingCommas = /[,]+$/, - regexNonNegativeInteger = /^\d+$/, - - // ( Positive or negative or unsigned integers or decimals, without or without exponents. - // Must include at least one digit. - // According to spec tests any decimal point must be followed by a digit. - // No leading plus sign is allowed.) - // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number - regexFloatingPoint = /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/, - - url, - descriptors, - currentDescriptor, - state, - c, - - // 2. Let position be a pointer into input, initially pointing at the start - // of the string. - pos = 0, - - // 3. Let candidates be an initially empty source set. - candidates = []; - - // 4. Splitting loop: Collect a sequence of characters that are space - // characters or U+002C COMMA characters. If any U+002C COMMA characters - // were collected, that is a parse error. - while (true) { - collectCharacters(regexLeadingCommasOrSpaces); - - // 5. If position is past the end of input, return candidates and abort these steps. - if (pos >= inputLength) { - return candidates; // (we're done, this is the sole return path) - } - - // 6. Collect a sequence of characters that are not space characters, - // and let that be url. - url = collectCharacters(regexLeadingNotSpaces); - - // 7. Let descriptors be a new empty list. - descriptors = []; - - // 8. If url ends with a U+002C COMMA character (,), follow these substeps: - // (1). Remove all trailing U+002C COMMA characters from url. If this removed - // more than one character, that is a parse error. - if (url.slice(-1) === ",") { - url = url.replace(regexTrailingCommas, ""); - // (Jump ahead to step 9 to skip tokenization and just push the candidate). - parseDescriptors(); - - // Otherwise, follow these substeps: - } else { - tokenize(); - } // (close else of step 8) - - // 16. Return to the step labeled splitting loop. - } // (Close of big while loop.) - - /** - * Tokenizes descriptor properties prior to parsing - * Returns undefined. - */ - function tokenize() { - - // 8.1. Descriptor tokeniser: Skip whitespace - collectCharacters(regexLeadingSpaces); - - // 8.2. Let current descriptor be the empty string. - currentDescriptor = ""; - - // 8.3. Let state be in descriptor. - state = "in descriptor"; - - while (true) { - - // 8.4. Let c be the character at position. - c = input.charAt(pos); - - // Do the following depending on the value of state. - // For the purpose of this step, "EOF" is a special character representing - // that position is past the end of input. - - // In descriptor - if (state === "in descriptor") { - // Do the following, depending on the value of c: - - // Space character - // If current descriptor is not empty, append current descriptor to - // descriptors and let current descriptor be the empty string. - // Set state to after descriptor. - if (isSpace(c)) { - if (currentDescriptor) { - descriptors.push(currentDescriptor); - currentDescriptor = ""; - state = "after descriptor"; - } - - // U+002C COMMA (,) - // Advance position to the next character in input. If current descriptor - // is not empty, append current descriptor to descriptors. Jump to the step - // labeled descriptor parser. - } else if (c === ",") { - pos += 1; - if (currentDescriptor) { - descriptors.push(currentDescriptor); - } - parseDescriptors(); - return; - - // U+0028 LEFT PARENTHESIS (() - // Append c to current descriptor. Set state to in parens. - } else if (c === "\u0028") { - currentDescriptor = currentDescriptor + c; - state = "in parens"; - - // EOF - // If current descriptor is not empty, append current descriptor to - // descriptors. Jump to the step labeled descriptor parser. - } else if (c === "") { - if (currentDescriptor) { - descriptors.push(currentDescriptor); - } - parseDescriptors(); - return; - - // Anything else - // Append c to current descriptor. - } else { - currentDescriptor = currentDescriptor + c; - } - // (end "in descriptor" - - // In parens - } else if (state === "in parens") { - - // U+0029 RIGHT PARENTHESIS ()) - // Append c to current descriptor. Set state to in descriptor. - if (c === ")") { - currentDescriptor = currentDescriptor + c; - state = "in descriptor"; - - // EOF - // Append current descriptor to descriptors. Jump to the step labeled - // descriptor parser. - } else if (c === "") { - descriptors.push(currentDescriptor); - parseDescriptors(); - return; - - // Anything else - // Append c to current descriptor. - } else { - currentDescriptor = currentDescriptor + c; - } - - // After descriptor - } else if (state === "after descriptor") { - - // Do the following, depending on the value of c: - // Space character: Stay in this state. - if (isSpace(c)) { - - // EOF: Jump to the step labeled descriptor parser. - } else if (c === "") { - parseDescriptors(); - return; - - // Anything else - // Set state to in descriptor. Set position to the previous character in input. - } else { - state = "in descriptor"; - pos -= 1; - - } - } - - // Advance position to the next character in input. - pos += 1; - - // Repeat this step. - } // (close while true loop) - } - - /** - * Adds descriptor properties to a candidate, pushes to the candidates array - * @return undefined - */ - // Declared outside of the while loop so that it's only created once. - function parseDescriptors() { - - // 9. Descriptor parser: Let error be no. - var pError = false, - - // 10. Let width be absent. - // 11. Let density be absent. - // 12. Let future-compat-h be absent. (We're implementing it now as h) - w, d, h, i, - candidate = {}, - desc, lastChar, value, intVal, floatVal; - - // 13. For each descriptor in descriptors, run the appropriate set of steps - // from the following list: - for (i = 0 ; i < descriptors.length; i++) { - desc = descriptors[ i ]; - - lastChar = desc[ desc.length - 1 ]; - value = desc.substring(0, desc.length - 1); - intVal = parseInt(value, 10); - floatVal = parseFloat(value); - - // If the descriptor consists of a valid non-negative integer followed by - // a U+0077 LATIN SMALL LETTER W character - if (regexNonNegativeInteger.test(value) && (lastChar === "w")) { - - // If width and density are not both absent, then let error be yes. - if (w || d) {pError = true;} - - // Apply the rules for parsing non-negative integers to the descriptor. - // If the result is zero, let error be yes. - // Otherwise, let width be the result. - if (intVal === 0) {pError = true;} else {w = intVal;} - - // If the descriptor consists of a valid floating-point number followed by - // a U+0078 LATIN SMALL LETTER X character - } else if (regexFloatingPoint.test(value) && (lastChar === "x")) { - - // If width, density and future-compat-h are not all absent, then let error - // be yes. - if (w || d || h) {pError = true;} - - // Apply the rules for parsing floating-point number values to the descriptor. - // If the result is less than zero, let error be yes. Otherwise, let density - // be the result. - if (floatVal < 0) {pError = true;} else {d = floatVal;} - - // If the descriptor consists of a valid non-negative integer followed by - // a U+0068 LATIN SMALL LETTER H character - } else if (regexNonNegativeInteger.test(value) && (lastChar === "h")) { - - // If height and density are not both absent, then let error be yes. - if (h || d) {pError = true;} - - // Apply the rules for parsing non-negative integers to the descriptor. - // If the result is zero, let error be yes. Otherwise, let future-compat-h - // be the result. - if (intVal === 0) {pError = true;} else {h = intVal;} - - // Anything else, Let error be yes. - } else {pError = true;} - } // (close step 13 for loop) - - // 15. If error is still no, then append a new image source to candidates whose - // URL is url, associated with a width width if not absent and a pixel - // density density if not absent. Otherwise, there is a parse error. - if (!pError) { - candidate.url = url; - if (w) { candidate.w = w;} - if (d) { candidate.d = d;} - if (h) { candidate.h = h;} - candidates.push(candidate); - } else if (console && console.log) { - console.log("Invalid srcset descriptor found in '" + - input + "' at '" + desc + "'."); - } - } // (close parseDescriptors fn) - - } -})); - -},{}],34:[function(require,module,exports){ -var x=String; -var create=function() {return {isColorSupported:false,reset:x,bold:x,dim:x,italic:x,underline:x,inverse:x,hidden:x,strikethrough:x,black:x,red:x,green:x,yellow:x,blue:x,magenta:x,cyan:x,white:x,gray:x,bgBlack:x,bgRed:x,bgGreen:x,bgYellow:x,bgBlue:x,bgMagenta:x,bgCyan:x,bgWhite:x}}; -module.exports=create(); -module.exports.createColors = create; - -},{}],35:[function(require,module,exports){ -'use strict' - -let Container = require('./container') - -class AtRule extends Container { - constructor(defaults) { - super(defaults) - this.type = 'atrule' - } - - append(...children) { - if (!this.proxyOf.nodes) this.nodes = [] - return super.append(...children) - } - - prepend(...children) { - if (!this.proxyOf.nodes) this.nodes = [] - return super.prepend(...children) - } -} - -module.exports = AtRule -AtRule.default = AtRule - -Container.registerAtRule(AtRule) - -},{"./container":37}],36:[function(require,module,exports){ -'use strict' - -let Node = require('./node') - -class Comment extends Node { - constructor(defaults) { - super(defaults) - this.type = 'comment' - } -} - -module.exports = Comment -Comment.default = Comment - -},{"./node":47}],37:[function(require,module,exports){ -'use strict' - -let { isClean, my } = require('./symbols') -let Declaration = require('./declaration') -let Comment = require('./comment') -let Node = require('./node') - -let parse, Rule, AtRule, Root - -function cleanSource(nodes) { - return nodes.map(i => { - if (i.nodes) i.nodes = cleanSource(i.nodes) - delete i.source - return i - }) -} - -function markDirtyUp(node) { - node[isClean] = false - if (node.proxyOf.nodes) { - for (let i of node.proxyOf.nodes) { - markDirtyUp(i) - } - } -} - -class Container extends Node { - append(...children) { - for (let child of children) { - let nodes = this.normalize(child, this.last) - for (let node of nodes) this.proxyOf.nodes.push(node) - } - - this.markDirty() - - return this - } - - cleanRaws(keepBetween) { - super.cleanRaws(keepBetween) - if (this.nodes) { - for (let node of this.nodes) node.cleanRaws(keepBetween) - } - } - - each(callback) { - if (!this.proxyOf.nodes) return undefined - let iterator = this.getIterator() - - let index, result - while (this.indexes[iterator] < this.proxyOf.nodes.length) { - index = this.indexes[iterator] - result = callback(this.proxyOf.nodes[index], index) - if (result === false) break - - this.indexes[iterator] += 1 - } - - delete this.indexes[iterator] - return result - } - - every(condition) { - return this.nodes.every(condition) - } - - getIterator() { - if (!this.lastEach) this.lastEach = 0 - if (!this.indexes) this.indexes = {} - - this.lastEach += 1 - let iterator = this.lastEach - this.indexes[iterator] = 0 - - return iterator - } - - getProxyProcessor() { - return { - get(node, prop) { - if (prop === 'proxyOf') { - return node - } else if (!node[prop]) { - return node[prop] - } else if ( - prop === 'each' || - (typeof prop === 'string' && prop.startsWith('walk')) - ) { - return (...args) => { - return node[prop]( - ...args.map(i => { - if (typeof i === 'function') { - return (child, index) => i(child.toProxy(), index) - } else { - return i - } - }) - ) - } - } else if (prop === 'every' || prop === 'some') { - return cb => { - return node[prop]((child, ...other) => - cb(child.toProxy(), ...other) - ) - } - } else if (prop === 'root') { - return () => node.root().toProxy() - } else if (prop === 'nodes') { - return node.nodes.map(i => i.toProxy()) - } else if (prop === 'first' || prop === 'last') { - return node[prop].toProxy() - } else { - return node[prop] - } - }, - - set(node, prop, value) { - if (node[prop] === value) return true - node[prop] = value - if (prop === 'name' || prop === 'params' || prop === 'selector') { - node.markDirty() - } - return true - } - } - } - - index(child) { - if (typeof child === 'number') return child - if (child.proxyOf) child = child.proxyOf - return this.proxyOf.nodes.indexOf(child) - } - - insertAfter(exist, add) { - let existIndex = this.index(exist) - let nodes = this.normalize(add, this.proxyOf.nodes[existIndex]).reverse() - existIndex = this.index(exist) - for (let node of nodes) this.proxyOf.nodes.splice(existIndex + 1, 0, node) - - let index - for (let id in this.indexes) { - index = this.indexes[id] - if (existIndex < index) { - this.indexes[id] = index + nodes.length - } - } - - this.markDirty() - - return this - } - - insertBefore(exist, add) { - let existIndex = this.index(exist) - let type = existIndex === 0 ? 'prepend' : false - let nodes = this.normalize(add, this.proxyOf.nodes[existIndex], type).reverse() - existIndex = this.index(exist) - for (let node of nodes) this.proxyOf.nodes.splice(existIndex, 0, node) - - let index - for (let id in this.indexes) { - index = this.indexes[id] - if (existIndex <= index) { - this.indexes[id] = index + nodes.length - } - } - - this.markDirty() - - return this - } - - normalize(nodes, sample) { - if (typeof nodes === 'string') { - nodes = cleanSource(parse(nodes).nodes) - } else if (Array.isArray(nodes)) { - nodes = nodes.slice(0) - for (let i of nodes) { - if (i.parent) i.parent.removeChild(i, 'ignore') - } - } else if (nodes.type === 'root' && this.type !== 'document') { - nodes = nodes.nodes.slice(0) - for (let i of nodes) { - if (i.parent) i.parent.removeChild(i, 'ignore') - } - } else if (nodes.type) { - nodes = [nodes] - } else if (nodes.prop) { - if (typeof nodes.value === 'undefined') { - throw new Error('Value field is missed in node creation') - } else if (typeof nodes.value !== 'string') { - nodes.value = String(nodes.value) - } - nodes = [new Declaration(nodes)] - } else if (nodes.selector) { - nodes = [new Rule(nodes)] - } else if (nodes.name) { - nodes = [new AtRule(nodes)] - } else if (nodes.text) { - nodes = [new Comment(nodes)] - } else { - throw new Error('Unknown node type in node creation') - } - - let processed = nodes.map(i => { - /* c8 ignore next */ - if (!i[my]) Container.rebuild(i) - i = i.proxyOf - if (i.parent) i.parent.removeChild(i) - if (i[isClean]) markDirtyUp(i) - if (typeof i.raws.before === 'undefined') { - if (sample && typeof sample.raws.before !== 'undefined') { - i.raws.before = sample.raws.before.replace(/\S/g, '') - } - } - i.parent = this.proxyOf - return i - }) - - return processed - } - - prepend(...children) { - children = children.reverse() - for (let child of children) { - let nodes = this.normalize(child, this.first, 'prepend').reverse() - for (let node of nodes) this.proxyOf.nodes.unshift(node) - for (let id in this.indexes) { - this.indexes[id] = this.indexes[id] + nodes.length - } - } - - this.markDirty() - - return this - } - - push(child) { - child.parent = this - this.proxyOf.nodes.push(child) - return this - } - - removeAll() { - for (let node of this.proxyOf.nodes) node.parent = undefined - this.proxyOf.nodes = [] - - this.markDirty() - - return this - } - - removeChild(child) { - child = this.index(child) - this.proxyOf.nodes[child].parent = undefined - this.proxyOf.nodes.splice(child, 1) - - let index - for (let id in this.indexes) { - index = this.indexes[id] - if (index >= child) { - this.indexes[id] = index - 1 - } - } - - this.markDirty() - - return this - } - - replaceValues(pattern, opts, callback) { - if (!callback) { - callback = opts - opts = {} - } - - this.walkDecls(decl => { - if (opts.props && !opts.props.includes(decl.prop)) return - if (opts.fast && !decl.value.includes(opts.fast)) return - - decl.value = decl.value.replace(pattern, callback) - }) - - this.markDirty() - - return this - } - - some(condition) { - return this.nodes.some(condition) - } - - walk(callback) { - return this.each((child, i) => { - let result - try { - result = callback(child, i) - } catch (e) { - throw child.addToError(e) - } - if (result !== false && child.walk) { - result = child.walk(callback) - } - - return result - }) - } - - walkAtRules(name, callback) { - if (!callback) { - callback = name - return this.walk((child, i) => { - if (child.type === 'atrule') { - return callback(child, i) - } - }) - } - if (name instanceof RegExp) { - return this.walk((child, i) => { - if (child.type === 'atrule' && name.test(child.name)) { - return callback(child, i) - } - }) - } - return this.walk((child, i) => { - if (child.type === 'atrule' && child.name === name) { - return callback(child, i) - } - }) - } - - walkComments(callback) { - return this.walk((child, i) => { - if (child.type === 'comment') { - return callback(child, i) - } - }) - } - - walkDecls(prop, callback) { - if (!callback) { - callback = prop - return this.walk((child, i) => { - if (child.type === 'decl') { - return callback(child, i) - } - }) - } - if (prop instanceof RegExp) { - return this.walk((child, i) => { - if (child.type === 'decl' && prop.test(child.prop)) { - return callback(child, i) - } - }) - } - return this.walk((child, i) => { - if (child.type === 'decl' && child.prop === prop) { - return callback(child, i) - } - }) - } - - walkRules(selector, callback) { - if (!callback) { - callback = selector - - return this.walk((child, i) => { - if (child.type === 'rule') { - return callback(child, i) - } - }) - } - if (selector instanceof RegExp) { - return this.walk((child, i) => { - if (child.type === 'rule' && selector.test(child.selector)) { - return callback(child, i) - } - }) - } - return this.walk((child, i) => { - if (child.type === 'rule' && child.selector === selector) { - return callback(child, i) - } - }) - } - - get first() { - if (!this.proxyOf.nodes) return undefined - return this.proxyOf.nodes[0] - } - - get last() { - if (!this.proxyOf.nodes) return undefined - return this.proxyOf.nodes[this.proxyOf.nodes.length - 1] - } -} - -Container.registerParse = dependant => { - parse = dependant -} - -Container.registerRule = dependant => { - Rule = dependant -} - -Container.registerAtRule = dependant => { - AtRule = dependant -} - -Container.registerRoot = dependant => { - Root = dependant -} - -module.exports = Container -Container.default = Container - -/* c8 ignore start */ -Container.rebuild = node => { - if (node.type === 'atrule') { - Object.setPrototypeOf(node, AtRule.prototype) - } else if (node.type === 'rule') { - Object.setPrototypeOf(node, Rule.prototype) - } else if (node.type === 'decl') { - Object.setPrototypeOf(node, Declaration.prototype) - } else if (node.type === 'comment') { - Object.setPrototypeOf(node, Comment.prototype) - } else if (node.type === 'root') { - Object.setPrototypeOf(node, Root.prototype) - } - - node[my] = true - - if (node.nodes) { - node.nodes.forEach(child => { - Container.rebuild(child) - }) - } -} -/* c8 ignore stop */ - -},{"./comment":36,"./declaration":39,"./node":47,"./symbols":58}],38:[function(require,module,exports){ -'use strict' - -let pico = require('picocolors') - -let terminalHighlight = require('./terminal-highlight') - -class CssSyntaxError extends Error { - constructor(message, line, column, source, file, plugin) { - super(message) - this.name = 'CssSyntaxError' - this.reason = message - - if (file) { - this.file = file - } - if (source) { - this.source = source - } - if (plugin) { - this.plugin = plugin - } - if (typeof line !== 'undefined' && typeof column !== 'undefined') { - if (typeof line === 'number') { - this.line = line - this.column = column - } else { - this.line = line.line - this.column = line.column - this.endLine = column.line - this.endColumn = column.column - } - } - - this.setMessage() - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, CssSyntaxError) - } - } - - setMessage() { - this.message = this.plugin ? this.plugin + ': ' : '' - this.message += this.file ? this.file : '' - if (typeof this.line !== 'undefined') { - this.message += ':' + this.line + ':' + this.column - } - this.message += ': ' + this.reason - } - - showSourceCode(color) { - if (!this.source) return '' - - let css = this.source - if (color == null) color = pico.isColorSupported - if (terminalHighlight) { - if (color) css = terminalHighlight(css) - } - - let lines = css.split(/\r?\n/) - let start = Math.max(this.line - 3, 0) - let end = Math.min(this.line + 2, lines.length) - - let maxWidth = String(end).length - - let mark, aside - if (color) { - let { bold, gray, red } = pico.createColors(true) - mark = text => bold(red(text)) - aside = text => gray(text) - } else { - mark = aside = str => str - } - - return lines - .slice(start, end) - .map((line, index) => { - let number = start + 1 + index - let gutter = ' ' + (' ' + number).slice(-maxWidth) + ' | ' - if (number === this.line) { - let spacing = - aside(gutter.replace(/\d/g, ' ')) + - line.slice(0, this.column - 1).replace(/[^\t]/g, ' ') - return mark('>') + aside(gutter) + line + '\n ' + spacing + mark('^') - } - return ' ' + aside(gutter) + line - }) - .join('\n') - } - - toString() { - let code = this.showSourceCode() - if (code) { - code = '\n\n' + code + '\n' - } - return this.name + ': ' + this.message + code - } -} - -module.exports = CssSyntaxError -CssSyntaxError.default = CssSyntaxError - -},{"./terminal-highlight":2,"picocolors":34}],39:[function(require,module,exports){ -'use strict' - -let Node = require('./node') - -class Declaration extends Node { - constructor(defaults) { - if ( - defaults && - typeof defaults.value !== 'undefined' && - typeof defaults.value !== 'string' - ) { - defaults = { ...defaults, value: String(defaults.value) } - } - super(defaults) - this.type = 'decl' - } - - get variable() { - return this.prop.startsWith('--') || this.prop[0] === '$' - } -} - -module.exports = Declaration -Declaration.default = Declaration - -},{"./node":47}],40:[function(require,module,exports){ -'use strict' - -let Container = require('./container') - -let LazyResult, Processor - -class Document extends Container { - constructor(defaults) { - // type needs to be passed to super, otherwise child roots won't be normalized correctly - super({ type: 'document', ...defaults }) - - if (!this.nodes) { - this.nodes = [] - } - } - - toResult(opts = {}) { - let lazy = new LazyResult(new Processor(), this, opts) - - return lazy.stringify() - } -} - -Document.registerLazyResult = dependant => { - LazyResult = dependant -} - -Document.registerProcessor = dependant => { - Processor = dependant -} - -module.exports = Document -Document.default = Document - -},{"./container":37}],41:[function(require,module,exports){ -'use strict' - -let Declaration = require('./declaration') -let PreviousMap = require('./previous-map') -let Comment = require('./comment') -let AtRule = require('./at-rule') -let Input = require('./input') -let Root = require('./root') -let Rule = require('./rule') - -function fromJSON(json, inputs) { - if (Array.isArray(json)) return json.map(n => fromJSON(n)) - - let { inputs: ownInputs, ...defaults } = json - if (ownInputs) { - inputs = [] - for (let input of ownInputs) { - let inputHydrated = { ...input, __proto__: Input.prototype } - if (inputHydrated.map) { - inputHydrated.map = { - ...inputHydrated.map, - __proto__: PreviousMap.prototype - } - } - inputs.push(inputHydrated) - } - } - if (defaults.nodes) { - defaults.nodes = json.nodes.map(n => fromJSON(n, inputs)) - } - if (defaults.source) { - let { inputId, ...source } = defaults.source - defaults.source = source - if (inputId != null) { - defaults.source.input = inputs[inputId] - } - } - if (defaults.type === 'root') { - return new Root(defaults) - } else if (defaults.type === 'decl') { - return new Declaration(defaults) - } else if (defaults.type === 'rule') { - return new Rule(defaults) - } else if (defaults.type === 'comment') { - return new Comment(defaults) - } else if (defaults.type === 'atrule') { - return new AtRule(defaults) - } else { - throw new Error('Unknown node type: ' + json.type) - } -} - -module.exports = fromJSON -fromJSON.default = fromJSON - -},{"./at-rule":35,"./comment":36,"./declaration":39,"./input":42,"./previous-map":51,"./root":54,"./rule":55}],42:[function(require,module,exports){ -'use strict' - -let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js') -let { fileURLToPath, pathToFileURL } = require('url') -let { isAbsolute, resolve } = require('path') -let { nanoid } = require('nanoid/non-secure') - -let terminalHighlight = require('./terminal-highlight') -let CssSyntaxError = require('./css-syntax-error') -let PreviousMap = require('./previous-map') - -let fromOffsetCache = Symbol('fromOffsetCache') - -let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator) -let pathAvailable = Boolean(resolve && isAbsolute) - -class Input { - constructor(css, opts = {}) { - if ( - css === null || - typeof css === 'undefined' || - (typeof css === 'object' && !css.toString) - ) { - throw new Error(`PostCSS received ${css} instead of CSS string`) - } - - this.css = css.toString() - - if (this.css[0] === '\uFEFF' || this.css[0] === '\uFFFE') { - this.hasBOM = true - this.css = this.css.slice(1) - } else { - this.hasBOM = false - } - - if (opts.from) { - if ( - !pathAvailable || - /^\w+:\/\//.test(opts.from) || - isAbsolute(opts.from) - ) { - this.file = opts.from - } else { - this.file = resolve(opts.from) - } - } - - if (pathAvailable && sourceMapAvailable) { - let map = new PreviousMap(this.css, opts) - if (map.text) { - this.map = map - let file = map.consumer().file - if (!this.file && file) this.file = this.mapResolve(file) - } - } - - if (!this.file) { - this.id = '' - } - if (this.map) this.map.file = this.from - } - - error(message, line, column, opts = {}) { - let result, endLine, endColumn - - if (line && typeof line === 'object') { - let start = line - let end = column - if (typeof start.offset === 'number') { - let pos = this.fromOffset(start.offset) - line = pos.line - column = pos.col - } else { - line = start.line - column = start.column - } - if (typeof end.offset === 'number') { - let pos = this.fromOffset(end.offset) - endLine = pos.line - endColumn = pos.col - } else { - endLine = end.line - endColumn = end.column - } - } else if (!column) { - let pos = this.fromOffset(line) - line = pos.line - column = pos.col - } - - let origin = this.origin(line, column, endLine, endColumn) - if (origin) { - result = new CssSyntaxError( - message, - origin.endLine === undefined - ? origin.line - : { column: origin.column, line: origin.line }, - origin.endLine === undefined - ? origin.column - : { column: origin.endColumn, line: origin.endLine }, - origin.source, - origin.file, - opts.plugin - ) - } else { - result = new CssSyntaxError( - message, - endLine === undefined ? line : { column, line }, - endLine === undefined ? column : { column: endColumn, line: endLine }, - this.css, - this.file, - opts.plugin - ) - } - - result.input = { column, endColumn, endLine, line, source: this.css } - if (this.file) { - if (pathToFileURL) { - result.input.url = pathToFileURL(this.file).toString() - } - result.input.file = this.file - } - - return result - } - - fromOffset(offset) { - let lastLine, lineToIndex - if (!this[fromOffsetCache]) { - let lines = this.css.split('\n') - lineToIndex = new Array(lines.length) - let prevIndex = 0 - - for (let i = 0, l = lines.length; i < l; i++) { - lineToIndex[i] = prevIndex - prevIndex += lines[i].length + 1 - } - - this[fromOffsetCache] = lineToIndex - } else { - lineToIndex = this[fromOffsetCache] - } - lastLine = lineToIndex[lineToIndex.length - 1] - - let min = 0 - if (offset >= lastLine) { - min = lineToIndex.length - 1 - } else { - let max = lineToIndex.length - 2 - let mid - while (min < max) { - mid = min + ((max - min) >> 1) - if (offset < lineToIndex[mid]) { - max = mid - 1 - } else if (offset >= lineToIndex[mid + 1]) { - min = mid + 1 - } else { - min = mid - break - } - } - } - return { - col: offset - lineToIndex[min] + 1, - line: min + 1 - } - } - - mapResolve(file) { - if (/^\w+:\/\//.test(file)) { - return file - } - return resolve(this.map.consumer().sourceRoot || this.map.root || '.', file) - } - - origin(line, column, endLine, endColumn) { - if (!this.map) return false - let consumer = this.map.consumer() - - let from = consumer.originalPositionFor({ column, line }) - if (!from.source) return false - - let to - if (typeof endLine === 'number') { - to = consumer.originalPositionFor({ column: endColumn, line: endLine }) - } - - let fromUrl - - if (isAbsolute(from.source)) { - fromUrl = pathToFileURL(from.source) - } else { - fromUrl = new URL( - from.source, - this.map.consumer().sourceRoot || pathToFileURL(this.map.mapFile) - ) - } - - let result = { - column: from.column, - endColumn: to && to.column, - endLine: to && to.line, - line: from.line, - url: fromUrl.toString() - } - - if (fromUrl.protocol === 'file:') { - if (fileURLToPath) { - result.file = fileURLToPath(fromUrl) - } else { - /* c8 ignore next 2 */ - throw new Error(`file: protocol is not available in this PostCSS build`) - } - } - - let source = consumer.sourceContentFor(from.source) - if (source) result.source = source - - return result - } - - toJSON() { - let json = {} - for (let name of ['hasBOM', 'css', 'file', 'id']) { - if (this[name] != null) { - json[name] = this[name] - } - } - if (this.map) { - json.map = { ...this.map } - if (json.map.consumerCache) { - json.map.consumerCache = undefined - } - } - return json - } - - get from() { - return this.file || this.id - } -} - -module.exports = Input -Input.default = Input - -if (terminalHighlight && terminalHighlight.registerInput) { - terminalHighlight.registerInput(Input) -} - -},{"./css-syntax-error":38,"./previous-map":51,"./terminal-highlight":2,"nanoid/non-secure":32,"path":2,"source-map-js":2,"url":2}],43:[function(require,module,exports){ -(function (process){(function (){ -'use strict' - -let { isClean, my } = require('./symbols') -let MapGenerator = require('./map-generator') -let stringify = require('./stringify') -let Container = require('./container') -let Document = require('./document') -let warnOnce = require('./warn-once') -let Result = require('./result') -let parse = require('./parse') -let Root = require('./root') - -const TYPE_TO_CLASS_NAME = { - atrule: 'AtRule', - comment: 'Comment', - decl: 'Declaration', - document: 'Document', - root: 'Root', - rule: 'Rule' -} - -const PLUGIN_PROPS = { - AtRule: true, - AtRuleExit: true, - Comment: true, - CommentExit: true, - Declaration: true, - DeclarationExit: true, - Document: true, - DocumentExit: true, - Once: true, - OnceExit: true, - postcssPlugin: true, - prepare: true, - Root: true, - RootExit: true, - Rule: true, - RuleExit: true -} - -const NOT_VISITORS = { - Once: true, - postcssPlugin: true, - prepare: true -} - -const CHILDREN = 0 - -function isPromise(obj) { - return typeof obj === 'object' && typeof obj.then === 'function' -} - -function getEvents(node) { - let key = false - let type = TYPE_TO_CLASS_NAME[node.type] - if (node.type === 'decl') { - key = node.prop.toLowerCase() - } else if (node.type === 'atrule') { - key = node.name.toLowerCase() - } - - if (key && node.append) { - return [ - type, - type + '-' + key, - CHILDREN, - type + 'Exit', - type + 'Exit-' + key - ] - } else if (key) { - return [type, type + '-' + key, type + 'Exit', type + 'Exit-' + key] - } else if (node.append) { - return [type, CHILDREN, type + 'Exit'] - } else { - return [type, type + 'Exit'] - } -} - -function toStack(node) { - let events - if (node.type === 'document') { - events = ['Document', CHILDREN, 'DocumentExit'] - } else if (node.type === 'root') { - events = ['Root', CHILDREN, 'RootExit'] - } else { - events = getEvents(node) - } - - return { - eventIndex: 0, - events, - iterator: 0, - node, - visitorIndex: 0, - visitors: [] - } -} - -function cleanMarks(node) { - node[isClean] = false - if (node.nodes) node.nodes.forEach(i => cleanMarks(i)) - return node -} - -let postcss = {} - -class LazyResult { - constructor(processor, css, opts) { - this.stringified = false - this.processed = false - - let root - if ( - typeof css === 'object' && - css !== null && - (css.type === 'root' || css.type === 'document') - ) { - root = cleanMarks(css) - } else if (css instanceof LazyResult || css instanceof Result) { - root = cleanMarks(css.root) - if (css.map) { - if (typeof opts.map === 'undefined') opts.map = {} - if (!opts.map.inline) opts.map.inline = false - opts.map.prev = css.map - } - } else { - let parser = parse - if (opts.syntax) parser = opts.syntax.parse - if (opts.parser) parser = opts.parser - if (parser.parse) parser = parser.parse - - try { - root = parser(css, opts) - } catch (error) { - this.processed = true - this.error = error - } - - if (root && !root[my]) { - /* c8 ignore next 2 */ - Container.rebuild(root) - } - } - - this.result = new Result(processor, root, opts) - this.helpers = { ...postcss, postcss, result: this.result } - this.plugins = this.processor.plugins.map(plugin => { - if (typeof plugin === 'object' && plugin.prepare) { - return { ...plugin, ...plugin.prepare(this.result) } - } else { - return plugin - } - }) - } - - async() { - if (this.error) return Promise.reject(this.error) - if (this.processed) return Promise.resolve(this.result) - if (!this.processing) { - this.processing = this.runAsync() - } - return this.processing - } - - catch(onRejected) { - return this.async().catch(onRejected) - } - - finally(onFinally) { - return this.async().then(onFinally, onFinally) - } - - getAsyncError() { - throw new Error('Use process(css).then(cb) to work with async plugins') - } - - handleError(error, node) { - let plugin = this.result.lastPlugin - try { - if (node) node.addToError(error) - this.error = error - if (error.name === 'CssSyntaxError' && !error.plugin) { - error.plugin = plugin.postcssPlugin - error.setMessage() - } else if (plugin.postcssVersion) { - if (process.env.NODE_ENV !== 'production') { - let pluginName = plugin.postcssPlugin - let pluginVer = plugin.postcssVersion - let runtimeVer = this.result.processor.version - let a = pluginVer.split('.') - let b = runtimeVer.split('.') - - if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) { - // eslint-disable-next-line no-console - console.error( - 'Unknown error from PostCSS plugin. Your current PostCSS ' + - 'version is ' + - runtimeVer + - ', but ' + - pluginName + - ' uses ' + - pluginVer + - '. Perhaps this is the source of the error below.' - ) - } - } - } - } catch (err) { - /* c8 ignore next 3 */ - // eslint-disable-next-line no-console - if (console && console.error) console.error(err) - } - return error - } - - prepareVisitors() { - this.listeners = {} - let add = (plugin, type, cb) => { - if (!this.listeners[type]) this.listeners[type] = [] - this.listeners[type].push([plugin, cb]) - } - for (let plugin of this.plugins) { - if (typeof plugin === 'object') { - for (let event in plugin) { - if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) { - throw new Error( - `Unknown event ${event} in ${plugin.postcssPlugin}. ` + - `Try to update PostCSS (${this.processor.version} now).` - ) - } - if (!NOT_VISITORS[event]) { - if (typeof plugin[event] === 'object') { - for (let filter in plugin[event]) { - if (filter === '*') { - add(plugin, event, plugin[event][filter]) - } else { - add( - plugin, - event + '-' + filter.toLowerCase(), - plugin[event][filter] - ) - } - } - } else if (typeof plugin[event] === 'function') { - add(plugin, event, plugin[event]) - } - } - } - } - } - this.hasListener = Object.keys(this.listeners).length > 0 - } - - async runAsync() { - this.plugin = 0 - for (let i = 0; i < this.plugins.length; i++) { - let plugin = this.plugins[i] - let promise = this.runOnRoot(plugin) - if (isPromise(promise)) { - try { - await promise - } catch (error) { - throw this.handleError(error) - } - } - } - - this.prepareVisitors() - if (this.hasListener) { - let root = this.result.root - while (!root[isClean]) { - root[isClean] = true - let stack = [toStack(root)] - while (stack.length > 0) { - let promise = this.visitTick(stack) - if (isPromise(promise)) { - try { - await promise - } catch (e) { - let node = stack[stack.length - 1].node - throw this.handleError(e, node) - } - } - } - } - - if (this.listeners.OnceExit) { - for (let [plugin, visitor] of this.listeners.OnceExit) { - this.result.lastPlugin = plugin - try { - if (root.type === 'document') { - let roots = root.nodes.map(subRoot => - visitor(subRoot, this.helpers) - ) - - await Promise.all(roots) - } else { - await visitor(root, this.helpers) - } - } catch (e) { - throw this.handleError(e) - } - } - } - } - - this.processed = true - return this.stringify() - } - - runOnRoot(plugin) { - this.result.lastPlugin = plugin - try { - if (typeof plugin === 'object' && plugin.Once) { - if (this.result.root.type === 'document') { - let roots = this.result.root.nodes.map(root => - plugin.Once(root, this.helpers) - ) - - if (isPromise(roots[0])) { - return Promise.all(roots) - } - - return roots - } - - return plugin.Once(this.result.root, this.helpers) - } else if (typeof plugin === 'function') { - return plugin(this.result.root, this.result) - } - } catch (error) { - throw this.handleError(error) - } - } - - stringify() { - if (this.error) throw this.error - if (this.stringified) return this.result - this.stringified = true - - this.sync() - - let opts = this.result.opts - let str = stringify - if (opts.syntax) str = opts.syntax.stringify - if (opts.stringifier) str = opts.stringifier - if (str.stringify) str = str.stringify - - let map = new MapGenerator(str, this.result.root, this.result.opts) - let data = map.generate() - this.result.css = data[0] - this.result.map = data[1] - - return this.result - } - - sync() { - if (this.error) throw this.error - if (this.processed) return this.result - this.processed = true - - if (this.processing) { - throw this.getAsyncError() - } - - for (let plugin of this.plugins) { - let promise = this.runOnRoot(plugin) - if (isPromise(promise)) { - throw this.getAsyncError() - } - } - - this.prepareVisitors() - if (this.hasListener) { - let root = this.result.root - while (!root[isClean]) { - root[isClean] = true - this.walkSync(root) - } - if (this.listeners.OnceExit) { - if (root.type === 'document') { - for (let subRoot of root.nodes) { - this.visitSync(this.listeners.OnceExit, subRoot) - } - } else { - this.visitSync(this.listeners.OnceExit, root) - } - } - } - - return this.result - } - - then(onFulfilled, onRejected) { - if (process.env.NODE_ENV !== 'production') { - if (!('from' in this.opts)) { - warnOnce( - 'Without `from` option PostCSS could generate wrong source map ' + - 'and will not find Browserslist config. Set it to CSS file path ' + - 'or to `undefined` to prevent this warning.' - ) - } - } - return this.async().then(onFulfilled, onRejected) - } - - toString() { - return this.css - } - - visitSync(visitors, node) { - for (let [plugin, visitor] of visitors) { - this.result.lastPlugin = plugin - let promise - try { - promise = visitor(node, this.helpers) - } catch (e) { - throw this.handleError(e, node.proxyOf) - } - if (node.type !== 'root' && node.type !== 'document' && !node.parent) { - return true - } - if (isPromise(promise)) { - throw this.getAsyncError() - } - } - } - - visitTick(stack) { - let visit = stack[stack.length - 1] - let { node, visitors } = visit - - if (node.type !== 'root' && node.type !== 'document' && !node.parent) { - stack.pop() - return - } - - if (visitors.length > 0 && visit.visitorIndex < visitors.length) { - let [plugin, visitor] = visitors[visit.visitorIndex] - visit.visitorIndex += 1 - if (visit.visitorIndex === visitors.length) { - visit.visitors = [] - visit.visitorIndex = 0 - } - this.result.lastPlugin = plugin - try { - return visitor(node.toProxy(), this.helpers) - } catch (e) { - throw this.handleError(e, node) - } - } - - if (visit.iterator !== 0) { - let iterator = visit.iterator - let child - while ((child = node.nodes[node.indexes[iterator]])) { - node.indexes[iterator] += 1 - if (!child[isClean]) { - child[isClean] = true - stack.push(toStack(child)) - return - } - } - visit.iterator = 0 - delete node.indexes[iterator] - } - - let events = visit.events - while (visit.eventIndex < events.length) { - let event = events[visit.eventIndex] - visit.eventIndex += 1 - if (event === CHILDREN) { - if (node.nodes && node.nodes.length) { - node[isClean] = true - visit.iterator = node.getIterator() - } - return - } else if (this.listeners[event]) { - visit.visitors = this.listeners[event] - return - } - } - stack.pop() - } - - walkSync(node) { - node[isClean] = true - let events = getEvents(node) - for (let event of events) { - if (event === CHILDREN) { - if (node.nodes) { - node.each(child => { - if (!child[isClean]) this.walkSync(child) - }) - } - } else { - let visitors = this.listeners[event] - if (visitors) { - if (this.visitSync(visitors, node.toProxy())) return - } - } - } - } - - warnings() { - return this.sync().warnings() - } - - get content() { - return this.stringify().content - } - - get css() { - return this.stringify().css - } - - get map() { - return this.stringify().map - } - - get messages() { - return this.sync().messages - } - - get opts() { - return this.result.opts - } - - get processor() { - return this.result.processor - } - - get root() { - return this.sync().root - } - - get [Symbol.toStringTag]() { - return 'LazyResult' - } -} - -LazyResult.registerPostcss = dependant => { - postcss = dependant -} - -module.exports = LazyResult -LazyResult.default = LazyResult - -Root.registerLazyResult(LazyResult) -Document.registerLazyResult(LazyResult) - -}).call(this)}).call(this,require('_process')) -},{"./container":37,"./document":40,"./map-generator":45,"./parse":48,"./result":53,"./root":54,"./stringify":57,"./symbols":58,"./warn-once":60,"_process":62}],44:[function(require,module,exports){ -'use strict' - -let list = { - comma(string) { - return list.split(string, [','], true) - }, - - space(string) { - let spaces = [' ', '\n', '\t'] - return list.split(string, spaces) - }, - - split(string, separators, last) { - let array = [] - let current = '' - let split = false - - let func = 0 - let inQuote = false - let prevQuote = '' - let escape = false - - for (let letter of string) { - if (escape) { - escape = false - } else if (letter === '\\') { - escape = true - } else if (inQuote) { - if (letter === prevQuote) { - inQuote = false - } - } else if (letter === '"' || letter === "'") { - inQuote = true - prevQuote = letter - } else if (letter === '(') { - func += 1 - } else if (letter === ')') { - if (func > 0) func -= 1 - } else if (func === 0) { - if (separators.includes(letter)) split = true - } - - if (split) { - if (current !== '') array.push(current.trim()) - current = '' - split = false - } else { - current += letter - } - } - - if (last || current !== '') array.push(current.trim()) - return array - } -} - -module.exports = list -list.default = list - -},{}],45:[function(require,module,exports){ -(function (Buffer){(function (){ -'use strict' - -let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js') -let { dirname, relative, resolve, sep } = require('path') -let { pathToFileURL } = require('url') - -let Input = require('./input') - -let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator) -let pathAvailable = Boolean(dirname && resolve && relative && sep) - -class MapGenerator { - constructor(stringify, root, opts, cssString) { - this.stringify = stringify - this.mapOpts = opts.map || {} - this.root = root - this.opts = opts - this.css = cssString - this.usesFileUrls = !this.mapOpts.from && this.mapOpts.absolute - - this.memoizedFileURLs = new Map() - this.memoizedPaths = new Map() - this.memoizedURLs = new Map() - } - - addAnnotation() { - let content - - if (this.isInline()) { - content = - 'data:application/json;base64,' + this.toBase64(this.map.toString()) - } else if (typeof this.mapOpts.annotation === 'string') { - content = this.mapOpts.annotation - } else if (typeof this.mapOpts.annotation === 'function') { - content = this.mapOpts.annotation(this.opts.to, this.root) - } else { - content = this.outputFile() + '.map' - } - let eol = '\n' - if (this.css.includes('\r\n')) eol = '\r\n' - - this.css += eol + '/*# sourceMappingURL=' + content + ' */' - } - - applyPrevMaps() { - for (let prev of this.previous()) { - let from = this.toUrl(this.path(prev.file)) - let root = prev.root || dirname(prev.file) - let map - - if (this.mapOpts.sourcesContent === false) { - map = new SourceMapConsumer(prev.text) - if (map.sourcesContent) { - map.sourcesContent = map.sourcesContent.map(() => null) - } - } else { - map = prev.consumer() - } - - this.map.applySourceMap(map, from, this.toUrl(this.path(root))) - } - } - - clearAnnotation() { - if (this.mapOpts.annotation === false) return - - if (this.root) { - let node - for (let i = this.root.nodes.length - 1; i >= 0; i--) { - node = this.root.nodes[i] - if (node.type !== 'comment') continue - if (node.text.indexOf('# sourceMappingURL=') === 0) { - this.root.removeChild(i) - } - } - } else if (this.css) { - this.css = this.css.replace(/(\n)?\/\*#[\S\s]*?\*\/$/gm, '') - } - } - - generate() { - this.clearAnnotation() - if (pathAvailable && sourceMapAvailable && this.isMap()) { - return this.generateMap() - } else { - let result = '' - this.stringify(this.root, i => { - result += i - }) - return [result] - } - } - - generateMap() { - if (this.root) { - this.generateString() - } else if (this.previous().length === 1) { - let prev = this.previous()[0].consumer() - prev.file = this.outputFile() - this.map = SourceMapGenerator.fromSourceMap(prev) - } else { - this.map = new SourceMapGenerator({ file: this.outputFile() }) - this.map.addMapping({ - generated: { column: 0, line: 1 }, - original: { column: 0, line: 1 }, - source: this.opts.from - ? this.toUrl(this.path(this.opts.from)) - : '' - }) - } - - if (this.isSourcesContent()) this.setSourcesContent() - if (this.root && this.previous().length > 0) this.applyPrevMaps() - if (this.isAnnotation()) this.addAnnotation() - - if (this.isInline()) { - return [this.css] - } else { - return [this.css, this.map] - } - } - - generateString() { - this.css = '' - this.map = new SourceMapGenerator({ file: this.outputFile() }) - - let line = 1 - let column = 1 - - let noSource = '' - let mapping = { - generated: { column: 0, line: 0 }, - original: { column: 0, line: 0 }, - source: '' - } - - let lines, last - this.stringify(this.root, (str, node, type) => { - this.css += str - - if (node && type !== 'end') { - mapping.generated.line = line - mapping.generated.column = column - 1 - if (node.source && node.source.start) { - mapping.source = this.sourcePath(node) - mapping.original.line = node.source.start.line - mapping.original.column = node.source.start.column - 1 - this.map.addMapping(mapping) - } else { - mapping.source = noSource - mapping.original.line = 1 - mapping.original.column = 0 - this.map.addMapping(mapping) - } - } - - lines = str.match(/\n/g) - if (lines) { - line += lines.length - last = str.lastIndexOf('\n') - column = str.length - last - } else { - column += str.length - } - - if (node && type !== 'start') { - let p = node.parent || { raws: {} } - let childless = - node.type === 'decl' || (node.type === 'atrule' && !node.nodes) - if (!childless || node !== p.last || p.raws.semicolon) { - if (node.source && node.source.end) { - mapping.source = this.sourcePath(node) - mapping.original.line = node.source.end.line - mapping.original.column = node.source.end.column - 1 - mapping.generated.line = line - mapping.generated.column = column - 2 - this.map.addMapping(mapping) - } else { - mapping.source = noSource - mapping.original.line = 1 - mapping.original.column = 0 - mapping.generated.line = line - mapping.generated.column = column - 1 - this.map.addMapping(mapping) - } - } - } - }) - } - - isAnnotation() { - if (this.isInline()) { - return true - } - if (typeof this.mapOpts.annotation !== 'undefined') { - return this.mapOpts.annotation - } - if (this.previous().length) { - return this.previous().some(i => i.annotation) - } - return true - } - - isInline() { - if (typeof this.mapOpts.inline !== 'undefined') { - return this.mapOpts.inline - } - - let annotation = this.mapOpts.annotation - if (typeof annotation !== 'undefined' && annotation !== true) { - return false - } - - if (this.previous().length) { - return this.previous().some(i => i.inline) - } - return true - } - - isMap() { - if (typeof this.opts.map !== 'undefined') { - return !!this.opts.map - } - return this.previous().length > 0 - } - - isSourcesContent() { - if (typeof this.mapOpts.sourcesContent !== 'undefined') { - return this.mapOpts.sourcesContent - } - if (this.previous().length) { - return this.previous().some(i => i.withContent()) - } - return true - } - - outputFile() { - if (this.opts.to) { - return this.path(this.opts.to) - } else if (this.opts.from) { - return this.path(this.opts.from) - } else { - return 'to.css' - } - } - - path(file) { - if (this.mapOpts.absolute) return file - if (file.charCodeAt(0) === 60 /* `<` */) return file - if (/^\w+:\/\//.test(file)) return file - let cached = this.memoizedPaths.get(file) - if (cached) return cached - - let from = this.opts.to ? dirname(this.opts.to) : '.' - - if (typeof this.mapOpts.annotation === 'string') { - from = dirname(resolve(from, this.mapOpts.annotation)) - } - - let path = relative(from, file) - this.memoizedPaths.set(file, path) - - return path - } - - previous() { - if (!this.previousMaps) { - this.previousMaps = [] - if (this.root) { - this.root.walk(node => { - if (node.source && node.source.input.map) { - let map = node.source.input.map - if (!this.previousMaps.includes(map)) { - this.previousMaps.push(map) - } - } - }) - } else { - let input = new Input(this.css, this.opts) - if (input.map) this.previousMaps.push(input.map) - } - } - - return this.previousMaps - } - - setSourcesContent() { - let already = {} - if (this.root) { - this.root.walk(node => { - if (node.source) { - let from = node.source.input.from - if (from && !already[from]) { - already[from] = true - let fromUrl = this.usesFileUrls - ? this.toFileUrl(from) - : this.toUrl(this.path(from)) - this.map.setSourceContent(fromUrl, node.source.input.css) - } - } - }) - } else if (this.css) { - let from = this.opts.from - ? this.toUrl(this.path(this.opts.from)) - : '' - this.map.setSourceContent(from, this.css) - } - } - - sourcePath(node) { - if (this.mapOpts.from) { - return this.toUrl(this.mapOpts.from) - } else if (this.usesFileUrls) { - return this.toFileUrl(node.source.input.from) - } else { - return this.toUrl(this.path(node.source.input.from)) - } - } - - toBase64(str) { - if (Buffer) { - return Buffer.from(str).toString('base64') - } else { - return window.btoa(unescape(encodeURIComponent(str))) - } - } - - toFileUrl(path) { - let cached = this.memoizedFileURLs.get(path) - if (cached) return cached - - if (pathToFileURL) { - let fileURL = pathToFileURL(path).toString() - this.memoizedFileURLs.set(path, fileURL) - - return fileURL - } else { - throw new Error( - '`map.absolute` option is not available in this PostCSS build' - ) - } - } - - toUrl(path) { - let cached = this.memoizedURLs.get(path) - if (cached) return cached - - if (sep === '\\') { - path = path.replace(/\\/g, '/') - } - - let url = encodeURI(path).replace(/[#?]/g, encodeURIComponent) - this.memoizedURLs.set(path, url) - - return url - } -} - -module.exports = MapGenerator - -}).call(this)}).call(this,require("buffer").Buffer) -},{"./input":42,"buffer":3,"path":2,"source-map-js":2,"url":2}],46:[function(require,module,exports){ -(function (process){(function (){ -'use strict' - -let MapGenerator = require('./map-generator') -let stringify = require('./stringify') -let warnOnce = require('./warn-once') -let parse = require('./parse') -const Result = require('./result') - -class NoWorkResult { - constructor(processor, css, opts) { - css = css.toString() - this.stringified = false - - this._processor = processor - this._css = css - this._opts = opts - this._map = undefined - let root - - let str = stringify - this.result = new Result(this._processor, root, this._opts) - this.result.css = css - - let self = this - Object.defineProperty(this.result, 'root', { - get() { - return self.root - } - }) - - let map = new MapGenerator(str, root, this._opts, css) - if (map.isMap()) { - let [generatedCSS, generatedMap] = map.generate() - if (generatedCSS) { - this.result.css = generatedCSS - } - if (generatedMap) { - this.result.map = generatedMap - } - } - } - - async() { - if (this.error) return Promise.reject(this.error) - return Promise.resolve(this.result) - } - - catch(onRejected) { - return this.async().catch(onRejected) - } - - finally(onFinally) { - return this.async().then(onFinally, onFinally) - } - - sync() { - if (this.error) throw this.error - return this.result - } - - then(onFulfilled, onRejected) { - if (process.env.NODE_ENV !== 'production') { - if (!('from' in this._opts)) { - warnOnce( - 'Without `from` option PostCSS could generate wrong source map ' + - 'and will not find Browserslist config. Set it to CSS file path ' + - 'or to `undefined` to prevent this warning.' - ) - } - } - - return this.async().then(onFulfilled, onRejected) - } - - toString() { - return this._css - } - - warnings() { - return [] - } - - get content() { - return this.result.css - } - - get css() { - return this.result.css - } - - get map() { - return this.result.map - } - - get messages() { - return [] - } - - get opts() { - return this.result.opts - } - - get processor() { - return this.result.processor - } - - get root() { - if (this._root) { - return this._root - } - - let root - let parser = parse - - try { - root = parser(this._css, this._opts) - } catch (error) { - this.error = error - } - - if (this.error) { - throw this.error - } else { - this._root = root - return root - } - } - - get [Symbol.toStringTag]() { - return 'NoWorkResult' - } -} - -module.exports = NoWorkResult -NoWorkResult.default = NoWorkResult - -}).call(this)}).call(this,require('_process')) -},{"./map-generator":45,"./parse":48,"./result":53,"./stringify":57,"./warn-once":60,"_process":62}],47:[function(require,module,exports){ -'use strict' - -let { isClean, my } = require('./symbols') -let CssSyntaxError = require('./css-syntax-error') -let Stringifier = require('./stringifier') -let stringify = require('./stringify') - -function cloneNode(obj, parent) { - let cloned = new obj.constructor() - - for (let i in obj) { - if (!Object.prototype.hasOwnProperty.call(obj, i)) { - /* c8 ignore next 2 */ - continue - } - if (i === 'proxyCache') continue - let value = obj[i] - let type = typeof value - - if (i === 'parent' && type === 'object') { - if (parent) cloned[i] = parent - } else if (i === 'source') { - cloned[i] = value - } else if (Array.isArray(value)) { - cloned[i] = value.map(j => cloneNode(j, cloned)) - } else { - if (type === 'object' && value !== null) value = cloneNode(value) - cloned[i] = value - } - } - - return cloned -} - -class Node { - constructor(defaults = {}) { - this.raws = {} - this[isClean] = false - this[my] = true - - for (let name in defaults) { - if (name === 'nodes') { - this.nodes = [] - for (let node of defaults[name]) { - if (typeof node.clone === 'function') { - this.append(node.clone()) - } else { - this.append(node) - } - } - } else { - this[name] = defaults[name] - } - } - } - - addToError(error) { - error.postcssNode = this - if (error.stack && this.source && /\n\s{4}at /.test(error.stack)) { - let s = this.source - error.stack = error.stack.replace( - /\n\s{4}at /, - `$&${s.input.from}:${s.start.line}:${s.start.column}$&` - ) - } - return error - } - - after(add) { - this.parent.insertAfter(this, add) - return this - } - - assign(overrides = {}) { - for (let name in overrides) { - this[name] = overrides[name] - } - return this - } - - before(add) { - this.parent.insertBefore(this, add) - return this - } - - cleanRaws(keepBetween) { - delete this.raws.before - delete this.raws.after - if (!keepBetween) delete this.raws.between - } - - clone(overrides = {}) { - let cloned = cloneNode(this) - for (let name in overrides) { - cloned[name] = overrides[name] - } - return cloned - } - - cloneAfter(overrides = {}) { - let cloned = this.clone(overrides) - this.parent.insertAfter(this, cloned) - return cloned - } - - cloneBefore(overrides = {}) { - let cloned = this.clone(overrides) - this.parent.insertBefore(this, cloned) - return cloned - } - - error(message, opts = {}) { - if (this.source) { - let { end, start } = this.rangeBy(opts) - return this.source.input.error( - message, - { column: start.column, line: start.line }, - { column: end.column, line: end.line }, - opts - ) - } - return new CssSyntaxError(message) - } - - getProxyProcessor() { - return { - get(node, prop) { - if (prop === 'proxyOf') { - return node - } else if (prop === 'root') { - return () => node.root().toProxy() - } else { - return node[prop] - } - }, - - set(node, prop, value) { - if (node[prop] === value) return true - node[prop] = value - if ( - prop === 'prop' || - prop === 'value' || - prop === 'name' || - prop === 'params' || - prop === 'important' || - /* c8 ignore next */ - prop === 'text' - ) { - node.markDirty() - } - return true - } - } - } - - markDirty() { - if (this[isClean]) { - this[isClean] = false - let next = this - while ((next = next.parent)) { - next[isClean] = false - } - } - } - - next() { - if (!this.parent) return undefined - let index = this.parent.index(this) - return this.parent.nodes[index + 1] - } - - positionBy(opts, stringRepresentation) { - let pos = this.source.start - if (opts.index) { - pos = this.positionInside(opts.index, stringRepresentation) - } else if (opts.word) { - stringRepresentation = this.toString() - let index = stringRepresentation.indexOf(opts.word) - if (index !== -1) pos = this.positionInside(index, stringRepresentation) - } - return pos - } - - positionInside(index, stringRepresentation) { - let string = stringRepresentation || this.toString() - let column = this.source.start.column - let line = this.source.start.line - - for (let i = 0; i < index; i++) { - if (string[i] === '\n') { - column = 1 - line += 1 - } else { - column += 1 - } - } - - return { column, line } - } - - prev() { - if (!this.parent) return undefined - let index = this.parent.index(this) - return this.parent.nodes[index - 1] - } - - rangeBy(opts) { - let start = { - column: this.source.start.column, - line: this.source.start.line - } - let end = this.source.end - ? { - column: this.source.end.column + 1, - line: this.source.end.line - } - : { - column: start.column + 1, - line: start.line - } - - if (opts.word) { - let stringRepresentation = this.toString() - let index = stringRepresentation.indexOf(opts.word) - if (index !== -1) { - start = this.positionInside(index, stringRepresentation) - end = this.positionInside(index + opts.word.length, stringRepresentation) - } - } else { - if (opts.start) { - start = { - column: opts.start.column, - line: opts.start.line - } - } else if (opts.index) { - start = this.positionInside(opts.index) - } - - if (opts.end) { - end = { - column: opts.end.column, - line: opts.end.line - } - } else if (opts.endIndex) { - end = this.positionInside(opts.endIndex) - } else if (opts.index) { - end = this.positionInside(opts.index + 1) - } - } - - if ( - end.line < start.line || - (end.line === start.line && end.column <= start.column) - ) { - end = { column: start.column + 1, line: start.line } - } - - return { end, start } - } - - raw(prop, defaultType) { - let str = new Stringifier() - return str.raw(this, prop, defaultType) - } - - remove() { - if (this.parent) { - this.parent.removeChild(this) - } - this.parent = undefined - return this - } - - replaceWith(...nodes) { - if (this.parent) { - let bookmark = this - let foundSelf = false - for (let node of nodes) { - if (node === this) { - foundSelf = true - } else if (foundSelf) { - this.parent.insertAfter(bookmark, node) - bookmark = node - } else { - this.parent.insertBefore(bookmark, node) - } - } - - if (!foundSelf) { - this.remove() - } - } - - return this - } - - root() { - let result = this - while (result.parent && result.parent.type !== 'document') { - result = result.parent - } - return result - } - - toJSON(_, inputs) { - let fixed = {} - let emitInputs = inputs == null - inputs = inputs || new Map() - let inputsNextIndex = 0 - - for (let name in this) { - if (!Object.prototype.hasOwnProperty.call(this, name)) { - /* c8 ignore next 2 */ - continue - } - if (name === 'parent' || name === 'proxyCache') continue - let value = this[name] - - if (Array.isArray(value)) { - fixed[name] = value.map(i => { - if (typeof i === 'object' && i.toJSON) { - return i.toJSON(null, inputs) - } else { - return i - } - }) - } else if (typeof value === 'object' && value.toJSON) { - fixed[name] = value.toJSON(null, inputs) - } else if (name === 'source') { - let inputId = inputs.get(value.input) - if (inputId == null) { - inputId = inputsNextIndex - inputs.set(value.input, inputsNextIndex) - inputsNextIndex++ - } - fixed[name] = { - end: value.end, - inputId, - start: value.start - } - } else { - fixed[name] = value - } - } - - if (emitInputs) { - fixed.inputs = [...inputs.keys()].map(input => input.toJSON()) - } - - return fixed - } - - toProxy() { - if (!this.proxyCache) { - this.proxyCache = new Proxy(this, this.getProxyProcessor()) - } - return this.proxyCache - } - - toString(stringifier = stringify) { - if (stringifier.stringify) stringifier = stringifier.stringify - let result = '' - stringifier(this, i => { - result += i - }) - return result - } - - warn(result, text, opts) { - let data = { node: this } - for (let i in opts) data[i] = opts[i] - return result.warn(text, data) - } - - get proxyOf() { - return this - } -} - -module.exports = Node -Node.default = Node - -},{"./css-syntax-error":38,"./stringifier":56,"./stringify":57,"./symbols":58}],48:[function(require,module,exports){ -(function (process){(function (){ -'use strict' - -let Container = require('./container') -let Parser = require('./parser') -let Input = require('./input') - -function parse(css, opts) { - let input = new Input(css, opts) - let parser = new Parser(input) - try { - parser.parse() - } catch (e) { - if (process.env.NODE_ENV !== 'production') { - if (e.name === 'CssSyntaxError' && opts && opts.from) { - if (/\.scss$/i.test(opts.from)) { - e.message += - '\nYou tried to parse SCSS with ' + - 'the standard CSS parser; ' + - 'try again with the postcss-scss parser' - } else if (/\.sass/i.test(opts.from)) { - e.message += - '\nYou tried to parse Sass with ' + - 'the standard CSS parser; ' + - 'try again with the postcss-sass parser' - } else if (/\.less$/i.test(opts.from)) { - e.message += - '\nYou tried to parse Less with ' + - 'the standard CSS parser; ' + - 'try again with the postcss-less parser' - } - } - } - throw e - } - - return parser.root -} - -module.exports = parse -parse.default = parse - -Container.registerParse(parse) - -}).call(this)}).call(this,require('_process')) -},{"./container":37,"./input":42,"./parser":49,"_process":62}],49:[function(require,module,exports){ -'use strict' - -let Declaration = require('./declaration') -let tokenizer = require('./tokenize') -let Comment = require('./comment') -let AtRule = require('./at-rule') -let Root = require('./root') -let Rule = require('./rule') - -const SAFE_COMMENT_NEIGHBOR = { - empty: true, - space: true -} - -function findLastWithPosition(tokens) { - for (let i = tokens.length - 1; i >= 0; i--) { - let token = tokens[i] - let pos = token[3] || token[2] - if (pos) return pos - } -} - -class Parser { - constructor(input) { - this.input = input - - this.root = new Root() - this.current = this.root - this.spaces = '' - this.semicolon = false - this.customProperty = false - - this.createTokenizer() - this.root.source = { input, start: { column: 1, line: 1, offset: 0 } } - } - - atrule(token) { - let node = new AtRule() - node.name = token[1].slice(1) - if (node.name === '') { - this.unnamedAtrule(node, token) - } - this.init(node, token[2]) - - let type - let prev - let shift - let last = false - let open = false - let params = [] - let brackets = [] - - while (!this.tokenizer.endOfFile()) { - token = this.tokenizer.nextToken() - type = token[0] - - if (type === '(' || type === '[') { - brackets.push(type === '(' ? ')' : ']') - } else if (type === '{' && brackets.length > 0) { - brackets.push('}') - } else if (type === brackets[brackets.length - 1]) { - brackets.pop() - } - - if (brackets.length === 0) { - if (type === ';') { - node.source.end = this.getPosition(token[2]) - node.source.end.offset++ - this.semicolon = true - break - } else if (type === '{') { - open = true - break - } else if (type === '}') { - if (params.length > 0) { - shift = params.length - 1 - prev = params[shift] - while (prev && prev[0] === 'space') { - prev = params[--shift] - } - if (prev) { - node.source.end = this.getPosition(prev[3] || prev[2]) - node.source.end.offset++ - } - } - this.end(token) - break - } else { - params.push(token) - } - } else { - params.push(token) - } - - if (this.tokenizer.endOfFile()) { - last = true - break - } - } - - node.raws.between = this.spacesAndCommentsFromEnd(params) - if (params.length) { - node.raws.afterName = this.spacesAndCommentsFromStart(params) - this.raw(node, 'params', params) - if (last) { - token = params[params.length - 1] - node.source.end = this.getPosition(token[3] || token[2]) - node.source.end.offset++ - this.spaces = node.raws.between - node.raws.between = '' - } - } else { - node.raws.afterName = '' - node.params = '' - } - - if (open) { - node.nodes = [] - this.current = node - } - } - - checkMissedSemicolon(tokens) { - let colon = this.colon(tokens) - if (colon === false) return - - let founded = 0 - let token - for (let j = colon - 1; j >= 0; j--) { - token = tokens[j] - if (token[0] !== 'space') { - founded += 1 - if (founded === 2) break - } - } - // If the token is a word, e.g. `!important`, `red` or any other valid property's value. - // Then we need to return the colon after that word token. [3] is the "end" colon of that word. - // And because we need it after that one we do +1 to get the next one. - throw this.input.error( - 'Missed semicolon', - token[0] === 'word' ? token[3] + 1 : token[2] - ) - } - - colon(tokens) { - let brackets = 0 - let token, type, prev - for (let [i, element] of tokens.entries()) { - token = element - type = token[0] - - if (type === '(') { - brackets += 1 - } - if (type === ')') { - brackets -= 1 - } - if (brackets === 0 && type === ':') { - if (!prev) { - this.doubleColon(token) - } else if (prev[0] === 'word' && prev[1] === 'progid') { - continue - } else { - return i - } - } - - prev = token - } - return false - } - - comment(token) { - let node = new Comment() - this.init(node, token[2]) - node.source.end = this.getPosition(token[3] || token[2]) - node.source.end.offset++ - - let text = token[1].slice(2, -2) - if (/^\s*$/.test(text)) { - node.text = '' - node.raws.left = text - node.raws.right = '' - } else { - let match = text.match(/^(\s*)([^]*\S)(\s*)$/) - node.text = match[2] - node.raws.left = match[1] - node.raws.right = match[3] - } - } - - createTokenizer() { - this.tokenizer = tokenizer(this.input) - } - - decl(tokens, customProperty) { - let node = new Declaration() - this.init(node, tokens[0][2]) - - let last = tokens[tokens.length - 1] - if (last[0] === ';') { - this.semicolon = true - tokens.pop() - } - - node.source.end = this.getPosition( - last[3] || last[2] || findLastWithPosition(tokens) - ) - node.source.end.offset++ - - while (tokens[0][0] !== 'word') { - if (tokens.length === 1) this.unknownWord(tokens) - node.raws.before += tokens.shift()[1] - } - node.source.start = this.getPosition(tokens[0][2]) - - node.prop = '' - while (tokens.length) { - let type = tokens[0][0] - if (type === ':' || type === 'space' || type === 'comment') { - break - } - node.prop += tokens.shift()[1] - } - - node.raws.between = '' - - let token - while (tokens.length) { - token = tokens.shift() - - if (token[0] === ':') { - node.raws.between += token[1] - break - } else { - if (token[0] === 'word' && /\w/.test(token[1])) { - this.unknownWord([token]) - } - node.raws.between += token[1] - } - } - - if (node.prop[0] === '_' || node.prop[0] === '*') { - node.raws.before += node.prop[0] - node.prop = node.prop.slice(1) - } - - let firstSpaces = [] - let next - while (tokens.length) { - next = tokens[0][0] - if (next !== 'space' && next !== 'comment') break - firstSpaces.push(tokens.shift()) - } - - this.precheckMissedSemicolon(tokens) - - for (let i = tokens.length - 1; i >= 0; i--) { - token = tokens[i] - if (token[1].toLowerCase() === '!important') { - node.important = true - let string = this.stringFrom(tokens, i) - string = this.spacesFromEnd(tokens) + string - if (string !== ' !important') node.raws.important = string - break - } else if (token[1].toLowerCase() === 'important') { - let cache = tokens.slice(0) - let str = '' - for (let j = i; j > 0; j--) { - let type = cache[j][0] - if (str.trim().indexOf('!') === 0 && type !== 'space') { - break - } - str = cache.pop()[1] + str - } - if (str.trim().indexOf('!') === 0) { - node.important = true - node.raws.important = str - tokens = cache - } - } - - if (token[0] !== 'space' && token[0] !== 'comment') { - break - } - } - - let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment') - - if (hasWord) { - node.raws.between += firstSpaces.map(i => i[1]).join('') - firstSpaces = [] - } - this.raw(node, 'value', firstSpaces.concat(tokens), customProperty) - - if (node.value.includes(':') && !customProperty) { - this.checkMissedSemicolon(tokens) - } - } - - doubleColon(token) { - throw this.input.error( - 'Double colon', - { offset: token[2] }, - { offset: token[2] + token[1].length } - ) - } - - emptyRule(token) { - let node = new Rule() - this.init(node, token[2]) - node.selector = '' - node.raws.between = '' - this.current = node - } - - end(token) { - if (this.current.nodes && this.current.nodes.length) { - this.current.raws.semicolon = this.semicolon - } - this.semicolon = false - - this.current.raws.after = (this.current.raws.after || '') + this.spaces - this.spaces = '' - - if (this.current.parent) { - this.current.source.end = this.getPosition(token[2]) - this.current.source.end.offset++ - this.current = this.current.parent - } else { - this.unexpectedClose(token) - } - } - - endFile() { - if (this.current.parent) this.unclosedBlock() - if (this.current.nodes && this.current.nodes.length) { - this.current.raws.semicolon = this.semicolon - } - this.current.raws.after = (this.current.raws.after || '') + this.spaces - this.root.source.end = this.getPosition(this.tokenizer.position()) - } - - freeSemicolon(token) { - this.spaces += token[1] - if (this.current.nodes) { - let prev = this.current.nodes[this.current.nodes.length - 1] - if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) { - prev.raws.ownSemicolon = this.spaces - this.spaces = '' - } - } - } - - // Helpers - - getPosition(offset) { - let pos = this.input.fromOffset(offset) - return { - column: pos.col, - line: pos.line, - offset - } - } - - init(node, offset) { - this.current.push(node) - node.source = { - input: this.input, - start: this.getPosition(offset) - } - node.raws.before = this.spaces - this.spaces = '' - if (node.type !== 'comment') this.semicolon = false - } - - other(start) { - let end = false - let type = null - let colon = false - let bracket = null - let brackets = [] - let customProperty = start[1].startsWith('--') - - let tokens = [] - let token = start - while (token) { - type = token[0] - tokens.push(token) - - if (type === '(' || type === '[') { - if (!bracket) bracket = token - brackets.push(type === '(' ? ')' : ']') - } else if (customProperty && colon && type === '{') { - if (!bracket) bracket = token - brackets.push('}') - } else if (brackets.length === 0) { - if (type === ';') { - if (colon) { - this.decl(tokens, customProperty) - return - } else { - break - } - } else if (type === '{') { - this.rule(tokens) - return - } else if (type === '}') { - this.tokenizer.back(tokens.pop()) - end = true - break - } else if (type === ':') { - colon = true - } - } else if (type === brackets[brackets.length - 1]) { - brackets.pop() - if (brackets.length === 0) bracket = null - } - - token = this.tokenizer.nextToken() - } - - if (this.tokenizer.endOfFile()) end = true - if (brackets.length > 0) this.unclosedBracket(bracket) - - if (end && colon) { - if (!customProperty) { - while (tokens.length) { - token = tokens[tokens.length - 1][0] - if (token !== 'space' && token !== 'comment') break - this.tokenizer.back(tokens.pop()) - } - } - this.decl(tokens, customProperty) - } else { - this.unknownWord(tokens) - } - } - - parse() { - let token - while (!this.tokenizer.endOfFile()) { - token = this.tokenizer.nextToken() - - switch (token[0]) { - case 'space': - this.spaces += token[1] - break - - case ';': - this.freeSemicolon(token) - break - - case '}': - this.end(token) - break - - case 'comment': - this.comment(token) - break - - case 'at-word': - this.atrule(token) - break - - case '{': - this.emptyRule(token) - break - - default: - this.other(token) - break - } - } - this.endFile() - } - - precheckMissedSemicolon(/* tokens */) { - // Hook for Safe Parser - } - - raw(node, prop, tokens, customProperty) { - let token, type - let length = tokens.length - let value = '' - let clean = true - let next, prev - - for (let i = 0; i < length; i += 1) { - token = tokens[i] - type = token[0] - if (type === 'space' && i === length - 1 && !customProperty) { - clean = false - } else if (type === 'comment') { - prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty' - next = tokens[i + 1] ? tokens[i + 1][0] : 'empty' - if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) { - if (value.slice(-1) === ',') { - clean = false - } else { - value += token[1] - } - } else { - clean = false - } - } else { - value += token[1] - } - } - if (!clean) { - let raw = tokens.reduce((all, i) => all + i[1], '') - node.raws[prop] = { raw, value } - } - node[prop] = value - } - - rule(tokens) { - tokens.pop() - - let node = new Rule() - this.init(node, tokens[0][2]) - - node.raws.between = this.spacesAndCommentsFromEnd(tokens) - this.raw(node, 'selector', tokens) - this.current = node - } - - spacesAndCommentsFromEnd(tokens) { - let lastTokenType - let spaces = '' - while (tokens.length) { - lastTokenType = tokens[tokens.length - 1][0] - if (lastTokenType !== 'space' && lastTokenType !== 'comment') break - spaces = tokens.pop()[1] + spaces - } - return spaces - } - - // Errors - - spacesAndCommentsFromStart(tokens) { - let next - let spaces = '' - while (tokens.length) { - next = tokens[0][0] - if (next !== 'space' && next !== 'comment') break - spaces += tokens.shift()[1] - } - return spaces - } - - spacesFromEnd(tokens) { - let lastTokenType - let spaces = '' - while (tokens.length) { - lastTokenType = tokens[tokens.length - 1][0] - if (lastTokenType !== 'space') break - spaces = tokens.pop()[1] + spaces - } - return spaces - } - - stringFrom(tokens, from) { - let result = '' - for (let i = from; i < tokens.length; i++) { - result += tokens[i][1] - } - tokens.splice(from, tokens.length - from) - return result - } - - unclosedBlock() { - let pos = this.current.source.start - throw this.input.error('Unclosed block', pos.line, pos.column) - } - - unclosedBracket(bracket) { - throw this.input.error( - 'Unclosed bracket', - { offset: bracket[2] }, - { offset: bracket[2] + 1 } - ) - } - - unexpectedClose(token) { - throw this.input.error( - 'Unexpected }', - { offset: token[2] }, - { offset: token[2] + 1 } - ) - } - - unknownWord(tokens) { - throw this.input.error( - 'Unknown word', - { offset: tokens[0][2] }, - { offset: tokens[0][2] + tokens[0][1].length } - ) - } - - unnamedAtrule(node, token) { - throw this.input.error( - 'At-rule without name', - { offset: token[2] }, - { offset: token[2] + token[1].length } - ) - } -} - -module.exports = Parser - -},{"./at-rule":35,"./comment":36,"./declaration":39,"./root":54,"./rule":55,"./tokenize":59}],50:[function(require,module,exports){ -(function (process){(function (){ -'use strict' - -let CssSyntaxError = require('./css-syntax-error') -let Declaration = require('./declaration') -let LazyResult = require('./lazy-result') -let Container = require('./container') -let Processor = require('./processor') -let stringify = require('./stringify') -let fromJSON = require('./fromJSON') -let Document = require('./document') -let Warning = require('./warning') -let Comment = require('./comment') -let AtRule = require('./at-rule') -let Result = require('./result.js') -let Input = require('./input') -let parse = require('./parse') -let list = require('./list') -let Rule = require('./rule') -let Root = require('./root') -let Node = require('./node') - -function postcss(...plugins) { - if (plugins.length === 1 && Array.isArray(plugins[0])) { - plugins = plugins[0] - } - return new Processor(plugins) -} - -postcss.plugin = function plugin(name, initializer) { - let warningPrinted = false - function creator(...args) { - // eslint-disable-next-line no-console - if (console && console.warn && !warningPrinted) { - warningPrinted = true - // eslint-disable-next-line no-console - console.warn( - name + - ': postcss.plugin was deprecated. Migration guide:\n' + - 'https://evilmartians.com/chronicles/postcss-8-plugin-migration' - ) - if (process.env.LANG && process.env.LANG.startsWith('cn')) { - /* c8 ignore next 7 */ - // eslint-disable-next-line no-console - console.warn( - name + - ': 里面 postcss.plugin 被弃用. 迁移指南:\n' + - 'https://www.w3ctech.com/topic/2226' - ) - } - } - let transformer = initializer(...args) - transformer.postcssPlugin = name - transformer.postcssVersion = new Processor().version - return transformer - } - - let cache - Object.defineProperty(creator, 'postcss', { - get() { - if (!cache) cache = creator() - return cache - } - }) - - creator.process = function (css, processOpts, pluginOpts) { - return postcss([creator(pluginOpts)]).process(css, processOpts) - } - - return creator -} - -postcss.stringify = stringify -postcss.parse = parse -postcss.fromJSON = fromJSON -postcss.list = list - -postcss.comment = defaults => new Comment(defaults) -postcss.atRule = defaults => new AtRule(defaults) -postcss.decl = defaults => new Declaration(defaults) -postcss.rule = defaults => new Rule(defaults) -postcss.root = defaults => new Root(defaults) -postcss.document = defaults => new Document(defaults) - -postcss.CssSyntaxError = CssSyntaxError -postcss.Declaration = Declaration -postcss.Container = Container -postcss.Processor = Processor -postcss.Document = Document -postcss.Comment = Comment -postcss.Warning = Warning -postcss.AtRule = AtRule -postcss.Result = Result -postcss.Input = Input -postcss.Rule = Rule -postcss.Root = Root -postcss.Node = Node - -LazyResult.registerPostcss(postcss) - -module.exports = postcss -postcss.default = postcss - -}).call(this)}).call(this,require('_process')) -},{"./at-rule":35,"./comment":36,"./container":37,"./css-syntax-error":38,"./declaration":39,"./document":40,"./fromJSON":41,"./input":42,"./lazy-result":43,"./list":44,"./node":47,"./parse":48,"./processor":52,"./result.js":53,"./root":54,"./rule":55,"./stringify":57,"./warning":61,"_process":62}],51:[function(require,module,exports){ -(function (Buffer){(function (){ -'use strict' - -let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js') -let { existsSync, readFileSync } = require('fs') -let { dirname, join } = require('path') - -function fromBase64(str) { - if (Buffer) { - return Buffer.from(str, 'base64').toString() - } else { - /* c8 ignore next 2 */ - return window.atob(str) - } -} - -class PreviousMap { - constructor(css, opts) { - if (opts.map === false) return - this.loadAnnotation(css) - this.inline = this.startWith(this.annotation, 'data:') - - let prev = opts.map ? opts.map.prev : undefined - let text = this.loadMap(opts.from, prev) - if (!this.mapFile && opts.from) { - this.mapFile = opts.from - } - if (this.mapFile) this.root = dirname(this.mapFile) - if (text) this.text = text - } - - consumer() { - if (!this.consumerCache) { - this.consumerCache = new SourceMapConsumer(this.text) - } - return this.consumerCache - } - - decodeInline(text) { - let baseCharsetUri = /^data:application\/json;charset=utf-?8;base64,/ - let baseUri = /^data:application\/json;base64,/ - let charsetUri = /^data:application\/json;charset=utf-?8,/ - let uri = /^data:application\/json,/ - - if (charsetUri.test(text) || uri.test(text)) { - return decodeURIComponent(text.substr(RegExp.lastMatch.length)) - } - - if (baseCharsetUri.test(text) || baseUri.test(text)) { - return fromBase64(text.substr(RegExp.lastMatch.length)) - } - - let encoding = text.match(/data:application\/json;([^,]+),/)[1] - throw new Error('Unsupported source map encoding ' + encoding) - } - - getAnnotationURL(sourceMapString) { - return sourceMapString.replace(/^\/\*\s*# sourceMappingURL=/, '').trim() - } - - isMap(map) { - if (typeof map !== 'object') return false - return ( - typeof map.mappings === 'string' || - typeof map._mappings === 'string' || - Array.isArray(map.sections) - ) - } - - loadAnnotation(css) { - let comments = css.match(/\/\*\s*# sourceMappingURL=/gm) - if (!comments) return - - // sourceMappingURLs from comments, strings, etc. - let start = css.lastIndexOf(comments.pop()) - let end = css.indexOf('*/', start) - - if (start > -1 && end > -1) { - // Locate the last sourceMappingURL to avoid pickin - this.annotation = this.getAnnotationURL(css.substring(start, end)) - } - } - - loadFile(path) { - this.root = dirname(path) - if (existsSync(path)) { - this.mapFile = path - return readFileSync(path, 'utf-8').toString().trim() - } - } - - loadMap(file, prev) { - if (prev === false) return false - - if (prev) { - if (typeof prev === 'string') { - return prev - } else if (typeof prev === 'function') { - let prevPath = prev(file) - if (prevPath) { - let map = this.loadFile(prevPath) - if (!map) { - throw new Error( - 'Unable to load previous source map: ' + prevPath.toString() - ) - } - return map - } - } else if (prev instanceof SourceMapConsumer) { - return SourceMapGenerator.fromSourceMap(prev).toString() - } else if (prev instanceof SourceMapGenerator) { - return prev.toString() - } else if (this.isMap(prev)) { - return JSON.stringify(prev) - } else { - throw new Error( - 'Unsupported previous source map format: ' + prev.toString() - ) - } - } else if (this.inline) { - return this.decodeInline(this.annotation) - } else if (this.annotation) { - let map = this.annotation - if (file) map = join(dirname(file), map) - return this.loadFile(map) - } - } - - startWith(string, start) { - if (!string) return false - return string.substr(0, start.length) === start - } - - withContent() { - return !!( - this.consumer().sourcesContent && - this.consumer().sourcesContent.length > 0 - ) - } -} - -module.exports = PreviousMap -PreviousMap.default = PreviousMap - -}).call(this)}).call(this,require("buffer").Buffer) -},{"buffer":3,"fs":2,"path":2,"source-map-js":2}],52:[function(require,module,exports){ -(function (process){(function (){ -'use strict' - -let NoWorkResult = require('./no-work-result') -let LazyResult = require('./lazy-result') -let Document = require('./document') -let Root = require('./root') - -class Processor { - constructor(plugins = []) { - this.version = '8.4.31' - this.plugins = this.normalize(plugins) - } - - normalize(plugins) { - let normalized = [] - for (let i of plugins) { - if (i.postcss === true) { - i = i() - } else if (i.postcss) { - i = i.postcss - } - - if (typeof i === 'object' && Array.isArray(i.plugins)) { - normalized = normalized.concat(i.plugins) - } else if (typeof i === 'object' && i.postcssPlugin) { - normalized.push(i) - } else if (typeof i === 'function') { - normalized.push(i) - } else if (typeof i === 'object' && (i.parse || i.stringify)) { - if (process.env.NODE_ENV !== 'production') { - throw new Error( - 'PostCSS syntaxes cannot be used as plugins. Instead, please use ' + - 'one of the syntax/parser/stringifier options as outlined ' + - 'in your PostCSS runner documentation.' - ) - } - } else { - throw new Error(i + ' is not a PostCSS plugin') - } - } - return normalized - } - - process(css, opts = {}) { - if ( - this.plugins.length === 0 && - typeof opts.parser === 'undefined' && - typeof opts.stringifier === 'undefined' && - typeof opts.syntax === 'undefined' - ) { - return new NoWorkResult(this, css, opts) - } else { - return new LazyResult(this, css, opts) - } - } - - use(plugin) { - this.plugins = this.plugins.concat(this.normalize([plugin])) - return this - } -} - -module.exports = Processor -Processor.default = Processor - -Root.registerProcessor(Processor) -Document.registerProcessor(Processor) - -}).call(this)}).call(this,require('_process')) -},{"./document":40,"./lazy-result":43,"./no-work-result":46,"./root":54,"_process":62}],53:[function(require,module,exports){ -'use strict' - -let Warning = require('./warning') - -class Result { - constructor(processor, root, opts) { - this.processor = processor - this.messages = [] - this.root = root - this.opts = opts - this.css = undefined - this.map = undefined - } - - toString() { - return this.css - } - - warn(text, opts = {}) { - if (!opts.plugin) { - if (this.lastPlugin && this.lastPlugin.postcssPlugin) { - opts.plugin = this.lastPlugin.postcssPlugin - } - } - - let warning = new Warning(text, opts) - this.messages.push(warning) - - return warning - } - - warnings() { - return this.messages.filter(i => i.type === 'warning') - } - - get content() { - return this.css - } -} - -module.exports = Result -Result.default = Result - -},{"./warning":61}],54:[function(require,module,exports){ -'use strict' - -let Container = require('./container') - -let LazyResult, Processor - -class Root extends Container { - constructor(defaults) { - super(defaults) - this.type = 'root' - if (!this.nodes) this.nodes = [] - } - - normalize(child, sample, type) { - let nodes = super.normalize(child) - - if (sample) { - if (type === 'prepend') { - if (this.nodes.length > 1) { - sample.raws.before = this.nodes[1].raws.before - } else { - delete sample.raws.before - } - } else if (this.first !== sample) { - for (let node of nodes) { - node.raws.before = sample.raws.before - } - } - } - - return nodes - } - - removeChild(child, ignore) { - let index = this.index(child) - - if (!ignore && index === 0 && this.nodes.length > 1) { - this.nodes[1].raws.before = this.nodes[index].raws.before - } - - return super.removeChild(child) - } - - toResult(opts = {}) { - let lazy = new LazyResult(new Processor(), this, opts) - return lazy.stringify() - } -} - -Root.registerLazyResult = dependant => { - LazyResult = dependant -} - -Root.registerProcessor = dependant => { - Processor = dependant -} - -module.exports = Root -Root.default = Root - -Container.registerRoot(Root) - -},{"./container":37}],55:[function(require,module,exports){ -'use strict' - -let Container = require('./container') -let list = require('./list') - -class Rule extends Container { - constructor(defaults) { - super(defaults) - this.type = 'rule' - if (!this.nodes) this.nodes = [] - } - - get selectors() { - return list.comma(this.selector) - } - - set selectors(values) { - let match = this.selector ? this.selector.match(/,\s*/) : null - let sep = match ? match[0] : ',' + this.raw('between', 'beforeOpen') - this.selector = values.join(sep) - } -} - -module.exports = Rule -Rule.default = Rule - -Container.registerRule(Rule) - -},{"./container":37,"./list":44}],56:[function(require,module,exports){ -'use strict' - -const DEFAULT_RAW = { - after: '\n', - beforeClose: '\n', - beforeComment: '\n', - beforeDecl: '\n', - beforeOpen: ' ', - beforeRule: '\n', - colon: ': ', - commentLeft: ' ', - commentRight: ' ', - emptyBody: '', - indent: ' ', - semicolon: false -} - -function capitalize(str) { - return str[0].toUpperCase() + str.slice(1) -} - -class Stringifier { - constructor(builder) { - this.builder = builder - } - - atrule(node, semicolon) { - let name = '@' + node.name - let params = node.params ? this.rawValue(node, 'params') : '' - - if (typeof node.raws.afterName !== 'undefined') { - name += node.raws.afterName - } else if (params) { - name += ' ' - } - - if (node.nodes) { - this.block(node, name + params) - } else { - let end = (node.raws.between || '') + (semicolon ? ';' : '') - this.builder(name + params + end, node) - } - } - - beforeAfter(node, detect) { - let value - if (node.type === 'decl') { - value = this.raw(node, null, 'beforeDecl') - } else if (node.type === 'comment') { - value = this.raw(node, null, 'beforeComment') - } else if (detect === 'before') { - value = this.raw(node, null, 'beforeRule') - } else { - value = this.raw(node, null, 'beforeClose') - } - - let buf = node.parent - let depth = 0 - while (buf && buf.type !== 'root') { - depth += 1 - buf = buf.parent - } - - if (value.includes('\n')) { - let indent = this.raw(node, null, 'indent') - if (indent.length) { - for (let step = 0; step < depth; step++) value += indent - } - } - - return value - } - - block(node, start) { - let between = this.raw(node, 'between', 'beforeOpen') - this.builder(start + between + '{', node, 'start') - - let after - if (node.nodes && node.nodes.length) { - this.body(node) - after = this.raw(node, 'after') - } else { - after = this.raw(node, 'after', 'emptyBody') - } - - if (after) this.builder(after) - this.builder('}', node, 'end') - } - - body(node) { - let last = node.nodes.length - 1 - while (last > 0) { - if (node.nodes[last].type !== 'comment') break - last -= 1 - } - - let semicolon = this.raw(node, 'semicolon') - for (let i = 0; i < node.nodes.length; i++) { - let child = node.nodes[i] - let before = this.raw(child, 'before') - if (before) this.builder(before) - this.stringify(child, last !== i || semicolon) - } - } - - comment(node) { - let left = this.raw(node, 'left', 'commentLeft') - let right = this.raw(node, 'right', 'commentRight') - this.builder('/*' + left + node.text + right + '*/', node) - } - - decl(node, semicolon) { - let between = this.raw(node, 'between', 'colon') - let string = node.prop + between + this.rawValue(node, 'value') - - if (node.important) { - string += node.raws.important || ' !important' - } - - if (semicolon) string += ';' - this.builder(string, node) - } - - document(node) { - this.body(node) - } - - raw(node, own, detect) { - let value - if (!detect) detect = own - - // Already had - if (own) { - value = node.raws[own] - if (typeof value !== 'undefined') return value - } - - let parent = node.parent - - if (detect === 'before') { - // Hack for first rule in CSS - if (!parent || (parent.type === 'root' && parent.first === node)) { - return '' - } - - // `root` nodes in `document` should use only their own raws - if (parent && parent.type === 'document') { - return '' - } - } - - // Floating child without parent - if (!parent) return DEFAULT_RAW[detect] - - // Detect style by other nodes - let root = node.root() - if (!root.rawCache) root.rawCache = {} - if (typeof root.rawCache[detect] !== 'undefined') { - return root.rawCache[detect] - } - - if (detect === 'before' || detect === 'after') { - return this.beforeAfter(node, detect) - } else { - let method = 'raw' + capitalize(detect) - if (this[method]) { - value = this[method](root, node) - } else { - root.walk(i => { - value = i.raws[own] - if (typeof value !== 'undefined') return false - }) - } - } - - if (typeof value === 'undefined') value = DEFAULT_RAW[detect] - - root.rawCache[detect] = value - return value - } - - rawBeforeClose(root) { - let value - root.walk(i => { - if (i.nodes && i.nodes.length > 0) { - if (typeof i.raws.after !== 'undefined') { - value = i.raws.after - if (value.includes('\n')) { - value = value.replace(/[^\n]+$/, '') - } - return false - } - } - }) - if (value) value = value.replace(/\S/g, '') - return value - } - - rawBeforeComment(root, node) { - let value - root.walkComments(i => { - if (typeof i.raws.before !== 'undefined') { - value = i.raws.before - if (value.includes('\n')) { - value = value.replace(/[^\n]+$/, '') - } - return false - } - }) - if (typeof value === 'undefined') { - value = this.raw(node, null, 'beforeDecl') - } else if (value) { - value = value.replace(/\S/g, '') - } - return value - } - - rawBeforeDecl(root, node) { - let value - root.walkDecls(i => { - if (typeof i.raws.before !== 'undefined') { - value = i.raws.before - if (value.includes('\n')) { - value = value.replace(/[^\n]+$/, '') - } - return false - } - }) - if (typeof value === 'undefined') { - value = this.raw(node, null, 'beforeRule') - } else if (value) { - value = value.replace(/\S/g, '') - } - return value - } - - rawBeforeOpen(root) { - let value - root.walk(i => { - if (i.type !== 'decl') { - value = i.raws.between - if (typeof value !== 'undefined') return false - } - }) - return value - } - - rawBeforeRule(root) { - let value - root.walk(i => { - if (i.nodes && (i.parent !== root || root.first !== i)) { - if (typeof i.raws.before !== 'undefined') { - value = i.raws.before - if (value.includes('\n')) { - value = value.replace(/[^\n]+$/, '') - } - return false - } - } - }) - if (value) value = value.replace(/\S/g, '') - return value - } - - rawColon(root) { - let value - root.walkDecls(i => { - if (typeof i.raws.between !== 'undefined') { - value = i.raws.between.replace(/[^\s:]/g, '') - return false - } - }) - return value - } - - rawEmptyBody(root) { - let value - root.walk(i => { - if (i.nodes && i.nodes.length === 0) { - value = i.raws.after - if (typeof value !== 'undefined') return false - } - }) - return value - } - - rawIndent(root) { - if (root.raws.indent) return root.raws.indent - let value - root.walk(i => { - let p = i.parent - if (p && p !== root && p.parent && p.parent === root) { - if (typeof i.raws.before !== 'undefined') { - let parts = i.raws.before.split('\n') - value = parts[parts.length - 1] - value = value.replace(/\S/g, '') - return false - } - } - }) - return value - } - - rawSemicolon(root) { - let value - root.walk(i => { - if (i.nodes && i.nodes.length && i.last.type === 'decl') { - value = i.raws.semicolon - if (typeof value !== 'undefined') return false - } - }) - return value - } - - rawValue(node, prop) { - let value = node[prop] - let raw = node.raws[prop] - if (raw && raw.value === value) { - return raw.raw - } - - return value - } - - root(node) { - this.body(node) - if (node.raws.after) this.builder(node.raws.after) - } - - rule(node) { - this.block(node, this.rawValue(node, 'selector')) - if (node.raws.ownSemicolon) { - this.builder(node.raws.ownSemicolon, node, 'end') - } - } - - stringify(node, semicolon) { - /* c8 ignore start */ - if (!this[node.type]) { - throw new Error( - 'Unknown AST node type ' + - node.type + - '. ' + - 'Maybe you need to change PostCSS stringifier.' - ) - } - /* c8 ignore stop */ - this[node.type](node, semicolon) - } -} - -module.exports = Stringifier -Stringifier.default = Stringifier - -},{}],57:[function(require,module,exports){ -'use strict' - -let Stringifier = require('./stringifier') - -function stringify(node, builder) { - let str = new Stringifier(builder) - str.stringify(node) -} - -module.exports = stringify -stringify.default = stringify - -},{"./stringifier":56}],58:[function(require,module,exports){ -'use strict' - -module.exports.isClean = Symbol('isClean') - -module.exports.my = Symbol('my') - -},{}],59:[function(require,module,exports){ -'use strict' - -const SINGLE_QUOTE = "'".charCodeAt(0) -const DOUBLE_QUOTE = '"'.charCodeAt(0) -const BACKSLASH = '\\'.charCodeAt(0) -const SLASH = '/'.charCodeAt(0) -const NEWLINE = '\n'.charCodeAt(0) -const SPACE = ' '.charCodeAt(0) -const FEED = '\f'.charCodeAt(0) -const TAB = '\t'.charCodeAt(0) -const CR = '\r'.charCodeAt(0) -const OPEN_SQUARE = '['.charCodeAt(0) -const CLOSE_SQUARE = ']'.charCodeAt(0) -const OPEN_PARENTHESES = '('.charCodeAt(0) -const CLOSE_PARENTHESES = ')'.charCodeAt(0) -const OPEN_CURLY = '{'.charCodeAt(0) -const CLOSE_CURLY = '}'.charCodeAt(0) -const SEMICOLON = ';'.charCodeAt(0) -const ASTERISK = '*'.charCodeAt(0) -const COLON = ':'.charCodeAt(0) -const AT = '@'.charCodeAt(0) - -const RE_AT_END = /[\t\n\f\r "#'()/;[\\\]{}]/g -const RE_WORD_END = /[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g -const RE_BAD_BRACKET = /.[\r\n"'(/\\]/ -const RE_HEX_ESCAPE = /[\da-f]/i - -module.exports = function tokenizer(input, options = {}) { - let css = input.css.valueOf() - let ignore = options.ignoreErrors - - let code, next, quote, content, escape - let escaped, escapePos, prev, n, currentToken - - let length = css.length - let pos = 0 - let buffer = [] - let returned = [] - - function position() { - return pos - } - - function unclosed(what) { - throw input.error('Unclosed ' + what, pos) - } - - function endOfFile() { - return returned.length === 0 && pos >= length - } - - function nextToken(opts) { - if (returned.length) return returned.pop() - if (pos >= length) return - - let ignoreUnclosed = opts ? opts.ignoreUnclosed : false - - code = css.charCodeAt(pos) - - switch (code) { - case NEWLINE: - case SPACE: - case TAB: - case CR: - case FEED: { - next = pos - do { - next += 1 - code = css.charCodeAt(next) - } while ( - code === SPACE || - code === NEWLINE || - code === TAB || - code === CR || - code === FEED - ) - - currentToken = ['space', css.slice(pos, next)] - pos = next - 1 - break - } - - case OPEN_SQUARE: - case CLOSE_SQUARE: - case OPEN_CURLY: - case CLOSE_CURLY: - case COLON: - case SEMICOLON: - case CLOSE_PARENTHESES: { - let controlChar = String.fromCharCode(code) - currentToken = [controlChar, controlChar, pos] - break - } - - case OPEN_PARENTHESES: { - prev = buffer.length ? buffer.pop()[1] : '' - n = css.charCodeAt(pos + 1) - if ( - prev === 'url' && - n !== SINGLE_QUOTE && - n !== DOUBLE_QUOTE && - n !== SPACE && - n !== NEWLINE && - n !== TAB && - n !== FEED && - n !== CR - ) { - next = pos - do { - escaped = false - next = css.indexOf(')', next + 1) - if (next === -1) { - if (ignore || ignoreUnclosed) { - next = pos - break } else { - unclosed('bracket') + this.cbs.oncomment(this.sectionStart, endIndex, 0); + } + } else if ( + this.state === State.InNumericEntity && + this.allowLegacyEntity() + ) { + this.emitNumericEntity(false); + // All trailing data will have been consumed + } else if ( + this.state === State.InHexEntity && + this.allowLegacyEntity() + ) { + this.emitNumericEntity(false); + // All trailing data will have been consumed + } else if ( + this.state === State.InTagName || + this.state === State.BeforeAttributeName || + this.state === State.BeforeAttributeValue || + this.state === State.AfterAttributeName || + this.state === State.InAttributeName || + this.state === State.InAttributeValueSq || + this.state === State.InAttributeValueDq || + this.state === State.InAttributeValueNq || + this.state === State.InClosingTagName + ) { + /* + * If we are currently in an opening or closing tag, us not calling the + * respective callback signals that the tag should be ignored. + */ + } else { + this.cbs.ontext(this.sectionStart, endIndex); + } + }; + Tokenizer.prototype.emitPartial = function (start, endIndex) { + if ( + this.baseState !== State.Text && + this.baseState !== State.InSpecialTag + ) { + this.cbs.onattribdata(start, endIndex); + } else { + this.cbs.ontext(start, endIndex); + } + }; + Tokenizer.prototype.emitCodePoint = function (cp) { + if ( + this.baseState !== State.Text && + this.baseState !== State.InSpecialTag + ) { + this.cbs.onattribentity(cp); + } else { + this.cbs.ontextentity(cp); + } + }; + return Tokenizer; + })(); + exports.default = Tokenizer; + }, + { 'entities/lib/decode.js': 18 }, + ], + 29: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if ( + !desc || + ('get' in desc + ? !m.__esModule + : desc.writable || desc.configurable) + ) { + desc = { + enumerable: true, + get: function () { + return m[k]; + }, + }; + } + Object.defineProperty(o, k2, desc); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { + enumerable: true, + value: v, + }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if ( + k !== 'default' && + Object.prototype.hasOwnProperty.call(mod, k) + ) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { 'default': mod }; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.DomUtils = + exports.parseFeed = + exports.getFeed = + exports.ElementType = + exports.Tokenizer = + exports.createDomStream = + exports.parseDOM = + exports.parseDocument = + exports.DefaultHandler = + exports.DomHandler = + exports.Parser = + void 0; + var Parser_js_1 = require('./Parser.js'); + var Parser_js_2 = require('./Parser.js'); + Object.defineProperty(exports, 'Parser', { + enumerable: true, + get: function () { + return Parser_js_2.Parser; + }, + }); + var domhandler_1 = require('domhandler'); + var domhandler_2 = require('domhandler'); + Object.defineProperty(exports, 'DomHandler', { + enumerable: true, + get: function () { + return domhandler_2.DomHandler; + }, + }); + // Old name for DomHandler + Object.defineProperty(exports, 'DefaultHandler', { + enumerable: true, + get: function () { + return domhandler_2.DomHandler; + }, + }); + // Helper methods + /** + * Parses the data, returns the resulting document. + * + * @param data The data that should be parsed. + * @param options Optional options for the parser and DOM builder. + */ + function parseDocument(data, options) { + var handler = new domhandler_1.DomHandler(undefined, options); + new Parser_js_1.Parser(handler, options).end(data); + return handler.root; + } + exports.parseDocument = parseDocument; + /** + * Parses data, returns an array of the root nodes. + * + * Note that the root nodes still have a `Document` node as their parent. + * Use `parseDocument` to get the `Document` node instead. + * + * @param data The data that should be parsed. + * @param options Optional options for the parser and DOM builder. + * @deprecated Use `parseDocument` instead. + */ + function parseDOM(data, options) { + return parseDocument(data, options).children; + } + exports.parseDOM = parseDOM; + /** + * Creates a parser instance, with an attached DOM handler. + * + * @param callback A callback that will be called once parsing has been completed. + * @param options Optional options for the parser and DOM builder. + * @param elementCallback An optional callback that will be called every time a tag has been completed inside of the DOM. + */ + function createDomStream(callback, options, elementCallback) { + var handler = new domhandler_1.DomHandler( + callback, + options, + elementCallback, + ); + return new Parser_js_1.Parser(handler, options); + } + exports.createDomStream = createDomStream; + var Tokenizer_js_1 = require('./Tokenizer.js'); + Object.defineProperty(exports, 'Tokenizer', { + enumerable: true, + get: function () { + return __importDefault(Tokenizer_js_1).default; + }, + }); + /* + * All of the following exports exist for backwards-compatibility. + * They should probably be removed eventually. + */ + exports.ElementType = __importStar(require('domelementtype')); + var domutils_1 = require('domutils'); + var domutils_2 = require('domutils'); + Object.defineProperty(exports, 'getFeed', { + enumerable: true, + get: function () { + return domutils_2.getFeed; + }, + }); + var parseFeedDefaultOptions = { xmlMode: true }; + /** + * Parse a feed. + * + * @param feed The feed that should be parsed, as a string. + * @param options Optionally, options for parsing. When using this, you should set `xmlMode` to `true`. + */ + function parseFeed(feed, options) { + if (options === void 0) { + options = parseFeedDefaultOptions; + } + return (0, domutils_1.getFeed)(parseDOM(feed, options)); + } + exports.parseFeed = parseFeed; + exports.DomUtils = __importStar(require('domutils')); + }, + { + './Parser.js': 27, + './Tokenizer.js': 28, + 'domelementtype': 7, + 'domhandler': 8, + 'domutils': 12, + }, + ], + 30: [ + function (require, module, exports) { + /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ + exports.read = function (buffer, offset, isLE, mLen, nBytes) { + var e, m; + var eLen = nBytes * 8 - mLen - 1; + var eMax = (1 << eLen) - 1; + var eBias = eMax >> 1; + var nBits = -7; + var i = isLE ? nBytes - 1 : 0; + var d = isLE ? -1 : 1; + var s = buffer[offset + i]; + + i += d; + + e = s & ((1 << -nBits) - 1); + s >>= -nBits; + nBits += eLen; + for ( + ; + nBits > 0; + e = e * 256 + buffer[offset + i], i += d, nBits -= 8 + ) {} + + m = e & ((1 << -nBits) - 1); + e >>= -nBits; + nBits += mLen; + for ( + ; + nBits > 0; + m = m * 256 + buffer[offset + i], i += d, nBits -= 8 + ) {} + + if (e === 0) { + e = 1 - eBias; + } else if (e === eMax) { + return m ? NaN : (s ? -1 : 1) * Infinity; + } else { + m = m + Math.pow(2, mLen); + e = e - eBias; + } + return (s ? -1 : 1) * m * Math.pow(2, e - mLen); + }; + + exports.write = function (buffer, value, offset, isLE, mLen, nBytes) { + var e, m, c; + var eLen = nBytes * 8 - mLen - 1; + var eMax = (1 << eLen) - 1; + var eBias = eMax >> 1; + var rt = mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0; + var i = isLE ? 0 : nBytes - 1; + var d = isLE ? 1 : -1; + var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0; + + value = Math.abs(value); + + if (isNaN(value) || value === Infinity) { + m = isNaN(value) ? 1 : 0; + e = eMax; + } else { + e = Math.floor(Math.log(value) / Math.LN2); + if (value * (c = Math.pow(2, -e)) < 1) { + e--; + c *= 2; + } + if (e + eBias >= 1) { + value += rt / c; + } else { + value += rt * Math.pow(2, 1 - eBias); + } + if (value * c >= 2) { + e++; + c /= 2; + } + + if (e + eBias >= eMax) { + m = 0; + e = eMax; + } else if (e + eBias >= 1) { + m = (value * c - 1) * Math.pow(2, mLen); + e = e + eBias; + } else { + m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen); + e = 0; + } + } + + for ( + ; + mLen >= 8; + buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8 + ) {} + + e = (e << mLen) | m; + eLen += mLen; + for ( + ; + eLen > 0; + buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8 + ) {} + + buffer[offset + i - d] |= s * 128; + }; + }, + {}, + ], + 31: [ + function (require, module, exports) { + 'use strict'; + + Object.defineProperty(exports, '__esModule', { value: true }); + + /*! + * is-plain-object + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ + + function isObject(o) { + return Object.prototype.toString.call(o) === '[object Object]'; + } + + function isPlainObject(o) { + var ctor, prot; + + if (isObject(o) === false) return false; + + // If has modified constructor + ctor = o.constructor; + if (ctor === undefined) return true; + + // If has modified prototype + prot = ctor.prototype; + if (isObject(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) { + return false; + } + + // Most likely a plain Object + return true; + } + + exports.isPlainObject = isPlainObject; + }, + {}, + ], + 32: [ + function (require, module, exports) { + let urlAlphabet = + 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; + let customAlphabet = (alphabet, defaultSize = 21) => { + return (size = defaultSize) => { + let id = ''; + let i = size; + while (i--) { + id += alphabet[(Math.random() * alphabet.length) | 0]; + } + return id; + }; + }; + let nanoid = (size = 21) => { + let id = ''; + let i = size; + while (i--) { + id += urlAlphabet[(Math.random() * 64) | 0]; + } + return id; + }; + module.exports = { nanoid, customAlphabet }; + }, + {}, + ], + 33: [ + function (require, module, exports) { + /** + * Srcset Parser + * + * By Alex Bell | MIT License + * + * JS Parser for the string value that appears in markup + * + * @returns Array [{url: _, d: _, w: _, h:_}, ...] + * + * Based super duper closely on the reference algorithm at: + * https://html.spec.whatwg.org/multipage/embedded-content.html#parse-a-srcset-attribute + * + * Most comments are copied in directly from the spec + * (except for comments in parens). + */ + + (function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.parseSrcset = factory(); + } + })(this, function () { + // 1. Let input be the value passed to this algorithm. + return function (input) { + // UTILITY FUNCTIONS + + // Manual is faster than RegEx + // http://bjorn.tipling.com/state-and-regular-expressions-in-javascript + // http://jsperf.com/whitespace-character/5 + function isSpace(c) { + return ( + c === '\u0020' || // space + c === '\u0009' || // horizontal tab + c === '\u000A' || // new line + c === '\u000C' || // form feed + c === '\u000D' + ); // carriage return + } + + function collectCharacters(regEx) { + var chars, + match = regEx.exec(input.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; } } - escapePos = next - while (css.charCodeAt(escapePos - 1) === BACKSLASH) { - escapePos -= 1 - escaped = !escaped + + var inputLength = input.length, + // (Don't use \s, to avoid matching non-breaking space) + regexLeadingSpaces = /^[ \t\n\r\u000c]+/, + regexLeadingCommasOrSpaces = /^[, \t\n\r\u000c]+/, + regexLeadingNotSpaces = /^[^ \t\n\r\u000c]+/, + regexTrailingCommas = /[,]+$/, + regexNonNegativeInteger = /^\d+$/, + // ( Positive or negative or unsigned integers or decimals, without or without exponents. + // Must include at least one digit. + // According to spec tests any decimal point must be followed by a digit. + // No leading plus sign is allowed.) + // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number + regexFloatingPoint = + /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/, + url, + descriptors, + currentDescriptor, + state, + c, + // 2. Let position be a pointer into input, initially pointing at the start + // of the string. + pos = 0, + // 3. Let candidates be an initially empty source set. + candidates = []; + + // 4. Splitting loop: Collect a sequence of characters that are space + // characters or U+002C COMMA characters. If any U+002C COMMA characters + // were collected, that is a parse error. + while (true) { + collectCharacters(regexLeadingCommasOrSpaces); + + // 5. If position is past the end of input, return candidates and abort these steps. + if (pos >= inputLength) { + return candidates; // (we're done, this is the sole return path) + } + + // 6. Collect a sequence of characters that are not space characters, + // and let that be url. + url = collectCharacters(regexLeadingNotSpaces); + + // 7. Let descriptors be a new empty list. + descriptors = []; + + // 8. If url ends with a U+002C COMMA character (,), follow these substeps: + // (1). Remove all trailing U+002C COMMA characters from url. If this removed + // more than one character, that is a parse error. + if (url.slice(-1) === ',') { + url = url.replace(regexTrailingCommas, ''); + // (Jump ahead to step 9 to skip tokenization and just push the candidate). + parseDescriptors(); + + // Otherwise, follow these substeps: + } else { + tokenize(); + } // (close else of step 8) + + // 16. Return to the step labeled splitting loop. + } // (Close of big while loop.) + + /** + * Tokenizes descriptor properties prior to parsing + * Returns undefined. + */ + function tokenize() { + // 8.1. Descriptor tokeniser: Skip whitespace + collectCharacters(regexLeadingSpaces); + + // 8.2. Let current descriptor be the empty string. + currentDescriptor = ''; + + // 8.3. Let state be in descriptor. + state = 'in descriptor'; + + while (true) { + // 8.4. Let c be the character at position. + c = input.charAt(pos); + + // Do the following depending on the value of state. + // For the purpose of this step, "EOF" is a special character representing + // that position is past the end of input. + + // In descriptor + if (state === 'in descriptor') { + // Do the following, depending on the value of c: + + // Space character + // If current descriptor is not empty, append current descriptor to + // descriptors and let current descriptor be the empty string. + // Set state to after descriptor. + if (isSpace(c)) { + if (currentDescriptor) { + descriptors.push(currentDescriptor); + currentDescriptor = ''; + state = 'after descriptor'; + } + + // U+002C COMMA (,) + // Advance position to the next character in input. If current descriptor + // is not empty, append current descriptor to descriptors. Jump to the step + // labeled descriptor parser. + } else if (c === ',') { + pos += 1; + if (currentDescriptor) { + descriptors.push(currentDescriptor); + } + parseDescriptors(); + return; + + // U+0028 LEFT PARENTHESIS (() + // Append c to current descriptor. Set state to in parens. + } else if (c === '\u0028') { + currentDescriptor = currentDescriptor + c; + state = 'in parens'; + + // EOF + // If current descriptor is not empty, append current descriptor to + // descriptors. Jump to the step labeled descriptor parser. + } else if (c === '') { + if (currentDescriptor) { + descriptors.push(currentDescriptor); + } + parseDescriptors(); + return; + + // Anything else + // Append c to current descriptor. + } else { + currentDescriptor = currentDescriptor + c; + } + // (end "in descriptor" + + // In parens + } else if (state === 'in parens') { + // U+0029 RIGHT PARENTHESIS ()) + // Append c to current descriptor. Set state to in descriptor. + if (c === ')') { + currentDescriptor = currentDescriptor + c; + state = 'in descriptor'; + + // EOF + // Append current descriptor to descriptors. Jump to the step labeled + // descriptor parser. + } else if (c === '') { + descriptors.push(currentDescriptor); + parseDescriptors(); + return; + + // Anything else + // Append c to current descriptor. + } else { + currentDescriptor = currentDescriptor + c; + } + + // After descriptor + } else if (state === 'after descriptor') { + // Do the following, depending on the value of c: + // Space character: Stay in this state. + if (isSpace(c)) { + // EOF: Jump to the step labeled descriptor parser. + } else if (c === '') { + parseDescriptors(); + return; + + // Anything else + // Set state to in descriptor. Set position to the previous character in input. + } else { + state = 'in descriptor'; + pos -= 1; + } + } + + // Advance position to the next character in input. + pos += 1; + + // Repeat this step. + } // (close while true loop) } - } while (escaped) - currentToken = ['brackets', css.slice(pos, next + 1), pos, next] + /** + * Adds descriptor properties to a candidate, pushes to the candidates array + * @return undefined + */ + // Declared outside of the while loop so that it's only created once. + function parseDescriptors() { + // 9. Descriptor parser: Let error be no. + var pError = false, + // 10. Let width be absent. + // 11. Let density be absent. + // 12. Let future-compat-h be absent. (We're implementing it now as h) + w, + d, + h, + i, + candidate = {}, + desc, + lastChar, + value, + intVal, + floatVal; - pos = next - } else { - next = css.indexOf(')', pos + 1) - content = css.slice(pos, next + 1) + // 13. For each descriptor in descriptors, run the appropriate set of steps + // from the following list: + for (i = 0; i < descriptors.length; i++) { + desc = descriptors[i]; - if (next === -1 || RE_BAD_BRACKET.test(content)) { - currentToken = ['(', '(', pos] - } else { - currentToken = ['brackets', content, pos, next] - pos = next + lastChar = desc[desc.length - 1]; + value = desc.substring(0, desc.length - 1); + intVal = parseInt(value, 10); + floatVal = parseFloat(value); + + // If the descriptor consists of a valid non-negative integer followed by + // a U+0077 LATIN SMALL LETTER W character + if (regexNonNegativeInteger.test(value) && lastChar === 'w') { + // If width and density are not both absent, then let error be yes. + if (w || d) { + pError = true; + } + + // Apply the rules for parsing non-negative integers to the descriptor. + // If the result is zero, let error be yes. + // Otherwise, let width be the result. + if (intVal === 0) { + pError = true; + } else { + w = intVal; + } + + // If the descriptor consists of a valid floating-point number followed by + // a U+0078 LATIN SMALL LETTER X character + } else if (regexFloatingPoint.test(value) && lastChar === 'x') { + // If width, density and future-compat-h are not all absent, then let error + // be yes. + if (w || d || h) { + pError = true; + } + + // Apply the rules for parsing floating-point number values to the descriptor. + // If the result is less than zero, let error be yes. Otherwise, let density + // be the result. + if (floatVal < 0) { + pError = true; + } else { + d = floatVal; + } + + // If the descriptor consists of a valid non-negative integer followed by + // a U+0068 LATIN SMALL LETTER H character + } else if ( + regexNonNegativeInteger.test(value) && + lastChar === 'h' + ) { + // If height and density are not both absent, then let error be yes. + if (h || d) { + pError = true; + } + + // Apply the rules for parsing non-negative integers to the descriptor. + // If the result is zero, let error be yes. Otherwise, let future-compat-h + // be the result. + if (intVal === 0) { + pError = true; + } else { + h = intVal; + } + + // Anything else, Let error be yes. + } else { + pError = true; + } + } // (close step 13 for loop) + + // 15. If error is still no, then append a new image source to candidates whose + // URL is url, associated with a width width if not absent and a pixel + // density density if not absent. Otherwise, there is a parse error. + if (!pError) { + candidate.url = url; + if (w) { + candidate.w = w; + } + if (d) { + candidate.d = d; + } + if (h) { + candidate.h = h; + } + candidates.push(candidate); + } else if (console && console.log) { + console.log( + "Invalid srcset descriptor found in '" + + input + + "' at '" + + desc + + "'.", + ); + } + } // (close parseDescriptors fn) + }; + }); + }, + {}, + ], + 34: [ + function (require, module, exports) { + var x = String; + var create = function () { + return { + isColorSupported: false, + reset: x, + bold: x, + dim: x, + italic: x, + underline: x, + inverse: x, + hidden: x, + strikethrough: x, + black: x, + red: x, + green: x, + yellow: x, + blue: x, + magenta: x, + cyan: x, + white: x, + gray: x, + bgBlack: x, + bgRed: x, + bgGreen: x, + bgYellow: x, + bgBlue: x, + bgMagenta: x, + bgCyan: x, + bgWhite: x, + }; + }; + module.exports = create(); + module.exports.createColors = create; + }, + {}, + ], + 35: [ + function (require, module, exports) { + 'use strict'; + + let Container = require('./container'); + + class AtRule extends Container { + constructor(defaults) { + super(defaults); + this.type = 'atrule'; + } + + append(...children) { + if (!this.proxyOf.nodes) this.nodes = []; + return super.append(...children); + } + + prepend(...children) { + if (!this.proxyOf.nodes) this.nodes = []; + return super.prepend(...children); } } - break - } + module.exports = AtRule; + AtRule.default = AtRule; - case SINGLE_QUOTE: - case DOUBLE_QUOTE: { - quote = code === SINGLE_QUOTE ? "'" : '"' - next = pos - do { - escaped = false - next = css.indexOf(quote, next + 1) - if (next === -1) { - if (ignore || ignoreUnclosed) { - next = pos + 1 - break + Container.registerAtRule(AtRule); + }, + { './container': 37 }, + ], + 36: [ + function (require, module, exports) { + 'use strict'; + + let Node = require('./node'); + + class Comment extends Node { + constructor(defaults) { + super(defaults); + this.type = 'comment'; + } + } + + module.exports = Comment; + Comment.default = Comment; + }, + { './node': 47 }, + ], + 37: [ + function (require, module, exports) { + 'use strict'; + + let { isClean, my } = require('./symbols'); + let Declaration = require('./declaration'); + let Comment = require('./comment'); + let Node = require('./node'); + + let parse, Rule, AtRule, Root; + + function cleanSource(nodes) { + return nodes.map(i => { + if (i.nodes) i.nodes = cleanSource(i.nodes); + delete i.source; + return i; + }); + } + + function markDirtyUp(node) { + node[isClean] = false; + if (node.proxyOf.nodes) { + for (let i of node.proxyOf.nodes) { + markDirtyUp(i); + } + } + } + + class Container extends Node { + append(...children) { + for (let child of children) { + let nodes = this.normalize(child, this.last); + for (let node of nodes) this.proxyOf.nodes.push(node); + } + + this.markDirty(); + + return this; + } + + cleanRaws(keepBetween) { + super.cleanRaws(keepBetween); + if (this.nodes) { + for (let node of this.nodes) node.cleanRaws(keepBetween); + } + } + + each(callback) { + if (!this.proxyOf.nodes) return undefined; + let iterator = this.getIterator(); + + let index, result; + while (this.indexes[iterator] < this.proxyOf.nodes.length) { + index = this.indexes[iterator]; + result = callback(this.proxyOf.nodes[index], index); + if (result === false) break; + + this.indexes[iterator] += 1; + } + + delete this.indexes[iterator]; + return result; + } + + every(condition) { + return this.nodes.every(condition); + } + + getIterator() { + if (!this.lastEach) this.lastEach = 0; + if (!this.indexes) this.indexes = {}; + + this.lastEach += 1; + let iterator = this.lastEach; + this.indexes[iterator] = 0; + + return iterator; + } + + getProxyProcessor() { + return { + get(node, prop) { + if (prop === 'proxyOf') { + return node; + } else if (!node[prop]) { + return node[prop]; + } else if ( + prop === 'each' || + (typeof prop === 'string' && prop.startsWith('walk')) + ) { + return (...args) => { + return node[prop]( + ...args.map(i => { + if (typeof i === 'function') { + return (child, index) => i(child.toProxy(), index); + } else { + return i; + } + }), + ); + }; + } else if (prop === 'every' || prop === 'some') { + return cb => { + return node[prop]((child, ...other) => + cb(child.toProxy(), ...other), + ); + }; + } else if (prop === 'root') { + return () => node.root().toProxy(); + } else if (prop === 'nodes') { + return node.nodes.map(i => i.toProxy()); + } else if (prop === 'first' || prop === 'last') { + return node[prop].toProxy(); + } else { + return node[prop]; + } + }, + + set(node, prop, value) { + if (node[prop] === value) return true; + node[prop] = value; + if ( + prop === 'name' || + prop === 'params' || + prop === 'selector' + ) { + node.markDirty(); + } + return true; + }, + }; + } + + index(child) { + if (typeof child === 'number') return child; + if (child.proxyOf) child = child.proxyOf; + return this.proxyOf.nodes.indexOf(child); + } + + insertAfter(exist, add) { + let existIndex = this.index(exist); + let nodes = this.normalize( + add, + this.proxyOf.nodes[existIndex], + ).reverse(); + existIndex = this.index(exist); + for (let node of nodes) + this.proxyOf.nodes.splice(existIndex + 1, 0, node); + + let index; + for (let id in this.indexes) { + index = this.indexes[id]; + if (existIndex < index) { + this.indexes[id] = index + nodes.length; + } + } + + this.markDirty(); + + return this; + } + + insertBefore(exist, add) { + let existIndex = this.index(exist); + let type = existIndex === 0 ? 'prepend' : false; + let nodes = this.normalize( + add, + this.proxyOf.nodes[existIndex], + type, + ).reverse(); + existIndex = this.index(exist); + for (let node of nodes) + this.proxyOf.nodes.splice(existIndex, 0, node); + + let index; + for (let id in this.indexes) { + index = this.indexes[id]; + if (existIndex <= index) { + this.indexes[id] = index + nodes.length; + } + } + + this.markDirty(); + + return this; + } + + normalize(nodes, sample) { + if (typeof nodes === 'string') { + nodes = cleanSource(parse(nodes).nodes); + } else if (Array.isArray(nodes)) { + nodes = nodes.slice(0); + for (let i of nodes) { + if (i.parent) i.parent.removeChild(i, 'ignore'); + } + } else if (nodes.type === 'root' && this.type !== 'document') { + nodes = nodes.nodes.slice(0); + for (let i of nodes) { + if (i.parent) i.parent.removeChild(i, 'ignore'); + } + } else if (nodes.type) { + nodes = [nodes]; + } else if (nodes.prop) { + if (typeof nodes.value === 'undefined') { + throw new Error('Value field is missed in node creation'); + } else if (typeof nodes.value !== 'string') { + nodes.value = String(nodes.value); + } + nodes = [new Declaration(nodes)]; + } else if (nodes.selector) { + nodes = [new Rule(nodes)]; + } else if (nodes.name) { + nodes = [new AtRule(nodes)]; + } else if (nodes.text) { + nodes = [new Comment(nodes)]; } else { - unclosed('string') + throw new Error('Unknown node type in node creation'); } + + let processed = nodes.map(i => { + /* c8 ignore next */ + if (!i[my]) Container.rebuild(i); + i = i.proxyOf; + if (i.parent) i.parent.removeChild(i); + if (i[isClean]) markDirtyUp(i); + if (typeof i.raws.before === 'undefined') { + if (sample && typeof sample.raws.before !== 'undefined') { + i.raws.before = sample.raws.before.replace(/\S/g, ''); + } + } + i.parent = this.proxyOf; + return i; + }); + + return processed; } - escapePos = next - while (css.charCodeAt(escapePos - 1) === BACKSLASH) { - escapePos -= 1 - escaped = !escaped + + prepend(...children) { + children = children.reverse(); + for (let child of children) { + let nodes = this.normalize( + child, + this.first, + 'prepend', + ).reverse(); + for (let node of nodes) this.proxyOf.nodes.unshift(node); + for (let id in this.indexes) { + this.indexes[id] = this.indexes[id] + nodes.length; + } + } + + this.markDirty(); + + return this; } - } while (escaped) - currentToken = ['string', css.slice(pos, next + 1), pos, next] - pos = next - break - } + push(child) { + child.parent = this; + this.proxyOf.nodes.push(child); + return this; + } - case AT: { - RE_AT_END.lastIndex = pos + 1 - RE_AT_END.test(css) - if (RE_AT_END.lastIndex === 0) { - next = css.length - 1 - } else { - next = RE_AT_END.lastIndex - 2 - } + removeAll() { + for (let node of this.proxyOf.nodes) node.parent = undefined; + this.proxyOf.nodes = []; - currentToken = ['at-word', css.slice(pos, next + 1), pos, next] + this.markDirty(); - pos = next - break - } + return this; + } - case BACKSLASH: { - next = pos - escape = true - while (css.charCodeAt(next + 1) === BACKSLASH) { - next += 1 - escape = !escape - } - code = css.charCodeAt(next + 1) - if ( - escape && - code !== SLASH && - code !== SPACE && - code !== NEWLINE && - code !== TAB && - code !== CR && - code !== FEED - ) { - next += 1 - if (RE_HEX_ESCAPE.test(css.charAt(next))) { - while (RE_HEX_ESCAPE.test(css.charAt(next + 1))) { - next += 1 + removeChild(child) { + child = this.index(child); + this.proxyOf.nodes[child].parent = undefined; + this.proxyOf.nodes.splice(child, 1); + + let index; + for (let id in this.indexes) { + index = this.indexes[id]; + if (index >= child) { + this.indexes[id] = index - 1; + } } - if (css.charCodeAt(next + 1) === SPACE) { - next += 1 + + this.markDirty(); + + return this; + } + + replaceValues(pattern, opts, callback) { + if (!callback) { + callback = opts; + opts = {}; } + + this.walkDecls(decl => { + if (opts.props && !opts.props.includes(decl.prop)) return; + if (opts.fast && !decl.value.includes(opts.fast)) return; + + decl.value = decl.value.replace(pattern, callback); + }); + + this.markDirty(); + + return this; + } + + some(condition) { + return this.nodes.some(condition); + } + + walk(callback) { + return this.each((child, i) => { + let result; + try { + result = callback(child, i); + } catch (e) { + throw child.addToError(e); + } + if (result !== false && child.walk) { + result = child.walk(callback); + } + + return result; + }); + } + + walkAtRules(name, callback) { + if (!callback) { + callback = name; + return this.walk((child, i) => { + if (child.type === 'atrule') { + return callback(child, i); + } + }); + } + if (name instanceof RegExp) { + return this.walk((child, i) => { + if (child.type === 'atrule' && name.test(child.name)) { + return callback(child, i); + } + }); + } + return this.walk((child, i) => { + if (child.type === 'atrule' && child.name === name) { + return callback(child, i); + } + }); + } + + walkComments(callback) { + return this.walk((child, i) => { + if (child.type === 'comment') { + return callback(child, i); + } + }); + } + + walkDecls(prop, callback) { + if (!callback) { + callback = prop; + return this.walk((child, i) => { + if (child.type === 'decl') { + return callback(child, i); + } + }); + } + if (prop instanceof RegExp) { + return this.walk((child, i) => { + if (child.type === 'decl' && prop.test(child.prop)) { + return callback(child, i); + } + }); + } + return this.walk((child, i) => { + if (child.type === 'decl' && child.prop === prop) { + return callback(child, i); + } + }); + } + + walkRules(selector, callback) { + if (!callback) { + callback = selector; + + return this.walk((child, i) => { + if (child.type === 'rule') { + return callback(child, i); + } + }); + } + if (selector instanceof RegExp) { + return this.walk((child, i) => { + if (child.type === 'rule' && selector.test(child.selector)) { + return callback(child, i); + } + }); + } + return this.walk((child, i) => { + if (child.type === 'rule' && child.selector === selector) { + return callback(child, i); + } + }); + } + + get first() { + if (!this.proxyOf.nodes) return undefined; + return this.proxyOf.nodes[0]; + } + + get last() { + if (!this.proxyOf.nodes) return undefined; + return this.proxyOf.nodes[this.proxyOf.nodes.length - 1]; } } - currentToken = ['word', css.slice(pos, next + 1), pos, next] + Container.registerParse = dependant => { + parse = dependant; + }; - pos = next - break - } + Container.registerRule = dependant => { + Rule = dependant; + }; - default: { - if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) { - next = css.indexOf('*/', pos + 2) + 1 - if (next === 0) { - if (ignore || ignoreUnclosed) { - next = css.length + Container.registerAtRule = dependant => { + AtRule = dependant; + }; + + Container.registerRoot = dependant => { + Root = dependant; + }; + + module.exports = Container; + Container.default = Container; + + /* c8 ignore start */ + Container.rebuild = node => { + if (node.type === 'atrule') { + Object.setPrototypeOf(node, AtRule.prototype); + } else if (node.type === 'rule') { + Object.setPrototypeOf(node, Rule.prototype); + } else if (node.type === 'decl') { + Object.setPrototypeOf(node, Declaration.prototype); + } else if (node.type === 'comment') { + Object.setPrototypeOf(node, Comment.prototype); + } else if (node.type === 'root') { + Object.setPrototypeOf(node, Root.prototype); + } + + node[my] = true; + + if (node.nodes) { + node.nodes.forEach(child => { + Container.rebuild(child); + }); + } + }; + /* c8 ignore stop */ + }, + { './comment': 36, './declaration': 39, './node': 47, './symbols': 58 }, + ], + 38: [ + function (require, module, exports) { + 'use strict'; + + let pico = require('picocolors'); + + let terminalHighlight = require('./terminal-highlight'); + + class CssSyntaxError extends Error { + constructor(message, line, column, source, file, plugin) { + super(message); + this.name = 'CssSyntaxError'; + this.reason = message; + + if (file) { + this.file = file; + } + if (source) { + this.source = source; + } + if (plugin) { + this.plugin = plugin; + } + if (typeof line !== 'undefined' && typeof column !== 'undefined') { + if (typeof line === 'number') { + this.line = line; + this.column = column; + } else { + this.line = line.line; + this.column = line.column; + this.endLine = column.line; + this.endColumn = column.column; + } + } + + this.setMessage(); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, CssSyntaxError); + } + } + + setMessage() { + this.message = this.plugin ? this.plugin + ': ' : ''; + this.message += this.file ? this.file : ''; + if (typeof this.line !== 'undefined') { + this.message += ':' + this.line + ':' + this.column; + } + this.message += ': ' + this.reason; + } + + showSourceCode(color) { + if (!this.source) return ''; + + let css = this.source; + if (color == null) color = pico.isColorSupported; + if (terminalHighlight) { + if (color) css = terminalHighlight(css); + } + + let lines = css.split(/\r?\n/); + let start = Math.max(this.line - 3, 0); + let end = Math.min(this.line + 2, lines.length); + + let maxWidth = String(end).length; + + let mark, aside; + if (color) { + let { bold, gray, red } = pico.createColors(true); + mark = text => bold(red(text)); + aside = text => gray(text); } else { - unclosed('comment') + mark = aside = str => str; + } + + return lines + .slice(start, end) + .map((line, index) => { + let number = start + 1 + index; + let gutter = ' ' + (' ' + number).slice(-maxWidth) + ' | '; + if (number === this.line) { + let spacing = + aside(gutter.replace(/\d/g, ' ')) + + line.slice(0, this.column - 1).replace(/[^\t]/g, ' '); + return ( + mark('>') + + aside(gutter) + + line + + '\n ' + + spacing + + mark('^') + ); + } + return ' ' + aside(gutter) + line; + }) + .join('\n'); + } + + toString() { + let code = this.showSourceCode(); + if (code) { + code = '\n\n' + code + '\n'; + } + return this.name + ': ' + this.message + code; + } + } + + module.exports = CssSyntaxError; + CssSyntaxError.default = CssSyntaxError; + }, + { './terminal-highlight': 2, 'picocolors': 34 }, + ], + 39: [ + function (require, module, exports) { + 'use strict'; + + let Node = require('./node'); + + class Declaration extends Node { + constructor(defaults) { + if ( + defaults && + typeof defaults.value !== 'undefined' && + typeof defaults.value !== 'string' + ) { + defaults = { ...defaults, value: String(defaults.value) }; + } + super(defaults); + this.type = 'decl'; + } + + get variable() { + return this.prop.startsWith('--') || this.prop[0] === '$'; + } + } + + module.exports = Declaration; + Declaration.default = Declaration; + }, + { './node': 47 }, + ], + 40: [ + function (require, module, exports) { + 'use strict'; + + let Container = require('./container'); + + let LazyResult, Processor; + + class Document extends Container { + constructor(defaults) { + // type needs to be passed to super, otherwise child roots won't be normalized correctly + super({ type: 'document', ...defaults }); + + if (!this.nodes) { + this.nodes = []; } } - currentToken = ['comment', css.slice(pos, next + 1), pos, next] - pos = next - } else { - RE_WORD_END.lastIndex = pos + 1 - RE_WORD_END.test(css) - if (RE_WORD_END.lastIndex === 0) { - next = css.length - 1 + toResult(opts = {}) { + let lazy = new LazyResult(new Processor(), this, opts); + + return lazy.stringify(); + } + } + + Document.registerLazyResult = dependant => { + LazyResult = dependant; + }; + + Document.registerProcessor = dependant => { + Processor = dependant; + }; + + module.exports = Document; + Document.default = Document; + }, + { './container': 37 }, + ], + 41: [ + function (require, module, exports) { + 'use strict'; + + let Declaration = require('./declaration'); + let PreviousMap = require('./previous-map'); + let Comment = require('./comment'); + let AtRule = require('./at-rule'); + let Input = require('./input'); + let Root = require('./root'); + let Rule = require('./rule'); + + function fromJSON(json, inputs) { + if (Array.isArray(json)) return json.map(n => fromJSON(n)); + + let { inputs: ownInputs, ...defaults } = json; + if (ownInputs) { + inputs = []; + for (let input of ownInputs) { + let inputHydrated = { ...input, __proto__: Input.prototype }; + if (inputHydrated.map) { + inputHydrated.map = { + ...inputHydrated.map, + __proto__: PreviousMap.prototype, + }; + } + inputs.push(inputHydrated); + } + } + if (defaults.nodes) { + defaults.nodes = json.nodes.map(n => fromJSON(n, inputs)); + } + if (defaults.source) { + let { inputId, ...source } = defaults.source; + defaults.source = source; + if (inputId != null) { + defaults.source.input = inputs[inputId]; + } + } + if (defaults.type === 'root') { + return new Root(defaults); + } else if (defaults.type === 'decl') { + return new Declaration(defaults); + } else if (defaults.type === 'rule') { + return new Rule(defaults); + } else if (defaults.type === 'comment') { + return new Comment(defaults); + } else if (defaults.type === 'atrule') { + return new AtRule(defaults); } else { - next = RE_WORD_END.lastIndex - 2 + throw new Error('Unknown node type: ' + json.type); } - - currentToken = ['word', css.slice(pos, next + 1), pos, next] - buffer.push(currentToken) - pos = next } - break - } - } + module.exports = fromJSON; + fromJSON.default = fromJSON; + }, + { + './at-rule': 35, + './comment': 36, + './declaration': 39, + './input': 42, + './previous-map': 51, + './root': 54, + './rule': 55, + }, + ], + 42: [ + function (require, module, exports) { + 'use strict'; - pos++ - return currentToken - } + let { + SourceMapConsumer, + SourceMapGenerator, + } = require('source-map-js'); + let { fileURLToPath, pathToFileURL } = require('url'); + let { isAbsolute, resolve } = require('path'); + let { nanoid } = require('nanoid/non-secure'); - function back(token) { - returned.push(token) - } + let terminalHighlight = require('./terminal-highlight'); + let CssSyntaxError = require('./css-syntax-error'); + let PreviousMap = require('./previous-map'); - return { - back, - endOfFile, - nextToken, - position - } -} + let fromOffsetCache = Symbol('fromOffsetCache'); -},{}],60:[function(require,module,exports){ -/* eslint-disable no-console */ -'use strict' + let sourceMapAvailable = Boolean( + SourceMapConsumer && SourceMapGenerator, + ); + let pathAvailable = Boolean(resolve && isAbsolute); -let printed = {} - -module.exports = function warnOnce(message) { - if (printed[message]) return - printed[message] = true - - if (typeof console !== 'undefined' && console.warn) { - console.warn(message) - } -} - -},{}],61:[function(require,module,exports){ -'use strict' - -class Warning { - constructor(text, opts = {}) { - this.type = 'warning' - this.text = text - - if (opts.node && opts.node.source) { - let range = opts.node.rangeBy(opts) - this.line = range.start.line - this.column = range.start.column - this.endLine = range.end.line - this.endColumn = range.end.column - } - - for (let opt in opts) this[opt] = opts[opt] - } - - toString() { - if (this.node) { - return this.node.error(this.text, { - index: this.index, - plugin: this.plugin, - word: this.word - }).message - } - - if (this.plugin) { - return this.plugin + ': ' + this.text - } - - return this.text - } -} - -module.exports = Warning -Warning.default = Warning - -},{}],62:[function(require,module,exports){ -// shim for using process in browser -var process = module.exports = {}; - -// cached from whatever global is present so that test runners that stub it -// don't break things. But we need to wrap it in a try catch in case it is -// wrapped in strict mode code which doesn't define any globals. It's inside a -// function because try/catches deoptimize in certain engines. - -var cachedSetTimeout; -var cachedClearTimeout; - -function defaultSetTimout() { - throw new Error('setTimeout has not been defined'); -} -function defaultClearTimeout () { - throw new Error('clearTimeout has not been defined'); -} -(function () { - try { - if (typeof setTimeout === 'function') { - cachedSetTimeout = setTimeout; - } else { - cachedSetTimeout = defaultSetTimout; - } - } catch (e) { - cachedSetTimeout = defaultSetTimout; - } - try { - if (typeof clearTimeout === 'function') { - cachedClearTimeout = clearTimeout; - } else { - cachedClearTimeout = defaultClearTimeout; - } - } catch (e) { - cachedClearTimeout = defaultClearTimeout; - } -} ()) -function runTimeout(fun) { - if (cachedSetTimeout === setTimeout) { - //normal enviroments in sane situations - return setTimeout(fun, 0); - } - // if setTimeout wasn't available but was latter defined - if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { - cachedSetTimeout = setTimeout; - return setTimeout(fun, 0); - } - try { - // when when somebody has screwed with setTimeout but no I.E. maddness - return cachedSetTimeout(fun, 0); - } catch(e){ - try { - // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally - return cachedSetTimeout.call(null, fun, 0); - } catch(e){ - // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error - return cachedSetTimeout.call(this, fun, 0); - } - } - - -} -function runClearTimeout(marker) { - if (cachedClearTimeout === clearTimeout) { - //normal enviroments in sane situations - return clearTimeout(marker); - } - // if clearTimeout wasn't available but was latter defined - if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { - cachedClearTimeout = clearTimeout; - return clearTimeout(marker); - } - try { - // when when somebody has screwed with setTimeout but no I.E. maddness - return cachedClearTimeout(marker); - } catch (e){ - try { - // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally - return cachedClearTimeout.call(null, marker); - } catch (e){ - // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. - // Some versions of I.E. have different rules for clearTimeout vs setTimeout - return cachedClearTimeout.call(this, marker); - } - } - - - -} -var queue = []; -var draining = false; -var currentQueue; -var queueIndex = -1; - -function cleanUpNextTick() { - if (!draining || !currentQueue) { - return; - } - draining = false; - if (currentQueue.length) { - queue = currentQueue.concat(queue); - } else { - queueIndex = -1; - } - if (queue.length) { - drainQueue(); - } -} - -function drainQueue() { - if (draining) { - return; - } - var timeout = runTimeout(cleanUpNextTick); - draining = true; - - var len = queue.length; - while(len) { - currentQueue = queue; - queue = []; - while (++queueIndex < len) { - if (currentQueue) { - currentQueue[queueIndex].run(); + class Input { + constructor(css, opts = {}) { + if ( + css === null || + typeof css === 'undefined' || + (typeof css === 'object' && !css.toString) + ) { + throw new Error(`PostCSS received ${css} instead of CSS string`); } - } - queueIndex = -1; - len = queue.length; - } - currentQueue = null; - draining = false; - runClearTimeout(timeout); -} -process.nextTick = function (fun) { - var args = new Array(arguments.length - 1); - if (arguments.length > 1) { - for (var i = 1; i < arguments.length; i++) { - args[i - 1] = arguments[i]; - } - } - queue.push(new Item(fun, args)); - if (queue.length === 1 && !draining) { - runTimeout(drainQueue); - } -}; + this.css = css.toString(); -// v8 likes predictible objects -function Item(fun, array) { - this.fun = fun; - this.array = array; -} -Item.prototype.run = function () { - this.fun.apply(null, this.array); -}; -process.title = 'browser'; -process.browser = true; -process.env = {}; -process.argv = []; -process.version = ''; // empty string to avoid regexp issues -process.versions = {}; + if (this.css[0] === '\uFEFF' || this.css[0] === '\uFFFE') { + this.hasBOM = true; + this.css = this.css.slice(1); + } else { + this.hasBOM = false; + } -function noop() {} + if (opts.from) { + if ( + !pathAvailable || + /^\w+:\/\//.test(opts.from) || + isAbsolute(opts.from) + ) { + this.file = opts.from; + } else { + this.file = resolve(opts.from); + } + } -process.on = noop; -process.addListener = noop; -process.once = noop; -process.off = noop; -process.removeListener = noop; -process.removeAllListeners = noop; -process.emit = noop; -process.prependListener = noop; -process.prependOnceListener = noop; + if (pathAvailable && sourceMapAvailable) { + let map = new PreviousMap(this.css, opts); + if (map.text) { + this.map = map; + let file = map.consumer().file; + if (!this.file && file) this.file = this.mapResolve(file); + } + } -process.listeners = function (name) { return [] } + if (!this.file) { + this.id = ''; + } + if (this.map) this.map.file = this.from; + } -process.binding = function (name) { - throw new Error('process.binding is not supported'); -}; + error(message, line, column, opts = {}) { + let result, endLine, endColumn; -process.cwd = function () { return '/' }; -process.chdir = function (dir) { - throw new Error('process.chdir is not supported'); -}; -process.umask = function() { return 0; }; + if (line && typeof line === 'object') { + let start = line; + let end = column; + if (typeof start.offset === 'number') { + let pos = this.fromOffset(start.offset); + line = pos.line; + column = pos.col; + } else { + line = start.line; + column = start.column; + } + if (typeof end.offset === 'number') { + let pos = this.fromOffset(end.offset); + endLine = pos.line; + endColumn = pos.col; + } else { + endLine = end.line; + endColumn = end.column; + } + } else if (!column) { + let pos = this.fromOffset(line); + line = pos.line; + column = pos.col; + } -},{}],63:[function(require,module,exports){ -const htmlparser = require('htmlparser2'); -const escapeStringRegexp = require('escape-string-regexp'); -const { isPlainObject } = require('is-plain-object'); -const deepmerge = require('deepmerge'); -const parseSrcset = require('parse-srcset'); -const { parse: postcssParse } = require('postcss'); -// Tags that can conceivably represent stand-alone media. -const mediaTags = [ - 'img', 'audio', 'video', 'picture', 'svg', - 'object', 'map', 'iframe', 'embed' -]; -// Tags that are inherently vulnerable to being used in XSS attacks. -const vulnerableTags = [ 'script', 'style' ]; + let origin = this.origin(line, column, endLine, endColumn); + if (origin) { + result = new CssSyntaxError( + message, + origin.endLine === undefined + ? origin.line + : { column: origin.column, line: origin.line }, + origin.endLine === undefined + ? origin.column + : { column: origin.endColumn, line: origin.endLine }, + origin.source, + origin.file, + opts.plugin, + ); + } else { + result = new CssSyntaxError( + message, + endLine === undefined ? line : { column, line }, + endLine === undefined + ? column + : { column: endColumn, line: endLine }, + this.css, + this.file, + opts.plugin, + ); + } -function each(obj, cb) { - if (obj) { - Object.keys(obj).forEach(function (key) { - cb(obj[key], key); - }); - } -} + result.input = { + column, + endColumn, + endLine, + line, + source: this.css, + }; + if (this.file) { + if (pathToFileURL) { + result.input.url = pathToFileURL(this.file).toString(); + } + result.input.file = this.file; + } -// Avoid false positives with .__proto__, .hasOwnProperty, etc. -function has(obj, key) { - return ({}).hasOwnProperty.call(obj, key); -} + return result; + } -// Returns those elements of `a` for which `cb(a)` returns truthy -function filter(a, cb) { - const n = []; - each(a, function(v) { - if (cb(v)) { - n.push(v); - } - }); - return n; -} + fromOffset(offset) { + let lastLine, lineToIndex; + if (!this[fromOffsetCache]) { + let lines = this.css.split('\n'); + lineToIndex = new Array(lines.length); + let prevIndex = 0; -function isEmptyObject(obj) { - for (const key in obj) { - if (has(obj, key)) { - return false; - } - } - return true; -} + for (let i = 0, l = lines.length; i < l; i++) { + lineToIndex[i] = prevIndex; + prevIndex += lines[i].length + 1; + } -function stringifySrcset(parsedSrcset) { - return parsedSrcset.map(function(part) { - if (!part.url) { - throw new Error('URL missing'); - } + this[fromOffsetCache] = lineToIndex; + } else { + lineToIndex = this[fromOffsetCache]; + } + lastLine = lineToIndex[lineToIndex.length - 1]; - return ( - part.url + - (part.w ? ` ${part.w}w` : '') + - (part.h ? ` ${part.h}h` : '') + - (part.d ? ` ${part.d}x` : '') - ); - }).join(', '); -} + let min = 0; + if (offset >= lastLine) { + min = lineToIndex.length - 1; + } else { + let max = lineToIndex.length - 2; + let mid; + while (min < max) { + mid = min + ((max - min) >> 1); + if (offset < lineToIndex[mid]) { + max = mid - 1; + } else if (offset >= lineToIndex[mid + 1]) { + min = mid + 1; + } else { + min = mid; + break; + } + } + } + return { + col: offset - lineToIndex[min] + 1, + line: min + 1, + }; + } -module.exports = sanitizeHtml; + mapResolve(file) { + if (/^\w+:\/\//.test(file)) { + return file; + } + return resolve( + this.map.consumer().sourceRoot || this.map.root || '.', + file, + ); + } -// A valid attribute name. -// We use a tolerant definition based on the set of strings defined by -// html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state -// and html.spec.whatwg.org/multipage/parsing.html#attribute-name-state . -// The characters accepted are ones which can be appended to the attribute -// name buffer without triggering a parse error: -// * unexpected-equals-sign-before-attribute-name -// * unexpected-null-character -// * unexpected-character-in-attribute-name -// We exclude the empty string because it's impossible to get to the after -// attribute name state with an empty attribute name buffer. -const VALID_HTML_ATTRIBUTE_NAME = /^[^\0\t\n\f\r /<=>]+$/; + origin(line, column, endLine, endColumn) { + if (!this.map) return false; + let consumer = this.map.consumer(); -// Ignore the _recursing flag; it's there for recursive -// invocation as a guard against this exploit: -// https://github.com/fb55/htmlparser2/issues/105 + let from = consumer.originalPositionFor({ column, line }); + if (!from.source) return false; -function sanitizeHtml(html, options, _recursing) { - if (html == null) { - return ''; - } - if (typeof html === 'number') { - html = html.toString(); - } + let to; + if (typeof endLine === 'number') { + to = consumer.originalPositionFor({ + column: endColumn, + line: endLine, + }); + } - let result = ''; - // Used for hot swapping the result variable with an empty string in order to "capture" the text written to it. - let tempResult = ''; + let fromUrl; - function Frame(tag, attribs) { - const that = this; - this.tag = tag; - this.attribs = attribs || {}; - this.tagPosition = result.length; - this.text = ''; // Node inner text - this.mediaChildren = []; + if (isAbsolute(from.source)) { + fromUrl = pathToFileURL(from.source); + } else { + fromUrl = new URL( + from.source, + this.map.consumer().sourceRoot || + pathToFileURL(this.map.mapFile), + ); + } - this.updateParentNodeText = function() { - if (stack.length) { - const parentFrame = stack[stack.length - 1]; - parentFrame.text += that.text; - } - }; + let result = { + column: from.column, + endColumn: to && to.column, + endLine: to && to.line, + line: from.line, + url: fromUrl.toString(), + }; - this.updateParentNodeMediaChildren = function() { - if (stack.length && mediaTags.includes(this.tag)) { - const parentFrame = stack[stack.length - 1]; - parentFrame.mediaChildren.push(this.tag); - } - }; - } + if (fromUrl.protocol === 'file:') { + if (fileURLToPath) { + result.file = fileURLToPath(fromUrl); + } else { + /* c8 ignore next 2 */ + throw new Error( + `file: protocol is not available in this PostCSS build`, + ); + } + } - options = Object.assign({}, sanitizeHtml.defaults, options); - options.parser = Object.assign({}, htmlParserDefaults, options.parser); + let source = consumer.sourceContentFor(from.source); + if (source) result.source = source; - const tagAllowed = function (name) { - return options.allowedTags === false || (options.allowedTags || []).indexOf(name) > -1; - }; + return result; + } - // vulnerableTags - vulnerableTags.forEach(function (tag) { - if (tagAllowed(tag) && !options.allowVulnerableTags) { - console.warn(`\n\n⚠️ Your \`allowedTags\` option includes, \`${tag}\`, which is inherently\nvulnerable to XSS attacks. Please remove it from \`allowedTags\`.\nOr, to disable this warning, add the \`allowVulnerableTags\` option\nand ensure you are accounting for this risk.\n\n`); - } - }); + toJSON() { + let json = {}; + for (let name of ['hasBOM', 'css', 'file', 'id']) { + if (this[name] != null) { + json[name] = this[name]; + } + } + if (this.map) { + json.map = { ...this.map }; + if (json.map.consumerCache) { + json.map.consumerCache = undefined; + } + } + return json; + } - // Tags that contain something other than HTML, or where discarding - // the text when the tag is disallowed makes sense for other reasons. - // If we are not allowing these tags, we should drop their content too. - // For other tags you would drop the tag but keep its content. - const nonTextTagsArray = options.nonTextTags || [ - 'script', - 'style', - 'textarea', - 'option' - ]; - let allowedAttributesMap; - let allowedAttributesGlobMap; - if (options.allowedAttributes) { - allowedAttributesMap = {}; - allowedAttributesGlobMap = {}; - each(options.allowedAttributes, function(attributes, tag) { - allowedAttributesMap[tag] = []; - const globRegex = []; - attributes.forEach(function(obj) { - if (typeof obj === 'string' && obj.indexOf('*') >= 0) { - globRegex.push(escapeStringRegexp(obj).replace(/\\\*/g, '.*')); - } else { - allowedAttributesMap[tag].push(obj); - } - }); - if (globRegex.length) { - allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$'); - } - }); - } - const allowedClassesMap = {}; - const allowedClassesGlobMap = {}; - const allowedClassesRegexMap = {}; - each(options.allowedClasses, function(classes, tag) { - // Implicitly allows the class attribute - if (allowedAttributesMap) { - if (!has(allowedAttributesMap, tag)) { - allowedAttributesMap[tag] = []; - } - allowedAttributesMap[tag].push('class'); - } - - allowedClassesMap[tag] = classes; - - if (Array.isArray(classes)) { - const globRegex = []; - allowedClassesMap[tag] = []; - allowedClassesRegexMap[tag] = []; - classes.forEach(function(obj) { - if (typeof obj === 'string' && obj.indexOf('*') >= 0) { - globRegex.push(escapeStringRegexp(obj).replace(/\\\*/g, '.*')); - } else if (obj instanceof RegExp) { - allowedClassesRegexMap[tag].push(obj); - } else { - allowedClassesMap[tag].push(obj); - } - }); - if (globRegex.length) { - allowedClassesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$'); - } - } - }); - - const transformTagsMap = {}; - let transformTagsAll; - each(options.transformTags, function(transform, tag) { - let transFun; - if (typeof transform === 'function') { - transFun = transform; - } else if (typeof transform === 'string') { - transFun = sanitizeHtml.simpleTransform(transform); - } - if (tag === '*') { - transformTagsAll = transFun; - } else { - transformTagsMap[tag] = transFun; - } - }); - - let depth; - let stack; - let skipMap; - let transformMap; - let skipText; - let skipTextDepth; - let addedText = false; - - initializeState(); - - const parser = new htmlparser.Parser({ - onopentag: function(name, attribs) { - // If `enforceHtmlBoundary` is `true` and this has found the opening - // `html` tag, reset the state. - if (options.enforceHtmlBoundary && name === 'html') { - initializeState(); - } - - if (skipText) { - skipTextDepth++; - return; - } - const frame = new Frame(name, attribs); - stack.push(frame); - - let skip = false; - const hasText = !!frame.text; - let transformedTag; - if (has(transformTagsMap, name)) { - transformedTag = transformTagsMap[name](name, attribs); - - frame.attribs = attribs = transformedTag.attribs; - - if (transformedTag.text !== undefined) { - frame.innerText = transformedTag.text; - } - - if (name !== transformedTag.tagName) { - frame.name = name = transformedTag.tagName; - transformMap[depth] = transformedTag.tagName; - } - } - if (transformTagsAll) { - transformedTag = transformTagsAll(name, attribs); - - frame.attribs = attribs = transformedTag.attribs; - if (name !== transformedTag.tagName) { - frame.name = name = transformedTag.tagName; - transformMap[depth] = transformedTag.tagName; - } - } - - if (!tagAllowed(name) || (options.disallowedTagsMode === 'recursiveEscape' && !isEmptyObject(skipMap)) || (options.nestingLimit != null && depth >= options.nestingLimit)) { - skip = true; - skipMap[depth] = true; - if (options.disallowedTagsMode === 'discard') { - if (nonTextTagsArray.indexOf(name) !== -1) { - skipText = true; - skipTextDepth = 1; + get from() { + return this.file || this.id; } } - skipMap[depth] = true; - } - depth++; - if (skip) { - if (options.disallowedTagsMode === 'discard') { - // We want the contents but not this tag - return; - } - tempResult = result; - result = ''; - } - result += '<' + name; - if (name === 'script') { - if (options.allowedScriptHostnames || options.allowedScriptDomains) { - frame.innerText = ''; - } - } + module.exports = Input; + Input.default = Input; - if (!allowedAttributesMap || has(allowedAttributesMap, name) || allowedAttributesMap['*']) { - each(attribs, function(value, a) { - if (!VALID_HTML_ATTRIBUTE_NAME.test(a)) { - // This prevents part of an attribute name in the output from being - // interpreted as the end of an attribute, or end of a tag. - delete frame.attribs[a]; - return; - } - // If the value is empty, and this is a known non-boolean attribute, delete it - // List taken from https://html.spec.whatwg.org/multipage/indices.html#attributes-3 - if (value === '' && (options.nonBooleanAttributes.includes(a) || options.nonBooleanAttributes.includes('*'))) { - delete frame.attribs[a]; - return; - } - // check allowedAttributesMap for the element and attribute and modify the value - // as necessary if there are specific values defined. - let passedAllowedAttributesMapCheck = false; - if (!allowedAttributesMap || - (has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1) || - (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1) || - (has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) || - (allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) { - passedAllowedAttributesMapCheck = true; - } else if (allowedAttributesMap && allowedAttributesMap[name]) { - for (const o of allowedAttributesMap[name]) { - if (isPlainObject(o) && o.name && (o.name === a)) { - passedAllowedAttributesMapCheck = true; - let newValue = ''; - if (o.multiple === true) { - // verify the values that are allowed - const splitStrArray = value.split(' '); - for (const s of splitStrArray) { - if (o.values.indexOf(s) !== -1) { - if (newValue === '') { - newValue = s; - } else { - newValue += ' ' + s; + if (terminalHighlight && terminalHighlight.registerInput) { + terminalHighlight.registerInput(Input); + } + }, + { + './css-syntax-error': 38, + './previous-map': 51, + './terminal-highlight': 2, + 'nanoid/non-secure': 32, + 'path': 2, + 'source-map-js': 2, + 'url': 2, + }, + ], + 43: [ + function (require, module, exports) { + (function (process) { + (function () { + 'use strict'; + + let { isClean, my } = require('./symbols'); + let MapGenerator = require('./map-generator'); + let stringify = require('./stringify'); + let Container = require('./container'); + let Document = require('./document'); + let warnOnce = require('./warn-once'); + let Result = require('./result'); + let parse = require('./parse'); + let Root = require('./root'); + + const TYPE_TO_CLASS_NAME = { + atrule: 'AtRule', + comment: 'Comment', + decl: 'Declaration', + document: 'Document', + root: 'Root', + rule: 'Rule', + }; + + const PLUGIN_PROPS = { + AtRule: true, + AtRuleExit: true, + Comment: true, + CommentExit: true, + Declaration: true, + DeclarationExit: true, + Document: true, + DocumentExit: true, + Once: true, + OnceExit: true, + postcssPlugin: true, + prepare: true, + Root: true, + RootExit: true, + Rule: true, + RuleExit: true, + }; + + const NOT_VISITORS = { + Once: true, + postcssPlugin: true, + prepare: true, + }; + + const CHILDREN = 0; + + function isPromise(obj) { + return typeof obj === 'object' && typeof obj.then === 'function'; + } + + function getEvents(node) { + let key = false; + let type = TYPE_TO_CLASS_NAME[node.type]; + if (node.type === 'decl') { + key = node.prop.toLowerCase(); + } else if (node.type === 'atrule') { + key = node.name.toLowerCase(); + } + + if (key && node.append) { + return [ + type, + type + '-' + key, + CHILDREN, + type + 'Exit', + type + 'Exit-' + key, + ]; + } else if (key) { + return [ + type, + type + '-' + key, + type + 'Exit', + type + 'Exit-' + key, + ]; + } else if (node.append) { + return [type, CHILDREN, type + 'Exit']; + } else { + return [type, type + 'Exit']; + } + } + + function toStack(node) { + let events; + if (node.type === 'document') { + events = ['Document', CHILDREN, 'DocumentExit']; + } else if (node.type === 'root') { + events = ['Root', CHILDREN, 'RootExit']; + } else { + events = getEvents(node); + } + + return { + eventIndex: 0, + events, + iterator: 0, + node, + visitorIndex: 0, + visitors: [], + }; + } + + function cleanMarks(node) { + node[isClean] = false; + if (node.nodes) node.nodes.forEach(i => cleanMarks(i)); + return node; + } + + let postcss = {}; + + class LazyResult { + constructor(processor, css, opts) { + this.stringified = false; + this.processed = false; + + let root; + if ( + typeof css === 'object' && + css !== null && + (css.type === 'root' || css.type === 'document') + ) { + root = cleanMarks(css); + } else if (css instanceof LazyResult || css instanceof Result) { + root = cleanMarks(css.root); + if (css.map) { + if (typeof opts.map === 'undefined') opts.map = {}; + if (!opts.map.inline) opts.map.inline = false; + opts.map.prev = css.map; + } + } else { + let parser = parse; + if (opts.syntax) parser = opts.syntax.parse; + if (opts.parser) parser = opts.parser; + if (parser.parse) parser = parser.parse; + + try { + root = parser(css, opts); + } catch (error) { + this.processed = true; + this.error = error; + } + + if (root && !root[my]) { + /* c8 ignore next 2 */ + Container.rebuild(root); + } + } + + this.result = new Result(processor, root, opts); + this.helpers = { ...postcss, postcss, result: this.result }; + this.plugins = this.processor.plugins.map(plugin => { + if (typeof plugin === 'object' && plugin.prepare) { + return { ...plugin, ...plugin.prepare(this.result) }; + } else { + return plugin; + } + }); + } + + async() { + if (this.error) return Promise.reject(this.error); + if (this.processed) return Promise.resolve(this.result); + if (!this.processing) { + this.processing = this.runAsync(); + } + return this.processing; + } + + catch(onRejected) { + return this.async().catch(onRejected); + } + + finally(onFinally) { + return this.async().then(onFinally, onFinally); + } + + getAsyncError() { + throw new Error( + 'Use process(css).then(cb) to work with async plugins', + ); + } + + handleError(error, node) { + let plugin = this.result.lastPlugin; + try { + if (node) node.addToError(error); + this.error = error; + if (error.name === 'CssSyntaxError' && !error.plugin) { + error.plugin = plugin.postcssPlugin; + error.setMessage(); + } else if (plugin.postcssVersion) { + if (process.env.NODE_ENV !== 'production') { + let pluginName = plugin.postcssPlugin; + let pluginVer = plugin.postcssVersion; + let runtimeVer = this.result.processor.version; + let a = pluginVer.split('.'); + let b = runtimeVer.split('.'); + + if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) { + // eslint-disable-next-line no-console + console.error( + 'Unknown error from PostCSS plugin. Your current PostCSS ' + + 'version is ' + + runtimeVer + + ', but ' + + pluginName + + ' uses ' + + pluginVer + + '. Perhaps this is the source of the error below.', + ); } } } - } else if (o.values.indexOf(value) >= 0) { - // verified an allowed value matches the entire attribute value - newValue = value; + } catch (err) { + /* c8 ignore next 3 */ + // eslint-disable-next-line no-console + if (console && console.error) console.error(err); } - value = newValue; - } - } - } - if (passedAllowedAttributesMapCheck) { - if (options.allowedSchemesAppliedToAttributes.indexOf(a) !== -1) { - if (naughtyHref(name, value)) { - delete frame.attribs[a]; - return; - } - } - - if (name === 'script' && a === 'src') { - - let allowed = true; - - try { - const parsed = parseUrl(value); - - if (options.allowedScriptHostnames || options.allowedScriptDomains) { - const allowedHostname = (options.allowedScriptHostnames || []).find(function (hostname) { - return hostname === parsed.url.hostname; - }); - const allowedDomain = (options.allowedScriptDomains || []).find(function(domain) { - return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`); - }); - allowed = allowedHostname || allowedDomain; - } - } catch (e) { - allowed = false; + return error; } - if (!allowed) { - delete frame.attribs[a]; - return; - } - } - - if (name === 'iframe' && a === 'src') { - let allowed = true; - try { - const parsed = parseUrl(value); - - if (parsed.isRelativeUrl) { - // default value of allowIframeRelativeUrls is true - // unless allowedIframeHostnames or allowedIframeDomains specified - allowed = has(options, 'allowIframeRelativeUrls') - ? options.allowIframeRelativeUrls - : (!options.allowedIframeHostnames && !options.allowedIframeDomains); - } else if (options.allowedIframeHostnames || options.allowedIframeDomains) { - const allowedHostname = (options.allowedIframeHostnames || []).find(function (hostname) { - return hostname === parsed.url.hostname; - }); - const allowedDomain = (options.allowedIframeDomains || []).find(function(domain) { - return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`); - }); - allowed = allowedHostname || allowedDomain; - } - } catch (e) { - // Unparseable iframe src - allowed = false; - } - if (!allowed) { - delete frame.attribs[a]; - return; - } - } - if (a === 'srcset') { - try { - let parsed = parseSrcset(value); - parsed.forEach(function(value) { - if (naughtyHref('srcset', value.url)) { - value.evil = true; + prepareVisitors() { + this.listeners = {}; + let add = (plugin, type, cb) => { + if (!this.listeners[type]) this.listeners[type] = []; + this.listeners[type].push([plugin, cb]); + }; + for (let plugin of this.plugins) { + if (typeof plugin === 'object') { + for (let event in plugin) { + if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) { + throw new Error( + `Unknown event ${event} in ${plugin.postcssPlugin}. ` + + `Try to update PostCSS (${this.processor.version} now).`, + ); + } + if (!NOT_VISITORS[event]) { + if (typeof plugin[event] === 'object') { + for (let filter in plugin[event]) { + if (filter === '*') { + add(plugin, event, plugin[event][filter]); + } else { + add( + plugin, + event + '-' + filter.toLowerCase(), + plugin[event][filter], + ); + } + } + } else if (typeof plugin[event] === 'function') { + add(plugin, event, plugin[event]); + } + } + } } - }); - parsed = filter(parsed, function(v) { - return !v.evil; - }); - if (!parsed.length) { - delete frame.attribs[a]; - return; - } else { - value = stringifySrcset(filter(parsed, function(v) { - return !v.evil; - })); - frame.attribs[a] = value; } - } catch (e) { - // Unparseable srcset - delete frame.attribs[a]; - return; + this.hasListener = Object.keys(this.listeners).length > 0; } - } - if (a === 'class') { - const allowedSpecificClasses = allowedClassesMap[name]; - const allowedWildcardClasses = allowedClassesMap['*']; - const allowedSpecificClassesGlob = allowedClassesGlobMap[name]; - const allowedSpecificClassesRegex = allowedClassesRegexMap[name]; - const allowedWildcardClassesGlob = allowedClassesGlobMap['*']; - const allowedClassesGlobs = [ - allowedSpecificClassesGlob, - allowedWildcardClassesGlob - ] - .concat(allowedSpecificClassesRegex) - .filter(function (t) { - return t; - }); - if (allowedSpecificClasses && allowedWildcardClasses) { - value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses), allowedClassesGlobs); - } else { - value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses, allowedClassesGlobs); + + async runAsync() { + this.plugin = 0; + for (let i = 0; i < this.plugins.length; i++) { + let plugin = this.plugins[i]; + let promise = this.runOnRoot(plugin); + if (isPromise(promise)) { + try { + await promise; + } catch (error) { + throw this.handleError(error); + } + } + } + + this.prepareVisitors(); + if (this.hasListener) { + let root = this.result.root; + while (!root[isClean]) { + root[isClean] = true; + let stack = [toStack(root)]; + while (stack.length > 0) { + let promise = this.visitTick(stack); + if (isPromise(promise)) { + try { + await promise; + } catch (e) { + let node = stack[stack.length - 1].node; + throw this.handleError(e, node); + } + } + } + } + + if (this.listeners.OnceExit) { + for (let [plugin, visitor] of this.listeners.OnceExit) { + this.result.lastPlugin = plugin; + try { + if (root.type === 'document') { + let roots = root.nodes.map(subRoot => + visitor(subRoot, this.helpers), + ); + + await Promise.all(roots); + } else { + await visitor(root, this.helpers); + } + } catch (e) { + throw this.handleError(e); + } + } + } + } + + this.processed = true; + return this.stringify(); } - if (!value.length) { - delete frame.attribs[a]; - return; - } - } - if (a === 'style') { - if (options.parseStyleAttributes) { + + runOnRoot(plugin) { + this.result.lastPlugin = plugin; try { - const abstractSyntaxTree = postcssParse(name + ' {' + value + '}'); - const filteredAST = filterCss(abstractSyntaxTree, options.allowedStyles); + if (typeof plugin === 'object' && plugin.Once) { + if (this.result.root.type === 'document') { + let roots = this.result.root.nodes.map(root => + plugin.Once(root, this.helpers), + ); - value = stringifyStyleAttributes(filteredAST); + if (isPromise(roots[0])) { + return Promise.all(roots); + } - if (value.length === 0) { - delete frame.attribs[a]; + return roots; + } + + return plugin.Once(this.result.root, this.helpers); + } else if (typeof plugin === 'function') { + return plugin(this.result.root, this.result); + } + } catch (error) { + throw this.handleError(error); + } + } + + stringify() { + if (this.error) throw this.error; + if (this.stringified) return this.result; + this.stringified = true; + + this.sync(); + + let opts = this.result.opts; + let str = stringify; + if (opts.syntax) str = opts.syntax.stringify; + if (opts.stringifier) str = opts.stringifier; + if (str.stringify) str = str.stringify; + + let map = new MapGenerator( + str, + this.result.root, + this.result.opts, + ); + let data = map.generate(); + this.result.css = data[0]; + this.result.map = data[1]; + + return this.result; + } + + sync() { + if (this.error) throw this.error; + if (this.processed) return this.result; + this.processed = true; + + if (this.processing) { + throw this.getAsyncError(); + } + + for (let plugin of this.plugins) { + let promise = this.runOnRoot(plugin); + if (isPromise(promise)) { + throw this.getAsyncError(); + } + } + + this.prepareVisitors(); + if (this.hasListener) { + let root = this.result.root; + while (!root[isClean]) { + root[isClean] = true; + this.walkSync(root); + } + if (this.listeners.OnceExit) { + if (root.type === 'document') { + for (let subRoot of root.nodes) { + this.visitSync(this.listeners.OnceExit, subRoot); + } + } else { + this.visitSync(this.listeners.OnceExit, root); + } + } + } + + return this.result; + } + + then(onFulfilled, onRejected) { + if (process.env.NODE_ENV !== 'production') { + if (!('from' in this.opts)) { + warnOnce( + 'Without `from` option PostCSS could generate wrong source map ' + + 'and will not find Browserslist config. Set it to CSS file path ' + + 'or to `undefined` to prevent this warning.', + ); + } + } + return this.async().then(onFulfilled, onRejected); + } + + toString() { + return this.css; + } + + visitSync(visitors, node) { + for (let [plugin, visitor] of visitors) { + this.result.lastPlugin = plugin; + let promise; + try { + promise = visitor(node, this.helpers); + } catch (e) { + throw this.handleError(e, node.proxyOf); + } + if ( + node.type !== 'root' && + node.type !== 'document' && + !node.parent + ) { + return true; + } + if (isPromise(promise)) { + throw this.getAsyncError(); + } + } + } + + visitTick(stack) { + let visit = stack[stack.length - 1]; + let { node, visitors } = visit; + + if ( + node.type !== 'root' && + node.type !== 'document' && + !node.parent + ) { + stack.pop(); + return; + } + + if ( + visitors.length > 0 && + visit.visitorIndex < visitors.length + ) { + let [plugin, visitor] = visitors[visit.visitorIndex]; + visit.visitorIndex += 1; + if (visit.visitorIndex === visitors.length) { + visit.visitors = []; + visit.visitorIndex = 0; + } + this.result.lastPlugin = plugin; + try { + return visitor(node.toProxy(), this.helpers); + } catch (e) { + throw this.handleError(e, node); + } + } + + if (visit.iterator !== 0) { + let iterator = visit.iterator; + let child; + while ((child = node.nodes[node.indexes[iterator]])) { + node.indexes[iterator] += 1; + if (!child[isClean]) { + child[isClean] = true; + stack.push(toStack(child)); + return; + } + } + visit.iterator = 0; + delete node.indexes[iterator]; + } + + let events = visit.events; + while (visit.eventIndex < events.length) { + let event = events[visit.eventIndex]; + visit.eventIndex += 1; + if (event === CHILDREN) { + if (node.nodes && node.nodes.length) { + node[isClean] = true; + visit.iterator = node.getIterator(); + } + return; + } else if (this.listeners[event]) { + visit.visitors = this.listeners[event]; return; } - } catch (e) { - if (typeof window !== 'undefined') { - console.warn('Failed to parse "' + name + ' {' + value + '}' + '", If you\'re running this in a browser, we recommend to disable style parsing: options.parseStyleAttributes: false, since this only works in a node environment due to a postcss dependency, More info: https://github.com/apostrophecms/sanitize-html/issues/547'); - } - delete frame.attribs[a]; - return; } - } else if (options.allowedStyles) { - throw new Error('allowedStyles option cannot be used together with parseStyleAttributes: false.'); + stack.pop(); + } + + walkSync(node) { + node[isClean] = true; + let events = getEvents(node); + for (let event of events) { + if (event === CHILDREN) { + if (node.nodes) { + node.each(child => { + if (!child[isClean]) this.walkSync(child); + }); + } + } else { + let visitors = this.listeners[event]; + if (visitors) { + if (this.visitSync(visitors, node.toProxy())) return; + } + } + } + } + + warnings() { + return this.sync().warnings(); + } + + get content() { + return this.stringify().content; + } + + get css() { + return this.stringify().css; + } + + get map() { + return this.stringify().map; + } + + get messages() { + return this.sync().messages; + } + + get opts() { + return this.result.opts; + } + + get processor() { + return this.result.processor; + } + + get root() { + return this.sync().root; + } + + get [Symbol.toStringTag]() { + return 'LazyResult'; } } - result += ' ' + a; - if (value && value.length) { - result += '="' + escapeHtml(value, true) + '"'; + + LazyResult.registerPostcss = dependant => { + postcss = dependant; + }; + + module.exports = LazyResult; + LazyResult.default = LazyResult; + + Root.registerLazyResult(LazyResult); + Document.registerLazyResult(LazyResult); + }).call(this); + }).call(this, require('_process')); + }, + { + './container': 37, + './document': 40, + './map-generator': 45, + './parse': 48, + './result': 53, + './root': 54, + './stringify': 57, + './symbols': 58, + './warn-once': 60, + '_process': 62, + }, + ], + 44: [ + function (require, module, exports) { + 'use strict'; + + let list = { + comma(string) { + return list.split(string, [','], true); + }, + + space(string) { + let spaces = [' ', '\n', '\t']; + return list.split(string, spaces); + }, + + split(string, separators, last) { + let array = []; + let current = ''; + let split = false; + + let func = 0; + let inQuote = false; + let prevQuote = ''; + let escape = false; + + for (let letter of string) { + if (escape) { + escape = false; + } else if (letter === '\\') { + escape = true; + } else if (inQuote) { + if (letter === prevQuote) { + inQuote = false; + } + } else if (letter === '"' || letter === "'") { + inQuote = true; + prevQuote = letter; + } else if (letter === '(') { + func += 1; + } else if (letter === ')') { + if (func > 0) func -= 1; + } else if (func === 0) { + if (separators.includes(letter)) split = true; + } + + if (split) { + if (current !== '') array.push(current.trim()); + current = ''; + split = false; + } else { + current += letter; + } + } + + if (last || current !== '') array.push(current.trim()); + return array; + }, + }; + + module.exports = list; + list.default = list; + }, + {}, + ], + 45: [ + function (require, module, exports) { + (function (Buffer) { + (function () { + 'use strict'; + + let { + SourceMapConsumer, + SourceMapGenerator, + } = require('source-map-js'); + let { dirname, relative, resolve, sep } = require('path'); + let { pathToFileURL } = require('url'); + + let Input = require('./input'); + + let sourceMapAvailable = Boolean( + SourceMapConsumer && SourceMapGenerator, + ); + let pathAvailable = Boolean(dirname && resolve && relative && sep); + + class MapGenerator { + constructor(stringify, root, opts, cssString) { + this.stringify = stringify; + this.mapOpts = opts.map || {}; + this.root = root; + this.opts = opts; + this.css = cssString; + this.usesFileUrls = !this.mapOpts.from && this.mapOpts.absolute; + + this.memoizedFileURLs = new Map(); + this.memoizedPaths = new Map(); + this.memoizedURLs = new Map(); + } + + addAnnotation() { + let content; + + if (this.isInline()) { + content = + 'data:application/json;base64,' + + this.toBase64(this.map.toString()); + } else if (typeof this.mapOpts.annotation === 'string') { + content = this.mapOpts.annotation; + } else if (typeof this.mapOpts.annotation === 'function') { + content = this.mapOpts.annotation(this.opts.to, this.root); + } else { + content = this.outputFile() + '.map'; + } + let eol = '\n'; + if (this.css.includes('\r\n')) eol = '\r\n'; + + this.css += eol + '/*# sourceMappingURL=' + content + ' */'; + } + + applyPrevMaps() { + for (let prev of this.previous()) { + let from = this.toUrl(this.path(prev.file)); + let root = prev.root || dirname(prev.file); + let map; + + if (this.mapOpts.sourcesContent === false) { + map = new SourceMapConsumer(prev.text); + if (map.sourcesContent) { + map.sourcesContent = map.sourcesContent.map(() => null); + } + } else { + map = prev.consumer(); + } + + this.map.applySourceMap( + map, + from, + this.toUrl(this.path(root)), + ); + } + } + + clearAnnotation() { + if (this.mapOpts.annotation === false) return; + + if (this.root) { + let node; + for (let i = this.root.nodes.length - 1; i >= 0; i--) { + node = this.root.nodes[i]; + if (node.type !== 'comment') continue; + if (node.text.indexOf('# sourceMappingURL=') === 0) { + this.root.removeChild(i); + } + } + } else if (this.css) { + this.css = this.css.replace(/(\n)?\/\*#[\S\s]*?\*\/$/gm, ''); + } + } + + generate() { + this.clearAnnotation(); + if (pathAvailable && sourceMapAvailable && this.isMap()) { + return this.generateMap(); + } else { + let result = ''; + this.stringify(this.root, i => { + result += i; + }); + return [result]; + } + } + + generateMap() { + if (this.root) { + this.generateString(); + } else if (this.previous().length === 1) { + let prev = this.previous()[0].consumer(); + prev.file = this.outputFile(); + this.map = SourceMapGenerator.fromSourceMap(prev); + } else { + this.map = new SourceMapGenerator({ + file: this.outputFile(), + }); + this.map.addMapping({ + generated: { column: 0, line: 1 }, + original: { column: 0, line: 1 }, + source: this.opts.from + ? this.toUrl(this.path(this.opts.from)) + : '', + }); + } + + if (this.isSourcesContent()) this.setSourcesContent(); + if (this.root && this.previous().length > 0) + this.applyPrevMaps(); + if (this.isAnnotation()) this.addAnnotation(); + + if (this.isInline()) { + return [this.css]; + } else { + return [this.css, this.map]; + } + } + + generateString() { + this.css = ''; + this.map = new SourceMapGenerator({ file: this.outputFile() }); + + let line = 1; + let column = 1; + + let noSource = ''; + let mapping = { + generated: { column: 0, line: 0 }, + original: { column: 0, line: 0 }, + source: '', + }; + + let lines, last; + this.stringify(this.root, (str, node, type) => { + this.css += str; + + if (node && type !== 'end') { + mapping.generated.line = line; + mapping.generated.column = column - 1; + if (node.source && node.source.start) { + mapping.source = this.sourcePath(node); + mapping.original.line = node.source.start.line; + mapping.original.column = node.source.start.column - 1; + this.map.addMapping(mapping); + } else { + mapping.source = noSource; + mapping.original.line = 1; + mapping.original.column = 0; + this.map.addMapping(mapping); + } + } + + lines = str.match(/\n/g); + if (lines) { + line += lines.length; + last = str.lastIndexOf('\n'); + column = str.length - last; + } else { + column += str.length; + } + + if (node && type !== 'start') { + let p = node.parent || { raws: {} }; + let childless = + node.type === 'decl' || + (node.type === 'atrule' && !node.nodes); + if (!childless || node !== p.last || p.raws.semicolon) { + if (node.source && node.source.end) { + mapping.source = this.sourcePath(node); + mapping.original.line = node.source.end.line; + mapping.original.column = node.source.end.column - 1; + mapping.generated.line = line; + mapping.generated.column = column - 2; + this.map.addMapping(mapping); + } else { + mapping.source = noSource; + mapping.original.line = 1; + mapping.original.column = 0; + mapping.generated.line = line; + mapping.generated.column = column - 1; + this.map.addMapping(mapping); + } + } + } + }); + } + + isAnnotation() { + if (this.isInline()) { + return true; + } + if (typeof this.mapOpts.annotation !== 'undefined') { + return this.mapOpts.annotation; + } + if (this.previous().length) { + return this.previous().some(i => i.annotation); + } + return true; + } + + isInline() { + if (typeof this.mapOpts.inline !== 'undefined') { + return this.mapOpts.inline; + } + + let annotation = this.mapOpts.annotation; + if (typeof annotation !== 'undefined' && annotation !== true) { + return false; + } + + if (this.previous().length) { + return this.previous().some(i => i.inline); + } + return true; + } + + isMap() { + if (typeof this.opts.map !== 'undefined') { + return !!this.opts.map; + } + return this.previous().length > 0; + } + + isSourcesContent() { + if (typeof this.mapOpts.sourcesContent !== 'undefined') { + return this.mapOpts.sourcesContent; + } + if (this.previous().length) { + return this.previous().some(i => i.withContent()); + } + return true; + } + + outputFile() { + if (this.opts.to) { + return this.path(this.opts.to); + } else if (this.opts.from) { + return this.path(this.opts.from); + } else { + return 'to.css'; + } + } + + path(file) { + if (this.mapOpts.absolute) return file; + if (file.charCodeAt(0) === 60 /* `<` */) return file; + if (/^\w+:\/\//.test(file)) return file; + let cached = this.memoizedPaths.get(file); + if (cached) return cached; + + let from = this.opts.to ? dirname(this.opts.to) : '.'; + + if (typeof this.mapOpts.annotation === 'string') { + from = dirname(resolve(from, this.mapOpts.annotation)); + } + + let path = relative(from, file); + this.memoizedPaths.set(file, path); + + return path; + } + + previous() { + if (!this.previousMaps) { + this.previousMaps = []; + if (this.root) { + this.root.walk(node => { + if (node.source && node.source.input.map) { + let map = node.source.input.map; + if (!this.previousMaps.includes(map)) { + this.previousMaps.push(map); + } + } + }); + } else { + let input = new Input(this.css, this.opts); + if (input.map) this.previousMaps.push(input.map); + } + } + + return this.previousMaps; + } + + setSourcesContent() { + let already = {}; + if (this.root) { + this.root.walk(node => { + if (node.source) { + let from = node.source.input.from; + if (from && !already[from]) { + already[from] = true; + let fromUrl = this.usesFileUrls + ? this.toFileUrl(from) + : this.toUrl(this.path(from)); + this.map.setSourceContent( + fromUrl, + node.source.input.css, + ); + } + } + }); + } else if (this.css) { + let from = this.opts.from + ? this.toUrl(this.path(this.opts.from)) + : ''; + this.map.setSourceContent(from, this.css); + } + } + + sourcePath(node) { + if (this.mapOpts.from) { + return this.toUrl(this.mapOpts.from); + } else if (this.usesFileUrls) { + return this.toFileUrl(node.source.input.from); + } else { + return this.toUrl(this.path(node.source.input.from)); + } + } + + toBase64(str) { + if (Buffer) { + return Buffer.from(str).toString('base64'); + } else { + return window.btoa(unescape(encodeURIComponent(str))); + } + } + + toFileUrl(path) { + let cached = this.memoizedFileURLs.get(path); + if (cached) return cached; + + if (pathToFileURL) { + let fileURL = pathToFileURL(path).toString(); + this.memoizedFileURLs.set(path, fileURL); + + return fileURL; + } else { + throw new Error( + '`map.absolute` option is not available in this PostCSS build', + ); + } + } + + toUrl(path) { + let cached = this.memoizedURLs.get(path); + if (cached) return cached; + + if (sep === '\\') { + path = path.replace(/\\/g, '/'); + } + + let url = encodeURI(path).replace(/[#?]/g, encodeURIComponent); + this.memoizedURLs.set(path, url); + + return url; + } + } + + module.exports = MapGenerator; + }).call(this); + }).call(this, require('buffer').Buffer); + }, + { './input': 42, 'buffer': 3, 'path': 2, 'source-map-js': 2, 'url': 2 }, + ], + 46: [ + function (require, module, exports) { + (function (process) { + (function () { + 'use strict'; + + let MapGenerator = require('./map-generator'); + let stringify = require('./stringify'); + let warnOnce = require('./warn-once'); + let parse = require('./parse'); + const Result = require('./result'); + + class NoWorkResult { + constructor(processor, css, opts) { + css = css.toString(); + this.stringified = false; + + this._processor = processor; + this._css = css; + this._opts = opts; + this._map = undefined; + let root; + + let str = stringify; + this.result = new Result(this._processor, root, this._opts); + this.result.css = css; + + let self = this; + Object.defineProperty(this.result, 'root', { + get() { + return self.root; + }, + }); + + let map = new MapGenerator(str, root, this._opts, css); + if (map.isMap()) { + let [generatedCSS, generatedMap] = map.generate(); + if (generatedCSS) { + this.result.css = generatedCSS; + } + if (generatedMap) { + this.result.map = generatedMap; + } + } + } + + async() { + if (this.error) return Promise.reject(this.error); + return Promise.resolve(this.result); + } + + catch(onRejected) { + return this.async().catch(onRejected); + } + + finally(onFinally) { + return this.async().then(onFinally, onFinally); + } + + sync() { + if (this.error) throw this.error; + return this.result; + } + + then(onFulfilled, onRejected) { + if (process.env.NODE_ENV !== 'production') { + if (!('from' in this._opts)) { + warnOnce( + 'Without `from` option PostCSS could generate wrong source map ' + + 'and will not find Browserslist config. Set it to CSS file path ' + + 'or to `undefined` to prevent this warning.', + ); + } + } + + return this.async().then(onFulfilled, onRejected); + } + + toString() { + return this._css; + } + + warnings() { + return []; + } + + get content() { + return this.result.css; + } + + get css() { + return this.result.css; + } + + get map() { + return this.result.map; + } + + get messages() { + return []; + } + + get opts() { + return this.result.opts; + } + + get processor() { + return this.result.processor; + } + + get root() { + if (this._root) { + return this._root; + } + + let root; + let parser = parse; + + try { + root = parser(this._css, this._opts); + } catch (error) { + this.error = error; + } + + if (this.error) { + throw this.error; + } else { + this._root = root; + return root; + } + } + + get [Symbol.toStringTag]() { + return 'NoWorkResult'; + } + } + + module.exports = NoWorkResult; + NoWorkResult.default = NoWorkResult; + }).call(this); + }).call(this, require('_process')); + }, + { + './map-generator': 45, + './parse': 48, + './result': 53, + './stringify': 57, + './warn-once': 60, + '_process': 62, + }, + ], + 47: [ + function (require, module, exports) { + 'use strict'; + + let { isClean, my } = require('./symbols'); + let CssSyntaxError = require('./css-syntax-error'); + let Stringifier = require('./stringifier'); + let stringify = require('./stringify'); + + function cloneNode(obj, parent) { + let cloned = new obj.constructor(); + + for (let i in obj) { + if (!Object.prototype.hasOwnProperty.call(obj, i)) { + /* c8 ignore next 2 */ + continue; + } + if (i === 'proxyCache') continue; + let value = obj[i]; + let type = typeof value; + + if (i === 'parent' && type === 'object') { + if (parent) cloned[i] = parent; + } else if (i === 'source') { + cloned[i] = value; + } else if (Array.isArray(value)) { + cloned[i] = value.map(j => cloneNode(j, cloned)); + } else { + if (type === 'object' && value !== null) value = cloneNode(value); + cloned[i] = value; } - } else { - delete frame.attribs[a]; } - }); - } - if (options.selfClosing.indexOf(name) !== -1) { - result += ' />'; - } else { - result += '>'; - if (frame.innerText && !hasText && !options.textFilter) { - result += escapeHtml(frame.innerText); - addedText = true; + + return cloned; } - } - if (skip) { - result = tempResult + escapeHtml(result); - tempResult = ''; - } - }, - ontext: function(text) { - if (skipText) { - return; - } - const lastFrame = stack[stack.length - 1]; - let tag; - if (lastFrame) { - tag = lastFrame.tag; - // If inner text was set by transform function then let's use it - text = lastFrame.innerText !== undefined ? lastFrame.innerText : text; - } + class Node { + constructor(defaults = {}) { + this.raws = {}; + this[isClean] = false; + this[my] = true; - if (options.disallowedTagsMode === 'discard' && ((tag === 'script') || (tag === 'style'))) { - // htmlparser2 gives us these as-is. Escaping them ruins the content. Allowing - // script tags is, by definition, game over for XSS protection, so if that's - // your concern, don't allow them. The same is essentially true for style tags - // which have their own collection of XSS vectors. - result += text; - } else { - const escaped = escapeHtml(text, false); - if (options.textFilter && !addedText) { - result += options.textFilter(escaped, tag); - } else if (!addedText) { - result += escaped; + for (let name in defaults) { + if (name === 'nodes') { + this.nodes = []; + for (let node of defaults[name]) { + if (typeof node.clone === 'function') { + this.append(node.clone()); + } else { + this.append(node); + } + } + } else { + this[name] = defaults[name]; + } + } + } + + addToError(error) { + error.postcssNode = this; + if (error.stack && this.source && /\n\s{4}at /.test(error.stack)) { + let s = this.source; + error.stack = error.stack.replace( + /\n\s{4}at /, + `$&${s.input.from}:${s.start.line}:${s.start.column}$&`, + ); + } + return error; + } + + after(add) { + this.parent.insertAfter(this, add); + return this; + } + + assign(overrides = {}) { + for (let name in overrides) { + this[name] = overrides[name]; + } + return this; + } + + before(add) { + this.parent.insertBefore(this, add); + return this; + } + + cleanRaws(keepBetween) { + delete this.raws.before; + delete this.raws.after; + if (!keepBetween) delete this.raws.between; + } + + clone(overrides = {}) { + let cloned = cloneNode(this); + for (let name in overrides) { + cloned[name] = overrides[name]; + } + return cloned; + } + + cloneAfter(overrides = {}) { + let cloned = this.clone(overrides); + this.parent.insertAfter(this, cloned); + return cloned; + } + + cloneBefore(overrides = {}) { + let cloned = this.clone(overrides); + this.parent.insertBefore(this, cloned); + return cloned; + } + + error(message, opts = {}) { + if (this.source) { + let { end, start } = this.rangeBy(opts); + return this.source.input.error( + message, + { column: start.column, line: start.line }, + { column: end.column, line: end.line }, + opts, + ); + } + return new CssSyntaxError(message); + } + + getProxyProcessor() { + return { + get(node, prop) { + if (prop === 'proxyOf') { + return node; + } else if (prop === 'root') { + return () => node.root().toProxy(); + } else { + return node[prop]; + } + }, + + set(node, prop, value) { + if (node[prop] === value) return true; + node[prop] = value; + if ( + prop === 'prop' || + prop === 'value' || + prop === 'name' || + prop === 'params' || + prop === 'important' || + /* c8 ignore next */ + prop === 'text' + ) { + node.markDirty(); + } + return true; + }, + }; + } + + markDirty() { + if (this[isClean]) { + this[isClean] = false; + let next = this; + while ((next = next.parent)) { + next[isClean] = false; + } + } + } + + next() { + if (!this.parent) return undefined; + let index = this.parent.index(this); + return this.parent.nodes[index + 1]; + } + + positionBy(opts, stringRepresentation) { + let pos = this.source.start; + if (opts.index) { + pos = this.positionInside(opts.index, stringRepresentation); + } else if (opts.word) { + stringRepresentation = this.toString(); + let index = stringRepresentation.indexOf(opts.word); + if (index !== -1) + pos = this.positionInside(index, stringRepresentation); + } + return pos; + } + + positionInside(index, stringRepresentation) { + let string = stringRepresentation || this.toString(); + let column = this.source.start.column; + let line = this.source.start.line; + + for (let i = 0; i < index; i++) { + if (string[i] === '\n') { + column = 1; + line += 1; + } else { + column += 1; + } + } + + return { column, line }; + } + + prev() { + if (!this.parent) return undefined; + let index = this.parent.index(this); + return this.parent.nodes[index - 1]; + } + + rangeBy(opts) { + let start = { + column: this.source.start.column, + line: this.source.start.line, + }; + let end = this.source.end + ? { + column: this.source.end.column + 1, + line: this.source.end.line, + } + : { + column: start.column + 1, + line: start.line, + }; + + if (opts.word) { + let stringRepresentation = this.toString(); + let index = stringRepresentation.indexOf(opts.word); + if (index !== -1) { + start = this.positionInside(index, stringRepresentation); + end = this.positionInside( + index + opts.word.length, + stringRepresentation, + ); + } + } else { + if (opts.start) { + start = { + column: opts.start.column, + line: opts.start.line, + }; + } else if (opts.index) { + start = this.positionInside(opts.index); + } + + if (opts.end) { + end = { + column: opts.end.column, + line: opts.end.line, + }; + } else if (opts.endIndex) { + end = this.positionInside(opts.endIndex); + } else if (opts.index) { + end = this.positionInside(opts.index + 1); + } + } + + if ( + end.line < start.line || + (end.line === start.line && end.column <= start.column) + ) { + end = { column: start.column + 1, line: start.line }; + } + + return { end, start }; + } + + raw(prop, defaultType) { + let str = new Stringifier(); + return str.raw(this, prop, defaultType); + } + + remove() { + if (this.parent) { + this.parent.removeChild(this); + } + this.parent = undefined; + return this; + } + + replaceWith(...nodes) { + if (this.parent) { + let bookmark = this; + let foundSelf = false; + for (let node of nodes) { + if (node === this) { + foundSelf = true; + } else if (foundSelf) { + this.parent.insertAfter(bookmark, node); + bookmark = node; + } else { + this.parent.insertBefore(bookmark, node); + } + } + + if (!foundSelf) { + this.remove(); + } + } + + return this; + } + + root() { + let result = this; + while (result.parent && result.parent.type !== 'document') { + result = result.parent; + } + return result; + } + + toJSON(_, inputs) { + let fixed = {}; + let emitInputs = inputs == null; + inputs = inputs || new Map(); + let inputsNextIndex = 0; + + for (let name in this) { + if (!Object.prototype.hasOwnProperty.call(this, name)) { + /* c8 ignore next 2 */ + continue; + } + if (name === 'parent' || name === 'proxyCache') continue; + let value = this[name]; + + if (Array.isArray(value)) { + fixed[name] = value.map(i => { + if (typeof i === 'object' && i.toJSON) { + return i.toJSON(null, inputs); + } else { + return i; + } + }); + } else if (typeof value === 'object' && value.toJSON) { + fixed[name] = value.toJSON(null, inputs); + } else if (name === 'source') { + let inputId = inputs.get(value.input); + if (inputId == null) { + inputId = inputsNextIndex; + inputs.set(value.input, inputsNextIndex); + inputsNextIndex++; + } + fixed[name] = { + end: value.end, + inputId, + start: value.start, + }; + } else { + fixed[name] = value; + } + } + + if (emitInputs) { + fixed.inputs = [...inputs.keys()].map(input => input.toJSON()); + } + + return fixed; + } + + toProxy() { + if (!this.proxyCache) { + this.proxyCache = new Proxy(this, this.getProxyProcessor()); + } + return this.proxyCache; + } + + toString(stringifier = stringify) { + if (stringifier.stringify) stringifier = stringifier.stringify; + let result = ''; + stringifier(this, i => { + result += i; + }); + return result; + } + + warn(result, text, opts) { + let data = { node: this }; + for (let i in opts) data[i] = opts[i]; + return result.warn(text, data); + } + + get proxyOf() { + return this; + } } - } - if (stack.length) { - const frame = stack[stack.length - 1]; - frame.text += text; - } - }, - onclosetag: function(name, isImplied) { - if (skipText) { - skipTextDepth--; - if (!skipTextDepth) { - skipText = false; - } else { - return; + module.exports = Node; + Node.default = Node; + }, + { + './css-syntax-error': 38, + './stringifier': 56, + './stringify': 57, + './symbols': 58, + }, + ], + 48: [ + function (require, module, exports) { + (function (process) { + (function () { + 'use strict'; + + let Container = require('./container'); + let Parser = require('./parser'); + let Input = require('./input'); + + function parse(css, opts) { + let input = new Input(css, opts); + let parser = new Parser(input); + try { + parser.parse(); + } catch (e) { + if (process.env.NODE_ENV !== 'production') { + if (e.name === 'CssSyntaxError' && opts && opts.from) { + if (/\.scss$/i.test(opts.from)) { + e.message += + '\nYou tried to parse SCSS with ' + + 'the standard CSS parser; ' + + 'try again with the postcss-scss parser'; + } else if (/\.sass/i.test(opts.from)) { + e.message += + '\nYou tried to parse Sass with ' + + 'the standard CSS parser; ' + + 'try again with the postcss-sass parser'; + } else if (/\.less$/i.test(opts.from)) { + e.message += + '\nYou tried to parse Less with ' + + 'the standard CSS parser; ' + + 'try again with the postcss-less parser'; + } + } + } + throw e; + } + + return parser.root; + } + + module.exports = parse; + parse.default = parse; + + Container.registerParse(parse); + }).call(this); + }).call(this, require('_process')); + }, + { './container': 37, './input': 42, './parser': 49, '_process': 62 }, + ], + 49: [ + function (require, module, exports) { + 'use strict'; + + let Declaration = require('./declaration'); + let tokenizer = require('./tokenize'); + let Comment = require('./comment'); + let AtRule = require('./at-rule'); + let Root = require('./root'); + let Rule = require('./rule'); + + const SAFE_COMMENT_NEIGHBOR = { + empty: true, + space: true, + }; + + function findLastWithPosition(tokens) { + for (let i = tokens.length - 1; i >= 0; i--) { + let token = tokens[i]; + let pos = token[3] || token[2]; + if (pos) return pos; + } } - } - const frame = stack.pop(); - if (!frame) { - // Do not crash on bad markup - return; - } + class Parser { + constructor(input) { + this.input = input; - if (frame.tag !== name) { - // Another case of bad markup. - // Push to stack, so that it will be used in future closing tags. - stack.push(frame); - return; - } + this.root = new Root(); + this.current = this.root; + this.spaces = ''; + this.semicolon = false; + this.customProperty = false; - skipText = options.enforceHtmlBoundary ? name === 'html' : false; - depth--; - const skip = skipMap[depth]; - if (skip) { - delete skipMap[depth]; - if (options.disallowedTagsMode === 'discard') { - frame.updateParentNodeText(); - return; + this.createTokenizer(); + this.root.source = { + input, + start: { column: 1, line: 1, offset: 0 }, + }; + } + + atrule(token) { + let node = new AtRule(); + node.name = token[1].slice(1); + if (node.name === '') { + this.unnamedAtrule(node, token); + } + this.init(node, token[2]); + + let type; + let prev; + let shift; + let last = false; + let open = false; + let params = []; + let brackets = []; + + while (!this.tokenizer.endOfFile()) { + token = this.tokenizer.nextToken(); + type = token[0]; + + if (type === '(' || type === '[') { + brackets.push(type === '(' ? ')' : ']'); + } else if (type === '{' && brackets.length > 0) { + brackets.push('}'); + } else if (type === brackets[brackets.length - 1]) { + brackets.pop(); + } + + if (brackets.length === 0) { + if (type === ';') { + node.source.end = this.getPosition(token[2]); + node.source.end.offset++; + this.semicolon = true; + break; + } else if (type === '{') { + open = true; + break; + } else if (type === '}') { + if (params.length > 0) { + shift = params.length - 1; + prev = params[shift]; + while (prev && prev[0] === 'space') { + prev = params[--shift]; + } + if (prev) { + node.source.end = this.getPosition(prev[3] || prev[2]); + node.source.end.offset++; + } + } + this.end(token); + break; + } else { + params.push(token); + } + } else { + params.push(token); + } + + if (this.tokenizer.endOfFile()) { + last = true; + break; + } + } + + node.raws.between = this.spacesAndCommentsFromEnd(params); + if (params.length) { + node.raws.afterName = this.spacesAndCommentsFromStart(params); + this.raw(node, 'params', params); + if (last) { + token = params[params.length - 1]; + node.source.end = this.getPosition(token[3] || token[2]); + node.source.end.offset++; + this.spaces = node.raws.between; + node.raws.between = ''; + } + } else { + node.raws.afterName = ''; + node.params = ''; + } + + if (open) { + node.nodes = []; + this.current = node; + } + } + + checkMissedSemicolon(tokens) { + let colon = this.colon(tokens); + if (colon === false) return; + + let founded = 0; + let token; + for (let j = colon - 1; j >= 0; j--) { + token = tokens[j]; + if (token[0] !== 'space') { + founded += 1; + if (founded === 2) break; + } + } + // If the token is a word, e.g. `!important`, `red` or any other valid property's value. + // Then we need to return the colon after that word token. [3] is the "end" colon of that word. + // And because we need it after that one we do +1 to get the next one. + throw this.input.error( + 'Missed semicolon', + token[0] === 'word' ? token[3] + 1 : token[2], + ); + } + + colon(tokens) { + let brackets = 0; + let token, type, prev; + for (let [i, element] of tokens.entries()) { + token = element; + type = token[0]; + + if (type === '(') { + brackets += 1; + } + if (type === ')') { + brackets -= 1; + } + if (brackets === 0 && type === ':') { + if (!prev) { + this.doubleColon(token); + } else if (prev[0] === 'word' && prev[1] === 'progid') { + continue; + } else { + return i; + } + } + + prev = token; + } + return false; + } + + comment(token) { + let node = new Comment(); + this.init(node, token[2]); + node.source.end = this.getPosition(token[3] || token[2]); + node.source.end.offset++; + + let text = token[1].slice(2, -2); + if (/^\s*$/.test(text)) { + node.text = ''; + node.raws.left = text; + node.raws.right = ''; + } else { + let match = text.match(/^(\s*)([^]*\S)(\s*)$/); + node.text = match[2]; + node.raws.left = match[1]; + node.raws.right = match[3]; + } + } + + createTokenizer() { + this.tokenizer = tokenizer(this.input); + } + + decl(tokens, customProperty) { + let node = new Declaration(); + this.init(node, tokens[0][2]); + + let last = tokens[tokens.length - 1]; + if (last[0] === ';') { + this.semicolon = true; + tokens.pop(); + } + + node.source.end = this.getPosition( + last[3] || last[2] || findLastWithPosition(tokens), + ); + node.source.end.offset++; + + while (tokens[0][0] !== 'word') { + if (tokens.length === 1) this.unknownWord(tokens); + node.raws.before += tokens.shift()[1]; + } + node.source.start = this.getPosition(tokens[0][2]); + + node.prop = ''; + while (tokens.length) { + let type = tokens[0][0]; + if (type === ':' || type === 'space' || type === 'comment') { + break; + } + node.prop += tokens.shift()[1]; + } + + node.raws.between = ''; + + let token; + while (tokens.length) { + token = tokens.shift(); + + if (token[0] === ':') { + node.raws.between += token[1]; + break; + } else { + if (token[0] === 'word' && /\w/.test(token[1])) { + this.unknownWord([token]); + } + node.raws.between += token[1]; + } + } + + if (node.prop[0] === '_' || node.prop[0] === '*') { + node.raws.before += node.prop[0]; + node.prop = node.prop.slice(1); + } + + let firstSpaces = []; + let next; + while (tokens.length) { + next = tokens[0][0]; + if (next !== 'space' && next !== 'comment') break; + firstSpaces.push(tokens.shift()); + } + + this.precheckMissedSemicolon(tokens); + + for (let i = tokens.length - 1; i >= 0; i--) { + token = tokens[i]; + if (token[1].toLowerCase() === '!important') { + node.important = true; + let string = this.stringFrom(tokens, i); + string = this.spacesFromEnd(tokens) + string; + if (string !== ' !important') node.raws.important = string; + break; + } else if (token[1].toLowerCase() === 'important') { + let cache = tokens.slice(0); + let str = ''; + for (let j = i; j > 0; j--) { + let type = cache[j][0]; + if (str.trim().indexOf('!') === 0 && type !== 'space') { + break; + } + str = cache.pop()[1] + str; + } + if (str.trim().indexOf('!') === 0) { + node.important = true; + node.raws.important = str; + tokens = cache; + } + } + + if (token[0] !== 'space' && token[0] !== 'comment') { + break; + } + } + + let hasWord = tokens.some( + i => i[0] !== 'space' && i[0] !== 'comment', + ); + + if (hasWord) { + node.raws.between += firstSpaces.map(i => i[1]).join(''); + firstSpaces = []; + } + this.raw(node, 'value', firstSpaces.concat(tokens), customProperty); + + if (node.value.includes(':') && !customProperty) { + this.checkMissedSemicolon(tokens); + } + } + + doubleColon(token) { + throw this.input.error( + 'Double colon', + { offset: token[2] }, + { offset: token[2] + token[1].length }, + ); + } + + emptyRule(token) { + let node = new Rule(); + this.init(node, token[2]); + node.selector = ''; + node.raws.between = ''; + this.current = node; + } + + end(token) { + if (this.current.nodes && this.current.nodes.length) { + this.current.raws.semicolon = this.semicolon; + } + this.semicolon = false; + + this.current.raws.after = + (this.current.raws.after || '') + this.spaces; + this.spaces = ''; + + if (this.current.parent) { + this.current.source.end = this.getPosition(token[2]); + this.current.source.end.offset++; + this.current = this.current.parent; + } else { + this.unexpectedClose(token); + } + } + + endFile() { + if (this.current.parent) this.unclosedBlock(); + if (this.current.nodes && this.current.nodes.length) { + this.current.raws.semicolon = this.semicolon; + } + this.current.raws.after = + (this.current.raws.after || '') + this.spaces; + this.root.source.end = this.getPosition(this.tokenizer.position()); + } + + freeSemicolon(token) { + this.spaces += token[1]; + if (this.current.nodes) { + let prev = this.current.nodes[this.current.nodes.length - 1]; + if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) { + prev.raws.ownSemicolon = this.spaces; + this.spaces = ''; + } + } + } + + // Helpers + + getPosition(offset) { + let pos = this.input.fromOffset(offset); + return { + column: pos.col, + line: pos.line, + offset, + }; + } + + init(node, offset) { + this.current.push(node); + node.source = { + input: this.input, + start: this.getPosition(offset), + }; + node.raws.before = this.spaces; + this.spaces = ''; + if (node.type !== 'comment') this.semicolon = false; + } + + other(start) { + let end = false; + let type = null; + let colon = false; + let bracket = null; + let brackets = []; + let customProperty = start[1].startsWith('--'); + + let tokens = []; + let token = start; + while (token) { + type = token[0]; + tokens.push(token); + + if (type === '(' || type === '[') { + if (!bracket) bracket = token; + brackets.push(type === '(' ? ')' : ']'); + } else if (customProperty && colon && type === '{') { + if (!bracket) bracket = token; + brackets.push('}'); + } else if (brackets.length === 0) { + if (type === ';') { + if (colon) { + this.decl(tokens, customProperty); + return; + } else { + break; + } + } else if (type === '{') { + this.rule(tokens); + return; + } else if (type === '}') { + this.tokenizer.back(tokens.pop()); + end = true; + break; + } else if (type === ':') { + colon = true; + } + } else if (type === brackets[brackets.length - 1]) { + brackets.pop(); + if (brackets.length === 0) bracket = null; + } + + token = this.tokenizer.nextToken(); + } + + if (this.tokenizer.endOfFile()) end = true; + if (brackets.length > 0) this.unclosedBracket(bracket); + + if (end && colon) { + if (!customProperty) { + while (tokens.length) { + token = tokens[tokens.length - 1][0]; + if (token !== 'space' && token !== 'comment') break; + this.tokenizer.back(tokens.pop()); + } + } + this.decl(tokens, customProperty); + } else { + this.unknownWord(tokens); + } + } + + parse() { + let token; + while (!this.tokenizer.endOfFile()) { + token = this.tokenizer.nextToken(); + + switch (token[0]) { + case 'space': + this.spaces += token[1]; + break; + + case ';': + this.freeSemicolon(token); + break; + + case '}': + this.end(token); + break; + + case 'comment': + this.comment(token); + break; + + case 'at-word': + this.atrule(token); + break; + + case '{': + this.emptyRule(token); + break; + + default: + this.other(token); + break; + } + } + this.endFile(); + } + + precheckMissedSemicolon(/* tokens */) { + // Hook for Safe Parser + } + + raw(node, prop, tokens, customProperty) { + let token, type; + let length = tokens.length; + let value = ''; + let clean = true; + let next, prev; + + for (let i = 0; i < length; i += 1) { + token = tokens[i]; + type = token[0]; + if (type === 'space' && i === length - 1 && !customProperty) { + clean = false; + } else if (type === 'comment') { + prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty'; + next = tokens[i + 1] ? tokens[i + 1][0] : 'empty'; + if ( + !SAFE_COMMENT_NEIGHBOR[prev] && + !SAFE_COMMENT_NEIGHBOR[next] + ) { + if (value.slice(-1) === ',') { + clean = false; + } else { + value += token[1]; + } + } else { + clean = false; + } + } else { + value += token[1]; + } + } + if (!clean) { + let raw = tokens.reduce((all, i) => all + i[1], ''); + node.raws[prop] = { raw, value }; + } + node[prop] = value; + } + + rule(tokens) { + tokens.pop(); + + let node = new Rule(); + this.init(node, tokens[0][2]); + + node.raws.between = this.spacesAndCommentsFromEnd(tokens); + this.raw(node, 'selector', tokens); + this.current = node; + } + + spacesAndCommentsFromEnd(tokens) { + let lastTokenType; + let spaces = ''; + while (tokens.length) { + lastTokenType = tokens[tokens.length - 1][0]; + if (lastTokenType !== 'space' && lastTokenType !== 'comment') + break; + spaces = tokens.pop()[1] + spaces; + } + return spaces; + } + + // Errors + + spacesAndCommentsFromStart(tokens) { + let next; + let spaces = ''; + while (tokens.length) { + next = tokens[0][0]; + if (next !== 'space' && next !== 'comment') break; + spaces += tokens.shift()[1]; + } + return spaces; + } + + spacesFromEnd(tokens) { + let lastTokenType; + let spaces = ''; + while (tokens.length) { + lastTokenType = tokens[tokens.length - 1][0]; + if (lastTokenType !== 'space') break; + spaces = tokens.pop()[1] + spaces; + } + return spaces; + } + + stringFrom(tokens, from) { + let result = ''; + for (let i = from; i < tokens.length; i++) { + result += tokens[i][1]; + } + tokens.splice(from, tokens.length - from); + return result; + } + + unclosedBlock() { + let pos = this.current.source.start; + throw this.input.error('Unclosed block', pos.line, pos.column); + } + + unclosedBracket(bracket) { + throw this.input.error( + 'Unclosed bracket', + { offset: bracket[2] }, + { offset: bracket[2] + 1 }, + ); + } + + unexpectedClose(token) { + throw this.input.error( + 'Unexpected }', + { offset: token[2] }, + { offset: token[2] + 1 }, + ); + } + + unknownWord(tokens) { + throw this.input.error( + 'Unknown word', + { offset: tokens[0][2] }, + { offset: tokens[0][2] + tokens[0][1].length }, + ); + } + + unnamedAtrule(node, token) { + throw this.input.error( + 'At-rule without name', + { offset: token[2] }, + { offset: token[2] + token[1].length }, + ); + } } - tempResult = result; - result = ''; - } - if (transformMap[depth]) { - name = transformMap[depth]; - delete transformMap[depth]; - } + module.exports = Parser; + }, + { + './at-rule': 35, + './comment': 36, + './declaration': 39, + './root': 54, + './rule': 55, + './tokenize': 59, + }, + ], + 50: [ + function (require, module, exports) { + (function (process) { + (function () { + 'use strict'; - if (options.exclusiveFilter && options.exclusiveFilter(frame)) { - result = result.substr(0, frame.tagPosition); - return; - } + let CssSyntaxError = require('./css-syntax-error'); + let Declaration = require('./declaration'); + let LazyResult = require('./lazy-result'); + let Container = require('./container'); + let Processor = require('./processor'); + let stringify = require('./stringify'); + let fromJSON = require('./fromJSON'); + let Document = require('./document'); + let Warning = require('./warning'); + let Comment = require('./comment'); + let AtRule = require('./at-rule'); + let Result = require('./result.js'); + let Input = require('./input'); + let parse = require('./parse'); + let list = require('./list'); + let Rule = require('./rule'); + let Root = require('./root'); + let Node = require('./node'); - frame.updateParentNodeMediaChildren(); - frame.updateParentNodeText(); + function postcss(...plugins) { + if (plugins.length === 1 && Array.isArray(plugins[0])) { + plugins = plugins[0]; + } + return new Processor(plugins); + } - if ( - // Already output /> - options.selfClosing.indexOf(name) !== -1 || - // Escaped tag, closing tag is implied - (isImplied && !tagAllowed(name) && [ 'escape', 'recursiveEscape' ].indexOf(options.disallowedTagsMode) >= 0) - ) { - if (skip) { - result = tempResult; - tempResult = ''; + postcss.plugin = function plugin(name, initializer) { + let warningPrinted = false; + function creator(...args) { + // eslint-disable-next-line no-console + if (console && console.warn && !warningPrinted) { + warningPrinted = true; + // eslint-disable-next-line no-console + console.warn( + name + + ': postcss.plugin was deprecated. Migration guide:\n' + + 'https://evilmartians.com/chronicles/postcss-8-plugin-migration', + ); + if (process.env.LANG && process.env.LANG.startsWith('cn')) { + /* c8 ignore next 7 */ + // eslint-disable-next-line no-console + console.warn( + name + + ': 里面 postcss.plugin 被弃用. 迁移指南:\n' + + 'https://www.w3ctech.com/topic/2226', + ); + } + } + let transformer = initializer(...args); + transformer.postcssPlugin = name; + transformer.postcssVersion = new Processor().version; + return transformer; + } + + let cache; + Object.defineProperty(creator, 'postcss', { + get() { + if (!cache) cache = creator(); + return cache; + }, + }); + + creator.process = function (css, processOpts, pluginOpts) { + return postcss([creator(pluginOpts)]).process(css, processOpts); + }; + + return creator; + }; + + postcss.stringify = stringify; + postcss.parse = parse; + postcss.fromJSON = fromJSON; + postcss.list = list; + + postcss.comment = defaults => new Comment(defaults); + postcss.atRule = defaults => new AtRule(defaults); + postcss.decl = defaults => new Declaration(defaults); + postcss.rule = defaults => new Rule(defaults); + postcss.root = defaults => new Root(defaults); + postcss.document = defaults => new Document(defaults); + + postcss.CssSyntaxError = CssSyntaxError; + postcss.Declaration = Declaration; + postcss.Container = Container; + postcss.Processor = Processor; + postcss.Document = Document; + postcss.Comment = Comment; + postcss.Warning = Warning; + postcss.AtRule = AtRule; + postcss.Result = Result; + postcss.Input = Input; + postcss.Rule = Rule; + postcss.Root = Root; + postcss.Node = Node; + + LazyResult.registerPostcss(postcss); + + module.exports = postcss; + postcss.default = postcss; + }).call(this); + }).call(this, require('_process')); + }, + { + './at-rule': 35, + './comment': 36, + './container': 37, + './css-syntax-error': 38, + './declaration': 39, + './document': 40, + './fromJSON': 41, + './input': 42, + './lazy-result': 43, + './list': 44, + './node': 47, + './parse': 48, + './processor': 52, + './result.js': 53, + './root': 54, + './rule': 55, + './stringify': 57, + './warning': 61, + '_process': 62, + }, + ], + 51: [ + function (require, module, exports) { + (function (Buffer) { + (function () { + 'use strict'; + + let { + SourceMapConsumer, + SourceMapGenerator, + } = require('source-map-js'); + let { existsSync, readFileSync } = require('fs'); + let { dirname, join } = require('path'); + + function fromBase64(str) { + if (Buffer) { + return Buffer.from(str, 'base64').toString(); + } else { + /* c8 ignore next 2 */ + return window.atob(str); + } + } + + class PreviousMap { + constructor(css, opts) { + if (opts.map === false) return; + this.loadAnnotation(css); + this.inline = this.startWith(this.annotation, 'data:'); + + let prev = opts.map ? opts.map.prev : undefined; + let text = this.loadMap(opts.from, prev); + if (!this.mapFile && opts.from) { + this.mapFile = opts.from; + } + if (this.mapFile) this.root = dirname(this.mapFile); + if (text) this.text = text; + } + + consumer() { + if (!this.consumerCache) { + this.consumerCache = new SourceMapConsumer(this.text); + } + return this.consumerCache; + } + + decodeInline(text) { + let baseCharsetUri = + /^data:application\/json;charset=utf-?8;base64,/; + let baseUri = /^data:application\/json;base64,/; + let charsetUri = /^data:application\/json;charset=utf-?8,/; + let uri = /^data:application\/json,/; + + if (charsetUri.test(text) || uri.test(text)) { + return decodeURIComponent( + text.substr(RegExp.lastMatch.length), + ); + } + + if (baseCharsetUri.test(text) || baseUri.test(text)) { + return fromBase64(text.substr(RegExp.lastMatch.length)); + } + + let encoding = text.match(/data:application\/json;([^,]+),/)[1]; + throw new Error('Unsupported source map encoding ' + encoding); + } + + getAnnotationURL(sourceMapString) { + return sourceMapString + .replace(/^\/\*\s*# sourceMappingURL=/, '') + .trim(); + } + + isMap(map) { + if (typeof map !== 'object') return false; + return ( + typeof map.mappings === 'string' || + typeof map._mappings === 'string' || + Array.isArray(map.sections) + ); + } + + loadAnnotation(css) { + let comments = css.match(/\/\*\s*# sourceMappingURL=/gm); + if (!comments) return; + + // sourceMappingURLs from comments, strings, etc. + let start = css.lastIndexOf(comments.pop()); + let end = css.indexOf('*/', start); + + if (start > -1 && end > -1) { + // Locate the last sourceMappingURL to avoid pickin + this.annotation = this.getAnnotationURL( + css.substring(start, end), + ); + } + } + + loadFile(path) { + this.root = dirname(path); + if (existsSync(path)) { + this.mapFile = path; + return readFileSync(path, 'utf-8').toString().trim(); + } + } + + loadMap(file, prev) { + if (prev === false) return false; + + if (prev) { + if (typeof prev === 'string') { + return prev; + } else if (typeof prev === 'function') { + let prevPath = prev(file); + if (prevPath) { + let map = this.loadFile(prevPath); + if (!map) { + throw new Error( + 'Unable to load previous source map: ' + + prevPath.toString(), + ); + } + return map; + } + } else if (prev instanceof SourceMapConsumer) { + return SourceMapGenerator.fromSourceMap(prev).toString(); + } else if (prev instanceof SourceMapGenerator) { + return prev.toString(); + } else if (this.isMap(prev)) { + return JSON.stringify(prev); + } else { + throw new Error( + 'Unsupported previous source map format: ' + + prev.toString(), + ); + } + } else if (this.inline) { + return this.decodeInline(this.annotation); + } else if (this.annotation) { + let map = this.annotation; + if (file) map = join(dirname(file), map); + return this.loadFile(map); + } + } + + startWith(string, start) { + if (!string) return false; + return string.substr(0, start.length) === start; + } + + withContent() { + return !!( + this.consumer().sourcesContent && + this.consumer().sourcesContent.length > 0 + ); + } + } + + module.exports = PreviousMap; + PreviousMap.default = PreviousMap; + }).call(this); + }).call(this, require('buffer').Buffer); + }, + { 'buffer': 3, 'fs': 2, 'path': 2, 'source-map-js': 2 }, + ], + 52: [ + function (require, module, exports) { + (function (process) { + (function () { + 'use strict'; + + let NoWorkResult = require('./no-work-result'); + let LazyResult = require('./lazy-result'); + let Document = require('./document'); + let Root = require('./root'); + + class Processor { + constructor(plugins = []) { + this.version = '8.4.31'; + this.plugins = this.normalize(plugins); + } + + normalize(plugins) { + let normalized = []; + for (let i of plugins) { + if (i.postcss === true) { + i = i(); + } else if (i.postcss) { + i = i.postcss; + } + + if (typeof i === 'object' && Array.isArray(i.plugins)) { + normalized = normalized.concat(i.plugins); + } else if (typeof i === 'object' && i.postcssPlugin) { + normalized.push(i); + } else if (typeof i === 'function') { + normalized.push(i); + } else if ( + typeof i === 'object' && + (i.parse || i.stringify) + ) { + if (process.env.NODE_ENV !== 'production') { + throw new Error( + 'PostCSS syntaxes cannot be used as plugins. Instead, please use ' + + 'one of the syntax/parser/stringifier options as outlined ' + + 'in your PostCSS runner documentation.', + ); + } + } else { + throw new Error(i + ' is not a PostCSS plugin'); + } + } + return normalized; + } + + process(css, opts = {}) { + if ( + this.plugins.length === 0 && + typeof opts.parser === 'undefined' && + typeof opts.stringifier === 'undefined' && + typeof opts.syntax === 'undefined' + ) { + return new NoWorkResult(this, css, opts); + } else { + return new LazyResult(this, css, opts); + } + } + + use(plugin) { + this.plugins = this.plugins.concat(this.normalize([plugin])); + return this; + } + } + + module.exports = Processor; + Processor.default = Processor; + + Root.registerProcessor(Processor); + Document.registerProcessor(Processor); + }).call(this); + }).call(this, require('_process')); + }, + { + './document': 40, + './lazy-result': 43, + './no-work-result': 46, + './root': 54, + '_process': 62, + }, + ], + 53: [ + function (require, module, exports) { + 'use strict'; + + let Warning = require('./warning'); + + class Result { + constructor(processor, root, opts) { + this.processor = processor; + this.messages = []; + this.root = root; + this.opts = opts; + this.css = undefined; + this.map = undefined; + } + + toString() { + return this.css; + } + + warn(text, opts = {}) { + if (!opts.plugin) { + if (this.lastPlugin && this.lastPlugin.postcssPlugin) { + opts.plugin = this.lastPlugin.postcssPlugin; + } + } + + let warning = new Warning(text, opts); + this.messages.push(warning); + + return warning; + } + + warnings() { + return this.messages.filter(i => i.type === 'warning'); + } + + get content() { + return this.css; + } } - return; - } - result += ''; - if (skip) { - result = tempResult + escapeHtml(result); - tempResult = ''; - } - addedText = false; - } - }, options.parser); - parser.write(html); - parser.end(); + module.exports = Result; + Result.default = Result; + }, + { './warning': 61 }, + ], + 54: [ + function (require, module, exports) { + 'use strict'; - return result; + let Container = require('./container'); - function initializeState() { - result = ''; - depth = 0; - stack = []; - skipMap = {}; - transformMap = {}; - skipText = false; - skipTextDepth = 0; - } + let LazyResult, Processor; - function escapeHtml(s, quote) { - if (typeof (s) !== 'string') { - s = s + ''; - } - if (options.parser.decodeEntities) { - s = s.replace(/&/g, '&').replace(//g, '>'); - if (quote) { - s = s.replace(/"/g, '"'); - } - } - // TODO: this is inadequate because it will pass `&0;`. This approach - // will not work, each & must be considered with regard to whether it - // is followed by a 100% syntactically valid entity or not, and escaped - // if it is not. If this bothers you, don't set parser.decodeEntities - // to false. (The default is true.) - s = s.replace(/&(?![a-zA-Z0-9#]{1,20};)/g, '&') // Match ampersands not part of existing HTML entity - .replace(//g, '>'); - if (quote) { - s = s.replace(/"/g, '"'); - } - return s; - } + class Root extends Container { + constructor(defaults) { + super(defaults); + this.type = 'root'; + if (!this.nodes) this.nodes = []; + } - function naughtyHref(name, href) { - // Browsers ignore character codes of 32 (space) and below in a surprising - // number of situations. Start reading here: - // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_tab - // eslint-disable-next-line no-control-regex - href = href.replace(/[\x00-\x20]+/g, ''); - // Clobber any comments in URLs, which the browser might - // interpret inside an XML data island, allowing - // a javascript: URL to be snuck through - while (true) { - const firstIndex = href.indexOf('', firstIndex + 4); - if (lastIndex === -1) { - break; - } - href = href.substring(0, firstIndex) + href.substring(lastIndex + 3); - } - // Case insensitive so we don't get faked out by JAVASCRIPT #1 - // Allow more characters after the first so we don't get faked - // out by certain schemes browsers accept - const matches = href.match(/^([a-zA-Z][a-zA-Z0-9.\-+]*):/); - if (!matches) { - // Protocol-relative URL starting with any combination of '/' and '\' - if (href.match(/^[/\\]{2}/)) { - return !options.allowProtocolRelative; - } + normalize(child, sample, type) { + let nodes = super.normalize(child); - // No scheme - return false; - } - const scheme = matches[1].toLowerCase(); + if (sample) { + if (type === 'prepend') { + if (this.nodes.length > 1) { + sample.raws.before = this.nodes[1].raws.before; + } else { + delete sample.raws.before; + } + } else if (this.first !== sample) { + for (let node of nodes) { + node.raws.before = sample.raws.before; + } + } + } - if (has(options.allowedSchemesByTag, name)) { - return options.allowedSchemesByTag[name].indexOf(scheme) === -1; - } + return nodes; + } - return !options.allowedSchemes || options.allowedSchemes.indexOf(scheme) === -1; - } + removeChild(child, ignore) { + let index = this.index(child); - function parseUrl(value) { - value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, '$1//'); - if (value.startsWith('relative:')) { - // An attempt to exploit our workaround for base URLs being - // mandatory for relative URL validation in the WHATWG - // URL parser, reject it - throw new Error('relative: exploit attempt'); - } - // naughtyHref is in charge of whether protocol relative URLs - // are cool. Here we are concerned just with allowed hostnames and - // whether to allow relative URLs. - // - // Build a placeholder "base URL" against which any reasonable - // relative URL may be parsed successfully - let base = 'relative://relative-site'; - for (let i = 0; (i < 100); i++) { - base += `/${i}`; - } + if (!ignore && index === 0 && this.nodes.length > 1) { + this.nodes[1].raws.before = this.nodes[index].raws.before; + } - const parsed = new URL(value, base); + return super.removeChild(child); + } - const isRelativeUrl = parsed && parsed.hostname === 'relative-site' && parsed.protocol === 'relative:'; - return { - isRelativeUrl, - url: parsed - }; - } - /** - * Filters user input css properties by allowlisted regex attributes. - * Modifies the abstractSyntaxTree object. - * - * @param {object} abstractSyntaxTree - Object representation of CSS attributes. - * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }. - * @param {object} allowedStyles - Keys are properties (i.e color), value is list of permitted regex rules (i.e /green/i). - * @return {object} - The modified tree. - */ - function filterCss(abstractSyntaxTree, allowedStyles) { - if (!allowedStyles) { - return abstractSyntaxTree; - } - - const astRules = abstractSyntaxTree.nodes[0]; - let selectedRule; - - // Merge global and tag-specific styles into new AST. - if (allowedStyles[astRules.selector] && allowedStyles['*']) { - selectedRule = deepmerge( - allowedStyles[astRules.selector], - allowedStyles['*'] - ); - } else { - selectedRule = allowedStyles[astRules.selector] || allowedStyles['*']; - } - - if (selectedRule) { - abstractSyntaxTree.nodes[0].nodes = astRules.nodes.reduce(filterDeclarations(selectedRule), []); - } - - return abstractSyntaxTree; - } - - /** - * Extracts the style attributes from an AbstractSyntaxTree and formats those - * values in the inline style attribute format. - * - * @param {AbstractSyntaxTree} filteredAST - * @return {string} - Example: "color:yellow;text-align:center !important;font-family:helvetica;" - */ - function stringifyStyleAttributes(filteredAST) { - return filteredAST.nodes[0].nodes - .reduce(function(extractedAttributes, attrObject) { - extractedAttributes.push( - `${attrObject.prop}:${attrObject.value}${attrObject.important ? ' !important' : ''}` - ); - return extractedAttributes; - }, []) - .join(';'); - } - - /** - * Filters the existing attributes for the given property. Discards any attributes - * which don't match the allowlist. - * - * @param {object} selectedRule - Example: { color: red, font-family: helvetica } - * @param {array} allowedDeclarationsList - List of declarations which pass the allowlist. - * @param {object} attributeObject - Object representing the current css property. - * @property {string} attributeObject.type - Typically 'declaration'. - * @property {string} attributeObject.prop - The CSS property, i.e 'color'. - * @property {string} attributeObject.value - The corresponding value to the css property, i.e 'red'. - * @return {function} - When used in Array.reduce, will return an array of Declaration objects - */ - function filterDeclarations(selectedRule) { - return function (allowedDeclarationsList, attributeObject) { - // If this property is allowlisted... - if (has(selectedRule, attributeObject.prop)) { - const matchesRegex = selectedRule[attributeObject.prop].some(function(regularExpression) { - return regularExpression.test(attributeObject.value); - }); - - if (matchesRegex) { - allowedDeclarationsList.push(attributeObject); + toResult(opts = {}) { + let lazy = new LazyResult(new Processor(), this, opts); + return lazy.stringify(); + } } - } - return allowedDeclarationsList; - }; - } - function filterClasses(classes, allowed, allowedGlobs) { - if (!allowed) { - // The class attribute is allowed without filtering on this tag - return classes; - } - classes = classes.split(/\s+/); - return classes.filter(function(clss) { - return allowed.indexOf(clss) !== -1 || allowedGlobs.some(function(glob) { - return glob.test(clss); - }); - }).join(' '); - } -} + Root.registerLazyResult = dependant => { + LazyResult = dependant; + }; -// Defaults are accessible to you so that you can use them as a starting point -// programmatically if you wish + Root.registerProcessor = dependant => { + Processor = dependant; + }; -const htmlParserDefaults = { - decodeEntities: true -}; -sanitizeHtml.defaults = { - allowedTags: [ - // Sections derived from MDN element categories and limited to the more - // benign categories. - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element - // Content sectioning - 'address', 'article', 'aside', 'footer', 'header', - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hgroup', - 'main', 'nav', 'section', - // Text content - 'blockquote', 'dd', 'div', 'dl', 'dt', 'figcaption', 'figure', - 'hr', 'li', 'main', 'ol', 'p', 'pre', 'ul', - // Inline text semantics - 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', - 'em', 'i', 'kbd', 'mark', 'q', - 'rb', 'rp', 'rt', 'rtc', 'ruby', - 's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr', - // Table content - 'caption', 'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr' - ], - // Tags that cannot be boolean - nonBooleanAttributes: [ - 'abbr', 'accept', 'accept-charset', 'accesskey', 'action', - 'allow', 'alt', 'as', 'autocapitalize', 'autocomplete', - 'blocking', 'charset', 'cite', 'class', 'color', 'cols', - 'colspan', 'content', 'contenteditable', 'coords', 'crossorigin', - 'data', 'datetime', 'decoding', 'dir', 'dirname', 'download', - 'draggable', 'enctype', 'enterkeyhint', 'fetchpriority', 'for', - 'form', 'formaction', 'formenctype', 'formmethod', 'formtarget', - 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', - 'http-equiv', 'id', 'imagesizes', 'imagesrcset', 'inputmode', - 'integrity', 'is', 'itemid', 'itemprop', 'itemref', 'itemtype', - 'kind', 'label', 'lang', 'list', 'loading', 'low', 'max', - 'maxlength', 'media', 'method', 'min', 'minlength', 'name', - 'nonce', 'optimum', 'pattern', 'ping', 'placeholder', 'popover', - 'popovertarget', 'popovertargetaction', 'poster', 'preload', - 'referrerpolicy', 'rel', 'rows', 'rowspan', 'sandbox', 'scope', - 'shape', 'size', 'sizes', 'slot', 'span', 'spellcheck', 'src', - 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'style', - 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', - 'value', 'width', 'wrap', - // Event handlers - 'onauxclick', 'onafterprint', 'onbeforematch', 'onbeforeprint', - 'onbeforeunload', 'onbeforetoggle', 'onblur', 'oncancel', - 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose', - 'oncontextlost', 'oncontextmenu', 'oncontextrestored', 'oncopy', - 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend', - 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', - 'ondrop', 'ondurationchange', 'onemptied', 'onended', - 'onerror', 'onfocus', 'onformdata', 'onhashchange', 'oninput', - 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', - 'onlanguagechange', 'onload', 'onloadeddata', 'onloadedmetadata', - 'onloadstart', 'onmessage', 'onmessageerror', 'onmousedown', - 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', - 'onmouseover', 'onmouseup', 'onoffline', 'ononline', 'onpagehide', - 'onpageshow', 'onpaste', 'onpause', 'onplay', 'onplaying', - 'onpopstate', 'onprogress', 'onratechange', 'onreset', 'onresize', - 'onrejectionhandled', 'onscroll', 'onscrollend', - 'onsecuritypolicyviolation', 'onseeked', 'onseeking', 'onselect', - 'onslotchange', 'onstalled', 'onstorage', 'onsubmit', 'onsuspend', - 'ontimeupdate', 'ontoggle', 'onunhandledrejection', 'onunload', - 'onvolumechange', 'onwaiting', 'onwheel' - ], - disallowedTagsMode: 'discard', - allowedAttributes: { - a: [ 'href', 'name', 'target' ], - // We don't currently allow img itself by default, but - // these attributes would make sense if we did. - img: [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ] + module.exports = Root; + Root.default = Root; + + Container.registerRoot(Root); + }, + { './container': 37 }, + ], + 55: [ + function (require, module, exports) { + 'use strict'; + + let Container = require('./container'); + let list = require('./list'); + + class Rule extends Container { + constructor(defaults) { + super(defaults); + this.type = 'rule'; + if (!this.nodes) this.nodes = []; + } + + get selectors() { + return list.comma(this.selector); + } + + set selectors(values) { + let match = this.selector ? this.selector.match(/,\s*/) : null; + let sep = match + ? match[0] + : ',' + this.raw('between', 'beforeOpen'); + this.selector = values.join(sep); + } + } + + module.exports = Rule; + Rule.default = Rule; + + Container.registerRule(Rule); + }, + { './container': 37, './list': 44 }, + ], + 56: [ + function (require, module, exports) { + 'use strict'; + + const DEFAULT_RAW = { + after: '\n', + beforeClose: '\n', + beforeComment: '\n', + beforeDecl: '\n', + beforeOpen: ' ', + beforeRule: '\n', + colon: ': ', + commentLeft: ' ', + commentRight: ' ', + emptyBody: '', + indent: ' ', + semicolon: false, + }; + + function capitalize(str) { + return str[0].toUpperCase() + str.slice(1); + } + + class Stringifier { + constructor(builder) { + this.builder = builder; + } + + atrule(node, semicolon) { + let name = '@' + node.name; + let params = node.params ? this.rawValue(node, 'params') : ''; + + if (typeof node.raws.afterName !== 'undefined') { + name += node.raws.afterName; + } else if (params) { + name += ' '; + } + + if (node.nodes) { + this.block(node, name + params); + } else { + let end = (node.raws.between || '') + (semicolon ? ';' : ''); + this.builder(name + params + end, node); + } + } + + beforeAfter(node, detect) { + let value; + if (node.type === 'decl') { + value = this.raw(node, null, 'beforeDecl'); + } else if (node.type === 'comment') { + value = this.raw(node, null, 'beforeComment'); + } else if (detect === 'before') { + value = this.raw(node, null, 'beforeRule'); + } else { + value = this.raw(node, null, 'beforeClose'); + } + + let buf = node.parent; + let depth = 0; + while (buf && buf.type !== 'root') { + depth += 1; + buf = buf.parent; + } + + if (value.includes('\n')) { + let indent = this.raw(node, null, 'indent'); + if (indent.length) { + for (let step = 0; step < depth; step++) value += indent; + } + } + + return value; + } + + block(node, start) { + let between = this.raw(node, 'between', 'beforeOpen'); + this.builder(start + between + '{', node, 'start'); + + let after; + if (node.nodes && node.nodes.length) { + this.body(node); + after = this.raw(node, 'after'); + } else { + after = this.raw(node, 'after', 'emptyBody'); + } + + if (after) this.builder(after); + this.builder('}', node, 'end'); + } + + body(node) { + let last = node.nodes.length - 1; + while (last > 0) { + if (node.nodes[last].type !== 'comment') break; + last -= 1; + } + + let semicolon = this.raw(node, 'semicolon'); + for (let i = 0; i < node.nodes.length; i++) { + let child = node.nodes[i]; + let before = this.raw(child, 'before'); + if (before) this.builder(before); + this.stringify(child, last !== i || semicolon); + } + } + + comment(node) { + let left = this.raw(node, 'left', 'commentLeft'); + let right = this.raw(node, 'right', 'commentRight'); + this.builder('/*' + left + node.text + right + '*/', node); + } + + decl(node, semicolon) { + let between = this.raw(node, 'between', 'colon'); + let string = node.prop + between + this.rawValue(node, 'value'); + + if (node.important) { + string += node.raws.important || ' !important'; + } + + if (semicolon) string += ';'; + this.builder(string, node); + } + + document(node) { + this.body(node); + } + + raw(node, own, detect) { + let value; + if (!detect) detect = own; + + // Already had + if (own) { + value = node.raws[own]; + if (typeof value !== 'undefined') return value; + } + + let parent = node.parent; + + if (detect === 'before') { + // Hack for first rule in CSS + if ( + !parent || + (parent.type === 'root' && parent.first === node) + ) { + return ''; + } + + // `root` nodes in `document` should use only their own raws + if (parent && parent.type === 'document') { + return ''; + } + } + + // Floating child without parent + if (!parent) return DEFAULT_RAW[detect]; + + // Detect style by other nodes + let root = node.root(); + if (!root.rawCache) root.rawCache = {}; + if (typeof root.rawCache[detect] !== 'undefined') { + return root.rawCache[detect]; + } + + if (detect === 'before' || detect === 'after') { + return this.beforeAfter(node, detect); + } else { + let method = 'raw' + capitalize(detect); + if (this[method]) { + value = this[method](root, node); + } else { + root.walk(i => { + value = i.raws[own]; + if (typeof value !== 'undefined') return false; + }); + } + } + + if (typeof value === 'undefined') value = DEFAULT_RAW[detect]; + + root.rawCache[detect] = value; + return value; + } + + rawBeforeClose(root) { + let value; + root.walk(i => { + if (i.nodes && i.nodes.length > 0) { + if (typeof i.raws.after !== 'undefined') { + value = i.raws.after; + if (value.includes('\n')) { + value = value.replace(/[^\n]+$/, ''); + } + return false; + } + } + }); + if (value) value = value.replace(/\S/g, ''); + return value; + } + + rawBeforeComment(root, node) { + let value; + root.walkComments(i => { + if (typeof i.raws.before !== 'undefined') { + value = i.raws.before; + if (value.includes('\n')) { + value = value.replace(/[^\n]+$/, ''); + } + return false; + } + }); + if (typeof value === 'undefined') { + value = this.raw(node, null, 'beforeDecl'); + } else if (value) { + value = value.replace(/\S/g, ''); + } + return value; + } + + rawBeforeDecl(root, node) { + let value; + root.walkDecls(i => { + if (typeof i.raws.before !== 'undefined') { + value = i.raws.before; + if (value.includes('\n')) { + value = value.replace(/[^\n]+$/, ''); + } + return false; + } + }); + if (typeof value === 'undefined') { + value = this.raw(node, null, 'beforeRule'); + } else if (value) { + value = value.replace(/\S/g, ''); + } + return value; + } + + rawBeforeOpen(root) { + let value; + root.walk(i => { + if (i.type !== 'decl') { + value = i.raws.between; + if (typeof value !== 'undefined') return false; + } + }); + return value; + } + + rawBeforeRule(root) { + let value; + root.walk(i => { + if (i.nodes && (i.parent !== root || root.first !== i)) { + if (typeof i.raws.before !== 'undefined') { + value = i.raws.before; + if (value.includes('\n')) { + value = value.replace(/[^\n]+$/, ''); + } + return false; + } + } + }); + if (value) value = value.replace(/\S/g, ''); + return value; + } + + rawColon(root) { + let value; + root.walkDecls(i => { + if (typeof i.raws.between !== 'undefined') { + value = i.raws.between.replace(/[^\s:]/g, ''); + return false; + } + }); + return value; + } + + rawEmptyBody(root) { + let value; + root.walk(i => { + if (i.nodes && i.nodes.length === 0) { + value = i.raws.after; + if (typeof value !== 'undefined') return false; + } + }); + return value; + } + + rawIndent(root) { + if (root.raws.indent) return root.raws.indent; + let value; + root.walk(i => { + let p = i.parent; + if (p && p !== root && p.parent && p.parent === root) { + if (typeof i.raws.before !== 'undefined') { + let parts = i.raws.before.split('\n'); + value = parts[parts.length - 1]; + value = value.replace(/\S/g, ''); + return false; + } + } + }); + return value; + } + + rawSemicolon(root) { + let value; + root.walk(i => { + if (i.nodes && i.nodes.length && i.last.type === 'decl') { + value = i.raws.semicolon; + if (typeof value !== 'undefined') return false; + } + }); + return value; + } + + rawValue(node, prop) { + let value = node[prop]; + let raw = node.raws[prop]; + if (raw && raw.value === value) { + return raw.raw; + } + + return value; + } + + root(node) { + this.body(node); + if (node.raws.after) this.builder(node.raws.after); + } + + rule(node) { + this.block(node, this.rawValue(node, 'selector')); + if (node.raws.ownSemicolon) { + this.builder(node.raws.ownSemicolon, node, 'end'); + } + } + + stringify(node, semicolon) { + /* c8 ignore start */ + if (!this[node.type]) { + throw new Error( + 'Unknown AST node type ' + + node.type + + '. ' + + 'Maybe you need to change PostCSS stringifier.', + ); + } + /* c8 ignore stop */ + this[node.type](node, semicolon); + } + } + + module.exports = Stringifier; + Stringifier.default = Stringifier; + }, + {}, + ], + 57: [ + function (require, module, exports) { + 'use strict'; + + let Stringifier = require('./stringifier'); + + function stringify(node, builder) { + let str = new Stringifier(builder); + str.stringify(node); + } + + module.exports = stringify; + stringify.default = stringify; + }, + { './stringifier': 56 }, + ], + 58: [ + function (require, module, exports) { + 'use strict'; + + module.exports.isClean = Symbol('isClean'); + + module.exports.my = Symbol('my'); + }, + {}, + ], + 59: [ + function (require, module, exports) { + 'use strict'; + + const SINGLE_QUOTE = "'".charCodeAt(0); + const DOUBLE_QUOTE = '"'.charCodeAt(0); + const BACKSLASH = '\\'.charCodeAt(0); + const SLASH = '/'.charCodeAt(0); + const NEWLINE = '\n'.charCodeAt(0); + const SPACE = ' '.charCodeAt(0); + const FEED = '\f'.charCodeAt(0); + const TAB = '\t'.charCodeAt(0); + const CR = '\r'.charCodeAt(0); + const OPEN_SQUARE = '['.charCodeAt(0); + const CLOSE_SQUARE = ']'.charCodeAt(0); + const OPEN_PARENTHESES = '('.charCodeAt(0); + const CLOSE_PARENTHESES = ')'.charCodeAt(0); + const OPEN_CURLY = '{'.charCodeAt(0); + const CLOSE_CURLY = '}'.charCodeAt(0); + const SEMICOLON = ';'.charCodeAt(0); + const ASTERISK = '*'.charCodeAt(0); + const COLON = ':'.charCodeAt(0); + const AT = '@'.charCodeAt(0); + + const RE_AT_END = /[\t\n\f\r "#'()/;[\\\]{}]/g; + const RE_WORD_END = /[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g; + const RE_BAD_BRACKET = /.[\r\n"'(/\\]/; + const RE_HEX_ESCAPE = /[\da-f]/i; + + module.exports = function tokenizer(input, options = {}) { + let css = input.css.valueOf(); + let ignore = options.ignoreErrors; + + let code, next, quote, content, escape; + let escaped, escapePos, prev, n, currentToken; + + let length = css.length; + let pos = 0; + let buffer = []; + let returned = []; + + function position() { + return pos; + } + + function unclosed(what) { + throw input.error('Unclosed ' + what, pos); + } + + function endOfFile() { + return returned.length === 0 && pos >= length; + } + + function nextToken(opts) { + if (returned.length) return returned.pop(); + if (pos >= length) return; + + let ignoreUnclosed = opts ? opts.ignoreUnclosed : false; + + code = css.charCodeAt(pos); + + switch (code) { + case NEWLINE: + case SPACE: + case TAB: + case CR: + case FEED: { + next = pos; + do { + next += 1; + code = css.charCodeAt(next); + } while ( + code === SPACE || + code === NEWLINE || + code === TAB || + code === CR || + code === FEED + ); + + currentToken = ['space', css.slice(pos, next)]; + pos = next - 1; + break; + } + + case OPEN_SQUARE: + case CLOSE_SQUARE: + case OPEN_CURLY: + case CLOSE_CURLY: + case COLON: + case SEMICOLON: + case CLOSE_PARENTHESES: { + let controlChar = String.fromCharCode(code); + currentToken = [controlChar, controlChar, pos]; + break; + } + + case OPEN_PARENTHESES: { + prev = buffer.length ? buffer.pop()[1] : ''; + n = css.charCodeAt(pos + 1); + if ( + prev === 'url' && + n !== SINGLE_QUOTE && + n !== DOUBLE_QUOTE && + n !== SPACE && + n !== NEWLINE && + n !== TAB && + n !== FEED && + n !== CR + ) { + next = pos; + do { + escaped = false; + next = css.indexOf(')', next + 1); + if (next === -1) { + if (ignore || ignoreUnclosed) { + next = pos; + break; + } else { + unclosed('bracket'); + } + } + escapePos = next; + while (css.charCodeAt(escapePos - 1) === BACKSLASH) { + escapePos -= 1; + escaped = !escaped; + } + } while (escaped); + + currentToken = [ + 'brackets', + css.slice(pos, next + 1), + pos, + next, + ]; + + pos = next; + } else { + next = css.indexOf(')', pos + 1); + content = css.slice(pos, next + 1); + + if (next === -1 || RE_BAD_BRACKET.test(content)) { + currentToken = ['(', '(', pos]; + } else { + currentToken = ['brackets', content, pos, next]; + pos = next; + } + } + + break; + } + + case SINGLE_QUOTE: + case DOUBLE_QUOTE: { + quote = code === SINGLE_QUOTE ? "'" : '"'; + next = pos; + do { + escaped = false; + next = css.indexOf(quote, next + 1); + if (next === -1) { + if (ignore || ignoreUnclosed) { + next = pos + 1; + break; + } else { + unclosed('string'); + } + } + escapePos = next; + while (css.charCodeAt(escapePos - 1) === BACKSLASH) { + escapePos -= 1; + escaped = !escaped; + } + } while (escaped); + + currentToken = ['string', css.slice(pos, next + 1), pos, next]; + pos = next; + break; + } + + case AT: { + RE_AT_END.lastIndex = pos + 1; + RE_AT_END.test(css); + if (RE_AT_END.lastIndex === 0) { + next = css.length - 1; + } else { + next = RE_AT_END.lastIndex - 2; + } + + currentToken = ['at-word', css.slice(pos, next + 1), pos, next]; + + pos = next; + break; + } + + case BACKSLASH: { + next = pos; + escape = true; + while (css.charCodeAt(next + 1) === BACKSLASH) { + next += 1; + escape = !escape; + } + code = css.charCodeAt(next + 1); + if ( + escape && + code !== SLASH && + code !== SPACE && + code !== NEWLINE && + code !== TAB && + code !== CR && + code !== FEED + ) { + next += 1; + if (RE_HEX_ESCAPE.test(css.charAt(next))) { + while (RE_HEX_ESCAPE.test(css.charAt(next + 1))) { + next += 1; + } + if (css.charCodeAt(next + 1) === SPACE) { + next += 1; + } + } + } + + currentToken = ['word', css.slice(pos, next + 1), pos, next]; + + pos = next; + break; + } + + default: { + if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) { + next = css.indexOf('*/', pos + 2) + 1; + if (next === 0) { + if (ignore || ignoreUnclosed) { + next = css.length; + } else { + unclosed('comment'); + } + } + + currentToken = [ + 'comment', + css.slice(pos, next + 1), + pos, + next, + ]; + pos = next; + } else { + RE_WORD_END.lastIndex = pos + 1; + RE_WORD_END.test(css); + if (RE_WORD_END.lastIndex === 0) { + next = css.length - 1; + } else { + next = RE_WORD_END.lastIndex - 2; + } + + currentToken = ['word', css.slice(pos, next + 1), pos, next]; + buffer.push(currentToken); + pos = next; + } + + break; + } + } + + pos++; + return currentToken; + } + + function back(token) { + returned.push(token); + } + + return { + back, + endOfFile, + nextToken, + position, + }; + }; + }, + {}, + ], + 60: [ + function (require, module, exports) { + /* eslint-disable no-console */ + 'use strict'; + + let printed = {}; + + module.exports = function warnOnce(message) { + if (printed[message]) return; + printed[message] = true; + + if (typeof console !== 'undefined' && console.warn) { + console.warn(message); + } + }; + }, + {}, + ], + 61: [ + function (require, module, exports) { + 'use strict'; + + class Warning { + constructor(text, opts = {}) { + this.type = 'warning'; + this.text = text; + + if (opts.node && opts.node.source) { + let range = opts.node.rangeBy(opts); + this.line = range.start.line; + this.column = range.start.column; + this.endLine = range.end.line; + this.endColumn = range.end.column; + } + + for (let opt in opts) this[opt] = opts[opt]; + } + + toString() { + if (this.node) { + return this.node.error(this.text, { + index: this.index, + plugin: this.plugin, + word: this.word, + }).message; + } + + if (this.plugin) { + return this.plugin + ': ' + this.text; + } + + return this.text; + } + } + + module.exports = Warning; + Warning.default = Warning; + }, + {}, + ], + 62: [ + function (require, module, exports) { + // shim for using process in browser + var process = (module.exports = {}); + + // cached from whatever global is present so that test runners that stub it + // don't break things. But we need to wrap it in a try catch in case it is + // wrapped in strict mode code which doesn't define any globals. It's inside a + // function because try/catches deoptimize in certain engines. + + var cachedSetTimeout; + var cachedClearTimeout; + + function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); + } + function defaultClearTimeout() { + throw new Error('clearTimeout has not been defined'); + } + (function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } + })(); + function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ( + (cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && + setTimeout + ) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch (e) { + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch (e) { + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + } + function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ( + (cachedClearTimeout === defaultClearTimeout || + !cachedClearTimeout) && + clearTimeout + ) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e) { + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e) { + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + } + var queue = []; + var draining = false; + var currentQueue; + var queueIndex = -1; + + function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } + } + + function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while (len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); + } + + process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } + }; + + // v8 likes predictible objects + function Item(fun, array) { + this.fun = fun; + this.array = array; + } + Item.prototype.run = function () { + this.fun.apply(null, this.array); + }; + process.title = 'browser'; + process.browser = true; + process.env = {}; + process.argv = []; + process.version = ''; // empty string to avoid regexp issues + process.versions = {}; + + function noop() {} + + process.on = noop; + process.addListener = noop; + process.once = noop; + process.off = noop; + process.removeListener = noop; + process.removeAllListeners = noop; + process.emit = noop; + process.prependListener = noop; + process.prependOnceListener = noop; + + process.listeners = function (name) { + return []; + }; + + process.binding = function (name) { + throw new Error('process.binding is not supported'); + }; + + process.cwd = function () { + return '/'; + }; + process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); + }; + process.umask = function () { + return 0; + }; + }, + {}, + ], + 63: [ + function (require, module, exports) { + const htmlparser = require('htmlparser2'); + const escapeStringRegexp = require('escape-string-regexp'); + const { isPlainObject } = require('is-plain-object'); + const deepmerge = require('deepmerge'); + const parseSrcset = require('parse-srcset'); + const { parse: postcssParse } = require('postcss'); + // Tags that can conceivably represent stand-alone media. + const mediaTags = [ + 'img', + 'audio', + 'video', + 'picture', + 'svg', + 'object', + 'map', + 'iframe', + 'embed', + ]; + // Tags that are inherently vulnerable to being used in XSS attacks. + const vulnerableTags = ['script', 'style']; + + function each(obj, cb) { + if (obj) { + Object.keys(obj).forEach(function (key) { + cb(obj[key], key); + }); + } + } + + // Avoid false positives with .__proto__, .hasOwnProperty, etc. + function has(obj, key) { + return {}.hasOwnProperty.call(obj, key); + } + + // Returns those elements of `a` for which `cb(a)` returns truthy + function filter(a, cb) { + const n = []; + each(a, function (v) { + if (cb(v)) { + n.push(v); + } + }); + return n; + } + + function isEmptyObject(obj) { + for (const key in obj) { + if (has(obj, key)) { + return false; + } + } + return true; + } + + function stringifySrcset(parsedSrcset) { + return parsedSrcset + .map(function (part) { + if (!part.url) { + throw new Error('URL missing'); + } + + return ( + part.url + + (part.w ? ` ${part.w}w` : '') + + (part.h ? ` ${part.h}h` : '') + + (part.d ? ` ${part.d}x` : '') + ); + }) + .join(', '); + } + + module.exports = sanitizeHtml; + + // A valid attribute name. + // We use a tolerant definition based on the set of strings defined by + // html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state + // and html.spec.whatwg.org/multipage/parsing.html#attribute-name-state . + // The characters accepted are ones which can be appended to the attribute + // name buffer without triggering a parse error: + // * unexpected-equals-sign-before-attribute-name + // * unexpected-null-character + // * unexpected-character-in-attribute-name + // We exclude the empty string because it's impossible to get to the after + // attribute name state with an empty attribute name buffer. + const VALID_HTML_ATTRIBUTE_NAME = /^[^\0\t\n\f\r /<=>]+$/; + + // Ignore the _recursing flag; it's there for recursive + // invocation as a guard against this exploit: + // https://github.com/fb55/htmlparser2/issues/105 + + function sanitizeHtml(html, options, _recursing) { + if (html == null) { + return ''; + } + if (typeof html === 'number') { + html = html.toString(); + } + + let result = ''; + // Used for hot swapping the result variable with an empty string in order to "capture" the text written to it. + let tempResult = ''; + + function Frame(tag, attribs) { + const that = this; + this.tag = tag; + this.attribs = attribs || {}; + this.tagPosition = result.length; + this.text = ''; // Node inner text + this.mediaChildren = []; + + this.updateParentNodeText = function () { + if (stack.length) { + const parentFrame = stack[stack.length - 1]; + parentFrame.text += that.text; + } + }; + + this.updateParentNodeMediaChildren = function () { + if (stack.length && mediaTags.includes(this.tag)) { + const parentFrame = stack[stack.length - 1]; + parentFrame.mediaChildren.push(this.tag); + } + }; + } + + options = Object.assign({}, sanitizeHtml.defaults, options); + options.parser = Object.assign( + {}, + htmlParserDefaults, + options.parser, + ); + + const tagAllowed = function (name) { + return ( + options.allowedTags === false || + (options.allowedTags || []).indexOf(name) > -1 + ); + }; + + // vulnerableTags + vulnerableTags.forEach(function (tag) { + if (tagAllowed(tag) && !options.allowVulnerableTags) { + console.warn( + `\n\n⚠️ Your \`allowedTags\` option includes, \`${tag}\`, which is inherently\nvulnerable to XSS attacks. Please remove it from \`allowedTags\`.\nOr, to disable this warning, add the \`allowVulnerableTags\` option\nand ensure you are accounting for this risk.\n\n`, + ); + } + }); + + // Tags that contain something other than HTML, or where discarding + // the text when the tag is disallowed makes sense for other reasons. + // If we are not allowing these tags, we should drop their content too. + // For other tags you would drop the tag but keep its content. + const nonTextTagsArray = options.nonTextTags || [ + 'script', + 'style', + 'textarea', + 'option', + ]; + let allowedAttributesMap; + let allowedAttributesGlobMap; + if (options.allowedAttributes) { + allowedAttributesMap = {}; + allowedAttributesGlobMap = {}; + each(options.allowedAttributes, function (attributes, tag) { + allowedAttributesMap[tag] = []; + const globRegex = []; + attributes.forEach(function (obj) { + if (typeof obj === 'string' && obj.indexOf('*') >= 0) { + globRegex.push( + escapeStringRegexp(obj).replace(/\\\*/g, '.*'), + ); + } else { + allowedAttributesMap[tag].push(obj); + } + }); + if (globRegex.length) { + allowedAttributesGlobMap[tag] = new RegExp( + '^(' + globRegex.join('|') + ')$', + ); + } + }); + } + const allowedClassesMap = {}; + const allowedClassesGlobMap = {}; + const allowedClassesRegexMap = {}; + each(options.allowedClasses, function (classes, tag) { + // Implicitly allows the class attribute + if (allowedAttributesMap) { + if (!has(allowedAttributesMap, tag)) { + allowedAttributesMap[tag] = []; + } + allowedAttributesMap[tag].push('class'); + } + + allowedClassesMap[tag] = classes; + + if (Array.isArray(classes)) { + const globRegex = []; + allowedClassesMap[tag] = []; + allowedClassesRegexMap[tag] = []; + classes.forEach(function (obj) { + if (typeof obj === 'string' && obj.indexOf('*') >= 0) { + globRegex.push( + escapeStringRegexp(obj).replace(/\\\*/g, '.*'), + ); + } else if (obj instanceof RegExp) { + allowedClassesRegexMap[tag].push(obj); + } else { + allowedClassesMap[tag].push(obj); + } + }); + if (globRegex.length) { + allowedClassesGlobMap[tag] = new RegExp( + '^(' + globRegex.join('|') + ')$', + ); + } + } + }); + + const transformTagsMap = {}; + let transformTagsAll; + each(options.transformTags, function (transform, tag) { + let transFun; + if (typeof transform === 'function') { + transFun = transform; + } else if (typeof transform === 'string') { + transFun = sanitizeHtml.simpleTransform(transform); + } + if (tag === '*') { + transformTagsAll = transFun; + } else { + transformTagsMap[tag] = transFun; + } + }); + + let depth; + let stack; + let skipMap; + let transformMap; + let skipText; + let skipTextDepth; + let addedText = false; + + initializeState(); + + const parser = new htmlparser.Parser( + { + onopentag: function (name, attribs) { + // If `enforceHtmlBoundary` is `true` and this has found the opening + // `html` tag, reset the state. + if (options.enforceHtmlBoundary && name === 'html') { + initializeState(); + } + + if (skipText) { + skipTextDepth++; + return; + } + const frame = new Frame(name, attribs); + stack.push(frame); + + let skip = false; + const hasText = !!frame.text; + let transformedTag; + if (has(transformTagsMap, name)) { + transformedTag = transformTagsMap[name](name, attribs); + + frame.attribs = attribs = transformedTag.attribs; + + if (transformedTag.text !== undefined) { + frame.innerText = transformedTag.text; + } + + if (name !== transformedTag.tagName) { + frame.name = name = transformedTag.tagName; + transformMap[depth] = transformedTag.tagName; + } + } + if (transformTagsAll) { + transformedTag = transformTagsAll(name, attribs); + + frame.attribs = attribs = transformedTag.attribs; + if (name !== transformedTag.tagName) { + frame.name = name = transformedTag.tagName; + transformMap[depth] = transformedTag.tagName; + } + } + + if ( + !tagAllowed(name) || + (options.disallowedTagsMode === 'recursiveEscape' && + !isEmptyObject(skipMap)) || + (options.nestingLimit != null && + depth >= options.nestingLimit) + ) { + skip = true; + skipMap[depth] = true; + if (options.disallowedTagsMode === 'discard') { + if (nonTextTagsArray.indexOf(name) !== -1) { + skipText = true; + skipTextDepth = 1; + } + } + skipMap[depth] = true; + } + depth++; + if (skip) { + if (options.disallowedTagsMode === 'discard') { + // We want the contents but not this tag + return; + } + tempResult = result; + result = ''; + } + result += '<' + name; + + if (name === 'script') { + if ( + options.allowedScriptHostnames || + options.allowedScriptDomains + ) { + frame.innerText = ''; + } + } + + if ( + !allowedAttributesMap || + has(allowedAttributesMap, name) || + allowedAttributesMap['*'] + ) { + each(attribs, function (value, a) { + if (!VALID_HTML_ATTRIBUTE_NAME.test(a)) { + // This prevents part of an attribute name in the output from being + // interpreted as the end of an attribute, or end of a tag. + delete frame.attribs[a]; + return; + } + // If the value is empty, and this is a known non-boolean attribute, delete it + // List taken from https://html.spec.whatwg.org/multipage/indices.html#attributes-3 + if ( + value === '' && + (options.nonBooleanAttributes.includes(a) || + options.nonBooleanAttributes.includes('*')) + ) { + delete frame.attribs[a]; + return; + } + // check allowedAttributesMap for the element and attribute and modify the value + // as necessary if there are specific values defined. + let passedAllowedAttributesMapCheck = false; + if ( + !allowedAttributesMap || + (has(allowedAttributesMap, name) && + allowedAttributesMap[name].indexOf(a) !== -1) || + (allowedAttributesMap['*'] && + allowedAttributesMap['*'].indexOf(a) !== -1) || + (has(allowedAttributesGlobMap, name) && + allowedAttributesGlobMap[name].test(a)) || + (allowedAttributesGlobMap['*'] && + allowedAttributesGlobMap['*'].test(a)) + ) { + passedAllowedAttributesMapCheck = true; + } else if ( + allowedAttributesMap && + allowedAttributesMap[name] + ) { + for (const o of allowedAttributesMap[name]) { + if (isPlainObject(o) && o.name && o.name === a) { + passedAllowedAttributesMapCheck = true; + let newValue = ''; + if (o.multiple === true) { + // verify the values that are allowed + const splitStrArray = value.split(' '); + for (const s of splitStrArray) { + if (o.values.indexOf(s) !== -1) { + if (newValue === '') { + newValue = s; + } else { + newValue += ' ' + s; + } + } + } + } else if (o.values.indexOf(value) >= 0) { + // verified an allowed value matches the entire attribute value + newValue = value; + } + value = newValue; + } + } + } + if (passedAllowedAttributesMapCheck) { + if ( + options.allowedSchemesAppliedToAttributes.indexOf(a) !== + -1 + ) { + if (naughtyHref(name, value)) { + delete frame.attribs[a]; + return; + } + } + + if (name === 'script' && a === 'src') { + let allowed = true; + + try { + const parsed = parseUrl(value); + + if ( + options.allowedScriptHostnames || + options.allowedScriptDomains + ) { + const allowedHostname = ( + options.allowedScriptHostnames || [] + ).find(function (hostname) { + return hostname === parsed.url.hostname; + }); + const allowedDomain = ( + options.allowedScriptDomains || [] + ).find(function (domain) { + return ( + parsed.url.hostname === domain || + parsed.url.hostname.endsWith(`.${domain}`) + ); + }); + allowed = allowedHostname || allowedDomain; + } + } catch (e) { + allowed = false; + } + + if (!allowed) { + delete frame.attribs[a]; + return; + } + } + + if (name === 'iframe' && a === 'src') { + let allowed = true; + try { + const parsed = parseUrl(value); + + if (parsed.isRelativeUrl) { + // default value of allowIframeRelativeUrls is true + // unless allowedIframeHostnames or allowedIframeDomains specified + allowed = has(options, 'allowIframeRelativeUrls') + ? options.allowIframeRelativeUrls + : !options.allowedIframeHostnames && + !options.allowedIframeDomains; + } else if ( + options.allowedIframeHostnames || + options.allowedIframeDomains + ) { + const allowedHostname = ( + options.allowedIframeHostnames || [] + ).find(function (hostname) { + return hostname === parsed.url.hostname; + }); + const allowedDomain = ( + options.allowedIframeDomains || [] + ).find(function (domain) { + return ( + parsed.url.hostname === domain || + parsed.url.hostname.endsWith(`.${domain}`) + ); + }); + allowed = allowedHostname || allowedDomain; + } + } catch (e) { + // Unparseable iframe src + allowed = false; + } + if (!allowed) { + delete frame.attribs[a]; + return; + } + } + if (a === 'srcset') { + try { + let parsed = parseSrcset(value); + parsed.forEach(function (value) { + if (naughtyHref('srcset', value.url)) { + value.evil = true; + } + }); + parsed = filter(parsed, function (v) { + return !v.evil; + }); + if (!parsed.length) { + delete frame.attribs[a]; + return; + } else { + value = stringifySrcset( + filter(parsed, function (v) { + return !v.evil; + }), + ); + frame.attribs[a] = value; + } + } catch (e) { + // Unparseable srcset + delete frame.attribs[a]; + return; + } + } + if (a === 'class') { + const allowedSpecificClasses = allowedClassesMap[name]; + const allowedWildcardClasses = allowedClassesMap['*']; + const allowedSpecificClassesGlob = + allowedClassesGlobMap[name]; + const allowedSpecificClassesRegex = + allowedClassesRegexMap[name]; + const allowedWildcardClassesGlob = + allowedClassesGlobMap['*']; + const allowedClassesGlobs = [ + allowedSpecificClassesGlob, + allowedWildcardClassesGlob, + ] + .concat(allowedSpecificClassesRegex) + .filter(function (t) { + return t; + }); + if (allowedSpecificClasses && allowedWildcardClasses) { + value = filterClasses( + value, + deepmerge( + allowedSpecificClasses, + allowedWildcardClasses, + ), + allowedClassesGlobs, + ); + } else { + value = filterClasses( + value, + allowedSpecificClasses || allowedWildcardClasses, + allowedClassesGlobs, + ); + } + if (!value.length) { + delete frame.attribs[a]; + return; + } + } + if (a === 'style') { + if (options.parseStyleAttributes) { + try { + const abstractSyntaxTree = postcssParse( + name + ' {' + value + '}', + ); + const filteredAST = filterCss( + abstractSyntaxTree, + options.allowedStyles, + ); + + value = stringifyStyleAttributes(filteredAST); + + if (value.length === 0) { + delete frame.attribs[a]; + return; + } + } catch (e) { + if (typeof window !== 'undefined') { + console.warn( + 'Failed to parse "' + + name + + ' {' + + value + + '}' + + '", If you\'re running this in a browser, we recommend to disable style parsing: options.parseStyleAttributes: false, since this only works in a node environment due to a postcss dependency, More info: https://github.com/apostrophecms/sanitize-html/issues/547', + ); + } + delete frame.attribs[a]; + return; + } + } else if (options.allowedStyles) { + throw new Error( + 'allowedStyles option cannot be used together with parseStyleAttributes: false.', + ); + } + } + result += ' ' + a; + if (value && value.length) { + result += '="' + escapeHtml(value, true) + '"'; + } + } else { + delete frame.attribs[a]; + } + }); + } + if (options.selfClosing.indexOf(name) !== -1) { + result += ' />'; + } else { + result += '>'; + if (frame.innerText && !hasText && !options.textFilter) { + result += escapeHtml(frame.innerText); + addedText = true; + } + } + if (skip) { + result = tempResult + escapeHtml(result); + tempResult = ''; + } + }, + ontext: function (text) { + if (skipText) { + return; + } + const lastFrame = stack[stack.length - 1]; + let tag; + + if (lastFrame) { + tag = lastFrame.tag; + // If inner text was set by transform function then let's use it + text = + lastFrame.innerText !== undefined + ? lastFrame.innerText + : text; + } + + if ( + options.disallowedTagsMode === 'discard' && + (tag === 'script' || tag === 'style') + ) { + // htmlparser2 gives us these as-is. Escaping them ruins the content. Allowing + // script tags is, by definition, game over for XSS protection, so if that's + // your concern, don't allow them. The same is essentially true for style tags + // which have their own collection of XSS vectors. + result += text; + } else { + const escaped = escapeHtml(text, false); + if (options.textFilter && !addedText) { + result += options.textFilter(escaped, tag); + } else if (!addedText) { + result += escaped; + } + } + if (stack.length) { + const frame = stack[stack.length - 1]; + frame.text += text; + } + }, + onclosetag: function (name, isImplied) { + if (skipText) { + skipTextDepth--; + if (!skipTextDepth) { + skipText = false; + } else { + return; + } + } + + const frame = stack.pop(); + if (!frame) { + // Do not crash on bad markup + return; + } + + if (frame.tag !== name) { + // Another case of bad markup. + // Push to stack, so that it will be used in future closing tags. + stack.push(frame); + return; + } + + skipText = options.enforceHtmlBoundary + ? name === 'html' + : false; + depth--; + const skip = skipMap[depth]; + if (skip) { + delete skipMap[depth]; + if (options.disallowedTagsMode === 'discard') { + frame.updateParentNodeText(); + return; + } + tempResult = result; + result = ''; + } + + if (transformMap[depth]) { + name = transformMap[depth]; + delete transformMap[depth]; + } + + if (options.exclusiveFilter && options.exclusiveFilter(frame)) { + result = result.substr(0, frame.tagPosition); + return; + } + + frame.updateParentNodeMediaChildren(); + frame.updateParentNodeText(); + + if ( + // Already output /> + options.selfClosing.indexOf(name) !== -1 || + // Escaped tag, closing tag is implied + (isImplied && + !tagAllowed(name) && + ['escape', 'recursiveEscape'].indexOf( + options.disallowedTagsMode, + ) >= 0) + ) { + if (skip) { + result = tempResult; + tempResult = ''; + } + return; + } + + result += ''; + if (skip) { + result = tempResult + escapeHtml(result); + tempResult = ''; + } + addedText = false; + }, + }, + options.parser, + ); + parser.write(html); + parser.end(); + + return result; + + function initializeState() { + result = ''; + depth = 0; + stack = []; + skipMap = {}; + transformMap = {}; + skipText = false; + skipTextDepth = 0; + } + + function escapeHtml(s, quote) { + if (typeof s !== 'string') { + s = s + ''; + } + if (options.parser.decodeEntities) { + s = s + .replace(/&/g, '&') + .replace(//g, '>'); + if (quote) { + s = s.replace(/"/g, '"'); + } + } + // TODO: this is inadequate because it will pass `&0;`. This approach + // will not work, each & must be considered with regard to whether it + // is followed by a 100% syntactically valid entity or not, and escaped + // if it is not. If this bothers you, don't set parser.decodeEntities + // to false. (The default is true.) + s = s + .replace(/&(?![a-zA-Z0-9#]{1,20};)/g, '&') // Match ampersands not part of existing HTML entity + .replace(//g, '>'); + if (quote) { + s = s.replace(/"/g, '"'); + } + return s; + } + + function naughtyHref(name, href) { + // Browsers ignore character codes of 32 (space) and below in a surprising + // number of situations. Start reading here: + // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_tab + // eslint-disable-next-line no-control-regex + href = href.replace(/[\x00-\x20]+/g, ''); + // Clobber any comments in URLs, which the browser might + // interpret inside an XML data island, allowing + // a javascript: URL to be snuck through + while (true) { + const firstIndex = href.indexOf('', firstIndex + 4); + if (lastIndex === -1) { + break; + } + href = + href.substring(0, firstIndex) + href.substring(lastIndex + 3); + } + // Case insensitive so we don't get faked out by JAVASCRIPT #1 + // Allow more characters after the first so we don't get faked + // out by certain schemes browsers accept + const matches = href.match(/^([a-zA-Z][a-zA-Z0-9.\-+]*):/); + if (!matches) { + // Protocol-relative URL starting with any combination of '/' and '\' + if (href.match(/^[/\\]{2}/)) { + return !options.allowProtocolRelative; + } + + // No scheme + return false; + } + const scheme = matches[1].toLowerCase(); + + if (has(options.allowedSchemesByTag, name)) { + return options.allowedSchemesByTag[name].indexOf(scheme) === -1; + } + + return ( + !options.allowedSchemes || + options.allowedSchemes.indexOf(scheme) === -1 + ); + } + + function parseUrl(value) { + value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, '$1//'); + if (value.startsWith('relative:')) { + // An attempt to exploit our workaround for base URLs being + // mandatory for relative URL validation in the WHATWG + // URL parser, reject it + throw new Error('relative: exploit attempt'); + } + // naughtyHref is in charge of whether protocol relative URLs + // are cool. Here we are concerned just with allowed hostnames and + // whether to allow relative URLs. + // + // Build a placeholder "base URL" against which any reasonable + // relative URL may be parsed successfully + let base = 'relative://relative-site'; + for (let i = 0; i < 100; i++) { + base += `/${i}`; + } + + const parsed = new URL(value, base); + + const isRelativeUrl = + parsed && + parsed.hostname === 'relative-site' && + parsed.protocol === 'relative:'; + return { + isRelativeUrl, + url: parsed, + }; + } + /** + * Filters user input css properties by allowlisted regex attributes. + * Modifies the abstractSyntaxTree object. + * + * @param {object} abstractSyntaxTree - Object representation of CSS attributes. + * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }. + * @param {object} allowedStyles - Keys are properties (i.e color), value is list of permitted regex rules (i.e /green/i). + * @return {object} - The modified tree. + */ + function filterCss(abstractSyntaxTree, allowedStyles) { + if (!allowedStyles) { + return abstractSyntaxTree; + } + + const astRules = abstractSyntaxTree.nodes[0]; + let selectedRule; + + // Merge global and tag-specific styles into new AST. + if (allowedStyles[astRules.selector] && allowedStyles['*']) { + selectedRule = deepmerge( + allowedStyles[astRules.selector], + allowedStyles['*'], + ); + } else { + selectedRule = + allowedStyles[astRules.selector] || allowedStyles['*']; + } + + if (selectedRule) { + abstractSyntaxTree.nodes[0].nodes = astRules.nodes.reduce( + filterDeclarations(selectedRule), + [], + ); + } + + return abstractSyntaxTree; + } + + /** + * Extracts the style attributes from an AbstractSyntaxTree and formats those + * values in the inline style attribute format. + * + * @param {AbstractSyntaxTree} filteredAST + * @return {string} - Example: "color:yellow;text-align:center !important;font-family:helvetica;" + */ + function stringifyStyleAttributes(filteredAST) { + return filteredAST.nodes[0].nodes + .reduce(function (extractedAttributes, attrObject) { + extractedAttributes.push( + `${attrObject.prop}:${attrObject.value}${attrObject.important ? ' !important' : ''}`, + ); + return extractedAttributes; + }, []) + .join(';'); + } + + /** + * Filters the existing attributes for the given property. Discards any attributes + * which don't match the allowlist. + * + * @param {object} selectedRule - Example: { color: red, font-family: helvetica } + * @param {array} allowedDeclarationsList - List of declarations which pass the allowlist. + * @param {object} attributeObject - Object representing the current css property. + * @property {string} attributeObject.type - Typically 'declaration'. + * @property {string} attributeObject.prop - The CSS property, i.e 'color'. + * @property {string} attributeObject.value - The corresponding value to the css property, i.e 'red'. + * @return {function} - When used in Array.reduce, will return an array of Declaration objects + */ + function filterDeclarations(selectedRule) { + return function (allowedDeclarationsList, attributeObject) { + // If this property is allowlisted... + if (has(selectedRule, attributeObject.prop)) { + const matchesRegex = selectedRule[attributeObject.prop].some( + function (regularExpression) { + return regularExpression.test(attributeObject.value); + }, + ); + + if (matchesRegex) { + allowedDeclarationsList.push(attributeObject); + } + } + return allowedDeclarationsList; + }; + } + + function filterClasses(classes, allowed, allowedGlobs) { + if (!allowed) { + // The class attribute is allowed without filtering on this tag + return classes; + } + classes = classes.split(/\s+/); + return classes + .filter(function (clss) { + return ( + allowed.indexOf(clss) !== -1 || + allowedGlobs.some(function (glob) { + return glob.test(clss); + }) + ); + }) + .join(' '); + } + } + + // Defaults are accessible to you so that you can use them as a starting point + // programmatically if you wish + + const htmlParserDefaults = { + decodeEntities: true, + }; + sanitizeHtml.defaults = { + allowedTags: [ + // Sections derived from MDN element categories and limited to the more + // benign categories. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element + // Content sectioning + 'address', + 'article', + 'aside', + 'footer', + 'header', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hgroup', + 'main', + 'nav', + 'section', + // Text content + 'blockquote', + 'dd', + 'div', + 'dl', + 'dt', + 'figcaption', + 'figure', + 'hr', + 'li', + 'main', + 'ol', + 'p', + 'pre', + 'ul', + // Inline text semantics + 'a', + 'abbr', + 'b', + 'bdi', + 'bdo', + 'br', + 'cite', + 'code', + 'data', + 'dfn', + 'em', + 'i', + 'kbd', + 'mark', + 'q', + 'rb', + 'rp', + 'rt', + 'rtc', + 'ruby', + 's', + 'samp', + 'small', + 'span', + 'strong', + 'sub', + 'sup', + 'time', + 'u', + 'var', + 'wbr', + // Table content + 'caption', + 'col', + 'colgroup', + 'table', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr', + ], + // Tags that cannot be boolean + nonBooleanAttributes: [ + 'abbr', + 'accept', + 'accept-charset', + 'accesskey', + 'action', + 'allow', + 'alt', + 'as', + 'autocapitalize', + 'autocomplete', + 'blocking', + 'charset', + 'cite', + 'class', + 'color', + 'cols', + 'colspan', + 'content', + 'contenteditable', + 'coords', + 'crossorigin', + 'data', + 'datetime', + 'decoding', + 'dir', + 'dirname', + 'download', + 'draggable', + 'enctype', + 'enterkeyhint', + 'fetchpriority', + 'for', + 'form', + 'formaction', + 'formenctype', + 'formmethod', + 'formtarget', + 'headers', + 'height', + 'hidden', + 'high', + 'href', + 'hreflang', + 'http-equiv', + 'id', + 'imagesizes', + 'imagesrcset', + 'inputmode', + 'integrity', + 'is', + 'itemid', + 'itemprop', + 'itemref', + 'itemtype', + 'kind', + 'label', + 'lang', + 'list', + 'loading', + 'low', + 'max', + 'maxlength', + 'media', + 'method', + 'min', + 'minlength', + 'name', + 'nonce', + 'optimum', + 'pattern', + 'ping', + 'placeholder', + 'popover', + 'popovertarget', + 'popovertargetaction', + 'poster', + 'preload', + 'referrerpolicy', + 'rel', + 'rows', + 'rowspan', + 'sandbox', + 'scope', + 'shape', + 'size', + 'sizes', + 'slot', + 'span', + 'spellcheck', + 'src', + 'srcdoc', + 'srclang', + 'srcset', + 'start', + 'step', + 'style', + 'tabindex', + 'target', + 'title', + 'translate', + 'type', + 'usemap', + 'value', + 'width', + 'wrap', + // Event handlers + 'onauxclick', + 'onafterprint', + 'onbeforematch', + 'onbeforeprint', + 'onbeforeunload', + 'onbeforetoggle', + 'onblur', + 'oncancel', + 'oncanplay', + 'oncanplaythrough', + 'onchange', + 'onclick', + 'onclose', + 'oncontextlost', + 'oncontextmenu', + 'oncontextrestored', + 'oncopy', + 'oncuechange', + 'oncut', + 'ondblclick', + 'ondrag', + 'ondragend', + 'ondragenter', + 'ondragleave', + 'ondragover', + 'ondragstart', + 'ondrop', + 'ondurationchange', + 'onemptied', + 'onended', + 'onerror', + 'onfocus', + 'onformdata', + 'onhashchange', + 'oninput', + 'oninvalid', + 'onkeydown', + 'onkeypress', + 'onkeyup', + 'onlanguagechange', + 'onload', + 'onloadeddata', + 'onloadedmetadata', + 'onloadstart', + 'onmessage', + 'onmessageerror', + 'onmousedown', + 'onmouseenter', + 'onmouseleave', + 'onmousemove', + 'onmouseout', + 'onmouseover', + 'onmouseup', + 'onoffline', + 'ononline', + 'onpagehide', + 'onpageshow', + 'onpaste', + 'onpause', + 'onplay', + 'onplaying', + 'onpopstate', + 'onprogress', + 'onratechange', + 'onreset', + 'onresize', + 'onrejectionhandled', + 'onscroll', + 'onscrollend', + 'onsecuritypolicyviolation', + 'onseeked', + 'onseeking', + 'onselect', + 'onslotchange', + 'onstalled', + 'onstorage', + 'onsubmit', + 'onsuspend', + 'ontimeupdate', + 'ontoggle', + 'onunhandledrejection', + 'onunload', + 'onvolumechange', + 'onwaiting', + 'onwheel', + ], + disallowedTagsMode: 'discard', + allowedAttributes: { + a: ['href', 'name', 'target'], + // We don't currently allow img itself by default, but + // these attributes would make sense if we did. + img: [ + 'src', + 'srcset', + 'alt', + 'title', + 'width', + 'height', + 'loading', + ], + }, + // Lots of these won't come up by default because we don't allow them + selfClosing: [ + 'img', + 'br', + 'hr', + 'area', + 'base', + 'basefont', + 'input', + 'link', + 'meta', + ], + // URL schemes we permit + allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'tel'], + allowedSchemesByTag: {}, + allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'], + allowProtocolRelative: true, + enforceHtmlBoundary: false, + parseStyleAttributes: true, + }; + + sanitizeHtml.simpleTransform = function ( + newTagName, + newAttribs, + merge, + ) { + merge = merge === undefined ? true : merge; + newAttribs = newAttribs || {}; + + return function (tagName, attribs) { + let attrib; + if (merge) { + for (attrib in newAttribs) { + attribs[attrib] = newAttribs[attrib]; + } + } else { + attribs = newAttribs; + } + + return { + tagName: newTagName, + attribs: attribs, + }; + }; + }; + }, + { + 'deepmerge': 4, + 'escape-string-regexp': 26, + 'htmlparser2': 29, + 'is-plain-object': 31, + 'parse-srcset': 33, + 'postcss': 50, + }, + ], + 64: [ + function (require, module, exports) { + const sanitizeHtml = require('sanitize-html'); + + /** @typedef {{removeExtraParagraphSpacing?: boolean}} Options */ + + const loadingGif = + 'https://raw.githubusercontent.com/LNReader/lnreader/plugins/android/app/src/main/assets/images/loading.gif'; + + /** + * + * @param {string} html + * @param {Options?} options + */ + window.sanitizeChapterText = (html, options) => { + if (html) { + html = html.replace(/</g, '<').replace(/>/g, '>'); + } + let text = sanitizeHtml(html, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat([ + 'img', + 'i', + 'em', + 'b', + 'a', + 'div', + ]), + allowedAttributes: { + img: ['src'], + a: ['href'], + }, + allowedSchemes: ['data', 'http', 'https', 'file'], + }); + if (text) { + if (options?.removeExtraParagraphSpacing) { + text = text + .replace(/<\s*br[^>]*>/gi, '\n') + .replace(/\n{2,}/g, '\n\n'); + } + const imgHandlerRegex = /]*src="[^"]+"[^>]*)>/g; + const ttsHandlerRegex = + /(< *\w+ *>[^<.?!]+< *\/\w+ *>|< *a *href *= *"[^"]+" *>\s*(]+>|[^<]+|(< *\w+ *>[^]+< *\/\w+ *>)*)\s*< *\/a *>|< *img[^>]+>|[^<>.?!]+[.?!])/g; + // first -> match whole tags which dont have attribute and [.?!] inside + // second -> match a tags and its childs + // third -> match img tags + // match sentence split by [.?!] + + text = text + .replace( + imgHandlerRegex, + `Plugin can't fetch this img`, + ) + .replace(ttsHandlerRegex, '$1'); + } else { + text = 'HTML is empty'; + } + return text; + }; + }, + { 'sanitize-html': 63 }, + ], }, - // Lots of these won't come up by default because we don't allow them - selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ], - // URL schemes we permit - allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'tel' ], - allowedSchemesByTag: {}, - allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ], - allowProtocolRelative: true, - enforceHtmlBoundary: false, - parseStyleAttributes: true -}; - -sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) { - merge = (merge === undefined) ? true : merge; - newAttribs = newAttribs || {}; - - return function(tagName, attribs) { - let attrib; - if (merge) { - for (attrib in newAttribs) { - attribs[attrib] = newAttribs[attrib]; - } - } else { - attribs = newAttribs; - } - - return { - tagName: newTagName, - attribs: attribs - }; - }; -}; - -},{"deepmerge":4,"escape-string-regexp":26,"htmlparser2":29,"is-plain-object":31,"parse-srcset":33,"postcss":50}],64:[function(require,module,exports){ -const sanitizeHtml = require("sanitize-html"); - -/** @typedef {{removeExtraParagraphSpacing?: boolean}} Options */ - -const loadingGif = - "https://raw.githubusercontent.com/LNReader/lnreader/plugins/android/app/src/main/assets/images/loading.gif"; - -/** - * - * @param {string} html - * @param {Options?} options - */ -window.sanitizeChapterText = (html, options) => { - if (html) { - html = html.replace(/</g, "<").replace(/>/g, ">"); - } - let text = sanitizeHtml(html, { - allowedTags: sanitizeHtml.defaults.allowedTags.concat([ - "img", - "i", - "em", - "b", - "a", - "div", - ]), - allowedAttributes: { - img: ["src"], - a: ["href"], - }, - allowedSchemes: ["data", "http", "https", "file"], - }); - if (text) { - if (options?.removeExtraParagraphSpacing) { - text = text - .replace(/<\s*br[^>]*>/gi, "\n") - .replace(/\n{2,}/g, "\n\n"); - } - const imgHandlerRegex = /]*src="[^"]+"[^>]*)>/g; - const ttsHandlerRegex = - /(< *\w+ *>[^<.?!]+< *\/\w+ *>|< *a *href *= *"[^"]+" *>\s*(]+>|[^<]+|(< *\w+ *>[^]+< *\/\w+ *>)*)\s*< *\/a *>|< *img[^>]+>|[^<>.?!]+[.?!])/g; - // first -> match whole tags which dont have attribute and [.?!] inside - // second -> match a tags and its childs - // third -> match img tags - // match sentence split by [.?!] - - text = text - .replace( - imgHandlerRegex, - `Plugin can't fetch this img` - ) - .replace(ttsHandlerRegex, "$1"); - } else { - text = "HTML is empty"; - } - return text; -}; - -},{"sanitize-html":63}]},{},[64]); + {}, + [64], +); diff --git a/test_web/static/js/index.js b/test_web/static/js/index.js index c13fe97..848543c 100644 --- a/test_web/static/js/index.js +++ b/test_web/static/js/index.js @@ -1,1661 +1,1618 @@ -// @ts-check -/* Imports */ -/** @typedef {import("@libs/filterInputs").Filters} Filters */ -/** @typedef {import("@typings/types").PluginList} PluginList */ -/** @typedef {import("@typings/plugin").Plugin.PluginItem} PluginItem */ -/** @typedef {import("@typings/plugin").Plugin.ChapterItem} ChapterItem */ -/** @typedef {import("@typings/plugin").Plugin.NovelItem} NovelItem */ -/** @typedef {import("@typings/plugin").Plugin.SourceNovel & {totalPages?: number}} SourceNovel */ -/** @typedef {import("@typings/plugin").Plugin.SourcePage} SourcePage */ -/** @typedef {import("@libs/filterInputs").FilterTypes} FilterTypes */ -/** @typedef {import("@libs/filterInputs").FilterTypes.CheckboxGroup} CheckboxGroup */ -/** - * @template {FilterTypes} T - * @typedef {import("@libs/filterInputs").ValueOfFilter} ValueOfFilter */ -/** - * @template {Filters} T - * @typedef {import("@libs/filterInputs").FilterToValues} FilterToValues - */ -/** @typedef {{backgroundColor:string, textColor:string}} Theme */ -/** @typedef {Theme[]} Themes */ -/** @typedef {import("./accordion").AccordionBox} AccordionBox */ -/// - -// #region DOM Queries -const nav = $("nav"); - -/** @type {JQuery} */ -const headerChanger = $("#headerChanger"); - -/** @type {JQuery} */ -const popularNovels = $("#popularNovels"); -/** @type {JQuery} */ -const popularNovels_page_select = $("#popularNovels .page-select"); -/** @type {JQuery} */ -const popularNovels_fetch_btn = $("#popularNovels .fetch-btn"); -/** @type {JQuery} */ -const popularNovels_spinner = $("#popularNovels .spinner-border"); -/** @type {JQuery} */ -const popularNovels_novel_list = $("#popularNovels .novel-list"); - -/** @type {JQuery} */ -const searchNovels = $("#searchNovels"); -/** @type {JQuery} */ -const seacrhNovels_novel_list = $("#searchNovels .novel-list"); -/** @type {JQuery} */ -const searchNovels_page_select = $("#searchNovels .page-select"); -/** @type {JQuery} */ -const searchNovels_fetch_btn = $("#searchNovels .fetch-btn"); -/** @type {JQuery} */ -const searchNovels_searchbar = $("#searchNovels .searchbar input"); - -/** @type {JQuery} */ -const parseNovel = $("#parseNovel"); -/** @type {JQuery} */ -const parsePage = $("#parsePage"); -/** @type {JQuery} */ -const parseChapter = $("#parseChapter"); - -/** @type {JQuery} */ -const fetchImage = $("#fetchImage"); -/** @type {JQuery} */ -const resolveUrl = $("#resolveUrl"); - -/** @type {JQuery}*/ -const plugin_language_selection = $("#language"); -/** @type {JQuery} */ -const plugin_search = $("#plugin"); -/** @type {JQuery} */ -const plugin_search_results = $("#search-results"); -/** @type {JQuery} */ -const clear_plugin_search = $("#clear-search"); - -/** @type {JQuery} */ -const previewSwitch = $("#raw-preview-switch"); -/** @type {JQuery} */ -const chapterViewer = $("#chapter-viewer"); -/** @type {JQuery} */ -const filtersModal = $("#filtersModal .modal-body"); -/** @type {JQuery} */ -const latestSwitch = $("#latest-switch"); - -// #endregion - -// #region pagination -class NovelListPagination { - constructor( - /** @type {boolean} */ sL, - /** @type {JQuery} */ sE, - /** @type {JQuery} */ fC, - /** @type {JQuery} */ nL - ) { - /** @type {Record} */ - this.pageStore = {}; - this._showlatest = sL; - this.lastFetchedPage = 0; - this.currentPage = 0; - this.canFetchMore = true; - this.selectElement = sE; - this.fetchContainer = fC; - this.resetStoreOnFetch = true; - this.novelList = nL; - } - get showLatest() { - return this._showlatest; - } - set showLatest(val) { - this._showlatest = val; - this.reset(); - } - get fetchBtn() { - return this.fetchContainer.children("button")[0]; - } - get numSelect() { - return this.selectElement.children("select")[0]; - } - lockFetch(cause = 0) { - this.canFetchMore = false; - this.fetchBtn.disabled = true; - if (cause === 1) this.fetchBtn.innerText = "no more to fetch"; - if (cause === 2) this.fetchBtn.innerText = "fetching"; - if (cause === 3) this.fetchBtn.innerText = "error"; - } - unlockFetch() { - this.canFetchMore = true; - this.fetchBtn.disabled = false; - this.fetchBtn.innerText = "fetch"; - } - async fetchNovels( - /** @type {(page:number, latest: boolean)=>Promise} */ fn - ) { - this.error = undefined; - if (this.resetStoreOnFetch) { - this.resetStoreOnFetch = false; - this.pageStore = {}; - } - try { - this.lockFetch(2); - const lfp = ++this.lastFetchedPage; - console.log("fetching ", lfp, "page"); - const novels = await fn(lfp, this.showLatest); - if (novels.length === 0) { - this.lastFetchedPage--; - this.lockFetch(1); - if (this.lastFetchedPage === 0) { - this.error = "No novels found!"; - this.render(); - } - return false; - } - this.loadNovels(lfp, novels); - this.unlockFetch(); - return true; - } catch (e) { - this.lastFetchedPage--; - this.lockFetch(3); - this.error = `${e}`; - this.render(); - return false; - } - } - showNextPage() { - console.log("lf", this.lastFetchedPage, "cp", this.currentPage); - if (this.lastFetchedPage > this.currentPage) { - this.currentPage = this.lastFetchedPage; - this.render(); - } - this.numSelect.value = `${this.currentPage}`; - } - arePagesTheSame( - /** @type {NovelItem[]} */ p1, - /** @type {NovelItem[]} */ p2 - ) { - if (p1.length !== p2.length) return false; - let i = 0; - for (const novel of p1) { - if (novel.path !== p2[i].path) return false; - i++; - } - return true; - } - loadNovels(/** @type {number} */ page, /** @type {NovelItem[]} */ list) { - this.pageStore[page] = list; - if (page - 1 > 0 && this.pageStore[page - 1]) { - if ( - this.arePagesTheSame( - this.pageStore[page], - this.pageStore[page - 1] - ) - ) { - if (this.numSelect.nextElementSibling) - this.numSelect.nextElementSibling.innerHTML = - "Pages are the same! It's possible that pagination is not implemented in the source!"; - } - } - } - reset() { - this.hideSelectBox(); - this.currentPage = 0; - this.lastFetchedPage = 0; - this.resetStoreOnFetch = true; - this.error = undefined; - if (this.numSelect.nextElementSibling) { - this.numSelect.nextElementSibling.innerHTML = ""; - } - this.unlockFetch(); - } - setupSelectBox() { - this.selectElement[0].style.display = "inline"; - this.numSelect.innerHTML = ""; - for (let i = 1; i <= this.lastFetchedPage; i++) { - const option = $("