refactor: make API more sane

This commit is contained in:
orion 2023-10-28 15:49:52 -05:00
parent 1ac32ff745
commit 2041858fd9
Signed by: orion
GPG Key ID: 6D4165AE4C928719
9 changed files with 128 additions and 382 deletions

View File

@ -2,8 +2,11 @@ package:
dependencies: dependencies:
- arrays - arrays
- effect - effect
- foreign
- foreign-object
- functions - functions
- maybe - maybe
- nullable
- prelude - prelude
- psci-support - psci-support
- test-unit - test-unit

View File

@ -1,66 +1,53 @@
import cheerio from 'cheerio' import {load} from 'cheerio'
// Attributes /** @typedef {import('cheerio').Element} Element */
export const attrImpl = function (nothing, just, name, cheerioInst) { /** @typedef {import('cheerio').Cheerio<Element>} CheerioNode */
if (cheerioInst.length > 0) {
const value = cheerioInst.attr(name) /** @type {(_1: string) => () => CheerioNode} */
return value != null ? just(value) : nothing 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) { /** @type {(_: CheerioNode) => () => Array<CheerioNode>} */
return cheerioInst.hasClass(className) export const toArrayImpl = n => () => Array(n.length) .fill(undefined) .map((_, ix) => n.slice(ix, ix + 1))
}
// Traversing /** @type {(_: CheerioNode) => () => CheerioNode | null} */
export const findImpl = function (selector, cheerioInst) { export const toNullableImpl = n => () => n.length === 0 ? null : n.first()
return cheerioInst.find(selector)
}
export const parent = function (cheerioInst) { /** @type {(_: CheerioNode) => () => CheerioNode} */
return cheerioInst.parent() export const childrenImpl = n => () => n.children()
}
export const next = function (cheerioInst) { /** @type {(_: CheerioNode) => () => CheerioNode} */
return cheerioInst.next() export const siblingsImpl = n => () => n.siblings()
}
export const prev = function (cheerioInst) { /** @type {(_: CheerioNode) => () => CheerioNode} */
return cheerioInst.prev() export const parentImpl = n => () => n.parent()
}
export const siblings = function (cheerioInst) { /** @type { (_2: CheerioNode) => () => Record<string, string>} */
return cheerioInst.siblings() export const attrsImpl = n => () => n.attr() || {}
}
export const children = function (cheerioInst) { /** @type {(_1: string) => (_2: CheerioNode) => () => string | null} */
return cheerioInst.children() export const attrImpl = k => n => () => n.attr(k) || null
}
export const first = function (cheerioInst) { /** @type {(_2: CheerioNode) => () => Record<string, string>} */
return cheerioInst.first() export const cssImpl = n => () => n.css() || {}
}
export const last = function (cheerioInst) { /** @type {(_2: CheerioNode) => () => string} */
return cheerioInst.last() export const htmlImpl = n => () => n.html() || ''
}
export const eqImpl = function (index, cheerioInst) { /** @type {(_2: CheerioNode) => () => string} */
return cheerioInst.eq(index) export const textImpl = n => () => n.text() || ''
}
// Rendering /** @type {(_1: string) => (_2: CheerioNode) => () => CheerioNode} */
export const htmlImpl = function (nothing, just, cheerioInst) { export const findImpl = s => n => () => n.find(s)
return cheerioInst.length ? just(cheerioInst.html()) : nothing
}
export const text = function (cheerioInst) {
return cheerioInst.text()
}
// Miscellaneous
export const length = function (cheerioInst) {
return cheerioInst.length
}

View File

@ -1,84 +1,64 @@
module Cheerio module Cheerio where
( Cheerio
, attr
, children
, eq
, first
, find
, hasClass
, html
, last
, length
, next
, parent
, prev
, siblings
, text
, toArray
) where
import Prelude hiding (eq) import Prelude
import Data.Array ((..)) import Data.Array as Array
import Data.Function.Uncurried (Fn2, Fn3, Fn4, runFn2, runFn3, runFn4) import Data.Map (Map)
import Data.Maybe (Maybe(..)) 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 loadImpl :: String -> Effect CheerioNode
foreign import attrImpl
:: forall a
. Fn4 (Maybe a) (a -> Maybe a) String Cheerio (Maybe String)
-- | Gets an attribute value from the first selected element, returning foreign import toArrayImpl :: CheerioNode -> Effect (Array CheerioNode)
-- | Nothing when there are no selected elements, or when the first selected foreign import toNullableImpl :: CheerioNode -> Effect (Nullable CheerioNode)
-- | element does not have the specified attribute.
attr :: String -> Cheerio -> Maybe String
attr = runFn4 attrImpl Nothing Just
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 foreign import attrsImpl :: CheerioNode -> Effect (Object String)
hasClass = runFn2 hasClassImpl 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 :: String -> CheerioNode -> Effect CheerioNode
foreign import findImpl :: Fn2 String Cheerio Cheerio
find :: String -> Cheerio -> Cheerio load :: String -> Effect CheerioNode
find = runFn2 findImpl load = loadImpl
foreign import parent :: Cheerio -> Cheerio parent :: CheerioNode -> Effect (Maybe CheerioNode)
foreign import next :: Cheerio -> Cheerio parent = map Nullable.toMaybe <<< toNullableImpl <=< parentImpl
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
foreign import eqImpl :: Fn2 Int Cheerio Cheerio siblings :: CheerioNode -> Effect (Array CheerioNode)
siblings = toArrayImpl <=< siblingsImpl
eq :: Int -> Cheerio -> Cheerio children :: CheerioNode -> Effect (Array CheerioNode)
eq = runFn2 eqImpl children = toArrayImpl <=< childrenImpl
-- Rendering attrs :: CheerioNode -> Effect (Map String String)
foreign import htmlImpl attrs = map Map.fromFoldableWithIndex <<< attrsImpl
:: forall a
. Fn3 (Maybe a) (a -> Maybe a) Cheerio (Maybe String)
-- | Gets an html content string from the first selected element, returning attr :: String -> CheerioNode -> Effect (Maybe String)
-- | Nothing when there are no selected elements. attr k = map Nullable.toMaybe <<< attrImpl k
html :: Cheerio -> Maybe String
html = runFn3 htmlImpl Nothing Just
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 text :: CheerioNode -> Effect String
foreign import length :: Cheerio -> Int text = textImpl
-- | Seperate each element out into its own Cheerio find :: String -> CheerioNode -> Effect (Array (CheerioNode))
toArray :: Cheerio -> Array Cheerio find s = toArrayImpl <=< findImpl s
toArray c
| length c == 0 = [] findFirst :: String -> CheerioNode -> Effect (Maybe (CheerioNode))
| otherwise = map (\i -> eq i c) (0 .. (length c - 1)) findFirst s = map Array.head <<< find s

View File

@ -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)
}

View File

@ -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

View File

@ -2,172 +2,69 @@ module Test.Cheerio where
import Prelude hiding (eq) 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 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 (TestSuite, suite, test)
import Test.Unit.Assert as Assert import Test.Unit.Assert as Assert
import Test.Unit.Main (runTest) 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 :: Effect Unit
main = runTest suites main = runTest suites
emptyCheerio :: Cheerio
emptyCheerio = loadRoot htmlEx # find ".no-such-element"
suites :: TestSuite suites :: TestSuite
suites = do suites = do
suite "Attributes" do suite "Attributes" do
test "attr" do test "attr" do
Assert.equal doc <- liftEffect $ Cheerio.load htmlEx
(Just "fruits") fruits <- liftEffect
(loadRoot htmlEx # find "ul" # attr "id") $ liftMaybe (error "ul should have id")
=<< Cheerio.attr "id"
Assert.equal =<< liftMaybe (error "ul should exist")
Nothing =<< Cheerio.findFirst "ul" doc
(loadRoot htmlEx # find "ul" # attr "no-such-attribute") Assert.equal "fruits" fruits
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")
suite "Traversing" do suite "Traversing" do
test "find" do test "find" do
Assert.equal doc <- liftEffect $ Cheerio.load htmlEx
3 lis <- liftEffect $ Cheerio.find "li" doc
(loadRoot htmlEx # find "#fruits" # find "li" # length) Assert.equal 3 (Array.length lis)
test "parent" do test "parent" do
Assert.equal doc <- liftEffect $ Cheerio.load htmlEx
(Just "fruits") pear <- liftEffect $ liftMaybe (error "pear should exist") =<< Cheerio.findFirst ".pear" doc
(loadRoot htmlEx # find ".pear" # parent # attr "id") parentId <- liftEffect
$ liftMaybe (error "parent should have id")
test "next" do =<< Cheerio.attr "id"
Assert.equal =<< liftMaybe (error "parent should exist")
true =<< Cheerio.parent pear
(loadRoot htmlEx # find ".apple" # next # hasClass "orange") Assert.equal "fruits" parentId
test "prev" do
Assert.equal
true
(loadRoot htmlEx # find ".orange" # prev # hasClass "apple")
test "siblings" do test "siblings" do
Assert.equal doc <- liftEffect $ Cheerio.load htmlEx
2 pear <- liftEffect $ liftMaybe (error "pear should exist") =<< Cheerio.findFirst ".pear" doc
(loadRoot htmlEx # find ".pear" # siblings # length) siblings <- liftEffect $ Cheerio.siblings pear
Assert.equal 2 (Array.length siblings)
test "children" do test "children" do
Assert.equal doc <- liftEffect $ Cheerio.load htmlEx
3 fruitsRoot <- liftEffect $ liftMaybe (error "fruits should exist") =<< Cheerio.findFirst "#fruits" doc
(loadRoot htmlEx # find "#fruits" # children # length) fruits <- liftEffect $ Cheerio.children fruitsRoot
Assert.equal 3 (Array.length fruits)
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)
suite "Rendering" do suite "Rendering" do
test "html" do test "html" do
Assert.equal doc <- liftEffect $ Cheerio.load htmlEx
(Just "Apple") appleHtml <- liftEffect $ Cheerio.html =<< liftMaybe (error "apple should exist") =<< Cheerio.findFirst ".apple" doc
(loadRoot htmlEx # find ".apple" # html) Assert.equal "Apple" appleHtml
Assert.equal
Nothing
(emptyCheerio # html)
test "text" do test "text" do
Assert.equal doc <- liftEffect $ Cheerio.load htmlEx
"Apple" appleText <- liftEffect $ Cheerio.text =<< liftMaybe (error "apple should exist") =<< Cheerio.findFirst ".apple" doc
(loadRoot htmlEx # find ".apple" # text) Assert.equal "Apple" appleText
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
)

View File

@ -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 """<li class="apple">Apple</li>""")
(loadRoot htmlEx # find ".apple" # html)

View File

@ -8,4 +8,4 @@ htmlEx =
<li class="orange">Orange</li> <li class="orange">Orange</li>
<li class="pear">Pear</li> <li class="pear">Pear</li>
</ul> </ul>
""" """

View File

@ -7,7 +7,6 @@ import Test.Unit (TestSuite)
import Test.Unit.Main (runTest) import Test.Unit.Main (runTest)
import Test.Cheerio as C import Test.Cheerio as C
import Test.Cheerio.Static as CS
main :: Effect Unit main :: Effect Unit
main = runTest suites main = runTest suites
@ -15,4 +14,3 @@ main = runTest suites
suites :: TestSuite suites :: TestSuite
suites = do suites = do
C.suites C.suites
CS.suites