git.iko.soy/typeable/html-parse
on master help sign in
Parsing and rendering for token trees and forests.
commit1147b1faaa748fe0be91220747a945efa2a20520 authorMatthias Fischmann <mf@zerobuzz.net> dateWed 28 Dec 2016, 18:40 (10 years ago) parentaf5d28d
3 files changed+122−2
Text/HTML/Tree.hs+83 html-parse.cabal+4−2 tests/Text/HTML/TreeSpec.hs+35
Text/HTML/Tree.hsnew file+83
@@ -0,0 +1,83 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE ViewPatterns #-}
4+
5+module Text.HTML.Tree
6+ ( -- * Class
7+ IsToken(..)
8+ -- * Parsing forests
9+ , parseTokenForest
10+ , ParseTokenForestError(..), PStack(..)
11+ -- * Rendering forests
12+ , renderTokenForest
13+ , renderTokenTree
14+ ) where
15+
16+import Data.Monoid
17+import Data.Tree
18+import Text.HTML.Parser
19+
20+
21+-- | If you want to store additional data into your tree, you can define your own token type and
22+-- instantiate this class to define a mapping between your type and 'Token'. See
23+-- 'parseTokenForest''.
24+class IsToken a where
25+ toToken :: a -> Maybe Token
26+ fromToken :: Token -> a
27+
28+instance IsToken Token where
29+ toToken = Just
30+ fromToken = id
31+
32+
33+parseTokenForest :: (IsToken token) => [token] -> Either (ParseTokenForestError token) (Forest token)
34+parseTokenForest = f (PStack [] [])
35+ where
36+ f (PStack ss []) [] = Right (reverse ss)
37+ f pstack [] = Left $ ParseTokenForestErrorBracketMismatch pstack Nothing
38+ f pstack (t : ts) = case toToken t of
39+ Just (TagOpen "br" _) -> f (pushFlatSibling t pstack) ts
40+ Just (TagOpen "hr" _) -> f (pushFlatSibling t pstack) ts
41+ Just (TagOpen "img" _) -> f (pushFlatSibling t pstack) ts
42+ Just (TagOpen _ _) -> f (pushParent t pstack) ts
43+ Just (TagClose n) -> (`f` ts) =<< popParent n pstack
44+ Just (ContentChar _) -> f (pushFlatSibling t pstack) ts
45+ Just (ContentText _) -> f (pushFlatSibling t pstack) ts
46+ Just (Comment _) -> f (pushFlatSibling t pstack) ts
47+ Just (Doctype _) -> f (pushFlatSibling t pstack) ts
48+ Nothing -> f (pushFlatSibling t pstack) ts
49+
50+data ParseTokenForestError t =
51+ ParseTokenForestErrorBracketMismatch (PStack t) (Maybe Token)
52+ deriving (Eq, Show)
53+
54+data PStack t = PStack
55+ { _pstackToplevelSiblings :: Forest t
56+ , _pstackParents :: [(t, Forest t)]
57+ }
58+ deriving (Eq, Show)
59+
60+pushParent :: t -> PStack t -> PStack t
61+pushParent t (PStack ss ps) = PStack [] ((t, ss) : ps)
62+
63+popParent :: (IsToken t) => TagName -> PStack t -> Either (ParseTokenForestError t) (PStack t)
64+popParent n (PStack ss ((p@(toToken -> Just (TagOpen n' _)), ss') : ps))
65+ | n == n' = Right $ PStack (Node p (reverse ss) : ss') ps
66+popParent n pstack
67+ = Left $ ParseTokenForestErrorBracketMismatch pstack (Just $ TagClose n)
68+
69+pushFlatSibling :: t -> PStack t -> PStack t
70+pushFlatSibling t (PStack ss ps) = PStack (Node t [] : ss) ps
71+
72+
73+renderTokenForest :: (IsToken t, Show t) => Forest t -> [t]
74+renderTokenForest = mconcat . fmap renderTokenTree
75+
76+renderTokenTree :: (IsToken t, Show t) => Tree t -> [t]
77+renderTokenTree = \case
78+ (Node o@(toToken -> Just (TagOpen n _)) ts)
79+ -> [o] <> renderTokenForest ts <> [fromToken $ TagClose n]
80+ (Node t [])
81+ -> [t]
82+ bad
83+ -> error $ "renderTokenTree: leaf node with children: " <> show bad
html-parse.cabal+4−2
@@ -26,12 +26,13 @@ source-repository head
2626 location: git://github.com/bgamari/html-parse
2727
2828library
29− exposed-modules: Text.HTML.Parser
29+ exposed-modules: Text.HTML.Parser, Text.HTML.Tree
3030 other-extensions: OverloadedStrings, DeriveGeneric
3131 build-depends: base >=4.7 && <4.10,
3232 deepseq >=1.4 && <1.5,
3333 attoparsec >=0.13 && <0.14,
34− text >=1.2 && <1.3
34+ text >=1.2 && <1.3,
35+ containers >=0.5 && <0.6
3536 default-language: Haskell2010
3637
3738benchmark bench
@@ -52,6 +53,7 @@ test-suite spec
5253 hs-source-dirs: tests
5354 ghc-options: -Wall -with-rtsopts=-T
5455 build-depends: base,
56+ containers,
5557 hspec,
5658 hspec-discover,
5759 html-parse,
tests/Text/HTML/TreeSpec.hsnew file+35
@@ -0,0 +1,35 @@
1+{-# LANGUAGE FlexibleInstances #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+
4+{-# OPTIONS_GHC -Wall -Werror -fno-warn-orphans #-}
5+
6+module Text.HTML.TreeSpec
7+where
8+
9+import Data.Tree
10+import Test.Hspec
11+import Test.QuickCheck
12+import Test.QuickCheck.Instances ()
13+import Text.HTML.Parser
14+import Text.HTML.ParserSpec
15+import Text.HTML.Tree
16+
17+
18+arbitraryTokenForest :: Gen (Forest Token)
19+arbitraryTokenForest = listOf $ oneof
20+ [ Node <$> validOpen <*> scale (`div` 5) arbitraryTokenForest
21+ , Node <$> validFlat <*> pure []
22+ ]
23+
24+shrinkTokenForest :: Forest Token -> [Forest Token]
25+shrinkTokenForest = fmap shrinkTokenTree
26+
27+shrinkTokenTree :: Tree Token -> [Tree Token]
28+shrinkTokenTree (Node n c) = Node <$> shrink n <*> shrink c
29+
30+
31+spec :: Spec
32+spec = do
33+ it "parseTokenForests and renderTokenForest are inverses"
34+ . property . forAllShrink arbitraryTokenForest shrinkTokenForest $
35+ \forest -> parseTokenForest (renderTokenForest forest) `shouldBe` Right forest