UNPKG

recipe-scrapers-js

Version:
387 lines (386 loc) 13 kB
import * as cheerio from "cheerio"; import { CheerioAPI } from "cheerio"; //#region src/logger.d.ts declare enum LogLevel { VERBOSE = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4, } declare class Logger { private context; private logLevel; constructor(context: string, logLevel?: LogLevel); verbose(...args: unknown[]): void; debug(...args: unknown[]): void; log(...args: unknown[]): void; info(...args: unknown[]): void; warn(...args: unknown[]): void; error(...args: unknown[]): void; } //#endregion //#region src/abstract-plugin.d.ts declare abstract class AbstractPlugin { readonly $: CheerioAPI; /** The name of the plugin */ abstract name: string; /** The priority of the plugin */ abstract priority: number; constructor($: CheerioAPI); } //#endregion //#region src/types/recipe.interface.d.ts type List = Set<string>; type IngredientsList = List; type IngredientGroup = Map<string, List>; type Ingredients = IngredientsList | IngredientGroup; interface Link { href: string; text: string; } interface RecipeData { /** * The host name of the website the Scraper class is for. * @example 'bbcgoodfood.com' */ host: string; /** * The website name, as defined in the page's HTML. * @default null */ siteName: string | null; /** * The author of the recipe. This is typically a person's name * but can sometimes be an organization or the name of the website * the recipe came from. If the recipe does not explicitly define an author, * this should return the name of the website. * @example 'Good Food team' */ author: string; /** * The title of the recipe, usually a short sentence or phrase. */ title: string; /** * The URL to the main image associated with the recipe, * usually a photograph of the completed recipe. */ image: string; /** * The canonical URL for the scraped recipe. * The canonical URL is the direct URL (defined by the website) at which the * recipe can be found. This URL will generally not contain any query * parameters or fragments, except those required to load the recipe. */ canonicalUrl: string; /** * The language of the recipe page, as defined within the page's HTML. * This is typically a two-letter BCP 47 language code, such as 'en' for * English or 'de' for German, but may also include the dialect or * variation, such as 'en-US' for American English. * * The language code is based on BCP 47 standards. * For a comprehensive list of BCP 47 language codes, * refer to this GitHub Gist: * @ref https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1 * * @default 'en' */ language: string; /** * An list of all links found in the page HTML defined within an anchor * `<a>` element. */ links: Link[]; /** * A description of the recipe. This is normally a sentence or short * paragraph describing the recipe. Often the website defines the * description, but sometimes it has to be inferred from the page content. */ description: string; /** * The ingredients needed to make the recipe. * * This can be either a list of ingredients, or a map of ingredients, * where each key is a group name and the value is a list of ingredients. * @example * { * 'For the sauce': [ * '2 tablespoons olive oil', * '1 onion, chopped', * // ...other ingredients * ] * } */ ingredients: Ingredients; /** * A list of instructions for preparing the recipe. * This is usually a step-by-step guide on how to make the recipe. * * This list is generated by splitting the text on newline characters. * * Occasionally, a recipe will have steps that have new lines within them. * At the moment, this library cannot distinguish between a newline within * a step and a newline between steps. */ instructions: List; /** * The category or categories that the recipe belongs to. * This can contain a mix of cuisine type (for example, country names), * mealtime (breakfast/dinner/etc) and dietary properties (gluten-free, * vegetarian). The value is defined by the website, so it may overlap with * other fields (e.g. `cuisine`). * @default Set() * @example ['Italian', 'Vegetarian', 'Dinner'] */ category: List; /** * The number of items or servings the recipe will make, * including the quantity and unit of the yield. * @example '4 servings', '6 items', '12 cookies'. */ yields: string; /** * The total time (in minutes) required to complete the recipe. * @example 45 */ totalTime: number | null; /** * The time (in minutes) to cook the recipe, excluding any time * to prepare the ingredients. * @default null * @example 30 */ cookTime: number | null; /** * The time (in minutes) to prepare the ingredients for the recipe. * @default null * @example 15 */ prepTime: number | null; /** * A list of cuisines that the recipe belongs to. * This is a `Set` of strings representing the cuisine types. * @example ['Italian', 'Vegetarian'] */ cuisine: List; /** * The method of cooking the recipe. * @default null */ cookingMethod: string | null; /** * The recipe rating. When available, this is usually the average * of all the ratings given to the recipe on the website. * @example 4.5 */ ratings: number; /** * The total number of ratings contributed to the recipes rating. * @example 150 */ ratingsCount: number; /** * A list of cooking equipment needed for the recipe. * @default Set() * @example ['Mixing Bowl', 'Whisk', 'Baking Tray'] */ equipment: List; /** * Reviews of the recipe from the website. * The keys of the `Map` are the reviewer's name * and the values are the review text. * @default Map() */ reviews: Map<string, string>; /** * The nutrition information for the recipe. * Each nutrition entry is usually given per unit of yield, * i.e. per servings, or per item. * The keys of the `Map` are the nutrients (including calories) * and the values are the amount of that nutrient, including the unit. * @default Map() * @example * { * calories: '389 calories', * fatContent: '19 grams fat', * // ... * } */ nutrients: Map<string, string>; /** * The dietary restrictions specified by the recipe. * @default Set() * @example ['Vegan Diet', 'Vegetarian Diet'] */ dietaryRestrictions: List; /** * A list of keywords associated with a recipe. * @example ['easy', 'quick', 'dinner'] */ keywords: List; } /** * The fields of a recipe that can be extracted by scraping the HTML. * The 'host' field is omitted because it is a static field * that is not scraped. */ type RecipeFields = Omit<RecipeData, 'host'>; /** * The fields that aren't required for a recipe to be valid. */ type OptionalRecipeFields = Pick<RecipeFields, 'siteName' | 'category' | 'cookTime' | 'prepTime' | 'totalTime' | 'cuisine' | 'cookingMethod' | 'ratings' | 'ratingsCount' | 'equipment' | 'reviews' | 'nutrients' | 'dietaryRestrictions' | 'keywords'>; interface RecipeObject extends Omit<RecipeData, 'category' | 'cuisine' | 'dietaryRestrictions' | 'equipment' | 'ingredients' | 'instructions' | 'keywords' | 'nutrients' | 'reviews'> { category: string[]; cuisine: string[]; dietaryRestrictions: string[]; equipment: string[]; ingredients: string[] | Record<string, string[]>; instructions: string[]; keywords: string[]; nutrients: Record<string, string>; reviews: Record<string, string>; } //#endregion //#region src/abstract-extractor-plugin.d.ts declare abstract class ExtractorPlugin extends AbstractPlugin { /** Whether this plugin can extract the given field */ abstract supports(field: keyof RecipeFields): boolean; /** * Extracts the field from the cheerio root. * @param field The field to extract * @returns The extracted field value */ abstract extract<Key extends keyof RecipeFields>(field: Key): RecipeFields[Key] | Promise<RecipeFields[Key]>; } //#endregion //#region src/abstract-postprocessor-plugin.d.ts declare abstract class PostProcessorPlugin { /** The name of the plugin */ abstract name: string; /** The priority of the plugin */ abstract priority: number; abstract shouldProcess<Key extends keyof RecipeFields>(field: Key): boolean; abstract process<T>(field: keyof RecipeFields, value: T): T | Promise<T>; } //#endregion //#region src/plugin-manager.d.ts declare class PluginManager { private extractorPlugins; private postProcessorPlugins; constructor(baseExtractors: ExtractorPlugin[], basePostProcessors: PostProcessorPlugin[], extraExtractors?: ExtractorPlugin[], extraPostProcessors?: PostProcessorPlugin[]); getExtractors(): ExtractorPlugin[]; getPostProcessors(): PostProcessorPlugin[]; } //#endregion //#region src/recipe-extractor.d.ts declare class RecipeExtractor { private plugins; private readonly scraperName; private readonly options; private readonly logger; constructor(plugins: ExtractorPlugin[], scraperName: string, options?: { logLevel?: LogLevel; }); private getContext; extract<Key extends keyof RecipeFields>(field: Key, extractor?: (prevValue: RecipeFields[Key] | undefined) => RecipeFields[Key] | Promise<RecipeFields[Key]>): Promise<RecipeFields[Key]>; } //#endregion //#region src/types/scraper.interface.d.ts interface ScraperOptions { /** * Additional extractors to be used by the scraper. * These extractors will be added to the default set of extractors. * Extractors are applied according to their priority. * Higher priority extractors will run first. * @default [] */ extraExtractors?: ExtractorPlugin[]; /** * Additional post-processors to be used by the scraper. * These post-processors will be added to the default set of post-processors. * Post-processors are applied after all extractors have run. * Post-processors are also applied according to their priority. * Higher priority post-processors will run first. * @default [] */ extraPostProcessors?: PostProcessorPlugin[]; /** * Whether link scraping is enabled. * @default false */ linksEnabled?: boolean; /** * Logging level for the scraper. * This controls the verbosity of logs produced by the scraper. * @default LogLevel.Warn */ logLevel?: LogLevel; } //#endregion //#region src/abstract-scraper.d.ts declare abstract class AbstractScraper { protected readonly html: string; protected readonly url: string; protected readonly options: ScraperOptions; protected readonly logger: Logger; protected readonly pluginManager: PluginManager; protected readonly recipeExtractor: RecipeExtractor; readonly $: cheerio.CheerioAPI; recipeData: RecipeData | null; constructor(html: string, url: string, options?: ScraperOptions); /** * Site-specific extractors (implemented by subclasses) * Each extractor is a function that takes the previous value * returned by the extractor chain (if any) and returns the field value. */ abstract extractors: { [K in keyof RecipeFields]?: (prevValue: RecipeFields[K] | undefined) => RecipeFields[K] | Promise<RecipeFields[K]> }; /** * Main extraction method - tries site-specific first, then plugins, * then applies post-processing. */ extract<Key extends keyof RecipeFields>(field: Key): Promise<RecipeFields[Key]>; /** * Static method to get the host of the scraper. * This should be implemented by subclasses to return the specific host. */ static host(): string; /***************************************************************************** * Default implementations for common fields that can be overridden * by subclasses. ****************************************************************************/ canonicalUrl(): RecipeFields['canonicalUrl']; language(): RecipeFields['language']; links(): RecipeFields['links']; /** * Scrape's the recipe and caches the data. */ scrape(): Promise<RecipeData>; /** * Converts the scraper's data into a JSON-serializable object. */ toObject(): Promise<RecipeObject>; } //#endregion //#region src/scrapers/allrecipes.d.ts declare class AllRecipes extends AbstractScraper { static host(): string; extractors: {}; } //#endregion //#region src/scrapers/_index.d.ts /** * A map of all scrapers. */ declare const scrapers: { readonly [x: string]: typeof AllRecipes; }; //#endregion //#region src/index.d.ts /** * Returns a scraper class for the given URL, if implemented. */ declare function getScraper(url: string): typeof AllRecipes; //#endregion export { ExtractorPlugin, IngredientGroup, Ingredients, IngredientsList, Link, List, LogLevel, Logger, OptionalRecipeFields, PostProcessorPlugin, RecipeData, RecipeFields, RecipeObject, ScraperOptions, getScraper, scrapers };