From 5b9b7f3a8271490019ef116bffac5c9aac3c8bde Mon Sep 17 00:00:00 2001 From: Iulian Onofrei <5748627+revolter@users.noreply.github.com> Date: Sat, 24 Feb 2024 03:00:41 +0200 Subject: [PATCH] Added IQ Puzzles recipe logic --- src/main.ts | 6 +- src/markdown/MarkdownTableFactory.ts | 4 + src/recipes/iq_puzzles/IQPuzzle.ts | 17 ++++ src/recipes/iq_puzzles/IQPuzzlesRecipe.ts | 111 +++++++++++++++++++++- 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/recipes/iq_puzzles/IQPuzzle.ts diff --git a/src/main.ts b/src/main.ts index 4dbaca3..81a121b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,7 +29,11 @@ export default class TrackALotPlugin extends Plugin { } if (settingsManager.settings.iqPuzzles) { - this.#addCommand(IQPuzzlesRecipe.NAME, new IQPuzzlesRecipe()); + this.#addCommand(IQPuzzlesRecipe.NAME, new IQPuzzlesRecipe( + markdownTableFactory, + markdownTableConverter, + trackablesUpdater + )); } } diff --git a/src/markdown/MarkdownTableFactory.ts b/src/markdown/MarkdownTableFactory.ts index a86d8bd..69b6c68 100644 --- a/src/markdown/MarkdownTableFactory.ts +++ b/src/markdown/MarkdownTableFactory.ts @@ -24,6 +24,10 @@ export class MarkdownTableFactory { return this.tableCellNode([this.textNode(text)]); } + imageTableCellNode(url: string, size: number): TableCell { + return this.tableCellNode([this.imageNode(url, size)]); + } + tableCellNode(contents: PhrasingContent[]): TableCell { return { type: 'tableCell', diff --git a/src/recipes/iq_puzzles/IQPuzzle.ts b/src/recipes/iq_puzzles/IQPuzzle.ts new file mode 100644 index 0000000..ae027bc --- /dev/null +++ b/src/recipes/iq_puzzles/IQPuzzle.ts @@ -0,0 +1,17 @@ +import { Trackable } from 'src/tracking/Trackable'; + +export class IQPuzzle implements Trackable { + identifier: string; + + constructor( + public readonly name: string, + public readonly imageLink: string, + public readonly status = '' + ) { + this.identifier = name; + } + + withStatus(newStatus: string): Trackable { + return new IQPuzzle(this.name, this.imageLink, newStatus); + } +} diff --git a/src/recipes/iq_puzzles/IQPuzzlesRecipe.ts b/src/recipes/iq_puzzles/IQPuzzlesRecipe.ts index b785f38..ef170f1 100644 --- a/src/recipes/iq_puzzles/IQPuzzlesRecipe.ts +++ b/src/recipes/iq_puzzles/IQPuzzlesRecipe.ts @@ -1,10 +1,119 @@ +import { MarkdownTableConverter } from 'src/markdown/MarkdownTableConverter'; +import { MarkdownTableFactory } from 'src/markdown/MarkdownTableFactory'; +import { RegexFactory } from 'src/regex/RegexFactory'; +import { WebsiteScraper } from 'src/scraping/WebsiteScraper'; +import { TrackablesUpdater } from 'src/tracking/TrackablesUpdater'; import { Recipe } from '../Recipe'; +import { RecipeListUpdater } from '../RecipeListUpdater'; +import { RecipeMarkdownListUpdater } from '../RecipeMarkdownListUpdater'; +import { RecipeMarker } from '../RecipeMarker'; +import { IQPuzzle } from './IQPuzzle'; export class IQPuzzlesRecipe implements Recipe { static NAME = 'IQ Puzzles'; static WEBPAGE = 'https://www.iqpuzzle.com'; + static #HEADERS = ['Name', 'Picture', 'Status']; + static #SCRAPE_URL = 'https://www.iqpuzzle.com'; + + #marker = new RecipeMarker(IQPuzzlesRecipe.NAME); + + constructor( + private markdownTableFactory: MarkdownTableFactory, + private markdownTableConverter: MarkdownTableConverter, + private trackablesUpdater: TrackablesUpdater + ) {} + async updatedListInContent(content: string): Promise { - return content; + const markdownUpdater = new RecipeMarkdownListUpdater(this.#marker); + const updater = new RecipeListUpdater( + IQPuzzlesRecipe.#HEADERS, + markdownUpdater, + this.trackablesUpdater + ); + + return await updater.update( + content, + + this.#markdownTableStringToPuzzles.bind(this), + this.#scrapePuzzles.bind(this), + this.#puzzlesToMarkdownTableString.bind(this) + ); + } + + async #scrapePuzzles(): Promise { + const nameRegex = new RegExp(/(?\w+)$/); // https://regex101.com/r/AuK9pb/1 + const cleanedLinkRegex = new RegExp(/^(?.+?\.jpg)/); // https://regex101.com/r/fd3A6U/1 + const scraper = new WebsiteScraper([IQPuzzlesRecipe.#SCRAPE_URL]); + + return await scraper.scrape( + content => { + const lists = Array.from(content.querySelectorAll('ul[data-hook="product-list-wrapper"]')); + + if (lists.length < 2) { + return []; + } + + const list = lists[1]; + + return Array.from(list.querySelectorAll('li')); + }, + product => { + const title = product.querySelector('div[data-hook="not-image-container"] a h3')?.textContent || ''; + const titleMatch = title.match(nameRegex); + const titleGroups = titleMatch?.groups; + + const name = titleGroups != null ? titleGroups.name : title; + + const image = product.querySelector('a wow-image img'); + const imageLink = image != null ? (image as HTMLImageElement).src : ''; + const cleanedImageLinkMatch = imageLink.match(cleanedLinkRegex); + const cleanedImageLink = (cleanedImageLinkMatch != null && cleanedImageLinkMatch.groups != null) + ? cleanedImageLinkMatch.groups.cleanedLink + : ''; + + return new IQPuzzle(name, cleanedImageLink); + } + ); + } + + #puzzlesToMarkdownTableString(headers: string[], puzzles: IQPuzzle[]): string { + const headerRow = this.markdownTableFactory.tableRowNode( + headers.map(header => this.markdownTableFactory.textTableCellNode(header)) + ); + const puzzleRows = puzzles.map(puzzle => + this.markdownTableFactory.tableRowNode([ + this.markdownTableFactory.textTableCellNode(puzzle.name), + this.markdownTableFactory.imageTableCellNode(puzzle.imageLink, 100), + this.markdownTableFactory.textTableCellNode(puzzle.status) + ]) + ); + const table = this.markdownTableFactory.table(headerRow, puzzleRows); + + return this.markdownTableConverter.tableToString(table); + } + + #markdownTableStringToPuzzles(markdownTableString: string): IQPuzzle[] { + const arrayOfArrays = this.markdownTableConverter.arrayOfArraysFromString(markdownTableString); + const imageLinkRegex = new RegexFactory().imageMarkdownLinkRegex(); + + return arrayOfArrays.flatMap(array => { + if (array.length < 5) { + return []; + } + + const name = array[0]; + + const image = array[1]; + const imageLinkMatch = image.match(imageLinkRegex); + if (imageLinkMatch == null || imageLinkMatch.groups == null) { + return []; + } + const imageLink = imageLinkMatch.groups.link; + + const status = array[2]; + + return new IQPuzzle(name, imageLink, status); + }); } }