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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module Models
5
+ # One nutrition fact split into its parts. {Models::Recipe#parsed_nutrients} holds these.
6
+ #
7
+ # @!attribute [r] name
8
+ # @return [String, nil] the key the page uses, such as "calories" or "fatContent"
9
+ # @!attribute [r] unit
10
+ # @return [String, nil] "g", "mg", "µg", "kcal" or "kJ" for mass and energy, any other unit as written
11
+ # @!attribute [r] amount
12
+ # @return [Float, nil] the number, with every decimal the page writes
13
+ Nutrient = Data.define(:name, :unit, :amount) do
14
+ def initialize(name: nil, unit: nil, amount: nil)
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module Models
5
+ # The recipe a page holds, with every field already read. {RecipeScrapers.scrape} and
6
+ # {RecipeScrapers.parse} return one. It is a Ruby Data object: frozen, compared by value, and free
7
+ # of the page it came from. A field the page does not publish is nil.
8
+ #
9
+ # {#to_h} converts the nested value objects too, so the result holds only strings, numbers,
10
+ # arrays and hashes, ready for JSON.
11
+ #
12
+ # @example
13
+ # recipe = RecipeScrapers.scrape("https://www.recipetineats.com/crispy-potato-straws-pommes-paille/")
14
+ # recipe.title # => "Crispy potato straws (Pommes Paille)"
15
+ # recipe.to_h[:parsed_ingredients].first
16
+ # # => { amount: 1.0, unit: nil, name: "potato, or other starchy or all-rounder potato" }
17
+ #
18
+ # @!attribute [r] url
19
+ # @return [String] the address the recipe was read from
20
+ # @!attribute [r] title
21
+ # @return [String, nil] the name of the recipe
22
+ # @!attribute [r] author
23
+ # @return [String, nil] the author, several joined with ", "
24
+ # @!attribute [r] site_name
25
+ # @return [String, nil] the name of the website
26
+ # @!attribute [r] language
27
+ # @return [String, nil] the language tag of the recipe, such as "en-US"
28
+ # @!attribute [r] canonical_url
29
+ # @return [String] the canonical link of the page resolved against {Models::Recipe#url}, or the
30
+ # url itself when the page has none
31
+ # @!attribute [r] host
32
+ # @return [String] the host of {Models::Recipe#url}, without "www."
33
+ # @!attribute [r] description
34
+ # @return [String, nil] a sentence or short paragraph about the recipe
35
+ # @!attribute [r] image
36
+ # @return [String, nil] the address of the main image, resolved against {Models::Recipe#url}
37
+ # @!attribute [r] ingredients
38
+ # The ingredient lines as the page writes them. A line with no letter and no digit, such as
39
+ # "*", is a separator and left out. A line that ends in a colon and has no digit, such as
40
+ # "For the sauce:", is a group heading and goes to {Models::Recipe#ingredient_groups}
41
+ # instead. See {Models::Recipe#parsed_ingredients}.
42
+ #
43
+ # @return [Array<String>, nil]
44
+ # @!attribute [r] parsed_ingredients
45
+ # Every line of {Models::Recipe#ingredients}, in the same order, split into an amount, a unit and a name.
46
+ #
47
+ # @example
48
+ # recipe.parsed_ingredients.first.to_h # => { amount: 1.5, unit: "cups", name: "vegetable oil" }
49
+ # @return [Array<Models::Ingredient>, nil]
50
+ # @!attribute [r] ingredient_groups
51
+ # The ingredients in the groups the page shows, such as "For the sauce". The groups come from
52
+ # the HTML when the page marks them up, otherwise from the heading lines of the ingredient
53
+ # list. A page without groups gives one group with a nil purpose.
54
+ #
55
+ # @return [Array<Models::IngredientGroup>, nil] nil when the page has no ingredients
56
+ # @!attribute [r] instructions_list
57
+ # @return [Array<String>, nil] the steps, with section headings as their own entries
58
+ # @!attribute [r] instructions
59
+ # @return [String, nil] the steps joined with newlines
60
+ # @!attribute [r] category
61
+ # @return [String, nil] the course, such as "Dessert"
62
+ # @!attribute [r] cuisine
63
+ # @return [String, nil] the cuisine, such as "Italian"
64
+ # @!attribute [r] cooking_method
65
+ # @return [String, nil] how it is cooked, such as "Baking"
66
+ # @!attribute [r] keywords
67
+ # @return [Array<String>, nil] the keywords, without facet entries such as "diet: vegan"
68
+ # @!attribute [r] dietary_restrictions
69
+ # @return [Array<String>, nil] the diets the recipe suits, such as "VeganDiet"
70
+ # @!attribute [r] equipment
71
+ # @return [Array<String>, nil] the tools a site declares, schema.org has none
72
+ # @!attribute [r] yields
73
+ # @return [String, nil] how much it makes, as "4 servings" or "12 items"
74
+ # @!attribute [r] total_time
75
+ # @return [Integer, nil] the total time in minutes
76
+ # @!attribute [r] prep_time
77
+ # @return [Integer, nil] the preparation time in minutes
78
+ # @!attribute [r] cook_time
79
+ # @return [Integer, nil] the cooking time in minutes
80
+ # @!attribute [r] nutrients
81
+ # The nutrition facts as the page publishes them, keyed by the schema.org property name.
82
+ # Nothing is parsed here. See {Models::Recipe#parsed_nutrients}.
83
+ #
84
+ # @example
85
+ # recipe.nutrients # => { "calories" => "219 kcal", "fatContent" => "7 g" }
86
+ # @return [Hash{String => String}, nil]
87
+ # @!attribute [r] parsed_nutrients
88
+ # Every entry of {Models::Recipe#nutrients}, in the order the page lists them, split into a name, a unit
89
+ # and an amount. The name is the key the page uses. A value with no number is left out.
90
+ #
91
+ # @example
92
+ # recipe.parsed_nutrients.first.to_h # => { name: "calories", unit: "kcal", amount: 219.0 }
93
+ # @return [Array<Models::Nutrient>, nil]
94
+ # @!attribute [r] ratings
95
+ # @return [Float, nil] the average rating
96
+ # @!attribute [r] ratings_count
97
+ # @return [Integer, nil] how many ratings the average is made of
98
+ # @!attribute [r] links
99
+ # @return [Array<String>] the href of every link on the page, as written
100
+ Recipe = Data.define(
101
+ :url, :title, :author, :site_name, :language, :canonical_url, :host, :description, :image, :ingredients,
102
+ :parsed_ingredients, :ingredient_groups, :instructions_list, :instructions, :category, :cuisine,
103
+ :cooking_method, :keywords, :dietary_restrictions, :equipment, :yields, :total_time, :prep_time, :cook_time,
104
+ :nutrients, :parsed_nutrients, :ratings, :ratings_count, :links
105
+ ) do
106
+ # The recipe as plain data. Every nested value object becomes a hash as well.
107
+ #
108
+ # @return [Hash{Symbol => Object}]
109
+ def to_h(&)
110
+ super.transform_values { |value| plain(value) }.to_h(&)
111
+ end
112
+
113
+ private
114
+
115
+ def plain(value)
116
+ case value
117
+ when Data then plain(value.to_h)
118
+ when Hash then value.transform_values { |item| plain(item) }
119
+ when Array then value.map { |item| plain(item) }
120
+ else value
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ # Turn text into values.
5
+ module Parsers
6
+ # Runs the parsers of a field in order until one returns something. A parser returns a hash of
7
+ # the model's attributes, which becomes the model, or nil. A parser that raises, or returns a
8
+ # hash with an unknown key, goes to {Configuration#error_tracker}, and the next one is tried.
9
+ #
10
+ # @api private
11
+ module Chain
12
+ FIELDS = %i[ingredients nutrients].freeze
13
+ MODELS = { ingredients: Models::Ingredient, nutrients: Models::Nutrient }.freeze
14
+ private_constant :MODELS
15
+
16
+ class << self
17
+ def defaults
18
+ { ingredients: [Ingredients], nutrients: [Nutrients] }
19
+ end
20
+
21
+ def field!(field)
22
+ return field if FIELDS.include?(field)
23
+
24
+ raise ArgumentError, "no parser can be set for #{field.inspect}, only for #{FIELDS.join(", ")}"
25
+ end
26
+
27
+ def run(field, parsers, text, language:)
28
+ parsers.each do |parser|
29
+ result = attempt(field, parser, text, language)
30
+ return result unless result.nil?
31
+ end
32
+ nil
33
+ end
34
+
35
+ private
36
+
37
+ def attempt(field, parser, text, language)
38
+ model(field, parser.call(text, language: language))
39
+ rescue StandardError => e
40
+ RecipeScrapers.config.error_tracker.call(e)
41
+ nil
42
+ end
43
+
44
+ def model(field, result)
45
+ return result if result.nil? || result.is_a?(Data)
46
+
47
+ MODELS.fetch(field).new(**result)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module Parsers
5
+ module Durations
6
+ LANGUAGES = {
7
+ english: { hours: %w[h hr hrs hour hours], minutes: %w[m min mins minute minutes],
8
+ seconds: %w[s sec secs second seconds] },
9
+ german: { hours: %w[std stunde stunden], minutes: %w[minuten],
10
+ seconds: %w[sek sekunde sekunden] },
11
+ french: { hours: %w[heure heures], minutes: %w[minute minutes], seconds: %w[seconde secondes] },
12
+ italian: { hours: %w[ora ore], minutes: %w[minuto minuti], seconds: %w[secondo secondi] },
13
+ dutch: { hours: %w[uur], minutes: %w[minuut minuten], seconds: %w[seconde seconden] },
14
+ swedish: { hours: %w[tim timme timmar], minutes: %w[minut minuter],
15
+ seconds: %w[sekund sekunder] },
16
+ norwegian: { hours: %w[time timer], minutes: %w[minutt minutter], seconds: %w[sekund sekunder] },
17
+ hungarian: { hours: %w[óra órát], minutes: %w[perc percet], seconds: %w[másodperc] },
18
+ russian: { hours: %w[ч час часа часов], minutes: %w[м мин минута минуты минут минуту],
19
+ seconds: %w[с сек секунда секунды секунд] }
20
+ }.freeze
21
+
22
+ HOURS, MINUTES, SECONDS = %i[hours minutes seconds].map do |unit|
23
+ words = LANGUAGES.values.flat_map { |language| language.fetch(unit) }.uniq
24
+ /(\d+)\s*(?:#{words.sort_by { |word| -word.length }.join("|")})\b/i
25
+ end
26
+
27
+ ISO = /\AP(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?)?\z/
28
+ ISO_UNIT_MINUTES = [10_080, 1440, 60, 1, 1 / 60.0].freeze
29
+ NUMBER = /\d+/
30
+ RANGE = /(\d+)\s*(?:-|–|—|to|до)\s*\d+/
31
+
32
+ class << self
33
+ def minutes(value)
34
+ return nil if value.nil?
35
+ return value if value.is_a?(Integer)
36
+ return minutes(value["@value"]) if value.is_a?(Hash)
37
+
38
+ text = Text.normalize(value)
39
+ return nil if text.nil?
40
+
41
+ from_iso(text) || from_words(text.gsub(RANGE, '\\1'))
42
+ end
43
+
44
+ private
45
+
46
+ def from_iso(text)
47
+ match = ISO.match(text)
48
+ return nil unless match&.captures&.any?
49
+
50
+ total = match.captures.zip(ISO_UNIT_MINUTES).sum { |count, minutes| count.to_f * minutes }.round
51
+ total.positive? ? total : nil
52
+ end
53
+
54
+ def from_words(text)
55
+ hours, mins, secs = [HOURS, MINUTES, SECONDS].map { |unit| text[unit, 1] }
56
+ return sole_number(text) if [hours, mins, secs].compact.empty?
57
+ return nil unless [hours, mins, secs].compact.size == text.scan(NUMBER).size
58
+
59
+ total = (hours.to_i * 60) + mins.to_i
60
+ total.positive? ? total : nil
61
+ end
62
+
63
+ def sole_number(text)
64
+ numbers = text.scan(NUMBER)
65
+ numbers.size == 1 ? numbers.first.to_i : nil
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module Parsers
5
+ # The bundled parser of ingredient lines. It reads the amount, the unit and the name with the
6
+ # words of the recipe language. Put it after your own parser to keep it as the fallback.
7
+ #
8
+ # @example
9
+ # RecipeScrapers::Parsers::Ingredients.call("1 1/2 cups flour", language: "en").to_h
10
+ # # => { amount: 1.5, unit: "cups", name: "flour" }
11
+ class Ingredients
12
+ QUANTITY = Quantity::QUANTITY
13
+ SUM = Quantity::SUM
14
+ UPPER_BOUND = /(?:(?:\s*[-–—]\s*|\s+(?:to|or|o|a|à|bis|до|или)\s+)#{QUANTITY})?/o
15
+ WORD_END = /(?![\p{L}\d'’-])/
16
+ PREPARATION = /\p{Ll}[\p{L}-]*(?:\s+\p{Ll}[\p{L}-]*)?/
17
+ CONNECTOR = /\A(?:(?:of|de|di)\s+)?(?:an?\s+)?/i
18
+ PARENTHESES = /\([^()]*\)/
19
+ SPACE_BEFORE_PUNCTUATION = /\s+([,.;:])/
20
+ EDGE_PUNCTUATION = /\A[\s,;:.]+|[\s,;:]+\z/
21
+ BULLET = /\A[♥♡**•·]+\s*/
22
+ ARTICLES = %w[a an].freeze
23
+ LOCK = Mutex.new
24
+ private_constant :QUANTITY, :SUM, :UPPER_BOUND, :WORD_END, :PREPARATION, :CONNECTOR, :PARENTHESES,
25
+ :SPACE_BEFORE_PUNCTUATION, :EDGE_PUNCTUATION, :BULLET, :ARTICLES, :LOCK
26
+
27
+ class << self
28
+ # @param line [String] one ingredient line
29
+ # @param language [String, nil] the language tag of the recipe
30
+ # @return [Models::Ingredient]
31
+ def call(line, language: nil)
32
+ for_language(language).call(line)
33
+ end
34
+
35
+ # @param language [String, nil]
36
+ # @return [Ingredients] a parser for the vocabulary of the language, built once per vocabulary
37
+ #
38
+ # @api private
39
+ def for_language(language)
40
+ names = Vocabulary.names_for(language)
41
+ LOCK.synchronize { instances[names] ||= new(Vocabulary.combine(names)) }
42
+ end
43
+
44
+ private
45
+
46
+ def instances
47
+ @instances ||= {}
48
+ end
49
+ end
50
+
51
+ # @return [Vocabulary] the words this parser reads
52
+ #
53
+ # @api private
54
+ attr_reader :vocabulary
55
+
56
+ # @param vocabulary [Vocabulary]
57
+ #
58
+ # @api private
59
+ def initialize(vocabulary)
60
+ @vocabulary = vocabulary
61
+ @patterns = build_patterns
62
+ end
63
+
64
+ # @param line [String] one ingredient line
65
+ # @return [Models::Ingredient]
66
+ #
67
+ # @api private
68
+ def call(line, **)
69
+ parsed = parse_text(without_parentheses(line.to_s))
70
+ return parsed unless parsed.name.nil? && line.to_s.include?("(")
71
+
72
+ parse_text(tidy(line.to_s.gsub(PARENTHESES, " ").delete("()")))
73
+ end
74
+
75
+ private
76
+
77
+ def build_patterns
78
+ [
79
+ /\A#{qualifier}#{measure}\s*(?<ingredient>.*)\z/,
80
+ /\A(?:#{adjective})?(?<unit>#{pinch_word})\s+(?:of\s+)?(?<ingredient>.+)\z/i,
81
+ /\A(?<note>#{PREPARATION})\s+(?<amount>#{SUM})#{UPPER_BOUND}\s*#{unit}\s+(?<ingredient>.+)\z/,
82
+ /\A(?<ingredient>[^:]+?)\s*:\s*#{qualifier}#{numeric_measure}(?:\.?\s+(?<note>\D+?))?\.?\z/,
83
+ /\A(?<ingredient>.+?)(?:\s*[,\-–—]\s*|\s+)#{qualifier}#{numeric_measure}\z/,
84
+ /\A(?<ingredient>.+?)\s*[:,\-–—]\s*(?<unit>#{pinch_word})\z/i,
85
+ /\A(?<ingredient>.+?)\s+(?<unit>#{unit_word})\s*(?<amount>#{QUANTITY})\z/
86
+ ].freeze
87
+ end
88
+
89
+ def words(list) = Vocabulary.alternation(list)
90
+ def unit_word = @unit_word ||= /(?:#{words(vocabulary.units)})/i
91
+ def pinch_word = @pinch_word ||= /(?:#{words(vocabulary.pinches)})#{WORD_END}/i
92
+ def adjective = @adjective ||= /(?:#{words(vocabulary.sizes)})\s+/i
93
+ def qualifier = @qualifier ||= /(?:(?:#{words(vocabulary.qualifiers)})\s+|~\s*)*/i
94
+ def size = @size ||= /#{QUANTITY}#{UPPER_BOUND}\s*-?\s*#{unit_word}\.?(?:-|\s)\s*/
95
+ def unit_lead = @unit_lead ||= /(?:#{size})?(?:#{adjective})?/
96
+ def alternative = @alternative ||= %r{(?:\s*[/+]\s*|\s+)#{QUANTITY}#{UPPER_BOUND}\s*#{unit_word}\.?#{WORD_END}}
97
+ def unit = @unit ||= /#{unit_lead}(?<unit>#{unit_word})\.?#{WORD_END}(?:#{alternative})*/
98
+ def counted_word = /(?:#{words(vocabulary.number_words.keys - ARTICLES)})(?=\s)/i
99
+ def article = /an?(?=\s+#{unit_lead}#{unit_word}#{WORD_END})/i
100
+ def numeric_measure = /(?<amount>#{SUM})#{UPPER_BOUND}\s*(?:#{unit})?/
101
+ def measure = /(?<amount>#{SUM}|#{counted_word}|#{article})#{UPPER_BOUND}\s*(?:#{unit})?/
102
+
103
+ def parse_text(text)
104
+ @patterns.each do |pattern|
105
+ match = text.match(pattern)
106
+ return measured(match) if match && !match[:ingredient].start_with?("%")
107
+ end
108
+ row(nil, nil, text)
109
+ end
110
+
111
+ def measured(match)
112
+ names = match.names
113
+ unit = match[:unit]&.squeeze(" ")
114
+ ingredient = match[:ingredient]
115
+ ingredient = ingredient.sub(CONNECTOR, "") if match.begin(:ingredient).positive?
116
+ ingredient = [ingredient, match[:note]].compact.join(", ") if names.include?("note")
117
+ row(names.include?("amount") ? amount(match[:amount]) : 1.0, unit, ingredient)
118
+ end
119
+
120
+ def row(amount, unit, ingredient)
121
+ cleaned = ingredient.gsub(EDGE_PUNCTUATION, "")
122
+ Models::Ingredient.new(amount: amount, unit: unit, name: cleaned.empty? ? nil : cleaned)
123
+ end
124
+
125
+ def amount(quantity)
126
+ word = vocabulary.number_words[quantity.downcase]
127
+ return word.to_f if word
128
+
129
+ Quantity.value(quantity)
130
+ end
131
+
132
+ def without_parentheses(text)
133
+ reduced = text.gsub(PARENTHESES, " ")
134
+ return tidy(reduced) if reduced == text
135
+
136
+ without_parentheses(reduced)
137
+ end
138
+
139
+ def tidy(text)
140
+ text.gsub(SPACE_BEFORE_PUNCTUATION, "\\1").squeeze(" ").strip.sub(BULLET, "")
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module Parsers
5
+ # The bundled parser of nutrient values. It reads the first number and the word after it, and
6
+ # drops text in parentheses. Mass and energy units are written as symbols, so "grams" and "г" become
7
+ # "g" and "calories" becomes "kcal".
8
+ #
9
+ # @example
10
+ # RecipeScrapers::Parsers::Nutrients.call("12.5 grams").to_h # => { name: nil, unit: "g", amount: 12.5 }
11
+ module Nutrients
12
+ PARENTHESES = /\([^()]*\)/
13
+ CONNECTORS = %w[of de di].freeze
14
+ MEASURE = /(?<amount>#{Quantity::QUANTITY})\s*(?<unit>[\p{L}µ][\p{L}-]*)?/o
15
+ CANONICAL_UNITS = {
16
+ "g" => %w[g gr grs gram grams gramm gramos gramas grammi gramme grammes г гр грамм грамма граммов],
17
+ "mg" => %w[mg milligram milligrams milligramm мг],
18
+ "µg" => %w[µg μg mcg microgram micrograms мкг],
19
+ "kcal" => %w[
20
+ kcal kcals kkal cal cals calorie calories kalorien kalorier calorías calorias ккал калорий калории
21
+ ],
22
+ "kJ" => %w[kj кдж]
23
+ }.flat_map { |canonical, spellings| spellings.map { |spelling| [spelling, canonical] } }.to_h.freeze
24
+ private_constant :PARENTHESES, :CONNECTORS, :MEASURE, :CANONICAL_UNITS
25
+
26
+ class << self
27
+ # @param text [String] one nutrient value
28
+ # @return [Models::Nutrient, nil] without a name, which {Models::Recipe#parsed_nutrients} fills in. Nil
29
+ # when the value has no number
30
+ def call(text, **)
31
+ match = text.to_s.gsub(PARENTHESES, " ").match(MEASURE)
32
+ return nil if match.nil?
33
+
34
+ Models::Nutrient.new(amount: Quantity.total(match[:amount]), unit: unit(match[:unit]))
35
+ end
36
+
37
+ private
38
+
39
+ def unit(written)
40
+ return nil if written.nil? || CONNECTORS.include?(written.downcase)
41
+
42
+ CANONICAL_UNITS.fetch(written.downcase, written)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module Parsers
5
+ module Quantity
6
+ NUMBER = /\d+(?:[.,]\d+)?/
7
+ SLASH_FRACTION = %r{\d+\s*[/⁄]\s*\d+}
8
+ VULGAR = /[½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅐⅛⅜⅝⅞⅑⅒]/
9
+ JOINER = /\s+(?:and|und|y|e)\s+|\s*と\s*/
10
+ QUANTITY = /
11
+ \d+(?:#{JOINER}|\s*)#{VULGAR}|\d+(?:#{JOINER}|\s+)#{SLASH_FRACTION}|#{SLASH_FRACTION}|#{NUMBER}|#{VULGAR}
12
+ /xo
13
+ PLUS = /\s*\+\s*/
14
+ SUM = /#{QUANTITY}(?:#{PLUS}#{QUANTITY})*/o
15
+ SLASH_SPACING = %r{\s*([/⁄])\s*}
16
+ WHOLE_AND_FRACTION = /#{JOINER}|\s+|(?=#{VULGAR})/o
17
+
18
+ class << self
19
+ def value(sum)
20
+ amount = total(sum).round(2)
21
+ amount.zero? ? nil : amount
22
+ end
23
+
24
+ def total(sum)
25
+ sum.split(PLUS).sum { |term| term_value(term) }.to_f
26
+ end
27
+
28
+ private
29
+
30
+ def term_value(term)
31
+ whole, fraction = term.gsub(SLASH_SPACING, "\\1").split(WHOLE_AND_FRACTION, 2)
32
+ part_value(whole) + part_value(fraction)
33
+ end
34
+
35
+ def part_value(part)
36
+ return 0 if part.nil?
37
+
38
+ numerator, denominator = part.unicode_normalize(:nfkd).split(%r{[/⁄]})
39
+ return Rational(numerator.to_i, denominator.to_i) if denominator.to_i.positive?
40
+
41
+ Rational(numerator.tr(",", "."))
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecipeScrapers
4
+ module Parsers
5
+ module Ratings
6
+ DECIMAL = /\d+(?:[.,]\d+)?/
7
+ DIGIT_GROUPING = /[,. ]/
8
+
9
+ class << self
10
+ def average(node)
11
+ return nil if node.nil?
12
+
13
+ value = node["ratingValue"].to_s[DECIMAL]&.tr(",", ".")&.to_f
14
+ value&.positive? ? value : nil
15
+ end
16
+
17
+ def count(node)
18
+ return nil if node.nil?
19
+
20
+ total = (node["ratingCount"] || node["reviewCount"]).to_s.gsub(DIGIT_GROUPING, "")[/\A\d+/]&.to_i
21
+ total&.positive? ? total : nil
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ units: [g, gr, grs, gram, grams, gramm, gramos, gramas, grammi, kg, kilo, kilos, kilogram, kilograms, quilo,
2
+ quilos, mg, ml, milliliter, milliliters, millilitre, millilitres, l, liter, liters, litre, litres, litro,
3
+ litros, dl, cl, krm, cm, mm, count]
4
+ qualifiers: [ca., circa, "null"]
5
+ number_words:
6
+ a: 1
7
+ an: 1
@@ -0,0 +1,5 @@
1
+ languages: [de]
2
+ units: [el, tl, msp, prise, stück, stk, st, bund, dose, dosen, päckchen, pck, pkt, pk, becher, scheibe,
3
+ scheiben, zehe, zehen, spritzer, tasse, tassen]
4
+ pinches: [prise]
5
+ sizes: [kleine, kleiner, kleines, kl., große, großer, großes]
@@ -0,0 +1,26 @@
1
+ languages: [en]
2
+ units: [cup, cups, c, tsp, tsps, teaspoon, teaspoons, t, tbsp, tbsps, tbs, tbl, tablespoon, tablespoons, oz,
3
+ ounce, ounces, lb, lbs, pound, pounds, quart, quarts, qt, pint, pints, pt, gallon, gallons, clove, cloves,
4
+ can, cans, tin, tins, jar, jars, package, packages, packet, packets, pkg, bag, bags, box, boxes, bunch,
5
+ bunches, sprig, sprigs, slice, slices, stick, sticks, head, heads, stalk, stalks, pinch, pinches, dash,
6
+ dashes, handful, handfuls, drizzle, splash, sprinkle, knob, piece, pieces, pc, pcs, sheet, sheets, scoop,
7
+ scoops, container, containers, ear, ears, drop, drops, bottle, bottles, envelope, envelopes, block, blocks,
8
+ shot, shots, each, count, serving, servings, portion, portions, ptn, ptns, thumb, thumbs, batch, foot, feet,
9
+ leaf, leaves, bar, bars, loaf, loaves, spear, spears, inch, inches, fl oz, fluid ounce, fluid ounces]
10
+ pinches: [pinch, pinches, dash, dashes, handful, handfuls, drizzle, splash, sprinkle, knob]
11
+ sizes: [large, small, medium, big, small-to-medium, heaping, heaped, level, scant, rounded, generous]
12
+ qualifiers: [about, approx., approx, approximately, around, scant, generous, heaping, heaped, "optional:"]
13
+ number_words:
14
+ one: 1
15
+ two: 2
16
+ three: 3
17
+ four: 4
18
+ five: 5
19
+ six: 6
20
+ seven: 7
21
+ eight: 8
22
+ nine: 9
23
+ ten: 10
24
+ eleven: 11
25
+ twelve: 12
26
+ half: 0.5
@@ -0,0 +1,4 @@
1
+ languages: [es]
2
+ units: [cucharada, cucharadas, cucharadita, cucharaditas, cda, cdas, cdta, cdtas, taza, tazas, diente,
3
+ dientes, pizca, pizcas, lata, latas, rama, ramas, ramita, ramitas, trozo, trozos, paquete, paquetes]
4
+ pinches: [pizca]
@@ -0,0 +1,5 @@
1
+ languages: [fr]
2
+ units: [cs, cc, pincée, pincées, poignée, poignées, gousse, gousses, brin, brins, feuille, feuilles, tranche,
3
+ tranches, tran, sachet, sachets, cuillère, cuillères, c. à soupe, c. à café, cuillère à soupe,
4
+ cuillères à soupe, cuillère à café, cuillères à café]
5
+ pinches: [pincée, poignée]
@@ -0,0 +1,3 @@
1
+ languages: [hu]
2
+ units: [ek, evőkanál, kk, kávéskanál, teáskanál, dkg, db, darab, gerezd, csipet, szál, fej, csomag, bögre]
3
+ sizes: [nagy, kis]
@@ -0,0 +1,4 @@
1
+ languages: [it]
2
+ units: [cucchiaio, cucchiai, cucchiaino, cucchiaini, pizzico, spicchio, spicchi, bicchiere, bicchieri,
3
+ bustina, bustine, fetta, fette, rametto, grappolo]
4
+ pinches: [pizzico]
@@ -0,0 +1,2 @@
1
+ languages: [ja]
2
+ units: [大さじ, 小さじ, 個, 缶, カップ, 本, 枚, 片]
@@ -0,0 +1,4 @@
1
+ languages: [nl]
2
+ units: [el, tl, eetlepel, eetlepels, theelepel, theelepels, teen, teentje, teentjes, takje, takjes, blik,
3
+ zakje, snuf]
4
+ pinches: [snuf]
@@ -0,0 +1,8 @@
1
+ languages: [pt]
2
+ units: [xícara, xícaras, colher, colheres, dente, dentes, pitada, pitadas, fatia, fatias, rodela, rodelas,
3
+ rolo, rolos, medida, medidas, lata, latas, tablete, tabletes, cháv, chávena, chávenas, unid, c. de sopa,
4
+ c. de café, c. de chá, xícara de chá, xícaras de chá, colher de sopa, colheres de sopa, colher de chá,
5
+ colheres de chá]
6
+ number_words:
7
+ meia: 0.5
8
+ meio: 0.5
@@ -0,0 +1,7 @@
1
+ languages: [ru]
2
+ units: [г, гр, грамм, грамма, граммов, кг, мл, л, стакан, стакана, стаканов, шт, зубчик, зубчика, зубчиков,
3
+ щепотка, щепотки, пучок, пучка, штука, штуки, штук, стебель, стебля, головка, головки, веточка, веточки,
4
+ веточек, ст. л, стол. л, ч. л, ст. ложка, ст. ложки, ст. ложек, ч. ложка, ч. ложки, ч. ложек, дес. ложка,
5
+ дес. ложки, ложка, ложки, ложек, столовая ложка, столовые ложки, столовых ложки, столовых ложек,
6
+ чайная ложка, чайные ложки, чайных ложки, чайных ложек]
7
+ pinches: [щепотка]
@@ -0,0 +1,3 @@
1
+ languages: [sv, "no", nb, nn, da]
2
+ units: [ss, ts, tsk, msk, spsk, burk, boks, pakke, kvist, kvister, skiva, skivor, skive, skiver, kruka, potte,
3
+ näve, stykk, stykker, stang, båt, båter, tärning, tärningar, st, stk, pkt]
@@ -0,0 +1,3 @@
1
+ languages: [sk]
2
+ units: [ks, pl, čl, štipka, lyžica, lyžice, lyžička, lyžičky]
3
+ pinches: [štipka]
@@ -0,0 +1,2 @@
1
+ languages: [tr]
2
+ units: [adet, diş, tutam, paket, çay kaşığı, yemek kaşığı, tatlı kaşığı, su bardağı, çay bardağı]