commit
						94d6f3667b
					
				| 
						 | 
					@ -2,5 +2,5 @@
 | 
				
			||||||
  "semi": true,
 | 
					  "semi": true,
 | 
				
			||||||
  "trailingComma": "none",
 | 
					  "trailingComma": "none",
 | 
				
			||||||
  "singleQuote": false,
 | 
					  "singleQuote": false,
 | 
				
			||||||
  "printWidth": 90
 | 
					  "printWidth": 100
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,18 @@
 | 
				
			||||||
            "name": "Test",
 | 
					            "name": "Test",
 | 
				
			||||||
            "request": "launch",
 | 
					            "request": "launch",
 | 
				
			||||||
            "command": "npm run test",
 | 
					            "command": "npm run test",
 | 
				
			||||||
            "cwd": "${workspaceFolder}"
 | 
					            "cwd": "${workspaceFolder}",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "type": "node-terminal",
 | 
				
			||||||
 | 
					            "name": "Example",
 | 
				
			||||||
 | 
					            "request": "launch",
 | 
				
			||||||
 | 
					            "command": "npm run run-example",
 | 
				
			||||||
 | 
					            "cwd": "${workspaceFolder}",
 | 
				
			||||||
 | 
					            "skipFiles": [
 | 
				
			||||||
 | 
					                "${workspaceFolder}/node_modules/**/*",
 | 
				
			||||||
 | 
					                "<node_internals>/**/*"
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,6 @@
 | 
				
			||||||
        "@types/chai": "^4.2.15",
 | 
					        "@types/chai": "^4.2.15",
 | 
				
			||||||
        "@types/chai-as-promised": "^7.1.3",
 | 
					        "@types/chai-as-promised": "^7.1.3",
 | 
				
			||||||
        "@types/inquirer": "^7.3.1",
 | 
					        "@types/inquirer": "^7.3.1",
 | 
				
			||||||
        "@types/lodash": "^4.14.168",
 | 
					 | 
				
			||||||
        "@types/luxon": "^1.25.2",
 | 
					        "@types/luxon": "^1.25.2",
 | 
				
			||||||
        "@types/mocha": "^8.2.1",
 | 
					        "@types/mocha": "^8.2.1",
 | 
				
			||||||
        "@types/node": "^14.14.27",
 | 
					        "@types/node": "^14.14.27",
 | 
				
			||||||
| 
						 | 
					@ -519,12 +518,6 @@
 | 
				
			||||||
      "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
 | 
					      "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/lodash": {
 | 
					 | 
				
			||||||
      "version": "4.14.168",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==",
 | 
					 | 
				
			||||||
      "dev": true
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@types/luxon": {
 | 
					    "node_modules/@types/luxon": {
 | 
				
			||||||
      "version": "1.25.2",
 | 
					      "version": "1.25.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.25.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.25.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5482,12 +5475,6 @@
 | 
				
			||||||
      "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
 | 
					      "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@types/lodash": {
 | 
					 | 
				
			||||||
      "version": "4.14.168",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==",
 | 
					 | 
				
			||||||
      "dev": true
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "@types/luxon": {
 | 
					    "@types/luxon": {
 | 
				
			||||||
      "version": "1.25.2",
 | 
					      "version": "1.25.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.25.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.25.2.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,7 +48,6 @@
 | 
				
			||||||
    "@types/chai": "^4.2.15",
 | 
					    "@types/chai": "^4.2.15",
 | 
				
			||||||
    "@types/chai-as-promised": "^7.1.3",
 | 
					    "@types/chai-as-promised": "^7.1.3",
 | 
				
			||||||
    "@types/inquirer": "^7.3.1",
 | 
					    "@types/inquirer": "^7.3.1",
 | 
				
			||||||
    "@types/lodash": "^4.14.168",
 | 
					 | 
				
			||||||
    "@types/luxon": "^1.25.2",
 | 
					    "@types/luxon": "^1.25.2",
 | 
				
			||||||
    "@types/mocha": "^8.2.1",
 | 
					    "@types/mocha": "^8.2.1",
 | 
				
			||||||
    "@types/node": "^14.14.27",
 | 
					    "@types/node": "^14.14.27",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,11 +60,7 @@ async function main() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Log in the platform
 | 
					  // Log in the platform
 | 
				
			||||||
  console.log("Authenticating...");
 | 
					  console.log("Authenticating...");
 | 
				
			||||||
  const result = await login(
 | 
					  const result = await login(process.env.F95_USERNAME, process.env.F95_PASSWORD, insert2faCode);
 | 
				
			||||||
    process.env.F95_USERNAME,
 | 
					 | 
				
			||||||
    process.env.F95_PASSWORD,
 | 
					 | 
				
			||||||
    insert2faCode
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  console.log(`Authentication result: ${result.message}\n`);
 | 
					  console.log(`Authentication result: ${result.message}\n`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Manage failed login
 | 
					  // Manage failed login
 | 
				
			||||||
| 
						 | 
					@ -87,9 +83,7 @@ async function main() {
 | 
				
			||||||
  latestQuery.includedTags = ["3d game"];
 | 
					  latestQuery.includedTags = ["3d game"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const latestUpdates = await getLatestUpdates<Game>(latestQuery, 1);
 | 
					  const latestUpdates = await getLatestUpdates<Game>(latestQuery, 1);
 | 
				
			||||||
  console.log(
 | 
					  console.log(`"${latestUpdates.shift().name}" was the last "3d game" tagged game to be updated\n`);
 | 
				
			||||||
    `"${latestUpdates.shift().name}" was the last "3d game" tagged game to be updated\n`
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Get game data
 | 
					  // Get game data
 | 
				
			||||||
  for (const gamename of gameList) {
 | 
					  for (const gamename of gameList) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,14 +6,7 @@
 | 
				
			||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Modules from files
 | 
					// Modules from files
 | 
				
			||||||
import {
 | 
					import { TAuthor, TRating, IHandiwork, TEngine, TCategory, TStatus } from "../../interfaces";
 | 
				
			||||||
  TAuthor,
 | 
					 | 
				
			||||||
  TRating,
 | 
					 | 
				
			||||||
  IHandiwork,
 | 
					 | 
				
			||||||
  TEngine,
 | 
					 | 
				
			||||||
  TCategory,
 | 
					 | 
				
			||||||
  TStatus
 | 
					 | 
				
			||||||
} from "../../interfaces";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * It represents a generic work, be it a game, a comic, an animation or an asset.
 | 
					 * It represents a generic work, be it a game, a comic, an animation or an asset.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,8 +96,7 @@ export default class HandiworkSearchQuery implements IQuery {
 | 
				
			||||||
    // If the keywords are set or the number
 | 
					    // If the keywords are set or the number
 | 
				
			||||||
    // of included tags is greather than 5,
 | 
					    // of included tags is greather than 5,
 | 
				
			||||||
    // we must perform a thread search
 | 
					    // we must perform a thread search
 | 
				
			||||||
    if (this.keywords || this.includedTags.length > MAX_TAGS_LATEST_SEARCH)
 | 
					    if (this.keywords || this.includedTags.length > MAX_TAGS_LATEST_SEARCH) return "thread";
 | 
				
			||||||
      return "thread";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return DEFAULT_SEARCH_TYPE;
 | 
					    return DEFAULT_SEARCH_TYPE;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -130,8 +130,7 @@ export default class ThreadSearchQuery implements IQuery {
 | 
				
			||||||
    if (this.excludedTags) params["c[excludeTags]"] = this.excludedTags.join(",");
 | 
					    if (this.excludedTags) params["c[excludeTags]"] = this.excludedTags.join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set minimum reply number
 | 
					    // Set minimum reply number
 | 
				
			||||||
    if (this.minimumReplies > 0)
 | 
					    if (this.minimumReplies > 0) params["c[min_reply_count]"] = this.minimumReplies.toString();
 | 
				
			||||||
      params["c[min_reply_count]"] = this.minimumReplies.toString();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Add prefixes
 | 
					    // Add prefixes
 | 
				
			||||||
    const parser = new PrefixParser();
 | 
					    const parser = new PrefixParser();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -210,8 +210,8 @@ export default class Session {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Search for expired cookies
 | 
					    // Search for expired cookies
 | 
				
			||||||
    const jarValid =
 | 
					    const jarValid =
 | 
				
			||||||
      this._cookieJar.getCookiesSync("https://f95zone.to").filter((el) => el.TTL() === 0)
 | 
					      this._cookieJar.getCookiesSync("https://f95zone.to").filter((el) => el.TTL() === 0).length ===
 | 
				
			||||||
        .length === 0;
 | 
					      0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return dateValid && hashValid && jarValid;
 | 
					    return dateValid && hashValid && jarValid;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -154,7 +154,15 @@ export const POST = {
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * For use within a `THREAD.POSTS_IN_PAGE` selector.
 | 
					   * For use within a `THREAD.POSTS_IN_PAGE` selector.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  BOOKMARKED: '* ul.message-attribution-opposite >li > a[title="Bookmark"].is-bookmarked'
 | 
					  BOOKMARKED: '* ul.message-attribution-opposite >li > a[title="Bookmark"].is-bookmarked',
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Button used to hide/show a spoiler element of a post.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  SPOILER_BUTTON: "button.bbCodeSpoiler-button",
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Contents of a spoiler element in a post.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  SPOILER_CONTENT: "div.bbCodeSpoiler-content > div.bbCodeBlock--spoiler > div.bbCodeBlock-content"
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MEMBER = {
 | 
					export const MEMBER = {
 | 
				
			||||||
| 
						 | 
					@ -205,8 +213,7 @@ export const MEMBER = {
 | 
				
			||||||
   * If the text is `Unfollow` then the user is followed.
 | 
					   * If the text is `Unfollow` then the user is followed.
 | 
				
			||||||
   * If the text is `Follow` then the user is not followed.
 | 
					   * If the text is `Follow` then the user is not followed.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  FOLLOWED:
 | 
					  FOLLOWED: "div.memberHeader-buttons > div.buttonGroup:first-child > a[data-sk-follow] > span",
 | 
				
			||||||
    "div.memberHeader-buttons > div.buttonGroup:first-child > a[data-sk-follow] > span",
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Button used to ignore/unignore the user.
 | 
					   * Button used to ignore/unignore the user.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,10 +83,7 @@ export type TCategory = "games" | "mods" | "comics" | "animations" | "assets";
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Valid names of classes that implement the IQuery interface.
 | 
					 * Valid names of classes that implement the IQuery interface.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type TQueryInterface =
 | 
					export type TQueryInterface = "LatestSearchQuery" | "ThreadSearchQuery" | "HandiworkSearchQuery";
 | 
				
			||||||
  | "LatestSearchQuery"
 | 
					 | 
				
			||||||
  | "ThreadSearchQuery"
 | 
					 | 
				
			||||||
  | "HandiworkSearchQuery";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Collection of values defined for each
 | 
					 * Collection of values defined for each
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -86,9 +86,7 @@ export async function fetchHTML(
 | 
				
			||||||
      error: null
 | 
					      error: null
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return isHTML
 | 
					    return isHTML ? success(response.value.data as string) : failure(unexpectedResponseError);
 | 
				
			||||||
      ? success(response.value.data as string)
 | 
					 | 
				
			||||||
      : failure(unexpectedResponseError);
 | 
					 | 
				
			||||||
  } else return failure(response.value as GenericAxiosError);
 | 
					  } else return failure(response.value as GenericAxiosError);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,8 +103,7 @@ export async function authenticate(
 | 
				
			||||||
  force: boolean = false
 | 
					  force: boolean = false
 | 
				
			||||||
): Promise<LoginResult> {
 | 
					): Promise<LoginResult> {
 | 
				
			||||||
  shared.logger.info(`Authenticating with user ${credentials.username}`);
 | 
					  shared.logger.info(`Authenticating with user ${credentials.username}`);
 | 
				
			||||||
  if (!credentials.token)
 | 
					  if (!credentials.token) throw new InvalidF95Token(`Invalid token for auth: ${credentials.token}`);
 | 
				
			||||||
    throw new InvalidF95Token(`Invalid token for auth: ${credentials.token}`);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Secure the URL
 | 
					  // Secure the URL
 | 
				
			||||||
  const secureURL = enforceHttpsUrl(urls.LOGIN);
 | 
					  const secureURL = enforceHttpsUrl(urls.LOGIN);
 | 
				
			||||||
| 
						 | 
					@ -216,9 +213,7 @@ export async function fetchGETResponse(
 | 
				
			||||||
    const response = await axios.get(secureURL, commonConfig);
 | 
					    const response = await axios.get(secureURL, commonConfig);
 | 
				
			||||||
    return success(response);
 | 
					    return success(response);
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch (e) {
 | 
				
			||||||
    shared.logger.error(
 | 
					    shared.logger.error(`(GET) Error ${e.message} occurred while trying to fetch ${secureURL}`);
 | 
				
			||||||
      `(GET) Error ${e.message} occurred while trying to fetch ${secureURL}`
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    const genericError = new GenericAxiosError({
 | 
					    const genericError = new GenericAxiosError({
 | 
				
			||||||
      id: 1,
 | 
					      id: 1,
 | 
				
			||||||
      message: `(GET) Error ${e.message} occurred while trying to fetch ${secureURL}`,
 | 
					      message: `(GET) Error ${e.message} occurred while trying to fetch ${secureURL}`,
 | 
				
			||||||
| 
						 | 
					@ -305,10 +300,7 @@ export function isStringAValidURL(url: string): boolean {
 | 
				
			||||||
 * If `true`, the function will consider redirects a violation and return `false`.
 | 
					 * If `true`, the function will consider redirects a violation and return `false`.
 | 
				
			||||||
 * Default: `false`
 | 
					 * Default: `false`
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function urlExists(
 | 
					export async function urlExists(url: string, checkRedirect: boolean = false): Promise<boolean> {
 | 
				
			||||||
  url: string,
 | 
					 | 
				
			||||||
  checkRedirect: boolean = false
 | 
					 | 
				
			||||||
): Promise<boolean> {
 | 
					 | 
				
			||||||
  // Local variables
 | 
					  // Local variables
 | 
				
			||||||
  let valid = false;
 | 
					  let valid = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -376,10 +368,7 @@ function manageLoginPOSTResponse(response: AxiosResponse<any>) {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Get the error message (if any) and remove the new line chars
 | 
					  // Get the error message (if any) and remove the new line chars
 | 
				
			||||||
  const errorMessage = $("body")
 | 
					  const errorMessage = $("body").find(GENERIC.LOGIN_MESSAGE_ERROR).text().replace(/\n/g, "");
 | 
				
			||||||
    .find(GENERIC.LOGIN_MESSAGE_ERROR)
 | 
					 | 
				
			||||||
    .text()
 | 
					 | 
				
			||||||
    .replace(/\n/g, "");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Return the result of the authentication
 | 
					  // Return the result of the authentication
 | 
				
			||||||
  const result = errorMessage.trim() === "";
 | 
					  const result = errorMessage.trim() === "";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -122,10 +122,7 @@ function stringToBoolean(s: string): boolean {
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Case-insensitive.
 | 
					 * Case-insensitive.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function getPostElementByName(
 | 
					function getPostElementByName(elements: IPostElement[], name: string): IPostElement | undefined {
 | 
				
			||||||
  elements: IPostElement[],
 | 
					 | 
				
			||||||
  name: string
 | 
					 | 
				
			||||||
): IPostElement | undefined {
 | 
					 | 
				
			||||||
  return elements.find((el) => el.name.toUpperCase() === name.toUpperCase());
 | 
					  return elements.find((el) => el.name.toUpperCase() === name.toUpperCase());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -162,8 +159,7 @@ function fillWithPrefixes(hw: HandiWork, prefixes: string[]) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check what the prefix indicates
 | 
					    // Check what the prefix indicates
 | 
				
			||||||
    if (stringInDict(prefix, shared.prefixes["engines"])) engine = prefix as TEngine;
 | 
					    if (stringInDict(prefix, shared.prefixes["engines"])) engine = prefix as TEngine;
 | 
				
			||||||
    else if (stringInDict(prefix, shared.prefixes["statuses"]))
 | 
					    else if (stringInDict(prefix, shared.prefixes["statuses"])) status = prefix as TStatus;
 | 
				
			||||||
      status = prefix as TStatus;
 | 
					 | 
				
			||||||
    else if (stringInDict(prefix, fakeModDict)) mod = true;
 | 
					    else if (stringInDict(prefix, fakeModDict)) mod = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Anyway add the prefix to list
 | 
					    // Anyway add the prefix to list
 | 
				
			||||||
| 
						 | 
					@ -206,8 +202,7 @@ function fillWithPostData(hw: HandiWork, elements: IPostElement[]) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Parse the censorship
 | 
					  // Parse the censorship
 | 
				
			||||||
  const censored =
 | 
					  const censored =
 | 
				
			||||||
    getPostElementByName(elements, "censored") ||
 | 
					    getPostElementByName(elements, "censored") || getPostElementByName(elements, "censorship");
 | 
				
			||||||
    getPostElementByName(elements, "censorship");
 | 
					 | 
				
			||||||
  if (censored) hw.censored = stringToBoolean(censored.text);
 | 
					  if (censored) hw.censored = stringToBoolean(censored.text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Get the genres
 | 
					  // Get the genres
 | 
				
			||||||
| 
						 | 
					@ -249,8 +244,7 @@ function fillWithPostData(hw: HandiWork, elements: IPostElement[]) {
 | 
				
			||||||
  //#region Get the changelog
 | 
					  //#region Get the changelog
 | 
				
			||||||
  hw.changelog = [];
 | 
					  hw.changelog = [];
 | 
				
			||||||
  const changelogElement =
 | 
					  const changelogElement =
 | 
				
			||||||
    getPostElementByName(elements, "changelog") ||
 | 
					    getPostElementByName(elements, "changelog") || getPostElementByName(elements, "change-log");
 | 
				
			||||||
    getPostElementByName(elements, "change-log");
 | 
					 | 
				
			||||||
  if (changelogElement) {
 | 
					  if (changelogElement) {
 | 
				
			||||||
    const changelogSpoiler = changelogElement?.content.find((el) => {
 | 
					    const changelogSpoiler = changelogElement?.content.find((el) => {
 | 
				
			||||||
      return el.type === "Spoiler" && el.content.length > 0;
 | 
					      return el.type === "Spoiler" && el.content.length > 0;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,9 +59,7 @@ function parseJSONLD(element: cheerio.Element): TJsonLD {
 | 
				
			||||||
  const html = cheerio(element).html().trim();
 | 
					  const html = cheerio(element).html().trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Obtain the JSON-LD
 | 
					  // Obtain the JSON-LD
 | 
				
			||||||
  const data = html
 | 
					  const data = html.replace('<script type="application/ld+json">', "").replace("</script>", "");
 | 
				
			||||||
    .replace('<script type="application/ld+json">', "")
 | 
					 | 
				
			||||||
    .replace("</script>", "");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Convert the string to an object
 | 
					  // Convert the string to an object
 | 
				
			||||||
  return JSON.parse(data);
 | 
					  return JSON.parse(data);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,9 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Import from files
 | 
				
			||||||
 | 
					import { POST } from "../constants/css-selector";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//#region Interfaces
 | 
					//#region Interfaces
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IPostElement {
 | 
					export interface IPostElement {
 | 
				
			||||||
| 
						 | 
					@ -22,13 +25,11 @@ export interface ILink extends IPostElement {
 | 
				
			||||||
//#endregion Interfaces
 | 
					//#endregion Interfaces
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//#region Public methods
 | 
					//#region Public methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Given a post of a thread page it extracts the information contained in the body.
 | 
					 * Given a post of a thread page it extracts the information contained in the body.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function parseF95ThreadPost(
 | 
					export function parseF95ThreadPost($: cheerio.Root, post: cheerio.Cheerio): IPostElement[] {
 | 
				
			||||||
  $: cheerio.Root,
 | 
					 | 
				
			||||||
  post: cheerio.Cheerio
 | 
					 | 
				
			||||||
): IPostElement[] {
 | 
					 | 
				
			||||||
  // The data is divided between "tag" and "text" elements.
 | 
					  // The data is divided between "tag" and "text" elements.
 | 
				
			||||||
  // Simple data is composed of a "tag" element followed
 | 
					  // Simple data is composed of a "tag" element followed
 | 
				
			||||||
  // by a "text" element, while more complex data (contained
 | 
					  // by a "text" element, while more complex data (contained
 | 
				
			||||||
| 
						 | 
					@ -40,34 +41,81 @@ export function parseF95ThreadPost(
 | 
				
			||||||
  const elements = post
 | 
					  const elements = post
 | 
				
			||||||
    .contents()
 | 
					    .contents()
 | 
				
			||||||
    .toArray()
 | 
					    .toArray()
 | 
				
			||||||
    .map((el) => parseCheerioNode($, el))
 | 
					    .map((el) => parseCheerioNode($, el)) // Parse the nodes
 | 
				
			||||||
    .filter((node) => node.name || node.text || node.content.length != 0);
 | 
					    .filter((el) => !isPostElementEmpty(el)) // Ignore the empty nodes
 | 
				
			||||||
 | 
					    .map((el) => reducePostElement(el)); // Compress the nodes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ... then parse the elements to create the pairs of title/data
 | 
					  // ... then parse the elements to create the pairs of title/data
 | 
				
			||||||
  return parsePostElements(elements);
 | 
					  return associateElementsWithName(elements);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//#endregion Public methods
 | 
					//#endregion Public methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//#region Private methods
 | 
					//#region Private methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#region Node type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if the node passed as a parameter is a formatting one (i.e. `<b>`).
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function isFormattingNode(node: cheerio.Element): boolean {
 | 
				
			||||||
 | 
					  const formattedTags = ["b", "i"];
 | 
				
			||||||
 | 
					  return node.type === "tag" && formattedTags.includes(node.name);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if the node passed as a parameter is of text type.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function isTextNode(node: cheerio.Element): boolean {
 | 
				
			||||||
 | 
					  return node.type === "text";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if the node is a spoiler.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function isSpoilerNode(node: cheerio.Cheerio): boolean {
 | 
				
			||||||
 | 
					  return node.attr("class") === "bbCodeSpoiler";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if the node is a link or a image.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function isLinkNode(node: cheerio.Element): boolean {
 | 
				
			||||||
 | 
					  // Local variables
 | 
				
			||||||
 | 
					  let valid = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // The node is a valid DOM element
 | 
				
			||||||
 | 
					  if (node.type === "tag") {
 | 
				
			||||||
 | 
					    const el = node as cheerio.TagElement;
 | 
				
			||||||
 | 
					    valid = el.name === "a" || el.name === "img";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return valid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if the node is a `noscript` tag.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function isNoScriptNode(node: cheerio.Element): boolean {
 | 
				
			||||||
 | 
					  return node.type === "tag" && node.name === "noscript";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#endregion Node Type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#region Parse Cheerio node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Process a spoiler element by getting its text broken
 | 
					 * Process a spoiler element by getting its text broken
 | 
				
			||||||
 * down by any other spoiler elements present.
 | 
					 * down by any other spoiler elements present.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function parseCheerioSpoilerNode(
 | 
					function parseCheerioSpoilerNode($: cheerio.Root, node: cheerio.Cheerio): IPostElement {
 | 
				
			||||||
  $: cheerio.Root,
 | 
					 | 
				
			||||||
  spoiler: cheerio.Cheerio
 | 
					 | 
				
			||||||
): IPostElement {
 | 
					 | 
				
			||||||
  // A spoiler block is composed of a div with class "bbCodeSpoiler",
 | 
					  // A spoiler block is composed of a div with class "bbCodeSpoiler",
 | 
				
			||||||
  // containing a div "bbCodeSpoiler-content" containing, in cascade,
 | 
					  // containing a div "bbCodeSpoiler-content" containing, in cascade,
 | 
				
			||||||
  // a div with class "bbCodeBlock--spoiler" and a div with class "bbCodeBlock-content".
 | 
					  // a div with class "bbCodeBlock--spoiler" and a div with class "bbCodeBlock-content".
 | 
				
			||||||
  // This last tag contains the required data.
 | 
					  // This last tag contains the required data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Local variables
 | 
					  // Local variables
 | 
				
			||||||
  const BUTTON_CLASS = "button.bbCodeSpoiler-button";
 | 
					  const spoiler: IPostElement = {
 | 
				
			||||||
  const SPOILER_CONTENT_CLASS =
 | 
					 | 
				
			||||||
    "div.bbCodeSpoiler-content > div.bbCodeBlock--spoiler > div.bbCodeBlock-content";
 | 
					 | 
				
			||||||
  const content: IPostElement = {
 | 
					 | 
				
			||||||
    type: "Spoiler",
 | 
					    type: "Spoiler",
 | 
				
			||||||
    name: "",
 | 
					    name: "",
 | 
				
			||||||
    text: "",
 | 
					    text: "",
 | 
				
			||||||
| 
						 | 
					@ -75,185 +123,219 @@ function parseCheerioSpoilerNode(
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Find the title of the spoiler (contained in the button)
 | 
					  // Find the title of the spoiler (contained in the button)
 | 
				
			||||||
  const button = spoiler.find(BUTTON_CLASS).toArray().shift();
 | 
					  spoiler.name = node.find(POST.SPOILER_BUTTON).first().text().trim();
 | 
				
			||||||
  content.name = $(button).text().trim();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Parse the content of the spoiler
 | 
					  // Parse the content of the spoiler
 | 
				
			||||||
  spoiler
 | 
					  spoiler.content = node
 | 
				
			||||||
    .find(SPOILER_CONTENT_CLASS)
 | 
					    .find(POST.SPOILER_CONTENT)
 | 
				
			||||||
    .contents()
 | 
					    .contents()
 | 
				
			||||||
    .map((idx, el) => {
 | 
					    .toArray()
 | 
				
			||||||
      // Convert the element
 | 
					    .map((el) => parseCheerioNode($, el));
 | 
				
			||||||
      const element = $(el);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Parse nested spoiler
 | 
					 | 
				
			||||||
      if (element.attr("class") === "bbCodeSpoiler") {
 | 
					 | 
				
			||||||
        const spoiler = parseCheerioSpoilerNode($, element);
 | 
					 | 
				
			||||||
        content.content.push(spoiler);
 | 
					 | 
				
			||||||
      } else if (el.type === "text") {
 | 
					 | 
				
			||||||
        // Append text
 | 
					 | 
				
			||||||
        content.text += element.text();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Clean text
 | 
					  // Clean text
 | 
				
			||||||
  content.text = content.text.replace(/\s\s+/g, " ").trim();
 | 
					  spoiler.text = spoiler.text.replace(/\s\s+/g, " ").trim();
 | 
				
			||||||
  return content;
 | 
					  return spoiler;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Check if the node passed as a parameter is of text type.
 | 
					 * Process a node that contains a link or image.
 | 
				
			||||||
 * This also includes formatted nodes (i.e. `<b>`).
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function isTextNode(node: cheerio.Element): boolean {
 | 
					function parseCheerioLinkNode(element: cheerio.Cheerio): ILink {
 | 
				
			||||||
  const formattedTags = ["b", "i"];
 | 
					  // Local variable
 | 
				
			||||||
  const isText = node.type === "text";
 | 
					  const link: ILink = {
 | 
				
			||||||
  const isFormatted = node.type === "tag" && formattedTags.includes(node.name);
 | 
					    type: "Link",
 | 
				
			||||||
 | 
					    name: "",
 | 
				
			||||||
 | 
					    text: "",
 | 
				
			||||||
 | 
					    href: "",
 | 
				
			||||||
 | 
					    content: []
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return isText || isFormatted;
 | 
					  if (element.is("img")) {
 | 
				
			||||||
 | 
					    link.type = "Image";
 | 
				
			||||||
 | 
					    link.text = element.attr("alt");
 | 
				
			||||||
 | 
					    link.href = element.attr("data-src");
 | 
				
			||||||
 | 
					  } else if (element.is("a")) {
 | 
				
			||||||
 | 
					    link.type = "Link";
 | 
				
			||||||
 | 
					    link.text = element.text().replace(/\s\s+/g, " ").trim();
 | 
				
			||||||
 | 
					    link.href = element.attr("href");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return link;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Process a text only node.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function parseCheerioTextNode(node: cheerio.Cheerio): IPostElement {
 | 
				
			||||||
 | 
					  const content: IPostElement = {
 | 
				
			||||||
 | 
					    type: "Text",
 | 
				
			||||||
 | 
					    name: "",
 | 
				
			||||||
 | 
					    text: getCheerioNonChildrenText(node),
 | 
				
			||||||
 | 
					    content: []
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return content;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#endregion Parse Cheerio node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#region IPostElement utility
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if the node has non empty `name` and `text`.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function isPostElementUnknown(node: IPostElement): boolean {
 | 
				
			||||||
 | 
					  return node.name.trim() === "" && node.text.trim() === "";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if the node has a non empty property
 | 
				
			||||||
 | 
					 * between `name`, `text` and `content`.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function isPostElementEmpty(node: IPostElement): boolean {
 | 
				
			||||||
 | 
					  return node.content.length === 0 && isPostElementUnknown(node);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Create a `IPostElement` without name, text or content.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function createEmptyElement(): IPostElement {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: "Empty",
 | 
				
			||||||
 | 
					    name: "",
 | 
				
			||||||
 | 
					    text: "",
 | 
				
			||||||
 | 
					    content: []
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if the element contains the overview of a thread (post #1).
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function elementIsOverview(element: IPostElement): boolean {
 | 
				
			||||||
 | 
					  // Search the text element that start with "overview"
 | 
				
			||||||
 | 
					  const result = element.content
 | 
				
			||||||
 | 
					    .filter((e) => e.type === "Text")
 | 
				
			||||||
 | 
					    .find((e) => e.text.toUpperCase().startsWith("OVERVIEW"));
 | 
				
			||||||
 | 
					  return result !== undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * If the element contains the overview of a thread, parse it.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function getOverviewFromElement(element: IPostElement): string {
 | 
				
			||||||
 | 
					  // Local variables
 | 
				
			||||||
 | 
					  const alphanumericRegex = new RegExp("[a-zA-Z0-9]+");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get all the text values of the overview
 | 
				
			||||||
 | 
					  const textes = element.content
 | 
				
			||||||
 | 
					    .filter((e) => e.type === "Text")
 | 
				
			||||||
 | 
					    .filter((e) => {
 | 
				
			||||||
 | 
					      const cleanValue = e.text.toUpperCase().replace("OVERVIEW", "").trim();
 | 
				
			||||||
 | 
					      const isAlphanumeric = alphanumericRegex.test(cleanValue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return cleanValue !== "" && isAlphanumeric;
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .map((e) => e.text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Joins the textes
 | 
				
			||||||
 | 
					  return textes.join(" ");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#endregion IPostElement utility
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Gets the text of the node only, excluding child nodes.
 | 
					 * Gets the text of the node only, excluding child nodes.
 | 
				
			||||||
 * Also includes formatted text elements (i.e. `<b>`).
 | 
					 * Also includes formatted text elements (i.e. `<b>`).
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function getCheerioNonChildrenText(node: cheerio.Cheerio): string {
 | 
					function getCheerioNonChildrenText(node: cheerio.Cheerio): string {
 | 
				
			||||||
  // Find all the text nodes in the node
 | 
					  // Local variable
 | 
				
			||||||
  const text = node
 | 
					  let text = "";
 | 
				
			||||||
    .first()
 | 
					
 | 
				
			||||||
    .contents()
 | 
					  // If the node has no children, return the node's text
 | 
				
			||||||
    .filter((idx, el) => {
 | 
					  if (node.contents().length === 1) {
 | 
				
			||||||
      return isTextNode(el);
 | 
					    // @todo Remove IF after cheerio RC6
 | 
				
			||||||
    })
 | 
					    text = node.text();
 | 
				
			||||||
    .text();
 | 
					  } else {
 | 
				
			||||||
 | 
					    // Find all the text nodes in the node
 | 
				
			||||||
 | 
					    text = node
 | 
				
			||||||
 | 
					      .first()
 | 
				
			||||||
 | 
					      .contents() // @todo Change to children() after cheerio RC6
 | 
				
			||||||
 | 
					      .filter((idx, el) => isTextNode(el))
 | 
				
			||||||
 | 
					      .text();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Clean and return the text
 | 
					  // Clean and return the text
 | 
				
			||||||
  return text.replace(/\s\s+/g, " ").trim();
 | 
					  return text.replace(/\s\s+/g, " ").trim();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Process a node and see if it contains a
 | 
					 | 
				
			||||||
 * link or image. If not, it returns `null`.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function parseCheerioLinkNode(element: cheerio.Cheerio): ILink | null {
 | 
					 | 
				
			||||||
  //@ts-ignore
 | 
					 | 
				
			||||||
  const name = element[0]?.name;
 | 
					 | 
				
			||||||
  const link: ILink = {
 | 
					 | 
				
			||||||
    name: "",
 | 
					 | 
				
			||||||
    type: "Link",
 | 
					 | 
				
			||||||
    text: "",
 | 
					 | 
				
			||||||
    href: "",
 | 
					 | 
				
			||||||
    content: []
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (name === "img") {
 | 
					 | 
				
			||||||
    link.type = "Image";
 | 
					 | 
				
			||||||
    link.text = element.attr("alt");
 | 
					 | 
				
			||||||
    link.href = element.attr("data-src");
 | 
					 | 
				
			||||||
  } else if (name === "a") {
 | 
					 | 
				
			||||||
    link.type = "Link";
 | 
					 | 
				
			||||||
    link.text = element.text().replace(/\s\s+/g, " ").trim();
 | 
					 | 
				
			||||||
    link.href = element.attr("href");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return link.href ? link : null;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Collapse an `IPostElement` element with a single subnode
 | 
					 * Collapse an `IPostElement` element with a single subnode
 | 
				
			||||||
 * in the `Content` field in case it has no information.
 | 
					 * in the `Content` field in case it has no information.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function reducePostElement(element: IPostElement): IPostElement {
 | 
					function reducePostElement(element: IPostElement, recursive = true): IPostElement {
 | 
				
			||||||
  if (element.content.length === 1) {
 | 
					  // Local variables
 | 
				
			||||||
    const content = element.content[0] as IPostElement;
 | 
					  const shallowCopy = Object.assign({}, element);
 | 
				
			||||||
    const nullValues =
 | 
					 | 
				
			||||||
      (!element.name || !content.name) && (!element.text || !content.text);
 | 
					 | 
				
			||||||
    const sameValues = element.name === content.name || element.text === content.text;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (nullValues || sameValues) {
 | 
					  // Find the posts without name and text
 | 
				
			||||||
      element.name = element.name || content.name;
 | 
					  const unknownChildrens = shallowCopy.content.filter((e) => isPostElementUnknown(e));
 | 
				
			||||||
      element.text = element.text || content.text;
 | 
					  if (recursive) {
 | 
				
			||||||
      element.content.push(...content.content);
 | 
					    const recursiveUnknownChildrens = unknownChildrens.map((e) => reducePostElement(e));
 | 
				
			||||||
      element.type = content.type;
 | 
					    unknownChildrens.push(...recursiveUnknownChildrens);
 | 
				
			||||||
 | 
					 | 
				
			||||||
      // If the content is a link, add the HREF to the element
 | 
					 | 
				
			||||||
      const contentILink = content as ILink;
 | 
					 | 
				
			||||||
      const elementILink = element as ILink;
 | 
					 | 
				
			||||||
      if (contentILink.href) elementILink.href = contentILink.href;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return element;
 | 
					  // Eliminate non-useful child nodes
 | 
				
			||||||
 | 
					  if (isPostElementUnknown(shallowCopy) && unknownChildrens.length > 0) {
 | 
				
			||||||
 | 
					    // Find the valid elements to add to the node
 | 
				
			||||||
 | 
					    const childContents = unknownChildrens
 | 
				
			||||||
 | 
					      .filter((e) => !shallowCopy.content.includes(e))
 | 
				
			||||||
 | 
					      .map((e) => (e.content.length > 0 ? e.content : e));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove the empty elements
 | 
				
			||||||
 | 
					    shallowCopy.content = shallowCopy.content.filter((e) => !unknownChildrens.includes(e));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Merge the non-empty children of this node with
 | 
				
			||||||
 | 
					    // the content of the empty children of this node
 | 
				
			||||||
 | 
					    const newContent = [].concat(...childContents);
 | 
				
			||||||
 | 
					    shallowCopy.content.push(...newContent);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // If the node has only one child, return it
 | 
				
			||||||
 | 
					  else if (isPostElementUnknown(shallowCopy) && shallowCopy.content.length === 1) {
 | 
				
			||||||
 | 
					    return shallowCopy.content[0];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return shallowCopy;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Transform a `cheerio.Cheerio` node into an `IPostElement` element with its subnodes.
 | 
					 * Transform a `cheerio.Cheerio` node into an `IPostElement` element with its subnodes.
 | 
				
			||||||
 * @param reduce Compress subsequent subnodes if they contain no information. Default: `true`.
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function parseCheerioNode(
 | 
					function parseCheerioNode($: cheerio.Root, node: cheerio.Element): IPostElement {
 | 
				
			||||||
  $: cheerio.Root,
 | 
					 | 
				
			||||||
  node: cheerio.Element,
 | 
					 | 
				
			||||||
  reduce = true
 | 
					 | 
				
			||||||
): IPostElement {
 | 
					 | 
				
			||||||
  // Local variables
 | 
					  // Local variables
 | 
				
			||||||
  const content: IPostElement = {
 | 
					  let post: IPostElement = createEmptyElement();
 | 
				
			||||||
    type: "Empty",
 | 
					 | 
				
			||||||
    name: "",
 | 
					 | 
				
			||||||
    text: "",
 | 
					 | 
				
			||||||
    content: []
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
  const cheerioNode = $(node);
 | 
					  const cheerioNode = $(node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (isTextNode(node)) {
 | 
					  // Parse the node
 | 
				
			||||||
    content.text = cheerioNode.text().replace(/\s\s+/g, " ").trim();
 | 
					  if (!isNoScriptNode(node)) {
 | 
				
			||||||
    content.type = "Text";
 | 
					    if (isTextNode(node) && !isFormattingNode(node)) post = parseCheerioTextNode(cheerioNode);
 | 
				
			||||||
  } else {
 | 
					    else if (isSpoilerNode(cheerioNode)) post = parseCheerioSpoilerNode($, cheerioNode);
 | 
				
			||||||
    // Get the number of children that the element own
 | 
					    else if (isLinkNode(node)) post = parseCheerioLinkNode(cheerioNode);
 | 
				
			||||||
    const nChildren = cheerioNode.children().length;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Get the text of the element without childrens
 | 
					    // Parse the node's childrens
 | 
				
			||||||
    content.text = getCheerioNonChildrenText(cheerioNode);
 | 
					    const childPosts = cheerioNode
 | 
				
			||||||
 | 
					      .contents() // @todo Change to children() after cheerio RC6
 | 
				
			||||||
    // Parse spoilers
 | 
					      .toArray()
 | 
				
			||||||
    if (cheerioNode.attr("class") === "bbCodeSpoiler") {
 | 
					      .filter((el) => el) // Ignore undefined elements
 | 
				
			||||||
      const spoiler = parseCheerioSpoilerNode($, cheerioNode);
 | 
					      .map((el) => parseCheerioNode($, el))
 | 
				
			||||||
 | 
					      .filter((el) => !isPostElementEmpty(el));
 | 
				
			||||||
      // Add element if not null
 | 
					    post.content.push(...childPosts);
 | 
				
			||||||
      if (spoiler) {
 | 
					 | 
				
			||||||
        content.content.push(spoiler);
 | 
					 | 
				
			||||||
        content.type = "Spoiler";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Parse links
 | 
					 | 
				
			||||||
    else if (nChildren === 0 && cheerioNode.length != 0) {
 | 
					 | 
				
			||||||
      const link = parseCheerioLinkNode(cheerioNode);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Add element if not null
 | 
					 | 
				
			||||||
      if (link) {
 | 
					 | 
				
			||||||
        content.content.push(link);
 | 
					 | 
				
			||||||
        content.type = "Link";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      cheerioNode.children().map((idx, el) => {
 | 
					 | 
				
			||||||
        // Parse the children of the element passed as parameter
 | 
					 | 
				
			||||||
        const childElement = parseCheerioNode($, el);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // If the children is valid (not empty) push it
 | 
					 | 
				
			||||||
        if ((childElement.text || childElement.content.length !== 0) && !isTextNode(el)) {
 | 
					 | 
				
			||||||
          content.content.push(childElement);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return reduce ? reducePostElement(content) : content;
 | 
					  return post;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * It simplifies the `IPostElement` elements by associating
 | 
					 * It simplifies the `IPostElement` elements by associating
 | 
				
			||||||
 * the corresponding value to each characterizing element (i.e. author).
 | 
					 * the corresponding value to each characterizing element (i.e. author).
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function parsePostElements(elements: IPostElement[]): IPostElement[] {
 | 
					function associateElementsWithName(elements: IPostElement[]): IPostElement[] {
 | 
				
			||||||
  // Local variables
 | 
					  // Local variables
 | 
				
			||||||
  const pairs: IPostElement[] = [];
 | 
					  const pairs: IPostElement[] = [];
 | 
				
			||||||
  const specialCharsRegex = /^[-!$%^&*()_+|~=`{}[\]:";'<>?,./]/;
 | 
					  const specialCharsRegex = /^[-!$%^&*()_+|~=`{}[\]:";'<>?,./]/;
 | 
				
			||||||
| 
						 | 
					@ -275,11 +357,11 @@ function parsePostElements(elements: IPostElement[]): IPostElement[] {
 | 
				
			||||||
      lastPair.content.push(...elements[i].content);
 | 
					      lastPair.content.push(...elements[i].content);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // This is a special case
 | 
					    // This is a special case
 | 
				
			||||||
    else if (elements[i].text.startsWith("Overview:\n")) {
 | 
					    else if (elementIsOverview(elements[i])) {
 | 
				
			||||||
      // We add the overview to the pairs as a text element
 | 
					      // We add the overview to the pairs as a text element
 | 
				
			||||||
      elements[i].type = "Text";
 | 
					      elements[i].type = "Text";
 | 
				
			||||||
      elements[i].name = "Overview";
 | 
					      elements[i].name = "Overview";
 | 
				
			||||||
      elements[i].text = elements[i].text.replace("Overview:\n", "");
 | 
					      elements[i].text = getOverviewFromElement(elements[i]);
 | 
				
			||||||
      pairs.push(elements[i]);
 | 
					      pairs.push(elements[i]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // We have an element referred to the previous "title"
 | 
					    // We have an element referred to the previous "title"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,8 +34,6 @@ export function suite(): void {
 | 
				
			||||||
  it("Fetch post with invalid ID", async function fetchWithInvalidID() {
 | 
					  it("Fetch post with invalid ID", async function fetchWithInvalidID() {
 | 
				
			||||||
    Shared.setIsLogged(true);
 | 
					    Shared.setIsLogged(true);
 | 
				
			||||||
    const thread = new Thread(-1);
 | 
					    const thread = new Thread(-1);
 | 
				
			||||||
    await expect(thread.getPost(0)).to.be.rejectedWith(
 | 
					    await expect(thread.getPost(0)).to.be.rejectedWith("Index must be greater or equal than 1");
 | 
				
			||||||
      "Index must be greater or equal than 1"
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,15 +29,7 @@ export function suite(): void {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Test values
 | 
					    // Test values
 | 
				
			||||||
    const testIDs = [103, 225, 44, 13, 2, 7, 22];
 | 
					    const testIDs = [103, 225, 44, 13, 2, 7, 22];
 | 
				
			||||||
    const testPrefixes = [
 | 
					    const testPrefixes = ["corruption", "pregnancy", "slave", "VN", "RPGM", "Ren'Py", "Abandoned"];
 | 
				
			||||||
      "corruption",
 | 
					 | 
				
			||||||
      "pregnancy",
 | 
					 | 
				
			||||||
      "slave",
 | 
					 | 
				
			||||||
      "VN",
 | 
					 | 
				
			||||||
      "RPGM",
 | 
					 | 
				
			||||||
      "Ren'Py",
 | 
					 | 
				
			||||||
      "Abandoned"
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Parse values
 | 
					    // Parse values
 | 
				
			||||||
    const ids = parser.prefixesToIDs(testPrefixes);
 | 
					    const ids = parser.prefixesToIDs(testPrefixes);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue