recipe_scrapers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +20 -0
  4. data/README.md +54 -0
  5. data/lib/recipe_scrapers/configuration.rb +119 -0
  6. data/lib/recipe_scrapers/declaration.rb +85 -0
  7. data/lib/recipe_scrapers/error_tracker.rb +14 -0
  8. data/lib/recipe_scrapers/errors.rb +16 -0
  9. data/lib/recipe_scrapers/http/adapter.rb +50 -0
  10. data/lib/recipe_scrapers/http/address_guard.rb +90 -0
  11. data/lib/recipe_scrapers/http/body_limit.rb +45 -0
  12. data/lib/recipe_scrapers/http/encoding.rb +48 -0
  13. data/lib/recipe_scrapers/http/follow_redirects.rb +75 -0
  14. data/lib/recipe_scrapers/models/ingredient.rb +20 -0
  15. data/lib/recipe_scrapers/models/ingredient_group.rb +20 -0
  16. data/lib/recipe_scrapers/models/nutrient.rb +19 -0
  17. data/lib/recipe_scrapers/models/recipe.rb +125 -0
  18. data/lib/recipe_scrapers/parsers/chain.rb +52 -0
  19. data/lib/recipe_scrapers/parsers/durations.rb +70 -0
  20. data/lib/recipe_scrapers/parsers/ingredients.rb +144 -0
  21. data/lib/recipe_scrapers/parsers/nutrients.rb +47 -0
  22. data/lib/recipe_scrapers/parsers/quantity.rb +46 -0
  23. data/lib/recipe_scrapers/parsers/ratings.rb +26 -0
  24. data/lib/recipe_scrapers/parsers/vocabulary/common.yml +7 -0
  25. data/lib/recipe_scrapers/parsers/vocabulary/de.yml +5 -0
  26. data/lib/recipe_scrapers/parsers/vocabulary/en.yml +26 -0
  27. data/lib/recipe_scrapers/parsers/vocabulary/es.yml +4 -0
  28. data/lib/recipe_scrapers/parsers/vocabulary/fr.yml +5 -0
  29. data/lib/recipe_scrapers/parsers/vocabulary/hu.yml +3 -0
  30. data/lib/recipe_scrapers/parsers/vocabulary/it.yml +4 -0
  31. data/lib/recipe_scrapers/parsers/vocabulary/ja.yml +2 -0
  32. data/lib/recipe_scrapers/parsers/vocabulary/nl.yml +4 -0
  33. data/lib/recipe_scrapers/parsers/vocabulary/pt.yml +8 -0
  34. data/lib/recipe_scrapers/parsers/vocabulary/ru.yml +7 -0
  35. data/lib/recipe_scrapers/parsers/vocabulary/scandinavian.yml +3 -0
  36. data/lib/recipe_scrapers/parsers/vocabulary/sk.yml +3 -0
  37. data/lib/recipe_scrapers/parsers/vocabulary/tr.yml +2 -0
  38. data/lib/recipe_scrapers/parsers/vocabulary.rb +120 -0
  39. data/lib/recipe_scrapers/parsers/yields.rb +30 -0
  40. data/lib/recipe_scrapers/registry.rb +88 -0
  41. data/lib/recipe_scrapers/scraper/parsed_fields.rb +69 -0
  42. data/lib/recipe_scrapers/scraper.rb +199 -0
  43. data/lib/recipe_scrapers/site_path.rb +21 -0
  44. data/lib/recipe_scrapers/sites/au/bestrecipes.rb +3 -0
  45. data/lib/recipe_scrapers/sites/be/15gram.rb +3 -0
  46. data/lib/recipe_scrapers/sites/br/panelinha.rb +3 -0
  47. data/lib/recipe_scrapers/sites/br/receitasnestle.rb +3 -0
  48. data/lib/recipe_scrapers/sites/br/receiteria.rb +3 -0
  49. data/lib/recipe_scrapers/sites/br/tudogostoso.rb +3 -0
  50. data/lib/recipe_scrapers/sites/ch/bettybossi.rb +3 -0
  51. data/lib/recipe_scrapers/sites/ch/migusto.rb +3 -0
  52. data/lib/recipe_scrapers/sites/ch/swissmilk.rb +3 -0
  53. data/lib/recipe_scrapers/sites/co/tasty.rb +3 -0
  54. data/lib/recipe_scrapers/sites/com/365daysofbakingandmore.rb +3 -0
  55. data/lib/recipe_scrapers/sites/com/40aprons.rb +3 -0
  56. data/lib/recipe_scrapers/sites/com/750g.rb +3 -0
  57. data/lib/recipe_scrapers/sites/com/abeautifulmess.rb +3 -0
  58. data/lib/recipe_scrapers/sites/com/abuelascounter.rb +3 -0
  59. data/lib/recipe_scrapers/sites/com/acozykitchen.rb +3 -0
  60. data/lib/recipe_scrapers/sites/com/addapinch.rb +3 -0
  61. data/lib/recipe_scrapers/sites/com/adozensundays.rb +3 -0
  62. data/lib/recipe_scrapers/sites/com/aflavorjournal.rb +3 -0
  63. data/lib/recipe_scrapers/sites/com/afullliving.rb +3 -0
  64. data/lib/recipe_scrapers/sites/com/ahealthysliceoflife.rb +5 -0
  65. data/lib/recipe_scrapers/sites/com/alexandracooks.rb +3 -0
  66. data/lib/recipe_scrapers/sites/com/allnutritious.rb +3 -0
  67. data/lib/recipe_scrapers/sites/com/allthehealthythings.rb +3 -0
  68. data/lib/recipe_scrapers/sites/com/altonbrown.rb +3 -0
  69. data/lib/recipe_scrapers/sites/com/amazingribs.rb +3 -0
  70. data/lib/recipe_scrapers/sites/com/amberskitchencooks.rb +3 -0
  71. data/lib/recipe_scrapers/sites/com/andy_cooks.rb +3 -0
  72. data/lib/recipe_scrapers/sites/com/anitalianinmykitchen.rb +3 -0
  73. data/lib/recipe_scrapers/sites/com/app.rb +27 -0
  74. data/lib/recipe_scrapers/sites/com/asweetpeachef.rb +3 -0
  75. data/lib/recipe_scrapers/sites/com/aubreyskitchen.rb +3 -0
  76. data/lib/recipe_scrapers/sites/com/averiecooks.rb +3 -0
  77. data/lib/recipe_scrapers/sites/com/bake_eat_repeat.rb +3 -0
  78. data/lib/recipe_scrapers/sites/com/bakerbynature.rb +3 -0
  79. data/lib/recipe_scrapers/sites/com/bakewithzoha.rb +3 -0
  80. data/lib/recipe_scrapers/sites/com/baking_sense.rb +3 -0
  81. data/lib/recipe_scrapers/sites/com/bakingmischief.rb +3 -0
  82. data/lib/recipe_scrapers/sites/com/barefeetinthekitchen.rb +3 -0
  83. data/lib/recipe_scrapers/sites/com/barefootinthepines.rb +3 -0
  84. data/lib/recipe_scrapers/sites/com/bbcgoodfood.rb +3 -0
  85. data/lib/recipe_scrapers/sites/com/belleofthekitchen.rb +3 -0
  86. data/lib/recipe_scrapers/sites/com/betterfoodguru.rb +3 -0
  87. data/lib/recipe_scrapers/sites/com/bettycrocker.rb +3 -0
  88. data/lib/recipe_scrapers/sites/com/beyondfrosting.rb +3 -0
  89. data/lib/recipe_scrapers/sites/com/biancazapatka.rb +3 -0
  90. data/lib/recipe_scrapers/sites/com/biggerbolderbaking.rb +3 -0
  91. data/lib/recipe_scrapers/sites/com/billyparisi.rb +3 -0
  92. data/lib/recipe_scrapers/sites/com/bitsofcarey.rb +3 -0
  93. data/lib/recipe_scrapers/sites/com/blogghetti.rb +3 -0
  94. data/lib/recipe_scrapers/sites/com/bluejeanchef.rb +3 -0
  95. data/lib/recipe_scrapers/sites/com/bonappetit.rb +3 -0
  96. data/lib/recipe_scrapers/sites/com/bonviveur.rb +3 -0
  97. data/lib/recipe_scrapers/sites/com/bowlofdelicious.rb +3 -0
  98. data/lib/recipe_scrapers/sites/com/breadtopia.rb +3 -0
  99. data/lib/recipe_scrapers/sites/com/brokenovenbaking.rb +3 -0
  100. data/lib/recipe_scrapers/sites/com/budgetbytes.rb +3 -0
  101. data/lib/recipe_scrapers/sites/com/cafedelites.rb +3 -0
  102. data/lib/recipe_scrapers/sites/com/cakemehometonight.rb +3 -0
  103. data/lib/recipe_scrapers/sites/com/cakewhiz.rb +3 -0
  104. data/lib/recipe_scrapers/sites/com/cambreabakes.rb +3 -0
  105. data/lib/recipe_scrapers/sites/com/carlsbadcravings.rb +3 -0
  106. data/lib/recipe_scrapers/sites/com/castironskilletcooking.rb +3 -0
  107. data/lib/recipe_scrapers/sites/com/celebratingsweets.rb +3 -0
  108. data/lib/recipe_scrapers/sites/com/chefjackovens.rb +3 -0
  109. data/lib/recipe_scrapers/sites/com/chefsavvy.rb +3 -0
  110. data/lib/recipe_scrapers/sites/com/chewoutloud.rb +3 -0
  111. data/lib/recipe_scrapers/sites/com/chocolatewithgrace.rb +3 -0
  112. data/lib/recipe_scrapers/sites/com/ciaoflorentina.rb +3 -0
  113. data/lib/recipe_scrapers/sites/com/closetcooking.rb +3 -0
  114. data/lib/recipe_scrapers/sites/com/cloudykitchen.rb +3 -0
  115. data/lib/recipe_scrapers/sites/com/coleycooks.rb +3 -0
  116. data/lib/recipe_scrapers/sites/com/cookedandloved.rb +3 -0
  117. data/lib/recipe_scrapers/sites/com/cookfastrecipes.rb +3 -0
  118. data/lib/recipe_scrapers/sites/com/cookieandkate.rb +3 -0
  119. data/lib/recipe_scrapers/sites/com/cookiesandcups.rb +3 -0
  120. data/lib/recipe_scrapers/sites/com/cooking.rb +3 -0
  121. data/lib/recipe_scrapers/sites/com/cookingclassy.rb +3 -0
  122. data/lib/recipe_scrapers/sites/com/cookinglsl.rb +3 -0
  123. data/lib/recipe_scrapers/sites/com/cookingwithjanica.rb +3 -0
  124. data/lib/recipe_scrapers/sites/com/cookpad.rb +3 -0
  125. data/lib/recipe_scrapers/sites/com/cookwell.rb +3 -0
  126. data/lib/recipe_scrapers/sites/com/cookwithdana.rb +3 -0
  127. data/lib/recipe_scrapers/sites/com/copykat.rb +3 -0
  128. data/lib/recipe_scrapers/sites/com/countryliving.rb +3 -0
  129. data/lib/recipe_scrapers/sites/com/crazyforcrust.rb +3 -0
  130. data/lib/recipe_scrapers/sites/com/creativecanning.rb +3 -0
  131. data/lib/recipe_scrapers/sites/com/cuisineaz.rb +3 -0
  132. data/lib/recipe_scrapers/sites/com/daringgourmet.rb +3 -0
  133. data/lib/recipe_scrapers/sites/com/dashfordinner.rb +3 -0
  134. data/lib/recipe_scrapers/sites/com/davidlebovitz.rb +3 -0
  135. data/lib/recipe_scrapers/sites/com/deliciouslyella.rb +3 -0
  136. data/lib/recipe_scrapers/sites/com/deliciouslysprinkled.rb +3 -0
  137. data/lib/recipe_scrapers/sites/com/delish.rb +3 -0
  138. data/lib/recipe_scrapers/sites/com/delscookingtwist.rb +3 -0
  139. data/lib/recipe_scrapers/sites/com/dinneratthezoo.rb +3 -0
  140. data/lib/recipe_scrapers/sites/com/directoalpaladar.rb +3 -0
  141. data/lib/recipe_scrapers/sites/com/domesticate_me.rb +3 -0
  142. data/lib/recipe_scrapers/sites/com/downshiftology.rb +3 -0
  143. data/lib/recipe_scrapers/sites/com/eatingbirdfood.rb +3 -0
  144. data/lib/recipe_scrapers/sites/com/eatingeuropean.rb +3 -0
  145. data/lib/recipe_scrapers/sites/com/eatingonadime.rb +3 -0
  146. data/lib/recipe_scrapers/sites/com/eatliverun.rb +3 -0
  147. data/lib/recipe_scrapers/sites/com/eatthismuch.rb +3 -0
  148. data/lib/recipe_scrapers/sites/com/elavegan.rb +3 -0
  149. data/lib/recipe_scrapers/sites/com/emilybites.rb +3 -0
  150. data/lib/recipe_scrapers/sites/com/erinliveswhole.rb +3 -0
  151. data/lib/recipe_scrapers/sites/com/erinscozykitchen.rb +3 -0
  152. data/lib/recipe_scrapers/sites/com/errenskitchen.rb +3 -0
  153. data/lib/recipe_scrapers/sites/com/everyday_delicious.rb +3 -0
  154. data/lib/recipe_scrapers/sites/com/everydaypie.rb +3 -0
  155. data/lib/recipe_scrapers/sites/com/evolvingtable.rb +3 -0
  156. data/lib/recipe_scrapers/sites/com/familyfoodonthetable.rb +3 -0
  157. data/lib/recipe_scrapers/sites/com/familyspice.rb +3 -0
  158. data/lib/recipe_scrapers/sites/com/fantabulosity.rb +3 -0
  159. data/lib/recipe_scrapers/sites/com/farmhouseonboone.rb +3 -0
  160. data/lib/recipe_scrapers/sites/com/farmtojar.rb +3 -0
  161. data/lib/recipe_scrapers/sites/com/feastingathome.rb +3 -0
  162. data/lib/recipe_scrapers/sites/com/fifteenspatulas.rb +3 -0
  163. data/lib/recipe_scrapers/sites/com/figjar.rb +3 -0
  164. data/lib/recipe_scrapers/sites/com/finedininglovers.rb +3 -0
  165. data/lib/recipe_scrapers/sites/com/fithealthymacros.rb +3 -0
  166. data/lib/recipe_scrapers/sites/com/fitslowcookerqueen.rb +3 -0
  167. data/lib/recipe_scrapers/sites/com/food.rb +3 -0
  168. data/lib/recipe_scrapers/sites/com/foodbymaria.rb +3 -0
  169. data/lib/recipe_scrapers/sites/com/foodfidelity.rb +3 -0
  170. data/lib/recipe_scrapers/sites/com/foodrepublic.rb +3 -0
  171. data/lib/recipe_scrapers/sites/com/forksoverknives.rb +3 -0
  172. data/lib/recipe_scrapers/sites/com/garlicandzest.rb +3 -0
  173. data/lib/recipe_scrapers/sites/com/garnishandglaze.rb +3 -0
  174. data/lib/recipe_scrapers/sites/com/gimmesomeoven.rb +3 -0
  175. data/lib/recipe_scrapers/sites/com/girlgonegourmet.rb +3 -0
  176. data/lib/recipe_scrapers/sites/com/girlversusdough.rb +3 -0
  177. data/lib/recipe_scrapers/sites/com/gloriousrecipes.rb +3 -0
  178. data/lib/recipe_scrapers/sites/com/glutenfreeonashoestring.rb +3 -0
  179. data/lib/recipe_scrapers/sites/com/gonnawantseconds.rb +3 -0
  180. data/lib/recipe_scrapers/sites/com/goodfooddiscoveries.rb +3 -0
  181. data/lib/recipe_scrapers/sites/com/goodhousekeeping.rb +3 -0
  182. data/lib/recipe_scrapers/sites/com/grandbaby_cakes.rb +3 -0
  183. data/lib/recipe_scrapers/sites/com/greatbritishchefs.rb +3 -0
  184. data/lib/recipe_scrapers/sites/com/halfbakedharvest.rb +3 -0
  185. data/lib/recipe_scrapers/sites/com/hassanchef.rb +3 -0
  186. data/lib/recipe_scrapers/sites/com/headbangerskitchen.rb +3 -0
  187. data/lib/recipe_scrapers/sites/com/healthywithachanceofsprinkles.rb +3 -0
  188. data/lib/recipe_scrapers/sites/com/heatherchristo.rb +3 -0
  189. data/lib/recipe_scrapers/sites/com/hilahcooking.rb +3 -0
  190. data/lib/recipe_scrapers/sites/com/hogarmania.rb +3 -0
  191. data/lib/recipe_scrapers/sites/com/homeandplate.rb +3 -0
  192. data/lib/recipe_scrapers/sites/com/hostthetoast.rb +3 -0
  193. data/lib/recipe_scrapers/sites/com/houseofnasheats.rb +3 -0
  194. data/lib/recipe_scrapers/sites/com/houseofyumm.rb +3 -0
  195. data/lib/recipe_scrapers/sites/com/iamafoodblog.rb +3 -0
  196. data/lib/recipe_scrapers/sites/com/im_worthy.rb +3 -0
  197. data/lib/recipe_scrapers/sites/com/inbloombakery.rb +3 -0
  198. data/lib/recipe_scrapers/sites/com/indianhealthyrecipes.rb +3 -0
  199. data/lib/recipe_scrapers/sites/com/ingoodflavor.rb +3 -0
  200. data/lib/recipe_scrapers/sites/com/inspiralized.rb +3 -0
  201. data/lib/recipe_scrapers/sites/com/itdoesnttastelikechicken.rb +3 -0
  202. data/lib/recipe_scrapers/sites/com/itsnotaboutnutrition.rb +3 -0
  203. data/lib/recipe_scrapers/sites/com/izzycooking.rb +3 -0
  204. data/lib/recipe_scrapers/sites/com/jennycancook.rb +3 -0
  205. data/lib/recipe_scrapers/sites/com/jimcooksfoodgood.rb +3 -0
  206. data/lib/recipe_scrapers/sites/com/jocooks.rb +3 -0
  207. data/lib/recipe_scrapers/sites/com/joyfullymad.rb +3 -0
  208. data/lib/recipe_scrapers/sites/com/joythebaker.rb +3 -0
  209. data/lib/recipe_scrapers/sites/com/juliasalbum.rb +3 -0
  210. data/lib/recipe_scrapers/sites/com/justataste.rb +3 -0
  211. data/lib/recipe_scrapers/sites/com/justinesnacks.rb +3 -0
  212. data/lib/recipe_scrapers/sites/com/kennethtemple.rb +3 -0
  213. data/lib/recipe_scrapers/sites/com/keytomylime.rb +3 -0
  214. data/lib/recipe_scrapers/sites/com/kitchendreaming.rb +3 -0
  215. data/lib/recipe_scrapers/sites/com/kitchenstories.rb +3 -0
  216. data/lib/recipe_scrapers/sites/com/kristineskitchenblog.rb +3 -0
  217. data/lib/recipe_scrapers/sites/com/krollskorner.rb +3 -0
  218. data/lib/recipe_scrapers/sites/com/lanascooking.rb +3 -0
  219. data/lib/recipe_scrapers/sites/com/laurenslatest.rb +3 -0
  220. data/lib/recipe_scrapers/sites/com/lazycatkitchen.rb +3 -0
  221. data/lib/recipe_scrapers/sites/com/lecremedelacrumb.rb +3 -0
  222. data/lib/recipe_scrapers/sites/com/leitesculinaria.rb +3 -0
  223. data/lib/recipe_scrapers/sites/com/lekkerensimpel.rb +3 -0
  224. data/lib/recipe_scrapers/sites/com/letscampsmore.rb +3 -0
  225. data/lib/recipe_scrapers/sites/com/lifestyleofafoodie.rb +3 -0
  226. data/lib/recipe_scrapers/sites/com/littleferrarokitchen.rb +3 -0
  227. data/lib/recipe_scrapers/sites/com/littlespicejar.rb +3 -0
  228. data/lib/recipe_scrapers/sites/com/littlespoonfarm.rb +3 -0
  229. data/lib/recipe_scrapers/sites/com/littlesunnykitchen.rb +3 -0
  230. data/lib/recipe_scrapers/sites/com/livelytable.rb +3 -0
  231. data/lib/recipe_scrapers/sites/com/lolascocina.rb +3 -0
  232. data/lib/recipe_scrapers/sites/com/lovingitvegan.rb +3 -0
  233. data/lib/recipe_scrapers/sites/com/madsvin.rb +3 -0
  234. data/lib/recipe_scrapers/sites/com/makeitdairyfree.rb +3 -0
  235. data/lib/recipe_scrapers/sites/com/mealprepmanual.rb +3 -0
  236. data/lib/recipe_scrapers/sites/com/meganvskitchen.rb +3 -0
  237. data/lib/recipe_scrapers/sites/com/melissaknorris.rb +3 -0
  238. data/lib/recipe_scrapers/sites/com/melskitchencafe.rb +3 -0
  239. data/lib/recipe_scrapers/sites/com/mexicanplease.rb +3 -0
  240. data/lib/recipe_scrapers/sites/com/minimalistbaker.rb +3 -0
  241. data/lib/recipe_scrapers/sites/com/ministryofcurry.rb +3 -0
  242. data/lib/recipe_scrapers/sites/com/modernhoney.rb +3 -0
  243. data/lib/recipe_scrapers/sites/com/momontimeout.rb +3 -0
  244. data/lib/recipe_scrapers/sites/com/momswithcrockpots.rb +3 -0
  245. data/lib/recipe_scrapers/sites/com/motherthyme.rb +3 -0
  246. data/lib/recipe_scrapers/sites/com/mybakingaddiction.rb +3 -0
  247. data/lib/recipe_scrapers/sites/com/myjewishlearning.rb +3 -0
  248. data/lib/recipe_scrapers/sites/com/mykidslickthebowl.rb +3 -0
  249. data/lib/recipe_scrapers/sites/com/mykoreankitchen.rb +3 -0
  250. data/lib/recipe_scrapers/sites/com/myriadrecipes.rb +3 -0
  251. data/lib/recipe_scrapers/sites/com/myvegetarianroots.rb +3 -0
  252. data/lib/recipe_scrapers/sites/com/natashaskitchen.rb +3 -0
  253. data/lib/recipe_scrapers/sites/com/naturallyella.rb +3 -0
  254. data/lib/recipe_scrapers/sites/com/noracooks.rb +3 -0
  255. data/lib/recipe_scrapers/sites/com/norecipes.rb +3 -0
  256. data/lib/recipe_scrapers/sites/com/notenoughcinnamon.rb +3 -0
  257. data/lib/recipe_scrapers/sites/com/nourishedbynutrition.rb +3 -0
  258. data/lib/recipe_scrapers/sites/com/number_2_pencil.rb +3 -0
  259. data/lib/recipe_scrapers/sites/com/onceuponachef.rb +3 -0
  260. data/lib/recipe_scrapers/sites/com/onesweetappetite.rb +3 -0
  261. data/lib/recipe_scrapers/sites/com/organicallyaddison.rb +3 -0
  262. data/lib/recipe_scrapers/sites/com/ourbestbites.rb +3 -0
  263. data/lib/recipe_scrapers/sites/com/paleorunningmomma.rb +3 -0
  264. data/lib/recipe_scrapers/sites/com/panlasangpinoy.rb +3 -0
  265. data/lib/recipe_scrapers/sites/com/peelwithzeal.rb +3 -0
  266. data/lib/recipe_scrapers/sites/com/pequerecetas.rb +3 -0
  267. data/lib/recipe_scrapers/sites/com/pilipinasrecipes.rb +3 -0
  268. data/lib/recipe_scrapers/sites/com/pinkowlkitchen.rb +3 -0
  269. data/lib/recipe_scrapers/sites/com/plantyou.rb +3 -0
  270. data/lib/recipe_scrapers/sites/com/platingpixels.rb +3 -0
  271. data/lib/recipe_scrapers/sites/com/platingsandpairings.rb +3 -0
  272. data/lib/recipe_scrapers/sites/com/plowingthroughlife.rb +3 -0
  273. data/lib/recipe_scrapers/sites/com/polishfoodies.rb +3 -0
  274. data/lib/recipe_scrapers/sites/com/practicalselfreliance.rb +3 -0
  275. data/lib/recipe_scrapers/sites/com/preppykitchen.rb +3 -0
  276. data/lib/recipe_scrapers/sites/com/pressureluckcooking.rb +3 -0
  277. data/lib/recipe_scrapers/sites/com/purelypope.rb +3 -0
  278. data/lib/recipe_scrapers/sites/com/rachlmansfield.rb +3 -0
  279. data/lib/recipe_scrapers/sites/com/rainbowplantlife.rb +3 -0
  280. data/lib/recipe_scrapers/sites/com/realfoodwell.rb +3 -0
  281. data/lib/recipe_scrapers/sites/com/realmomnutrition.rb +3 -0
  282. data/lib/recipe_scrapers/sites/com/receitas.rb +3 -0
  283. data/lib/recipe_scrapers/sites/com/recipeforperfection.rb +3 -0
  284. data/lib/recipe_scrapers/sites/com/recipegirl.rb +3 -0
  285. data/lib/recipe_scrapers/sites/com/reciperunner.rb +3 -0
  286. data/lib/recipe_scrapers/sites/com/recipetineats.rb +3 -0
  287. data/lib/recipe_scrapers/sites/com/redhousespice.rb +3 -0
  288. data/lib/recipe_scrapers/sites/com/ricardocuisine.rb +3 -0
  289. data/lib/recipe_scrapers/sites/com/sallysbakingaddiction.rb +3 -0
  290. data/lib/recipe_scrapers/sites/com/saltpepperskillet.rb +3 -0
  291. data/lib/recipe_scrapers/sites/com/sandwichtribunal.rb +3 -0
  292. data/lib/recipe_scrapers/sites/com/savoringthegood.rb +3 -0
  293. data/lib/recipe_scrapers/sites/com/savorynothings.rb +3 -0
  294. data/lib/recipe_scrapers/sites/com/savorythoughts.rb +3 -0
  295. data/lib/recipe_scrapers/sites/com/scrummylane.rb +3 -0
  296. data/lib/recipe_scrapers/sites/com/shelikesfood.rb +3 -0
  297. data/lib/recipe_scrapers/sites/com/simple_veganista.rb +3 -0
  298. data/lib/recipe_scrapers/sites/com/simplehomeedit.rb +3 -0
  299. data/lib/recipe_scrapers/sites/com/simplyquinoa.rb +3 -0
  300. data/lib/recipe_scrapers/sites/com/simplywhisked.rb +3 -0
  301. data/lib/recipe_scrapers/sites/com/sipandfeast.rb +3 -0
  302. data/lib/recipe_scrapers/sites/com/sizzlefish.rb +3 -0
  303. data/lib/recipe_scrapers/sites/com/skinnytaste.rb +3 -0
  304. data/lib/recipe_scrapers/sites/com/smalltownwoman.rb +3 -0
  305. data/lib/recipe_scrapers/sites/com/somuchfoodblog.rb +3 -0
  306. data/lib/recipe_scrapers/sites/com/southernbite.rb +3 -0
  307. data/lib/recipe_scrapers/sites/com/spainonafork.rb +3 -0
  308. data/lib/recipe_scrapers/sites/com/spendwithpennies.rb +3 -0
  309. data/lib/recipe_scrapers/sites/com/spicysouthernkitchen.rb +3 -0
  310. data/lib/recipe_scrapers/sites/com/stacyling.rb +3 -0
  311. data/lib/recipe_scrapers/sites/com/staysnatched.rb +3 -0
  312. data/lib/recipe_scrapers/sites/com/steamykitchen.rb +3 -0
  313. data/lib/recipe_scrapers/sites/com/strongrfastr.rb +3 -0
  314. data/lib/recipe_scrapers/sites/com/sudachirecipes.rb +3 -0
  315. data/lib/recipe_scrapers/sites/com/sugarhero.rb +3 -0
  316. data/lib/recipe_scrapers/sites/com/sugarmaplefarmhouse.rb +3 -0
  317. data/lib/recipe_scrapers/sites/com/sugarspunrun.rb +3 -0
  318. data/lib/recipe_scrapers/sites/com/sunset.rb +3 -0
  319. data/lib/recipe_scrapers/sites/com/sweetcsdesigns.rb +3 -0
  320. data/lib/recipe_scrapers/sites/com/tableanddish.rb +3 -0
  321. data/lib/recipe_scrapers/sites/com/tasteandtellblog.rb +3 -0
  322. data/lib/recipe_scrapers/sites/com/tasteofhome.rb +3 -0
  323. data/lib/recipe_scrapers/sites/com/tastesbetterfromscratch.rb +3 -0
  324. data/lib/recipe_scrapers/sites/com/tastesoflizzyt.rb +3 -0
  325. data/lib/recipe_scrapers/sites/com/tastyoven.rb +3 -0
  326. data/lib/recipe_scrapers/sites/com/tatyanaseverydayfood.rb +3 -0
  327. data/lib/recipe_scrapers/sites/com/teakandthyme.rb +3 -0
  328. data/lib/recipe_scrapers/sites/com/thealmondeater.rb +3 -0
  329. data/lib/recipe_scrapers/sites/com/thebigmansworld.rb +3 -0
  330. data/lib/recipe_scrapers/sites/com/theclevercarrot.rb +3 -0
  331. data/lib/recipe_scrapers/sites/com/thecookierookie.rb +3 -0
  332. data/lib/recipe_scrapers/sites/com/thefoodcharlatan.rb +3 -0
  333. data/lib/recipe_scrapers/sites/com/thefoodietakesflight.rb +3 -0
  334. data/lib/recipe_scrapers/sites/com/theglutenfreeaustrian.rb +3 -0
  335. data/lib/recipe_scrapers/sites/com/theguardian.rb +3 -0
  336. data/lib/recipe_scrapers/sites/com/theicecreamconfectionals.rb +3 -0
  337. data/lib/recipe_scrapers/sites/com/thekitchenmagpie.rb +3 -0
  338. data/lib/recipe_scrapers/sites/com/themagicalslowcooker.rb +3 -0
  339. data/lib/recipe_scrapers/sites/com/themediterraneandish.rb +3 -0
  340. data/lib/recipe_scrapers/sites/com/themodernproper.rb +3 -0
  341. data/lib/recipe_scrapers/sites/com/thepalatablelife.rb +3 -0
  342. data/lib/recipe_scrapers/sites/com/thepioneerwoman.rb +3 -0
  343. data/lib/recipe_scrapers/sites/com/theppk.rb +3 -0
  344. data/lib/recipe_scrapers/sites/com/therecipecritic.rb +3 -0
  345. data/lib/recipe_scrapers/sites/com/thesaltymarshmallow.rb +3 -0
  346. data/lib/recipe_scrapers/sites/com/thespicetrain.rb +3 -0
  347. data/lib/recipe_scrapers/sites/com/thesuburbansoapbox.rb +3 -0
  348. data/lib/recipe_scrapers/sites/com/thevintagemixer.rb +3 -0
  349. data/lib/recipe_scrapers/sites/com/thewoodenskillet.rb +3 -0
  350. data/lib/recipe_scrapers/sites/com/thinlicious.rb +3 -0
  351. data/lib/recipe_scrapers/sites/com/thishealthytable.rb +3 -0
  352. data/lib/recipe_scrapers/sites/com/toriavey.rb +3 -0
  353. data/lib/recipe_scrapers/sites/com/twopeasandtheirpod.rb +3 -0
  354. data/lib/recipe_scrapers/sites/com/unsophisticook.rb +3 -0
  355. data/lib/recipe_scrapers/sites/com/valentinascorner.rb +3 -0
  356. data/lib/recipe_scrapers/sites/com/vanillaandbean.rb +3 -0
  357. data/lib/recipe_scrapers/sites/com/veganricha.rb +3 -0
  358. data/lib/recipe_scrapers/sites/com/vegrecipesofindia.rb +3 -0
  359. data/lib/recipe_scrapers/sites/com/wearenotmartha.rb +3 -0
  360. data/lib/recipe_scrapers/sites/com/wedishitup.rb +3 -0
  361. data/lib/recipe_scrapers/sites/com/wellplated.rb +3 -0
  362. data/lib/recipe_scrapers/sites/com/whatsgabycooking.rb +3 -0
  363. data/lib/recipe_scrapers/sites/com/wyseguide.rb +3 -0
  364. data/lib/recipe_scrapers/sites/com/yemek.rb +3 -0
  365. data/lib/recipe_scrapers/sites/com/zenbelly.rb +3 -0
  366. data/lib/recipe_scrapers/sites/com/zestfulkitchen.rb +3 -0
  367. data/lib/recipe_scrapers/sites/de/bofrost.rb +3 -0
  368. data/lib/recipe_scrapers/sites/de/chefkoch.rb +26 -0
  369. data/lib/recipe_scrapers/sites/de/eattolerant.rb +3 -0
  370. data/lib/recipe_scrapers/sites/de/emmikochteinfach.rb +3 -0
  371. data/lib/recipe_scrapers/sites/de/kochbar.rb +3 -0
  372. data/lib/recipe_scrapers/sites/de/springlane.rb +3 -0
  373. data/lib/recipe_scrapers/sites/dk/madensverden.rb +3 -0
  374. data/lib/recipe_scrapers/sites/dk/sundpaabudget.rb +3 -0
  375. data/lib/recipe_scrapers/sites/fr/jow.rb +3 -0
  376. data/lib/recipe_scrapers/sites/fr/madame.rb +35 -0
  377. data/lib/recipe_scrapers/sites/hu/nosalty.rb +3 -0
  378. data/lib/recipe_scrapers/sites/hu/sobors.rb +3 -0
  379. data/lib/recipe_scrapers/sites/info/misya.rb +3 -0
  380. data/lib/recipe_scrapers/sites/it/cucchiaio.rb +3 -0
  381. data/lib/recipe_scrapers/sites/it/ricette.rb +3 -0
  382. data/lib/recipe_scrapers/sites/it/ricetteperbimby.rb +3 -0
  383. data/lib/recipe_scrapers/sites/me/leckerschmecker.rb +3 -0
  384. data/lib/recipe_scrapers/sites/menu/1000.rb +3 -0
  385. data/lib/recipe_scrapers/sites/mx/cocinafacil.rb +3 -0
  386. data/lib/recipe_scrapers/sites/mx/directoalpaladar.rb +3 -0
  387. data/lib/recipe_scrapers/sites/mx/recetasnestle.rb +3 -0
  388. data/lib/recipe_scrapers/sites/net/bellyfull.rb +3 -0
  389. data/lib/recipe_scrapers/sites/net/castironketo.rb +3 -0
  390. data/lib/recipe_scrapers/sites/net/feelgoodfoodie.rb +3 -0
  391. data/lib/recipe_scrapers/sites/net/hungryhappens.rb +3 -0
  392. data/lib/recipe_scrapers/sites/net/iambaker.rb +3 -0
  393. data/lib/recipe_scrapers/sites/net/inspiredtaste.rb +3 -0
  394. data/lib/recipe_scrapers/sites/net/piesandplots.rb +3 -0
  395. data/lib/recipe_scrapers/sites/net/tidymom.rb +3 -0
  396. data/lib/recipe_scrapers/sites/nl/leukerecepten.rb +3 -0
  397. data/lib/recipe_scrapers/sites/nl/projectgezond.rb +3 -0
  398. data/lib/recipe_scrapers/sites/no/godt.rb +3 -0
  399. data/lib/recipe_scrapers/sites/no/matprat.rb +3 -0
  400. data/lib/recipe_scrapers/sites/no/nrk.rb +3 -0
  401. data/lib/recipe_scrapers/sites/no/tine.rb +3 -0
  402. data/lib/recipe_scrapers/sites/org/lmld.rb +3 -0
  403. data/lib/recipe_scrapers/sites/org/marmiton.rb +3 -0
  404. data/lib/recipe_scrapers/sites/org/nutritionfacts.rb +3 -0
  405. data/lib/recipe_scrapers/sites/org/thekitchencommunity.rb +3 -0
  406. data/lib/recipe_scrapers/sites/plus/recette.rb +3 -0
  407. data/lib/recipe_scrapers/sites/pt/pingodoce.rb +3 -0
  408. data/lib/recipe_scrapers/sites/pt/teleculinaria.rb +3 -0
  409. data/lib/recipe_scrapers/sites/recipes/goodstuff.rb +3 -0
  410. data/lib/recipe_scrapers/sites/recipes/howtocook.rb +3 -0
  411. data/lib/recipe_scrapers/sites/ru/eda.rb +3 -0
  412. data/lib/recipe_scrapers/sites/ru/edimdoma.rb +3 -0
  413. data/lib/recipe_scrapers/sites/ru/gastronom.rb +3 -0
  414. data/lib/recipe_scrapers/sites/ru/iamcook.rb +6 -0
  415. data/lib/recipe_scrapers/sites/ru/povar.rb +3 -0
  416. data/lib/recipe_scrapers/sites/ru/russianfood.rb +8 -0
  417. data/lib/recipe_scrapers/sites/se/alltommat.rb +3 -0
  418. data/lib/recipe_scrapers/sites/se/ica.rb +3 -0
  419. data/lib/recipe_scrapers/sites/se/kiddokitchen.rb +3 -0
  420. data/lib/recipe_scrapers/sites/se/koket.rb +3 -0
  421. data/lib/recipe_scrapers/sites/se/recept.rb +3 -0
  422. data/lib/recipe_scrapers/sites/sk/receptyprevas.rb +3 -0
  423. data/lib/recipe_scrapers/sources/declared.rb +38 -0
  424. data/lib/recipe_scrapers/sources/ingredient_groups.rb +74 -0
  425. data/lib/recipe_scrapers/sources/json_ld.rb +93 -0
  426. data/lib/recipe_scrapers/sources/microdata.rb +113 -0
  427. data/lib/recipe_scrapers/sources/open_graph.rb +26 -0
  428. data/lib/recipe_scrapers/sources/schema_org/ingredient_list.rb +36 -0
  429. data/lib/recipe_scrapers/sources/schema_org/nutrition_facts.rb +21 -0
  430. data/lib/recipe_scrapers/sources/schema_org.rb +160 -0
  431. data/lib/recipe_scrapers/text.rb +59 -0
  432. data/lib/recipe_scrapers/version.rb +6 -0
  433. data/lib/recipe_scrapers.rb +137 -0
  434. metadata +560 -0
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module RecipeScrapers
6
+ module Parsers
7
+ # The words {Ingredients} reads: units, pinch words, size adjectives, qualifiers and number words.
8
+ # The gem bundles one vocabulary per language, plus a common one with the metric units.
9
+ #
10
+ # @api private
11
+ class Vocabulary
12
+ WORD_LISTS = %i[units pinches sizes qualifiers].freeze
13
+ BUNDLED = File.expand_path("vocabulary/*.yml", __dir__)
14
+ NEVER = "(?!)"
15
+ private_constant :WORD_LISTS, :BUNDLED, :NEVER
16
+
17
+ # @return [Array<String>] units, where a space matches any run of spaces or none
18
+ attr_reader :units
19
+
20
+ # @return [Array<String>] words that are a unit and an amount of one, such as "pinch"
21
+ attr_reader :pinches
22
+
23
+ # @return [Array<String>] adjectives that can come before a unit, such as "large"
24
+ attr_reader :sizes
25
+
26
+ # @return [Array<String>] words before an amount that are not part of it, such as "about"
27
+ attr_reader :qualifiers
28
+
29
+ # @return [Hash{String => Numeric}] amounts written as words, such as "two" => 2
30
+ attr_reader :number_words
31
+
32
+ class << self
33
+ # Adds a vocabulary to the catalog, next to the bundled ones.
34
+ #
35
+ # @param name [Symbol]
36
+ # @param languages [Array<String>] the language codes it is used for, such as %w[pl]
37
+ # @param words [Hash] the word lists, as {#initialize} takes them
38
+ # @return [Hash] the catalog entry
39
+ def define(name, languages: [], **words)
40
+ catalog[name] = entry(languages, words)
41
+ end
42
+
43
+ # @param language [String, nil] a language tag, such as "en-US"
44
+ # @return [Array<Symbol>] the common vocabulary plus those of the language, or every vocabulary
45
+ # when no file declares the language
46
+ #
47
+ # @api private
48
+ def names_for(language)
49
+ code = language.to_s.downcase[/\A[a-z]+/]
50
+ spoken = catalog.filter_map { |name, entry| name if entry[:languages].include?(code) }
51
+ spoken.empty? ? catalog.keys.sort : [:common, *spoken.sort]
52
+ end
53
+
54
+ # @param language [String, nil]
55
+ # @return [Vocabulary] the vocabularies of the language combined
56
+ def for(language)
57
+ combine(names_for(language))
58
+ end
59
+
60
+ # @param names [Array<Symbol>]
61
+ # @return [Vocabulary] the named vocabularies added together
62
+ #
63
+ # @api private
64
+ def combine(names)
65
+ names.map { |name| catalog.fetch(name)[:vocabulary] }.reduce(:+)
66
+ end
67
+
68
+ # @return [Hash{Symbol => Hash}] every vocabulary by name, the bundled ones loaded on first use
69
+ #
70
+ # @api private
71
+ def catalog
72
+ @catalog ||= load_bundled
73
+ end
74
+
75
+ # @param words [Array<String>]
76
+ # @return [String] a regexp alternation of the words, longest first, that never matches when empty
77
+ #
78
+ # @api private
79
+ def alternation(words)
80
+ return NEVER if words.empty?
81
+
82
+ words.sort_by { |word| -word.length }.map { |word| Regexp.escape(word).gsub("\\ ", "\\s*") }.join("|")
83
+ end
84
+
85
+ private
86
+
87
+ def load_bundled
88
+ Dir[BUNDLED].to_h do |path|
89
+ words = YAML.safe_load_file(path).transform_keys(&:to_sym)
90
+ [File.basename(path, ".yml").to_sym, entry(words.delete(:languages), words)]
91
+ end
92
+ end
93
+
94
+ def entry(languages, words)
95
+ { languages: Array(languages), vocabulary: new(**words) }
96
+ end
97
+ end
98
+
99
+ # @param units [Array<String>]
100
+ # @param pinches [Array<String>]
101
+ # @param sizes [Array<String>]
102
+ # @param qualifiers [Array<String>]
103
+ # @param number_words [Hash{String => Numeric}]
104
+ def initialize(units: [], pinches: [], sizes: [], qualifiers: [], number_words: {})
105
+ @units = units.uniq.freeze
106
+ @pinches = pinches.uniq.freeze
107
+ @sizes = sizes.uniq.freeze
108
+ @qualifiers = qualifiers.uniq.freeze
109
+ @number_words = number_words.freeze
110
+ end
111
+
112
+ # @param other [Vocabulary]
113
+ # @return [Vocabulary] both word lists without repeats. The other one wins for a number word in both
114
+ def +(other)
115
+ lists = WORD_LISTS.to_h { |list| [list, public_send(list) + other.public_send(list)] }
116
+ Vocabulary.new(**lists, number_words: number_words.merge(other.number_words))
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module Parsers
5
+ module Yields
6
+ SERVING_WORDS = /serv|portion|порц|person|people/i
7
+ NUMBER = /(\d+)/
8
+
9
+ class << self
10
+ def parse(value)
11
+ return nil if value.nil?
12
+
13
+ text = Text.normalize(value.is_a?(Array) ? value.first : value)
14
+ return nil if text.nil?
15
+
16
+ count = text[NUMBER, 1]
17
+ return nil if count.nil?
18
+
19
+ "#{count} #{noun_for(text, count)}"
20
+ end
21
+
22
+ private
23
+
24
+ def noun_for(text, count)
25
+ text.match?(SERVING_WORDS) || text == count ? "servings" : "items"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module RecipeScrapers
6
+ # Maps hosts to the sites the gem supports. A host maps to a {Declaration}, or to a {Scraper}
7
+ # subclass for a site that needs code. A leading "www." is ignored.
8
+ module Registry
9
+ class << self
10
+ # @return [Hash{String => Declaration, Class}] every registered host and what reads it
11
+ #
12
+ # @api private
13
+ def entries
14
+ @entries ||= {}
15
+ end
16
+
17
+ # Registers a site by declaration.
18
+ #
19
+ # @param host [String] the host of the site
20
+ # @param also [Array<String>] more hosts read the same way, such as a regional domain
21
+ # @yield the declaration body, evaluated on the {Declaration}
22
+ # @return [Declaration]
23
+ def register(host, also: [], &)
24
+ declaration = Declaration.build(host, &)
25
+ ([host] + Array(also)).each { |name| entries[normalize(name)] = declaration }
26
+ declaration
27
+ end
28
+
29
+ # Registers a {Scraper} subclass under the host it declares with {Scraper.host}.
30
+ #
31
+ # @param klass [Class<Scraper>]
32
+ # @return [Class<Scraper>] the class it was given
33
+ def register_class(klass)
34
+ entries[normalize(klass.declared_host)] = klass
35
+ klass
36
+ end
37
+
38
+ # Finds what reads a URL.
39
+ #
40
+ # @param url [String]
41
+ # @return [Declaration, Class<Scraper>, nil] nil when the host is not registered or the URL does not parse
42
+ def for(url)
43
+ entries[normalize(URI.parse(url).host.to_s)]
44
+ rescue URI::Error
45
+ nil
46
+ end
47
+
48
+ # @return [Array<String>] every supported host
49
+ def hosts
50
+ entries.keys
51
+ end
52
+
53
+ # Forgets every registered site.
54
+ #
55
+ # @return [void]
56
+ #
57
+ # @api private
58
+ def clear!
59
+ @entries = {}
60
+ end
61
+
62
+ # A copy of the entries, to put back with {restore}. Specs use it to register a site for one example.
63
+ #
64
+ # @return [Hash{String => Declaration, Class}]
65
+ #
66
+ # @api private
67
+ def snapshot
68
+ entries.dup
69
+ end
70
+
71
+ # Puts back entries a {snapshot} took.
72
+ #
73
+ # @param saved [Hash{String => Declaration, Class}]
74
+ # @return [void]
75
+ #
76
+ # @api private
77
+ def restore(saved)
78
+ @entries = saved
79
+ end
80
+
81
+ private
82
+
83
+ def normalize(host)
84
+ host.to_s.downcase.delete_prefix("www.")
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ class Scraper
5
+ # The fields that split text into value objects, with the parsers {Configuration#parsers} holds.
6
+ module ParsedFields
7
+ # @param scraper [Class<Scraper>] gets {ClassMethods}
8
+ # @return [void]
9
+ #
10
+ # @api private
11
+ def self.included(scraper)
12
+ scraper.extend(ClassMethods)
13
+ end
14
+
15
+ # The class methods a scraper class gets from {ParsedFields}.
16
+ module ClassMethods
17
+ # Sets the parsers of a field for every page of this scraper class. It wins over the site
18
+ # declaration and the configuration.
19
+ #
20
+ # @param field [Symbol] :ingredients or :nutrients
21
+ # @param parsers [Array<#call>] tried in order, the first result that is not nil wins
22
+ # @raise [ArgumentError] for a field that takes no parser
23
+ def parser(field, *parsers)
24
+ own_parsers[Parsers::Chain.field!(field)] = parsers
25
+ end
26
+
27
+ # @return [Hash{Symbol => Array<#call>}] the parsers this class sets with {#parser}
28
+ #
29
+ # @api private
30
+ def own_parsers
31
+ @own_parsers ||= {}
32
+ end
33
+ end
34
+
35
+ # (see Models::Recipe#parsed_ingredients)
36
+ def parsed_ingredients
37
+ spoken = language
38
+ ingredients&.map { |line| parse(:ingredients, line, spoken) }
39
+ end
40
+
41
+ # (see Models::Recipe#parsed_nutrients)
42
+ def parsed_nutrients
43
+ spoken = language
44
+ parsed = nutrients.to_h.filter_map do |name, text|
45
+ nutrient = parse(:nutrients, text, spoken)
46
+ nutrient && (nutrient.name ? nutrient : nutrient.with(name: name))
47
+ end
48
+ parsed.empty? ? nil : parsed
49
+ end
50
+
51
+ private
52
+
53
+ def with_parsed_ingredients(groups)
54
+ spoken = language
55
+ groups.map do |group|
56
+ group.with(parsed_ingredients: group.ingredients.map { |line| parse(:ingredients, line, spoken) })
57
+ end
58
+ end
59
+
60
+ def parse(field, text, spoken)
61
+ Parsers::Chain.run(field, parsers_for(field), text, language: spoken)
62
+ end
63
+
64
+ def parsers_for(field)
65
+ self.class.own_parsers[field] || @declaration&.parsers_for(field) || RecipeScrapers.config.parsers.fetch(field)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "uri"
5
+
6
+ module RecipeScrapers
7
+ # Reads the recipe fields of one page. {RecipeScrapers.parse} builds one and returns its
8
+ # {#to_recipe}, so an application gets a {Models::Recipe} and never the page itself. A field the
9
+ # page does not publish is nil.
10
+ #
11
+ # Every field reads the site declaration first, then the schema.org recipe, then OpenGraph where
12
+ # that applies. A site that needs code subclasses Scraper, names its host with {.host} and
13
+ # registers with {Registry.register_class}.
14
+ #
15
+ # @example A site with its own schema.org reader
16
+ # class Chefkoch < RecipeScrapers::Scraper
17
+ # class SchemaReader < RecipeScrapers::Sources::SchemaOrg
18
+ # def ingredients = super&.map(&:strip)
19
+ # end
20
+ #
21
+ # host "chefkoch.de"
22
+ # schema_reader SchemaReader
23
+ # end
24
+ # RecipeScrapers::Registry.register_class(Chefkoch)
25
+ class Scraper
26
+ include ParsedFields
27
+
28
+ # Every field a scraper answers, in the order {#to_h} returns them.
29
+ CONTRACT = (Models::Recipe.members - %i[url]).freeze
30
+
31
+ class << self
32
+ # Names the host a subclass reads, or returns it.
33
+ #
34
+ # @param value [String, nil]
35
+ # @return [String, nil]
36
+ def host(value = nil)
37
+ @declared_host = value if value
38
+ @declared_host
39
+ end
40
+
41
+ alias declared_host host
42
+
43
+ # Sets the class that reads the schema.org recipe for a subclass, or returns it.
44
+ #
45
+ # @param value [Class<Sources::SchemaOrg>, nil]
46
+ # @return [Class<Sources::SchemaOrg>] {Sources::SchemaOrg} unless a subclass sets its own
47
+ def schema_reader(value = nil)
48
+ @schema_reader = value if value
49
+ @schema_reader || Sources::SchemaOrg
50
+ end
51
+ end
52
+
53
+ # @return [String] the address the page was read from
54
+ attr_reader :url
55
+
56
+ # @return [Nokogiri::HTML5::Document] the parsed page, for a subclass that reads it directly
57
+ attr_reader :document
58
+
59
+ # @param html [String] the page source
60
+ # @param url [String] the address of the page
61
+ # @param declaration [Declaration, nil] the declaration of the site
62
+ def initialize(html, url:, declaration: nil)
63
+ @url = url
64
+ @declaration = declaration
65
+ @document = Nokogiri::HTML5(Text.recode(html, declaration&.encoding_name))
66
+ @schema = self.class.schema_reader.new(Sources::JsonLd.new(@document), Sources::Microdata.new(@document))
67
+ @open_graph = Sources::OpenGraph.new(@document)
68
+ @declared = Sources::Declared.new(@document, declaration)
69
+ end
70
+
71
+ # (see Models::Recipe#host)
72
+ def host
73
+ URI.parse(url).host.to_s.delete_prefix("www.")
74
+ end
75
+
76
+ # (see Models::Recipe#canonical_url)
77
+ def canonical_url
78
+ link = document.at_css('link[rel="canonical"][href]')
79
+ return url unless link
80
+
81
+ URI.join(url, link["href"]).to_s
82
+ rescue URI::Error
83
+ url
84
+ end
85
+
86
+ # (see Models::Recipe#title)
87
+ def title = @declared.text(:title) || @schema.title || @open_graph.title
88
+ # (see Models::Recipe#author)
89
+ def author = @declared.text(:author) || @schema.author
90
+ # (see Models::Recipe#site_name)
91
+ def site_name = @declared.text(:site_name) || @schema.site_name || @open_graph.site_name
92
+ # (see Models::Recipe#language)
93
+ def language = @declared.text(:language) || @schema.language || document_language
94
+ # (see Models::Recipe#description)
95
+ def description = @declared.text(:description) || @schema.description || @open_graph.description
96
+ # (see Models::Recipe#category)
97
+ def category = @declared.text(:category) || @schema.category
98
+ # (see Models::Recipe#cuisine)
99
+ def cuisine = @declared.text(:cuisine) || @schema.cuisine
100
+ # (see Models::Recipe#cooking_method)
101
+ def cooking_method = @declared.text(:cooking_method) || @schema.cooking_method
102
+ # (see Models::Recipe#keywords)
103
+ def keywords = @declared.rows(:keywords) || @schema.keywords
104
+ # (see Models::Recipe#equipment)
105
+ def equipment = @declared.rows(:equipment)
106
+ # (see Models::Recipe#nutrients)
107
+ def nutrients = @schema.nutrients
108
+ # (see Models::Recipe#dietary_restrictions)
109
+ def dietary_restrictions = @schema.dietary_restrictions
110
+ # (see Models::Recipe#ratings)
111
+ def ratings = @schema.ratings
112
+ # (see Models::Recipe#ratings_count)
113
+ def ratings_count = @schema.ratings_count
114
+
115
+ # (see Models::Recipe#yields)
116
+ def yields = Parsers::Yields.parse(@declared.text(:yields)) || @schema.yields
117
+ # (see Models::Recipe#total_time)
118
+ def total_time = Parsers::Durations.minutes(@declared.text(:total_time)) || @schema.total_time
119
+ # (see Models::Recipe#cook_time)
120
+ def cook_time = Parsers::Durations.minutes(@declared.text(:cook_time)) || @schema.cook_time
121
+ # (see Models::Recipe#prep_time)
122
+ def prep_time = Parsers::Durations.minutes(@declared.text(:prep_time)) || @schema.prep_time
123
+
124
+ # (see Models::Recipe#ingredients)
125
+ def ingredients
126
+ lines = ingredient_sections.flat_map(&:last)
127
+ lines.empty? ? nil : lines
128
+ end
129
+
130
+ # (see Models::Recipe#instructions_list)
131
+ def instructions_list = @declared.rows(:instructions) || @schema.instructions_list
132
+ # (see Models::Recipe#instructions)
133
+ def instructions = instructions_list&.join("\n")
134
+
135
+ # (see Models::Recipe#ingredient_groups)
136
+ def ingredient_groups
137
+ lines = ingredients
138
+ return nil if lines.nil?
139
+
140
+ marked = marked_groups(lines)
141
+ groups = marked.any?(&:purpose) ? marked : Sources::IngredientGroups.from_sections(ingredient_sections)
142
+ with_parsed_ingredients(groups)
143
+ end
144
+
145
+ # (see Models::Recipe#image)
146
+ def image
147
+ address = @declared.text(:image) || @schema.image || @open_graph.image
148
+ address && URI.join(url, address).to_s
149
+ rescue URI::Error
150
+ address
151
+ end
152
+
153
+ # (see Models::Recipe#links)
154
+ def links
155
+ document.css("a[href]").map { |anchor| anchor["href"] }
156
+ end
157
+
158
+ # @return [Hash{Symbol => Object}] every field of {CONTRACT} with its value
159
+ #
160
+ # @api private
161
+ def to_h
162
+ CONTRACT.to_h { |field| [field, public_send(field)] }
163
+ end
164
+
165
+ # Reads every field once and returns them without the page.
166
+ #
167
+ # @return [Models::Recipe]
168
+ #
169
+ # @api private
170
+ def to_recipe
171
+ Models::Recipe.new(url: url, **to_h)
172
+ end
173
+
174
+ # @return [Boolean] whether the page has a title and ingredients
175
+ #
176
+ # @api private
177
+ def recipe?
178
+ !title.nil? && !ingredients.nil?
179
+ end
180
+
181
+ private
182
+
183
+ def ingredient_sections
184
+ @ingredient_sections ||=
185
+ Sources::IngredientGroups.sections(@declared.rows(:ingredients) || @schema.ingredients || [])
186
+ end
187
+
188
+ def marked_groups(lines)
189
+ rule = @declared.rule(:ingredient_groups)
190
+ return Sources::IngredientGroups.detect(document: document, ingredients: lines) if rule.nil?
191
+
192
+ Sources::IngredientGroups.call(document: document, ingredients: lines, heading: rule[:heading], item: rule[:item])
193
+ end
194
+
195
+ def document_language
196
+ Text.normalize(document.at_css("html")&.[]("lang"))
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module SitePath
5
+ class << self
6
+ def for(host)
7
+ "#{directory(host)}/#{slug(host)}"
8
+ end
9
+
10
+ private
11
+
12
+ def directory(host)
13
+ host.split(".").last
14
+ end
15
+
16
+ def slug(host)
17
+ host.split(".").first.tr("-", "_")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "bestrecipes.com.au"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "15gram.be"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "panelinha.com.br"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "receitasnestle.com.br"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "receiteria.com.br"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "tudogostoso.com.br"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "bettybossi.ch"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "migusto.migros.ch"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "swissmilk.ch"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "tasty.co"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "365daysofbakingandmore.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "40aprons.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "750g.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "abeautifulmess.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "abuelascounter.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "acozykitchen.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "addapinch.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "adozensundays.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "aflavorjournal.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "afullliving.com"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "ahealthysliceoflife.com" do
4
+ ingredients rows: ".tasty-recipes-ingredients-body p"
5
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "alexandracooks.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "allnutritious.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "allthehealthythings.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "altonbrown.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "amazingribs.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "amberskitchencooks.com"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecipeScrapers.register "andy-cooks.com"