From 2041858fd9829a7fd3ca43304dc3be1864180c4c Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Sat, 28 Oct 2023 15:49:52 -0500 Subject: [PATCH] refactor: make API more sane --- spago.yaml | 3 + src/Cheerio.js | 89 +++++++---------- src/Cheerio.purs | 112 +++++++++------------ src/Cheerio/Static.js | 24 ----- src/Cheerio/Static.purs | 45 --------- test/Test/Cheerio.purs | 183 ++++++++-------------------------- test/Test/Cheerio/Static.purs | 50 ---------- test/Test/HtmlEx.purs | 2 +- test/Test/Main.purs | 2 - 9 files changed, 128 insertions(+), 382 deletions(-) delete mode 100644 src/Cheerio/Static.js delete mode 100644 src/Cheerio/Static.purs delete mode 100644 test/Test/Cheerio/Static.purs diff --git a/spago.yaml b/spago.yaml index 738a99d..2d67825 100644 --- a/spago.yaml +++ b/spago.yaml @@ -2,8 +2,11 @@ package: dependencies: - arrays - effect + - foreign + - foreign-object - functions - maybe + - nullable - prelude - psci-support - test-unit diff --git a/src/Cheerio.js b/src/Cheerio.js index 574487e..04f5764 100644 --- a/src/Cheerio.js +++ b/src/Cheerio.js @@ -1,66 +1,53 @@ -import cheerio from 'cheerio' +import {load} from 'cheerio' -// Attributes -export const attrImpl = function (nothing, just, name, cheerioInst) { - if (cheerioInst.length > 0) { - const value = cheerioInst.attr(name) - return value != null ? just(value) : nothing +/** @typedef {import('cheerio').Element} Element */ +/** @typedef {import('cheerio').Cheerio} CheerioNode */ + +/** @type {(_1: string) => () => CheerioNode} */ +export const loadImpl = html => () => { + const root = load(html).root() + const htmlC = root.first().children().first() + + if (!htmlC.is('html')) { + throw new Error('invariant condition: root node should contain HTML!') } - return nothing + if (htmlC.length !== 1) { + throw new Error('invariant condition: HTML element should be only child!') + } + + return htmlC } -export const hasClassImpl = function (className, cheerioInst) { - return cheerioInst.hasClass(className) -} +/** @type {(_: CheerioNode) => () => Array} */ +export const toArrayImpl = n => () => Array(n.length) .fill(undefined) .map((_, ix) => n.slice(ix, ix + 1)) -// Traversing -export const findImpl = function (selector, cheerioInst) { - return cheerioInst.find(selector) -} +/** @type {(_: CheerioNode) => () => CheerioNode | null} */ +export const toNullableImpl = n => () => n.length === 0 ? null : n.first() -export const parent = function (cheerioInst) { - return cheerioInst.parent() -} +/** @type {(_: CheerioNode) => () => CheerioNode} */ +export const childrenImpl = n => () => n.children() -export const next = function (cheerioInst) { - return cheerioInst.next() -} +/** @type {(_: CheerioNode) => () => CheerioNode} */ +export const siblingsImpl = n => () => n.siblings() -export const prev = function (cheerioInst) { - return cheerioInst.prev() -} +/** @type {(_: CheerioNode) => () => CheerioNode} */ +export const parentImpl = n => () => n.parent() -export const siblings = function (cheerioInst) { - return cheerioInst.siblings() -} +/** @type { (_2: CheerioNode) => () => Record} */ +export const attrsImpl = n => () => n.attr() || {} -export const children = function (cheerioInst) { - return cheerioInst.children() -} +/** @type {(_1: string) => (_2: CheerioNode) => () => string | null} */ +export const attrImpl = k => n => () => n.attr(k) || null -export const first = function (cheerioInst) { - return cheerioInst.first() -} +/** @type {(_2: CheerioNode) => () => Record} */ +export const cssImpl = n => () => n.css() || {} -export const last = function (cheerioInst) { - return cheerioInst.last() -} +/** @type {(_2: CheerioNode) => () => string} */ +export const htmlImpl = n => () => n.html() || '' -export const eqImpl = function (index, cheerioInst) { - return cheerioInst.eq(index) -} +/** @type {(_2: CheerioNode) => () => string} */ +export const textImpl = n => () => n.text() || '' -// Rendering -export const htmlImpl = function (nothing, just, cheerioInst) { - return cheerioInst.length ? just(cheerioInst.html()) : nothing -} - -export const text = function (cheerioInst) { - return cheerioInst.text() -} - -// Miscellaneous -export const length = function (cheerioInst) { - return cheerioInst.length -} +/** @type {(_1: string) => (_2: CheerioNode) => () => CheerioNode} */ +export const findImpl = s => n => () => n.find(s) diff --git a/src/Cheerio.purs b/src/Cheerio.purs index 791e045..1185982 100644 --- a/src/Cheerio.purs +++ b/src/Cheerio.purs @@ -1,84 +1,64 @@ -module Cheerio - ( Cheerio - , attr - , children - , eq - , first - , find - , hasClass - , html - , last - , length - , next - , parent - , prev - , siblings - , text - , toArray - ) where +module Cheerio where -import Prelude hiding (eq) +import Prelude -import Data.Array ((..)) -import Data.Function.Uncurried (Fn2, Fn3, Fn4, runFn2, runFn3, runFn4) -import Data.Maybe (Maybe(..)) +import Data.Array as Array +import Data.Map (Map) +import Data.Map as Map +import Data.Maybe (Maybe) +import Data.Nullable (Nullable) +import Data.Nullable as Nullable +import Effect (Effect) +import Foreign.Object (Object) -foreign import data Cheerio :: Type +foreign import data CheerioNode :: Type --- Attributes -foreign import attrImpl - :: forall a - . Fn4 (Maybe a) (a -> Maybe a) String Cheerio (Maybe String) +foreign import loadImpl :: String -> Effect CheerioNode --- | Gets an attribute value from the first selected element, returning --- | Nothing when there are no selected elements, or when the first selected --- | element does not have the specified attribute. -attr :: String -> Cheerio -> Maybe String -attr = runFn4 attrImpl Nothing Just +foreign import toArrayImpl :: CheerioNode -> Effect (Array CheerioNode) +foreign import toNullableImpl :: CheerioNode -> Effect (Nullable CheerioNode) -foreign import hasClassImpl :: Fn2 String Cheerio Boolean +foreign import siblingsImpl :: CheerioNode -> Effect CheerioNode +foreign import childrenImpl :: CheerioNode -> Effect CheerioNode +foreign import parentImpl :: CheerioNode -> Effect CheerioNode -hasClass :: String -> Cheerio -> Boolean -hasClass = runFn2 hasClassImpl +foreign import attrsImpl :: CheerioNode -> Effect (Object String) +foreign import attrImpl :: String -> CheerioNode -> Effect (Nullable String) +foreign import cssImpl :: CheerioNode -> Effect (Object String) +foreign import htmlImpl :: CheerioNode -> Effect String +foreign import textImpl :: CheerioNode -> Effect String --- Traversing -foreign import findImpl :: Fn2 String Cheerio Cheerio +foreign import findImpl :: String -> CheerioNode -> Effect CheerioNode -find :: String -> Cheerio -> Cheerio -find = runFn2 findImpl +load :: String -> Effect CheerioNode +load = loadImpl -foreign import parent :: Cheerio -> Cheerio -foreign import next :: Cheerio -> Cheerio -foreign import prev :: Cheerio -> Cheerio -foreign import siblings :: Cheerio -> Cheerio -foreign import children :: Cheerio -> Cheerio -foreign import first :: Cheerio -> Cheerio -foreign import last :: Cheerio -> Cheerio +parent :: CheerioNode -> Effect (Maybe CheerioNode) +parent = map Nullable.toMaybe <<< toNullableImpl <=< parentImpl -foreign import eqImpl :: Fn2 Int Cheerio Cheerio +siblings :: CheerioNode -> Effect (Array CheerioNode) +siblings = toArrayImpl <=< siblingsImpl -eq :: Int -> Cheerio -> Cheerio -eq = runFn2 eqImpl +children :: CheerioNode -> Effect (Array CheerioNode) +children = toArrayImpl <=< childrenImpl --- Rendering -foreign import htmlImpl - :: forall a - . Fn3 (Maybe a) (a -> Maybe a) Cheerio (Maybe String) +attrs :: CheerioNode -> Effect (Map String String) +attrs = map Map.fromFoldableWithIndex <<< attrsImpl --- | Gets an html content string from the first selected element, returning --- | Nothing when there are no selected elements. -html :: Cheerio -> Maybe String -html = runFn3 htmlImpl Nothing Just +attr :: String -> CheerioNode -> Effect (Maybe String) +attr k = map Nullable.toMaybe <<< attrImpl k -foreign import text :: Cheerio -> String +css :: CheerioNode -> Effect (Map String String) +css = map Map.fromFoldableWithIndex <<< cssImpl --- Miscellaneous +html :: CheerioNode -> Effect String +html = htmlImpl --- | Get how many elements there are in the given Cheerio -foreign import length :: Cheerio -> Int +text :: CheerioNode -> Effect String +text = textImpl --- | Seperate each element out into its own Cheerio -toArray :: Cheerio -> Array Cheerio -toArray c - | length c == 0 = [] - | otherwise = map (\i -> eq i c) (0 .. (length c - 1)) +find :: String -> CheerioNode -> Effect (Array (CheerioNode)) +find s = toArrayImpl <=< findImpl s + +findFirst :: String -> CheerioNode -> Effect (Maybe (CheerioNode)) +findFirst s = map Array.head <<< find s diff --git a/src/Cheerio/Static.js b/src/Cheerio/Static.js deleted file mode 100644 index d8787db..0000000 --- a/src/Cheerio/Static.js +++ /dev/null @@ -1,24 +0,0 @@ -import cheerio from 'cheerio' - -// Loading -export const load = cheerio.load - -// Selecting -export const selectImpl = function (str, cheerioStatic) { - return cheerioStatic(str) -} - -export const selectDeepImpl = function (strArr, cheerioStatic) { - return cheerioStatic.apply(cheerioStatic, strArr) -} - -// Rendering -export const htmlImpl = function (nothing, just, cheerioInst) { - const ret = cheerio.html(cheerioInst) - return ret != null ? just(ret) : nothing -} - -// Utilities -export const root = function (cheerioStatic) { - return cheerio.root.call(cheerioStatic) -} diff --git a/src/Cheerio/Static.purs b/src/Cheerio/Static.purs deleted file mode 100644 index 8264743..0000000 --- a/src/Cheerio/Static.purs +++ /dev/null @@ -1,45 +0,0 @@ -module Cheerio.Static - ( CheerioStatic - , html - , load - , loadRoot - , select - , selectDeep - , root - ) where - -import Prelude -import Cheerio (Cheerio) -import Data.Function.Uncurried (Fn2, Fn3, runFn2, runFn3) -import Data.Maybe (Maybe(..)) - -foreign import data CheerioStatic :: Type - --- Loading -foreign import load :: String -> CheerioStatic - --- Selectors -foreign import selectImpl :: Fn2 String CheerioStatic Cheerio - -select :: String -> CheerioStatic -> Cheerio -select = runFn2 selectImpl - -foreign import selectDeepImpl :: Fn2 (Array String) CheerioStatic Cheerio - -selectDeep :: Array String -> CheerioStatic -> Cheerio -selectDeep = runFn2 selectDeepImpl - --- Rendering -foreign import htmlImpl - :: forall a - . Fn3 (Maybe a) (a -> Maybe a) Cheerio (Maybe String) - -html :: Cheerio -> Maybe String -html = runFn3 htmlImpl Nothing Just - --- Utilities -foreign import root :: CheerioStatic -> Cheerio - --- Convenience -loadRoot :: String -> Cheerio -loadRoot = load >>> root diff --git a/test/Test/Cheerio.purs b/test/Test/Cheerio.purs index a89b877..5b89e9e 100644 --- a/test/Test/Cheerio.purs +++ b/test/Test/Cheerio.purs @@ -2,172 +2,69 @@ module Test.Cheerio where import Prelude hiding (eq) -import Effect (Effect) +import Cheerio (CheerioNode) +import Cheerio as Cheerio +import Control.Monad.Error.Class (liftMaybe) +import Data.Array as Array import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Class (liftEffect) +import Effect.Exception (error) +import Test.HtmlEx (htmlEx) import Test.Unit (TestSuite, suite, test) import Test.Unit.Assert as Assert import Test.Unit.Main (runTest) -import Cheerio - ( Cheerio - , attr - , children - , eq - , find - , first - , hasClass - , html - , last - , length - , next - , parent - , prev - , siblings - , text - , toArray - ) - -import Cheerio.Static (loadRoot) - -import Test.HtmlEx (htmlEx) - main :: Effect Unit main = runTest suites -emptyCheerio :: Cheerio -emptyCheerio = loadRoot htmlEx # find ".no-such-element" - suites :: TestSuite suites = do suite "Attributes" do test "attr" do - Assert.equal - (Just "fruits") - (loadRoot htmlEx # find "ul" # attr "id") - - Assert.equal - Nothing - (loadRoot htmlEx # find "ul" # attr "no-such-attribute") - - Assert.equal - Nothing - (emptyCheerio # attr "id") - - test "hasClass" do - Assert.equal - true - (loadRoot htmlEx # find ".pear" # hasClass "pear") - - Assert.equal - false - (loadRoot htmlEx # find "apple" # hasClass "fruit") - - Assert.equal - true - (loadRoot htmlEx # find "li" # hasClass "pear") - - Assert.equal - false - (emptyCheerio # hasClass "pear") + doc <- liftEffect $ Cheerio.load htmlEx + fruits <- liftEffect + $ liftMaybe (error "ul should have id") + =<< Cheerio.attr "id" + =<< liftMaybe (error "ul should exist") + =<< Cheerio.findFirst "ul" doc + Assert.equal "fruits" fruits suite "Traversing" do test "find" do - Assert.equal - 3 - (loadRoot htmlEx # find "#fruits" # find "li" # length) + doc <- liftEffect $ Cheerio.load htmlEx + lis <- liftEffect $ Cheerio.find "li" doc + Assert.equal 3 (Array.length lis) test "parent" do - Assert.equal - (Just "fruits") - (loadRoot htmlEx # find ".pear" # parent # attr "id") - - test "next" do - Assert.equal - true - (loadRoot htmlEx # find ".apple" # next # hasClass "orange") - - test "prev" do - Assert.equal - true - (loadRoot htmlEx # find ".orange" # prev # hasClass "apple") + doc <- liftEffect $ Cheerio.load htmlEx + pear <- liftEffect $ liftMaybe (error "pear should exist") =<< Cheerio.findFirst ".pear" doc + parentId <- liftEffect + $ liftMaybe (error "parent should have id") + =<< Cheerio.attr "id" + =<< liftMaybe (error "parent should exist") + =<< Cheerio.parent pear + Assert.equal "fruits" parentId test "siblings" do - Assert.equal - 2 - (loadRoot htmlEx # find ".pear" # siblings # length) + doc <- liftEffect $ Cheerio.load htmlEx + pear <- liftEffect $ liftMaybe (error "pear should exist") =<< Cheerio.findFirst ".pear" doc + siblings <- liftEffect $ Cheerio.siblings pear + Assert.equal 2 (Array.length siblings) test "children" do - Assert.equal - 3 - (loadRoot htmlEx # find "#fruits" # children # length) - - test "first" do - Assert.equal - "Apple" - (loadRoot htmlEx # find "#fruits" # children # first # text) - - test "last" do - Assert.equal - "Pear" - (loadRoot htmlEx # find "#fruits" # children # last # text) - - test "eq" do - Assert.equal - "Apple" - (loadRoot htmlEx # find "li" # eq 0 # text) - - Assert.equal - "Pear" - (loadRoot htmlEx # find "li" # eq (-1) # text) + doc <- liftEffect $ Cheerio.load htmlEx + fruitsRoot <- liftEffect $ liftMaybe (error "fruits should exist") =<< Cheerio.findFirst "#fruits" doc + fruits <- liftEffect $ Cheerio.children fruitsRoot + Assert.equal 3 (Array.length fruits) suite "Rendering" do test "html" do - Assert.equal - (Just "Apple") - (loadRoot htmlEx # find ".apple" # html) - - Assert.equal - Nothing - (emptyCheerio # html) + doc <- liftEffect $ Cheerio.load htmlEx + appleHtml <- liftEffect $ Cheerio.html =<< liftMaybe (error "apple should exist") =<< Cheerio.findFirst ".apple" doc + Assert.equal "Apple" appleHtml test "text" do - Assert.equal - "Apple" - (loadRoot htmlEx # find ".apple" # text) - - Assert.equal - "" - (emptyCheerio # text) - - suite "Miscellaneous" do - test "length" do - Assert.equal - 0 - (emptyCheerio # length) - - Assert.equal - 3 - (loadRoot htmlEx # find "li" # length) - - test "toArray" do - Assert.equal - [] - (emptyCheerio # toArray # map (attr "class")) - - Assert.equal - (map Just [ "apple", "orange", "pear" ]) - (loadRoot htmlEx # find "li" # toArray # map (attr "class")) - - suite "More" do - test "Long chain" do - Assert.equal - "Apple" - ( loadRoot htmlEx - # find ".apple" - # siblings - # eq 1 - # parent - # children - # first - # text - ) + doc <- liftEffect $ Cheerio.load htmlEx + appleText <- liftEffect $ Cheerio.text =<< liftMaybe (error "apple should exist") =<< Cheerio.findFirst ".apple" doc + Assert.equal "Apple" appleText diff --git a/test/Test/Cheerio/Static.purs b/test/Test/Cheerio/Static.purs deleted file mode 100644 index dd2f0f4..0000000 --- a/test/Test/Cheerio/Static.purs +++ /dev/null @@ -1,50 +0,0 @@ -module Test.Cheerio.Static where - -import Prelude hiding (eq) - -import Effect (Effect) -import Data.Maybe (Maybe(..)) -import Test.Unit (TestSuite, suite, test) -import Test.Unit.Assert as Assert -import Test.Unit.Main (runTest) - -import Cheerio (attr, find, length, text) - -import Cheerio.Static - ( load - , select - , selectDeep - , html - , root - , loadRoot - ) - -import Test.HtmlEx (htmlEx) - -main :: Effect Unit -main = runTest suites - -suites :: TestSuite -suites = do - suite "Loading" do - test "load" do - Assert.equal - 1 - (load htmlEx # root # find "#fruits" # length) - - suite "Selectors" do - test "select" do - Assert.equal - (Just "pear") - (load htmlEx # select "ul .pear" # attr "class") - - test "selectDeep" do - Assert.equal - "Apple" - (load htmlEx # selectDeep [ ".apple", "#fruits" ] # text) - - suite "Rendering" do - test "html" do - Assert.equal - (Just """
  • Apple
  • """) - (loadRoot htmlEx # find ".apple" # html) diff --git a/test/Test/HtmlEx.purs b/test/Test/HtmlEx.purs index 50cc797..72edce6 100644 --- a/test/Test/HtmlEx.purs +++ b/test/Test/HtmlEx.purs @@ -8,4 +8,4 @@ htmlEx =
  • Orange
  • Pear
  • -""" \ No newline at end of file +""" diff --git a/test/Test/Main.purs b/test/Test/Main.purs index abdc132..f9742a6 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -7,7 +7,6 @@ import Test.Unit (TestSuite) import Test.Unit.Main (runTest) import Test.Cheerio as C -import Test.Cheerio.Static as CS main :: Effect Unit main = runTest suites @@ -15,4 +14,3 @@ main = runTest suites suites :: TestSuite suites = do C.suites - CS.suites