readability (empty) → 0.0.1.0
raw patch · 12 files changed
+707/−0 lines, 12 filesdep +basedep +bytestringdep +containerssetup-changed
Dependencies added: base, bytestring, containers, html-conduit, http-conduit, optparse-applicative, readability, text, xml-conduit
Files
- CHANGELOG.md +5/−0
- LICENSE +30/−0
- README.md +131/−0
- Setup.hs +2/−0
- app/Main.hs +41/−0
- readability.cabal +69/−0
- src/Readability.hs +54/−0
- src/Readability/Clean.hs +129/−0
- src/Readability/Helper.hs +30/−0
- src/Readability/Internal.hs +56/−0
- src/Readability/Metrics.hs +100/−0
- src/Readability/Types.hs +60/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for readability++## 0.0.1.0 – 2020-06-26++* First version
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2020, G. Eyaeb++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of G. Eyaeb nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,131 @@+# readability++Give `readability` an HTML document and it will detect and extract text of the article while removing everything unnecessary like menus, advertisements or sidebars. It is more or less reimplementation of [python-readability](https://github.com/buriy/python-readability).++The package contains both a library and simple executable.++## Example of using `readability` executable++Having an article that looks like following image:++++we can extract text by calling:++```shell+$> readability https://mises.org/wire/why-central-banks-are-threat-our-savings+```++and we get the following HTML:+++++If we are interested in plain text, we can further use `pandoc`:++```text+$> readability https://mises.org/wire/why-central-banks-are-threat-our-savings | pandoc -f html -t plain++The US personal savings rate jumped to 33 percent in April from 12.7+percent in March and 8 percent in April last year. An increase in+savings is regarded by popular economics as less expenditure on+consumption. Since consumption expenditure is considered as the main+driving force of the economy, obviously a rebound in savings, which+implies less consumption, cannot be good for economic activity, so it is+held. Saving and wealth—what is the relation?++To maintain their life and well-being, individuals require access to+consumer goods. An increase in various consumer goods permits an+increase in individuals’ living standards. What allows an increase in+the production of consumer goods is the maintenance and the enhancement+of the infrastructure of an economy. With better infrastructure, a+greater quantity and better quality of consumer goods could be generated+and more real wealth can be produced.++The enhancement and the maintenance of the infrastructure becomes+possible because of the availability of final consumer goods that+sustain the various individuals who are busy expanding and maintaining+the infrastructure. It is the producers of final consumer goods who pay+the various individuals engaged in maintenaning and enhancing the+infrastructure. The producers of final consumer goods pay these+individuals (i.e., the intermediary producers) out of the saved or+unconsumed production of final consumer goods.++Note that when a producer of final consumer goods decides to save more,+i.e., to consume less, the fall in his consumption is offset by the+increase in the consumption of individuals who are engaged in the+intermediary stages of production. This means that overall consumption+is not declining because of an increase in saving—as popular thinking+has it.+```++Had we not processed the article through `readability`, we would have gotten:++``` text+$> curl https://mises.org/wire/why-central-banks-are-threat-our-savings | pandoc -f html -t plain++Skip to main content++[Home]++Toggle navigation++- Blog+- Mises Wire+- Books+- Podcast+- Video+- Events+- Store+- Graduate Program++- Ver en Español++Stay Connected++GO++SUPPORT MISES++JOIN OR RENEW TODAY++SUPPORT MISES++JOIN OR RENEW TODAY++Mises Wire++GET NEWS AND ARTICLES IN YOUR INBOXPrint++A++A++Home | Wire | Why Central Banks Are a Threat to Our Savings++Why Central Banks Are a Threat to Our Savings++- [dollars]++0 Views++Tags++Money and Banking++06/25/2020Frank Shostak++The US personal savings rate jumped to 33 percent in April from 12.7+percent in March and 8 percent in April last year. An increase in+savings is regarded by popular economics as less expenditure on+consumption. Since consumption expenditure is considered as the main+driving force of the economy, obviously a rebound in savings, which+implies less consumption, cannot be good for economic activity, so it is+held. Saving and wealth—what is the relation?+```++## Contribute++Project is hosted at https://sr.ht/~geyaeb/haskell-readability/ . The homepage provides links to [Mercurial repository](https://hg.sr.ht/~geyaeb/haskell-readability), [mailing list](https://lists.sr.ht/~geyaeb/haskell-readability) and [ticket tracker](https://todo.sr.ht/~geyaeb/haskell-readability).++Patches, suggestions, questions and general discussions can be send to the [mailing list](https://lists.sr.ht/~geyaeb/haskell-readability). Detailed information about sending patches by email can be found at [https://man.sr.ht/hg.sr.ht/email.md](https://man.sr.ht/hg.sr.ht/email.md).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE BlockArguments #-}++module Main where++import qualified Data.Text.Lazy.IO as TL+import Network.HTTP.Simple+import Options.Applicative+import qualified Readability as R+import Text.XML++data Source = File FilePath | Web Request deriving (Show)++data Opts = Opts {optInput :: Source} deriving (Show)++main :: IO ()+main = do+ (Opts src) <- execParser opts+ article <- case src of+ File f -> R.fromFile f+ Web req -> R.fromByteString . getResponseBody <$> httpLBS req+ case R.summary <$> article of+ Just doc -> TL.putStrLn $ renderText def {rsPretty = True, rsXMLDeclaration = True} doc+ Nothing -> return ()++opts :: ParserInfo Opts+opts =+ info+ (options <**> helper)+ ( fullDesc+ <> progDesc "Provide FILE or URL as source"+ <> header "readability - extract article from HTML"+ )++options :: Parser Opts+options = do+ inf <- argument source (metavar "SOURCE")+ pure $ Opts inf++source :: ReadM Source+source = maybeReader \s -> Web <$> parseRequest s <|> Just (File s)
+ readability.cabal view
@@ -0,0 +1,69 @@+cabal-version: >= 1.10++name: readability+version: 0.0.1.0+synopsis: Extracts text of main article from HTML document+description: Give @readability@ an HTML document and it will detect and extract text of the article while removing everything unnecessary like menus, advertisements or sidebars. It is more or less reimplementation of [python-readability](https://github.com/buriy/python-readability).+homepage: https://sr.ht/~geyaeb/haskell-readability+bug-reports: https://todo.sr.ht/~geyaeb/haskell-readability+license: BSD3+license-file: LICENSE+author: G. Eyaeb+maintainer: geyaeb@protonmail.com+copyright: 2020 G. Eyaeb+category: Text, HTML+build-type: Simple+extra-source-files: CHANGELOG.md, README.md++source-repository head+ type: mercurial+ location: https://hg.sr.ht/~geyaeb/haskell-readability++library+ exposed-modules:+ Readability+ Readability.Clean+ Readability.Helper+ Readability.Internal+ Readability.Metrics+ Readability.Types+ hs-source-dirs:+ src+ ghc-options:+ -Wall+ -Wincomplete-uni-patterns+ -Wincomplete-record-updates+ -Wcompat+ -Widentities+ -Wredundant-constraints+ -fhide-source-paths+ -Wmissing-export-lists+ -Wpartial-fields+ build-depends:+ base >=4.7 && <5+ , bytestring == 0.10.*+ , containers == 0.6.*+ , html-conduit == 1.3.*+ , text == 1.2.*+ , xml-conduit >=1.7 && <2+ default-language:+ Haskell2010++executable readability+ main-is: Main.hs+ hs-source-dirs:+ app+ ghc-options:+ -threaded+ -rtsopts+ -with-rtsopts=-N+ -Wall+ build-depends:+ base >=4.7 && <5+ , http-conduit == 2.3.*+ , optparse-applicative == 0.15.*+ , readability+ , text == 1.2.*+ , xml-conduit >=1.7 && <2+ default-language:+ Haskell2010
+ src/Readability.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings #-}++module Readability+ ( -- * Data types+ Article (..),++ -- * Construction+ fromByteString,+ fromDocument,+ fromFile,+ fromText,+ )+where++import Data.ByteString.Lazy (ByteString)+import Data.Text.Lazy (Text)+import Readability.Internal as I+import Readability.Types+import qualified Text.HTML.DOM as DOM+import Text.XML (Document)++-- | Extracts article from HTML represented as HTML document.+fromDocument :: Document -> Maybe Article+fromDocument = fromDocument' strictSettings++-- | Extracts article from HTML represented as HTML document.+fromDocument' :: Settings -> Document -> Maybe Article+fromDocument' s d = Article <$> I.summary s d++-- | Extracts article from HTML in ByteString.+fromByteString :: ByteString -> Maybe Article+fromByteString = fromDocument . DOM.parseLBS++-- | Extracts article from HTML in given file.+fromFile :: FilePath -> IO (Maybe Article)+fromFile f = fromDocument <$> DOM.readFile f++-- | Extracts article from HTML in given text.+fromText :: Text -> Maybe Article+fromText = fromDocument . DOM.parseLT++strictSettings :: Settings+strictSettings =+ Settings+ { reRemoveAttributes = (`elem` ["class"])+ }++{-+looseSettings :: Settings+looseSettings =+ Settings+ { reRemoveAttributes = const False+ }+-}
+ src/Readability/Clean.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Readability.Clean+ ( cleanElement,+ paradivs,+ sanitizeNode,+ )+where++import qualified Data.Map.Strict as Map+import Data.Map.Strict (Map)+import Data.Maybe (fromMaybe, mapMaybe)+import qualified Data.Text as T+import Data.Text (Text)+import Readability.Helper+import Readability.Metrics+import Readability.Types+import Text.XML+import Text.XML.Cursor++-- | Remove elements that do not contribute to the article.+--+-- Removes:+--+-- * headings that are part of ads, menus, widgets etc.+-- * forms, textareas+-- * iframes+-- * block elements (div, section, table etc.) with enough textual content+--+-- Preserves:+--+-- * textual content+sanitizeNode :: Settings -> Scores -> Node -> Maybe Node+sanitizeNode s _ n@(NodeElement e@(Element t as children))+ | t `elem` ["h1", "h2", "h3", "h4", "h5", "h6"] =+ guarded (const $ classWeight e >= 0 && linkDensity (fromNode n) <= 0.33) (NodeElement (Element t (sanitizeAttributes s as) children))+sanitizeNode _ _ (NodeElement e) | e `elin` ["form", "textarea"] = Nothing+sanitizeNode _ _ (NodeElement e)+ | e `elin` ["iframe"] =+ case Map.lookup "src" $ elementAttributes e of+ Just v | "videoRe" `T.isInfixOf` v -> Just (NodeContent "VIDEO")+ _ -> Nothing+sanitizeNode s m (NodeElement e@(Element t as children))+ | e `elin` ["table", "ul", "div", "aside", "header", "footer", "section"] =+ sanitizeContent m (NodeElement $ Element t (sanitizeAttributes s as) (mapMaybe (sanitizeNode s m) children))+sanitizeNode s m (NodeElement (Element t as children)) =+ Just $ NodeElement $ Element t (sanitizeAttributes s as) (mapMaybe (sanitizeNode s m) children)+sanitizeNode _ _ c@(NodeContent _) = Just c+sanitizeNode _ _ _ = Nothing++-- | Remove content of block elements that do not have enough textual content.+sanitizeContent :: Scores -> Node -> Maybe Node+sanitizeContent m n+ | score + weight < 0 = Nothing+ | T.count "," txt >= 10 = Just n+ | cntP > 0 && (fromIntegral cntImg :: Double) > 1.0 + fromIntegral cntP * 1.3 = Nothing+ | cntLi > cntP && not isList = Nothing+ | (fromIntegral cntInput :: Double) > fromIntegral cntP / 3 = Nothing+ | len < 25 && cntImg == 0 = Nothing+ | len < 25 && cntImg > 2 = Nothing+ | weight < 25 && density > 0.2 = Nothing+ | weight >= 25 && density > 0.5 = Nothing+ | (cntEmbed == 1 && len < 75) || cntEmbed > 1 = Nothing+ | len > 0 && preds + succs > 1000 = Just n+ | otherwise = Just n+ where+ score = fromMaybe 0 $ lookupScore n m+ weight = maybe 0 classWeight $ getElement n+ cursor = fromMaybe (fromNode n) $ lookupCursor n m+ txt = innerText cursor+ density = linkDensity cursor+ len = T.length txt+ cntP = count "p" cursor+ cntImg = count "img" cursor+ cntLi = count "li" cursor - 100+ cntEmbed = count "embed" cursor+ cntInput = count "input" cursor - cntInputHidden+ cntInputHidden = length (checkElement isHiddenInput cursor)+ isList = maybe False (`elin` ["ol", "ul"]) $ getElement n+ count :: Name -> Cursor -> Int+ count en = length . checkName (== en)+ preds = sum $ take 1 $ T.length . innerText <$> precedingSibling cursor+ succs = sum $ take 1 $ T.length . innerText <$> followingSibling cursor++-- | Remove element attributes.+sanitizeAttributes :: Settings -> Map Name T.Text -> Map Name T.Text+sanitizeAttributes Settings {..} = Map.filterWithKey (\k _ -> not $ reRemoveAttributes k)++-- | Tests whether element is `input` of type `hidden`.+isHiddenInput :: Element -> Bool+isHiddenInput (Element n as _) = n == "input" && Map.lookup "type" as == Just "hidden"++cleanNode :: Bool -> Node -> Maybe Node+cleanNode _ (NodeElement e) | elementName e `elem` ["script", "style", "link"] = Nothing+cleanNode True (NodeElement e) | isUnlikely e = Nothing+cleanNode r (NodeElement e) = Just $ NodeElement $ cleanElement r e+cleanNode _ c@(NodeContent _) = Just c+cleanNode _ _ = Nothing++cleanElement :: Bool -> Element -> Element+cleanElement ruthless (Element t as children) =+ Element+ t+ (Map.filterWithKey (\k _ -> k /= "style") as)+ (mapMaybe (cleanNode ruthless) children)++unlikely :: Text -> Bool+unlikely t = any (`T.isInfixOf` t) ["combx", "comment", "community", "disqus", "extra", "foot", "header", "menu", "remark", "rss", "shoutbox", "sidebar", "sponsor", "ad-break", "agegate", "pagination", "pager", "popup", "tweet", "twitter"]++likely :: Text -> Bool+likely t = any (`T.isInfixOf` t) ["and", "article", "body", "column", "main", "shadow"]++isUnlikely :: Element -> Bool+isUnlikely e = maybe False unl (attr "class" <> Just " " <> attr "id")+ where+ attr = flip Map.lookup (elementAttributes e)+ unl s = unlikely s && not (likely s) && ((nameLocalName . elementName) e `notElem` ["html", "body"])++{- This works slightly differently than 'transform_misused_divs_into_paragraphs',+ mainly because I did not understand how that was supposed to work. But also,+ this implementation gives better results for our testcases. -}+paradivs :: Node -> Node+paradivs n@(NodeElement (Element "div" as children)) =+ if null $ fromNode n $// checkName (`elem` ["blockquote", "dl", "div", "img", "ol", "p", "pre", "table", "ul"])+ then NodeElement $ Element "p" as children+ else NodeElement $ Element "div" as (paradivs <$> children)+paradivs (NodeElement (Element n as children)) = NodeElement $ Element n as (paradivs <$> children)+paradivs e = e
+ src/Readability/Helper.hs view
@@ -0,0 +1,30 @@+module Readability.Helper+ ( innerText,+ content',+ elin,+ getElement,+ guarded,+ )+where++import Control.Applicative (Alternative (..))+import Data.Text as T (Text, concat, strip)+import Text.XML+import Text.XML.Cursor++innerText :: Cursor -> Text+innerText c = T.concat (c $// content')++content' :: Cursor -> [Text]+content' = content &| T.strip++getElement :: Node -> Maybe Element+getElement (NodeElement e) = Just e+getElement _ = Nothing++elin :: Element -> [Name] -> Bool+elin e [n] = elementName e == n+elin e ns = elementName e `elem` ns++guarded :: (Alternative f) => (a -> Bool) -> a -> f a+guarded p x = if p x then pure x else empty
+ src/Readability/Internal.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}++module Readability.Internal+ ( Readability.Internal.summary,+ rootSummary,+ )+where++import qualified Data.Map.Strict as Map+import qualified Data.Text as T+import Readability.Clean+import Readability.Helper+import Readability.Metrics+import Readability.Types+import Text.XML+import Text.XML.Cursor+import Prelude hiding (div)++summary :: Settings -> Document -> Maybe Document+summary s doc = document <$> rootSummary s True (documentRoot doc)++rootSummary :: Settings -> Bool -> Element -> Maybe Element+rootSummary s ruthless root =+ if ruthless && T.length (innerText clean) <= 250+ then rootSummary s False root+ else cleanedArticle+ where+ clean = fromNode $ paradivs $ NodeElement (cleanElement ruthless root)+ candidates = clean $// checkName (`elem` ["p", "pre", "td"])++ cleanedArticle = case scoreParagraphs candidates of+ Nothing -> Nothing+ Just scores ->+ let (bestCursor, score) = maxScore scores+ article = getArticle bestCursor score scores+ in sanitizeNode s scores article >>= getElement++getArticle :: Cursor -> Double -> Scores -> Node+getArticle best score scores = html $ body $ div $ node <$> filter f siblings+ where+ siblings = precedingSibling best ++ best : followingSibling best+ threshold = 10 `max` score * 0.2+ f c =+ node c == node best+ || any (>= threshold) (lookupScore (node c) scores)+ || (any (\e -> elementName e == "p") (getElement (node c)) && textualSibling c)++html, body :: Node -> Node+html n = NodeElement $ Element "html" Map.empty [n]+body n = NodeElement $ Element "body" Map.empty [n]++div :: [Node] -> Node+div ns = NodeElement $ Element "div" Map.empty ns++document :: Element -> Document+document e = Document (Prologue [] Nothing []) e []
+ src/Readability/Metrics.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Readability.Metrics+ ( classWeight,+ contentScore,+ linkDensity,+ scoreAncestor,+ scoreParagraph,+ scoreParagraphs,+ textualSibling,+ )+where++import qualified Data.Map.Strict as Map+import Data.Monoid (Sum (..))+import Data.Text (Text)+import qualified Data.Text as T+import Readability.Helper+import Readability.Types+import Text.XML+import Text.XML.Cursor++positive :: Text -> Bool+positive t = any (`T.isInfixOf` t) ["article", "body", "content", "entry", "hentry", "main", "page", "pagination", "post", "text", "blog", "story"]++negative :: Text -> Bool+negative t = any (`T.isInfixOf` t) ["combx", "comment", "com-", "contact", "foot", "footer", "footnote", "masthead", "media", "meta", "outbrain", "promo", "related", "scroll", "shoutbox", "sidebar", "sponsor", "shopping", "tags", "tool", "widget"]++linkDensity :: Cursor -> Double+linkDensity c = links / (total `max` 1)+ where+ len = fromIntegral . T.length . T.strip+ total = len $ innerText c+ links = len $ T.concat $ c $// element "a" &// content'++classWeight :: Element -> Double+classWeight e = maybe 0 getSum (sa "class" `mappend` sa "id")+ where+ sa :: Name -> Maybe (Sum Double)+ sa attr = Sum . st <$> Map.lookup attr (elementAttributes e)+ st :: Text -> Double+ st t+ | positive t = 25+ | negative t = -25+ st _ = 0++contentScore :: Cursor -> Maybe Double+contentScore c = if len < 25 then Nothing else Just score+ where+ text = innerText c+ -- TODO clean(elem.text_content() or "")+ len = T.length text+ score =+ 1+ + fromIntegral (length $ T.split (== ',') text)+ + min 3 (fromIntegral len / 100)++scoreAncestor :: Cursor -> Double+scoreAncestor cursor = maybe 0 (\e -> score e + classWeight e) elm+ where+ elm = getElement $ node cursor+ score :: Element -> Double+ score e = case T.toLower $ nameLocalName $ elementName e of+ n+ | n `elem` ["div", "article"] -> 5+ | n `elem` ["pre", "td", "blockquote"] -> 3+ | n `elem` ["address", "ol", "ul", "dl", "dd", "dt", "li", "form", "aside"] -> -3+ | n `elem` ["h1", "h2", "h3", "h4", "h5", "h6", "th", "header", "footer", "nav"] -> -5+ _ -> 0++scoreParagraphs :: [Cursor] -> Maybe Scores+scoreParagraphs cs = if nullScores scores then Nothing else Just scores+ where+ scores = mapScores (\c s -> (c, s * (1 - linkDensity c))) $ foldl scoreParagraph emptyScores cs++scoreParagraph :: Scores -> Cursor -> Scores+scoreParagraph scores cursor =+ case contentScore cursor of+ Nothing -> scores+ Just score ->+ case take 2 $ ancestor cursor of+ [pc, gpc] -> insert (scoreAncestor pc) score pc $ insert (scoreAncestor gpc) (score / 2) gpc scores+ [pc] -> insert (scoreAncestor pc) score pc scores+ _ -> scores+ where+ insert ns cs c =+ alterScores+ ( \case+ Nothing -> Just (c, ns + cs)+ Just (c', v) -> Just (c', v + cs)+ )+ (node c)++textualSibling :: Cursor -> Bool+textualSibling c = (nodelen > 80 && ld < 0.25) || (nodelen <= 80 && ld == 0 && (". " `T.isInfixOf` nodetxt || "." `T.isSuffixOf` nodetxt))+ where+ nodetxt = innerText c+ nodelen = T.length nodetxt+ ld = linkDensity c
+ src/Readability/Types.hs view
@@ -0,0 +1,60 @@+module Readability.Types+ ( Article (..),+ Settings (..),+ Scores (..),+ alterScores,+ emptyScores,+ lookupCursor,+ lookupScore,+ mapScores,+ maxScore,+ nullScores,+ )+where++import Data.List (maximumBy)+import qualified Data.Map.Strict as M+-- import qualified Data.Text as T+import Text.XML+import Text.XML.Cursor++-- | Result of processing HTML through /readability/.+data Article = Article+ { -- | Body of article of original HTML.+ summary :: Document+ {- Title of original HTML if found.+ title :: Maybe T.Text,+ -- Possibly simplified title.+ shortTitle :: Maybe T.Text-}+ }+ deriving (Show)++data Settings = Settings+ { -- | Remove HTML attributes for which the function returns true+ reRemoveAttributes :: Name -> Bool+ }++newtype Scores = Scores {scoreMap :: M.Map Node (Cursor, Double)} deriving (Show)++alterScores :: (Maybe (Cursor, Double) -> Maybe (Cursor, Double)) -> Node -> Scores -> Scores+alterScores f n (Scores sm) = Scores (M.alter f n sm)++emptyScores :: Scores+emptyScores = Scores M.empty++nullScores :: Scores -> Bool+nullScores (Scores sm) = M.null sm++mapScores :: (Cursor -> Double -> (Cursor, Double)) -> Scores -> Scores+mapScores f (Scores sm) = Scores $ M.map (uncurry f) sm++lookupScore :: Node -> Scores -> Maybe Double+lookupScore n (Scores sm) = snd <$> M.lookup n sm++lookupCursor :: Node -> Scores -> Maybe Cursor+lookupCursor n (Scores sm) = fst <$> M.lookup n sm++maxScore :: Scores -> (Cursor, Double)+maxScore (Scores sm) = snd $ maximumBy (\a b -> compare (ssnd a) (ssnd b)) $ M.toList sm+ where+ ssnd = snd . snd