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:
- arrays
- effect
- foreign
- foreign-object
- functions
- maybe
- nullable
- prelude
- psci-support
- test-unit

View File

@ -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<Element>} 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<CheerioNode>} */
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<string, string>} */
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<string, string>} */
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)

View File

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

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

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="pear">Pear</li>
</ul>
"""
"""

View File

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