|
| 1 | +import { MarkdownTableConverter } from 'src/markdown/MarkdownTableConverter'; |
| 2 | +import { MarkdownTableFactory } from 'src/markdown/MarkdownTableFactory'; |
| 3 | +import { RegexFactory } from 'src/regex/RegexFactory'; |
| 4 | +import { WebsiteScraper } from 'src/scraping/WebsiteScraper'; |
| 5 | +import { TrackablesUpdater } from 'src/tracking/TrackablesUpdater'; |
1 | 6 | import { Recipe } from '../Recipe'; |
| 7 | +import { RecipeListUpdater } from '../RecipeListUpdater'; |
| 8 | +import { RecipeMarkdownListUpdater } from '../RecipeMarkdownListUpdater'; |
| 9 | +import { RecipeMarker } from '../RecipeMarker'; |
| 10 | +import { IQPuzzle } from './IQPuzzle'; |
2 | 11 |
|
3 | 12 | export class IQPuzzlesRecipe implements Recipe { |
4 | 13 | static NAME = 'IQ Puzzles'; |
5 | 14 | static WEBPAGE = 'https://www.iqpuzzle.com'; |
6 | 15 |
|
| 16 | + static #HEADERS = ['Name', 'Picture', 'Status']; |
| 17 | + static #SCRAPE_URL = 'https://www.iqpuzzle.com'; |
| 18 | + |
| 19 | + #marker = new RecipeMarker(IQPuzzlesRecipe.NAME); |
| 20 | + |
| 21 | + constructor( |
| 22 | + private markdownTableFactory: MarkdownTableFactory, |
| 23 | + private markdownTableConverter: MarkdownTableConverter, |
| 24 | + private trackablesUpdater: TrackablesUpdater |
| 25 | + ) {} |
| 26 | + |
7 | 27 | async updatedListInContent(content: string): Promise<string> { |
8 | | - return content; |
| 28 | + const markdownUpdater = new RecipeMarkdownListUpdater(this.#marker); |
| 29 | + const updater = new RecipeListUpdater<IQPuzzle>( |
| 30 | + IQPuzzlesRecipe.#HEADERS, |
| 31 | + markdownUpdater, |
| 32 | + this.trackablesUpdater |
| 33 | + ); |
| 34 | + |
| 35 | + return await updater.update( |
| 36 | + content, |
| 37 | + |
| 38 | + this.#markdownTableStringToPuzzles.bind(this), |
| 39 | + this.#scrapePuzzles.bind(this), |
| 40 | + this.#puzzlesToMarkdownTableString.bind(this) |
| 41 | + ); |
| 42 | + } |
| 43 | + |
| 44 | + async #scrapePuzzles(): Promise<IQPuzzle[]> { |
| 45 | + const nameRegex = new RegExp(/(?<name>\w+)$/); // https://regex101.com/r/AuK9pb/1 |
| 46 | + const cleanedLinkRegex = new RegExp(/^(?<cleanedLink>.+?\.jpg)/); // https://regex101.com/r/fd3A6U/1 |
| 47 | + const scraper = new WebsiteScraper([IQPuzzlesRecipe.#SCRAPE_URL]); |
| 48 | + |
| 49 | + return await scraper.scrape( |
| 50 | + content => { |
| 51 | + const lists = Array.from(content.querySelectorAll('ul[data-hook="product-list-wrapper"]')); |
| 52 | + |
| 53 | + if (lists.length < 2) { |
| 54 | + return []; |
| 55 | + } |
| 56 | + |
| 57 | + const list = lists[1]; |
| 58 | + |
| 59 | + return Array.from(list.querySelectorAll('li')); |
| 60 | + }, |
| 61 | + product => { |
| 62 | + const title = product.querySelector('div[data-hook="not-image-container"] a h3')?.textContent || ''; |
| 63 | + const titleMatch = title.match(nameRegex); |
| 64 | + const titleGroups = titleMatch?.groups; |
| 65 | + |
| 66 | + const name = titleGroups != null ? titleGroups.name : title; |
| 67 | + |
| 68 | + const image = product.querySelector('a wow-image img'); |
| 69 | + const imageLink = image != null ? (image as HTMLImageElement).src : ''; |
| 70 | + const cleanedImageLinkMatch = imageLink.match(cleanedLinkRegex); |
| 71 | + const cleanedImageLink = (cleanedImageLinkMatch != null && cleanedImageLinkMatch.groups != null) |
| 72 | + ? cleanedImageLinkMatch.groups.cleanedLink |
| 73 | + : ''; |
| 74 | + |
| 75 | + return new IQPuzzle(name, cleanedImageLink); |
| 76 | + } |
| 77 | + ); |
| 78 | + } |
| 79 | + |
| 80 | + #puzzlesToMarkdownTableString(headers: string[], puzzles: IQPuzzle[]): string { |
| 81 | + const headerRow = this.markdownTableFactory.tableRowNode( |
| 82 | + headers.map(header => this.markdownTableFactory.textTableCellNode(header)) |
| 83 | + ); |
| 84 | + const puzzleRows = puzzles.map(puzzle => |
| 85 | + this.markdownTableFactory.tableRowNode([ |
| 86 | + this.markdownTableFactory.textTableCellNode(puzzle.name), |
| 87 | + this.markdownTableFactory.imageTableCellNode(puzzle.imageLink, 100), |
| 88 | + this.markdownTableFactory.textTableCellNode(puzzle.status) |
| 89 | + ]) |
| 90 | + ); |
| 91 | + const table = this.markdownTableFactory.table(headerRow, puzzleRows); |
| 92 | + |
| 93 | + return this.markdownTableConverter.tableToString(table); |
| 94 | + } |
| 95 | + |
| 96 | + #markdownTableStringToPuzzles(markdownTableString: string): IQPuzzle[] { |
| 97 | + const arrayOfArrays = this.markdownTableConverter.arrayOfArraysFromString(markdownTableString); |
| 98 | + const imageLinkRegex = new RegexFactory().imageMarkdownLinkRegex(); |
| 99 | + |
| 100 | + return arrayOfArrays.flatMap(array => { |
| 101 | + if (array.length < 5) { |
| 102 | + return []; |
| 103 | + } |
| 104 | + |
| 105 | + const name = array[0]; |
| 106 | + |
| 107 | + const image = array[1]; |
| 108 | + const imageLinkMatch = image.match(imageLinkRegex); |
| 109 | + if (imageLinkMatch == null || imageLinkMatch.groups == null) { |
| 110 | + return []; |
| 111 | + } |
| 112 | + const imageLink = imageLinkMatch.groups.link; |
| 113 | + |
| 114 | + const status = array[2]; |
| 115 | + |
| 116 | + return new IQPuzzle(name, imageLink, status); |
| 117 | + }); |
9 | 118 | } |
10 | 119 | } |
0 commit comments