diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,31 @@
+Copyright (c) 2012-2013 Stevens Institute of Technology
+
+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 Stevens Institute of Technology nor the
+      names of the authors and 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.
diff --git a/Main.hs b/Main.hs
new file mode 100644
--- /dev/null
+++ b/Main.hs
@@ -0,0 +1,59 @@
+module Main where
+
+import Text.Html.Consolidate
+import System.Console.CmdArgs.Explicit
+import System.IO
+import Network.URI
+import Network.Browser
+import Network.Browser.Simple
+
+-- | The entry point to the `jespresso` program. Can be run in two
+-- modes:
+-- * extraction; `jespresso [-e] [<uri>]`. Takes text (HTML) via stdin
+-- and outputs all the JavaScript in it (including externally
+-- referenced) via stdout. If an (optional) url is supplied, the HTML
+-- is downloaded from that URL instead of reading it from the stdin.
+-- * normalization: `jespresso -n [<uri>]`. Transforms the HTML and
+-- JavaScript into an equivalent one so that all the JavaScript is
+-- inlined and in a single <script> tag. If an (optional) url is
+-- supplied, the HTML is downloaded from that URL instead of reading
+-- it from the stdin.
+
+main :: IO Int
+main = do args <- processArgs arguments 
+          (input, cookies) <- case muri args of
+                Just uri -> download uri []
+                Nothing  -> getContents >>= \c -> return (c, [])
+          output <- case pmode args of
+            Extraction -> extract input (muri args)
+            Consolidation -> consolidate input (muri args)
+          putStr output
+          return 1
+
+arguments :: Mode Parameters
+arguments = (modeEmpty defaultArgs) 
+            {modeNames = ["jespresso"]
+            ,modeHelp = ""
+            ,modeArgs = ([flagArg addURI "URI"], Nothing)
+            ,modeGroupFlags = 
+              toGroup
+              [flagNone ["e", "extract"] addExtract extractHelp
+              ,flagNone ["n", "normalize"] addNormalize normalizeHelp]}
+  where defaultArgs = Parameters {pmode = Extraction
+                                 ,muri  = Nothing}
+        addURI u params = case parseURI u of
+          Nothing  -> Left "Invalid URI"
+          Just uri -> Right $ params {muri = Just uri}
+        addExtract   params = params {pmode = Extraction}
+        addNormalize params = params {pmode = Consolidation}
+        extractHelp = "Extract JavaScript from the supplied HTML and \
+                      \print to stdout"
+        normalizeHelp = "Transform HTML (with JavaScript in it) into an \
+                        \equivalent one so that all the JavaScript is inlined \
+                        \and in a single <script> tag."
+
+data Parameters = Parameters {pmode :: ProgramMode
+                             ,muri  :: Maybe URI}
+                  deriving (Show)
+data ProgramMode = Extraction | Consolidation
+                 deriving (Show)
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/jespresso.cabal b/jespresso.cabal
new file mode 100644
--- /dev/null
+++ b/jespresso.cabal
@@ -0,0 +1,70 @@
+-- Initial jespresso.cabal generated by cabal init.  For further 
+-- documentation, see http://haskell.org/cabal/users-guide/
+
+name:                jespresso
+version:             0.9
+synopsis:            Extract all JavaScript from an HTML page and consolidate it in one script.
+description:         Allows extraction and consolidation of JavaScript code in an HTML page so that it behaves like the original. Consolidation is a process of transforming an HTML page into an equivalent, but containing JavaScript code only in one inlined script tag.
+license:             BSD3
+license-file:        LICENSE
+author:              Andrey Chudnov
+maintainer:          Andrey Chudnov <oss@chudnov.com>
+Homepage:            http://github.com/achudnov/jespresso
+Bug-reports:         http://github.com/achudnov/jespresso/issues
+copyright:           (c) 2012-2013 Stevens Institute of Technology
+category:            Web
+stability:           Experimental
+tested-with:         GHC==7.6.3
+build-type:          Simple
+cabal-version:       >=1.10
+extra-source-files:  test-data/cases/*.html, test-data/expects/*.html
+
+Source-repository head
+   type: git
+   location: git://github.com/achudnov/jespresso.git
+
+Source-repository this
+   type: git
+   location: git://github.com/achudnov/jespresso.git
+   tag: 0.9
+
+library
+  build-depends:       base >=4.2.0 && < 5
+                      ,hxt >= 9.2.0 && < 10
+                      ,hxt-tagsoup >= 9.1.1 && < 10
+                      ,arrows >= 0.4 && < 0.5
+                      ,HTTP >= 4000.2.0 && < 5000
+                      ,language-ecmascript >= 0.15 && < 1.0
+                      ,random >= 1 && < 2
+                      ,bytestring >= 0.9 && < 0.11
+                      ,http-encodings >= 0.9.1 && < 1.0
+                      ,network >= 2.4 && < 2.5
+                      ,data-default-class >= 0.0.1 && < 0.1
+  exposed-modules:    Text.Html.Consolidate
+                      Network.Browser.Simple
+  hs-source-dirs:     src
+  default-language:  Haskell2010
+
+executable jespresso
+  main-is:             Main.hs
+  build-depends:       base >=4.2.0 && < 5
+                      ,jespresso
+                      ,cmdargs >= 0.9 && < 0.11
+                      ,HTTP >= 4000.2.0 && < 5000
+                      ,network >= 2.4 && < 2.5
+  default-language:  Haskell2010
+
+Test-Suite unittest
+  Hs-Source-Dirs: test
+  Type: exitcode-stdio-1.0
+  Main-Is: UnitTest.hs
+  Build-Depends: base >= 4 && < 5
+                ,jespresso
+                ,tasty >= 0.4 && < 0.5
+                ,tasty-golden >= 2.2 && < 2.3
+                ,directory >= 1.2 && < 1.3
+                ,filepath >= 1.1 && < 1.4
+                ,hxt >= 9.2.0 && < 10
+                ,arrows >= 0.4 && < 0.5
+                ,transformers >= 0.3 && < 0.4
+  Default-Language: Haskell2010
diff --git a/src/Network/Browser/Simple.hs b/src/Network/Browser/Simple.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Browser/Simple.hs
@@ -0,0 +1,25 @@
+-- | A wrapper around Network.HTTP to present a simpler interface for
+-- requesting pages via HTTP
+module Network.Browser.Simple (download) where
+
+import Network.URI
+import Network.HTTP
+import Network.Browser
+import Network.HTTP.Encoding
+import Data.ByteString.Lazy
+
+-- | Requests a web page specified by the URI, including optional
+-- cookies. Returns the contents of the page as a @String@ and new
+-- cookies. Follows redirects, decodes the response body, if
+-- possible. Can fail if decoding is impossible.
+download :: URI -> [Cookie] -> IO (String, [Cookie])
+download uri cookies = 
+  browse $ do setAllowRedirects True
+              setUserAgent "Mozilla/5.0 (compatible; jespresso/1.0)"
+              setCookies cookies
+              (_, rsp) <- request (defaultGETRequest_ uri :: Request ByteString)
+              cks <- getCookies
+              return (rsp, cks)
+  >>= \(rsp, cks) -> case decodeBody rsp of
+               Left msg  -> fail $ show msg
+               Right res -> return (decodedBody res, cks)
diff --git a/src/Text/Html/Consolidate.hs b/src/Text/Html/Consolidate.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Html/Consolidate.hs
@@ -0,0 +1,378 @@
+{-# LANGUAGE NoMonomorphismRestriction #-}
+{-# LANGUAGE Arrows #-}
+-- | Implements extraction and consolidation of JavaScript code in an
+-- HTML page.
+module Text.Html.Consolidate (-- * Simple API
+                              consolidate
+                             ,extract
+                              -- * Advanced arrow-based API
+                             ,TArr                              
+                             ,consolidateArr
+                             ,extractJSArr
+                             ,initialConsState
+                             ,insertJSArr
+                             ,parseHTML
+                             ,renderHTML
+                             ) where
+
+import Text.XML.HXT.Core hiding (swap)
+import Text.XML.HXT.Arrow.XmlState.RunIOStateArrow
+import Text.XML.HXT.Arrow.XmlArrow
+import Text.XML.HXT.TagSoup
+import Language.ECMAScript3.Syntax
+import Language.ECMAScript3.Syntax.Annotations
+import Language.ECMAScript3.PrettyPrint
+import Language.ECMAScript3.Parser
+import Data.List (isInfixOf)
+import Data.Default.Class
+import Network.HTTP
+import Network.Browser (Cookie)
+import Network.Browser.Simple
+import Network.URI
+import Network.HTTP.Encoding
+import Data.ByteString.Lazy (ByteString)
+import System.Random
+import Data.Char
+import Data.Maybe (isJust, fromJust, maybeToList)
+import Control.Monad hiding (when)
+
+data ConsState = ConsState Bool  -- Ignore errors?
+                           (Maybe URI) -- Base URI of the web page,
+                                       -- for resolving relative URI's
+                           [Cookie] -- Cookies to include with all
+                                    -- HTTP requests
+                           [Statement ()]
+
+-- | A constructor function for making an initial consolidation state
+-- (needed for running the arrows in the advanced API)
+initialConsState :: Bool -- ^ Whether to ignore errors (parse errors,
+                         -- resource not found etc.)
+                 -> Maybe URI -- ^ base URI
+                 -> [Cookie] -- ^ Cookies
+                 -> ConsState
+initialConsState grace base cookies = ConsState grace base cookies []
+
+-- | Our XML transformation arrow type
+type TArr a b = IOStateArrow ConsState a b
+
+-- | A wrapper around the hxt parser with commonly used arguments
+parseHTML :: String -> Maybe URI -> TArr a XmlTree
+parseHTML s mbase_uri = 
+  let config = map (withDefaultBaseURI . show) (maybeToList mbase_uri)
+             ++[withParseHTML yes
+               ,withTagSoup
+               ,withValidate no
+               ,withSubstDTDEntities no
+               ,withSubstHTMLEntities yes
+               ,withCanonicalize no
+               ,withOutputHTML]
+  in readString config s
+
+-- | A wrapper around hxt to pretty print html out of the arrow
+renderHTML :: ConsState -> TArr XmlTree XmlTree -> IO String
+renderHTML ns a = 
+  let state  = initialState ns
+  in liftM head $ runXIOState state ((single a) >>> writeDocumentToString [withOutputHTML, withOutputEncoding utf8])
+
+-- | Takes an HTML page source as a string and an optional base URI
+-- (for resolving relative URI's) and produces an HTML page with all
+-- the scripts consolidated in one inline script element.
+consolidate :: String -> Maybe URI -> IO String
+consolidate s mbase_uri = 
+  renderHTML (initialConsState True mbase_uri []) $ 
+  parseHTML s mbase_uri >>> consolidateArr
+
+-- | Main normalization arrow. Factored into extractJS and insertJS to
+-- allow custom transformation of JavaScript inbetween.
+consolidateArr :: TArr XmlTree XmlTree
+consolidateArr = extractJSArr >>> insertJSArr
+
+-- | Extacts and pretty-print all the JavaScript code in the given
+-- HTML page source as a single program. Takes an optional base URI
+-- for resolving relative URI's.
+extract :: String -> Maybe URI -> IO String
+extract s mbase_uri =
+  let state = initialState $ initialConsState True mbase_uri [] in
+  do [(_, js)] <- runXIOState state $ single $ 
+                  parseHTML s mbase_uri >>> extractJSArr
+     return js
+
+-- | Extracts all the JavaScript from HTML. There shouldn't be any
+-- JavaScript in the resulting XmlTree
+extractJSArr :: TArr XmlTree (XmlTree, String)
+extractJSArr =
+  ((choiceA [isAJavaScript :-> ifA (hasAttr "src") extractExternalScript
+                                                   extractInlineScript
+            ,(isElem >>> hasOneOfNames src_tags >>> hasOneOfAttrs src_attrs) :-> extractURLProp
+            ,(isElem >>> hasOneOfAttrs event_handlers) :-> extractEventHandler
+            ,this :-> this
+            ])
+   `processTopDownUntilAndWhenMatches` isFrame)
+  >>>returnScript
+  where returnScript = (returnA &&& getUserState)
+                    >>> second (arr (\s -> let ConsState _ _ _ stmts = s 
+                                           in  show $ prettyPrint stmts))
+        isFrame = isElem >>> (hasName "frame" <+> hasName "iframe")
+        hasOneOfNames tagNames = (getName >>> isA (`elem` tagNames)) `guards` this
+        hasOneOfAttrs attrNames = (getAttrl >>> hasOneOfNames attrNames) `guards` this
+        -- Elements and properties that might contain a 'javascript:' url 
+        -- IMG.SRC, A.HREF, FORM.ACTION, FRAME.SRC, IFRAME.SRC, LINK.HREF
+        src_tags = ["img", "a", "form", "frame", "iframe", "link"]
+        src_attrs = ["src", "href", "action"]
+        event_handlers = ["onabort", "onblur", "onclick", "oncompositionstart"
+                         ,"oncompositionupdate", "oncompositionend", "ondblclick"
+                         ,"onerror", "onfocus", "onfocusin", "onfocusout"
+                         ,"onkeydown", "onkeypress", "onkeyup", "onload"
+                         ,"onmousedown", "onmouseenter", "onmouseleave"
+                         ,"onmousemove", "onmouseout", "onmouseover"
+                         ,"onmouseup", "onreset", "onresize","onscroll"
+                         ,"onselect", "onsubmit", "ontextinput", "onunload"
+                         , "onwheel"]
+
+-- | Like Control.Arrow.ArrowTree, but instead has a separate arrow
+-- that serves as a predicate signalling that this subtree shouldn't
+-- be transformed any further; 'processTopDownUntilMatches transformer
+-- predicate'. Almost dual of 'processBottomUpWhenNot', but transforms
+-- the node that matches the predicate as well (but doesn't look
+-- inside that node).
+processTopDownUntilAndWhenMatches :: (ArrowTree a, Tree t)
+                                  => a (t b) (t b) -- ^ the transformer arrow
+                                  -> a (t b) (t b) -- ^ the predicate arrow
+                                  -> a (t b) (t b)
+processTopDownUntilAndWhenMatches t p =
+  t >>> (processChildren (processTopDownUntilAndWhenMatches t p) `whenNot` p)
+  -- ifA p (t >>> processChildren (processTopDownUntilAndWhenMatches t p)) returnA
+                       
+-- | Inserts JavaScript at the end of the HTML body.
+insertJSArr :: TArr (XmlTree, String) XmlTree
+insertJSArr = (swap ^<< (second scriptElement)) >>>
+              arr2A (\scr ->  processTopDown $ changeChildren (++ [scr]) `when`
+                              hasName "body")
+              
+-- extractors
+-- | Extracts the contents of inline scripts
+extractInlineScript :: TArr XmlTree XmlTree
+extractInlineScript = 
+  (firstChild >>> 
+   getText >>> 
+   parseJS >>> 
+   arr removeAnnotations >>> 
+   appendScript >>>
+   cmt "Removed Inline Script") 
+
+-- | Extracts the contents of externally references scripts
+extractExternalScript :: TArr XmlTree XmlTree
+extractExternalScript = 
+  ((getAttrValue "src" >>>
+    (downloadArr >>>
+     parseJS >>>
+     arr removeAnnotations >>>  
+     appendScript) 
+    &&&
+    (arr ("Removed External Script: " ++) >>>
+     mkCmt)) >>>
+   arr snd)
+  
+-- |Downloads the content, considering the input as a URL; performs
+-- decoding automatically.
+downloadArr :: TArr String String
+downloadArr = 
+  (returnA &&& arr parseURIReference) >>> 
+  arrIO (\(url, muri) -> 
+          failIfNothing ("download: error parsing a URI: " ++ url) muri) >>>
+  consolidateURI >>>
+  (getUserState &&& returnA) >>>
+  arrIO (\(ConsState _ _ cookies _, uri) -> liftM fst (download uri cookies))
+
+  where failIfNothing :: String -> Maybe a -> IO a
+        failIfNothing message Nothing  = fail message
+        failIfNothing _       (Just x) = return x
+        -- RFC 3986, section 4.1: if the URI-reference's prefix does
+        -- not match the syntax of a scheme followed by its colon
+        -- separator, then the URI reference is a relative reference.
+        isURIRelative :: URI -> Bool
+        isURIRelative = null . uriScheme
+        consolidateURI :: TArr URI URI
+        consolidateURI = 
+          (getUserState &&& returnA) >>>
+          arrIO (\(ConsState _ mbaseURI _ _, uri) ->
+          return $ if isURIRelative uri && isJust mbaseURI
+                   then uri `relativeTo` fromJust mbaseURI
+                   else uri)
+        
+-- Removes the URL containing properties from elements and adds a
+-- JavaScript assignment instead.
+extractURLProp :: TArr XmlTree XmlTree
+extractURLProp = 
+  -- Elements and properties that might contain a 'javascript:' url 
+  -- IMG.SRC, A.HREF, FORM.ACTION, FRAME.SRC, IFRAME.SRC, LINK.HREF
+  -- isElem >>>
+  -- selectTags ["img", "a", "form", "frame", "iframe", "link"] >>>
+  addIdIfNotPresent >>>
+  (((selectAttrValues ["src", "href", "action"] 
+     &&& selectId) >>>
+    arr (\((url, attrName), id) -> 
+     Script () [ExprStmt () $ AssignExpr () OpAssign 
+                (LDot () (CallExpr ()
+                          (DotRef () (VarRef () (Id () "document")) (Id () "getElementById"))
+                          [StringLit () id]) attrName) (StringLit () url)]) >>>
+   appendScript) &&& 
+  removeAttributes ["src", "href", "action"]) >>>
+  arr snd
+  
+-- Removes the event handlers from HTML tags and converts them to
+-- JavaScript assignments
+extractEventHandler :: TArr XmlTree XmlTree
+extractEventHandler = 
+  -- Names of HTML tag attributes that are event handler declarations:
+  let attrNames = ["onabort", "onblur", "onclick", "oncompositionstart",
+                   "oncompositionupdate", "oncompositionend", "ondblclick", 
+                   "onerror", "onfocus", "onfocusin", "onfocusout", "onkeydown",
+                   "onkeypress", "onkeyup", "onload", "onmousedown",
+                   "onmouseenter", "onmouseleave", "onmousemove", "onmouseout", 
+                   "onmouseover", "onmouseup", "onreset", "onresize","onscroll",
+                   "onselect", "onsubmit", "ontextinput", "onunload", "onwheel"]
+  -- in isElem >>>
+  --    hasAnyAttrs attrNames >>>
+  in   addIdIfNotPresent >>>
+     (((selectAttrValues attrNames &&& selectId) >>>
+      arr (\((handler, attrName), id) ->
+        Script () [ExprStmt () $ AssignExpr () OpAssign 
+                   (LDot () (CallExpr ()
+                             (DotRef () (VarRef () (Id () "document")) (Id () "getElementById"))
+                            [StringLit () id]) attrName) (StringLit () handler)])
+      >>> appendScript) &&&
+     removeAttributes attrNames) >>>
+     arr snd
+
+parseJS :: TArr String (JavaScript SourcePos)
+parseJS = arr (parse program "") >>> eitherToFailure
+
+-- Arrow tools
+-- | Failure reporting arrow constructor
+arrowFail :: ArrowIO ar => String -> ar b c
+arrowFail = arrIO . fail
+
+isStrict :: TArr a Bool
+isStrict = getUserState >>> arr (\s -> let ConsState strict _ _ _ = s in strict)
+
+-- | If in strict mode, fails with the message given; otherwise,
+-- behaves like an identity arrow
+failIfStrict :: String -> TArr a a
+failIfStrict msg = 
+  proc a -> do is <- isStrict -< ()
+               if is then arrowFail msg -< () else returnA -< a
+
+eitherToFailure :: (Show err, Default a) => TArr (Either err a) a
+eitherToFailure = (isStrict &&& returnA) >>> arrIO f
+  where f (False, Left err) = return def
+        f (True,  Left err) = fail $ show err
+        f (_   ,  Right x)  = return x
+
+maybeToFailure :: (Default a) => Maybe String -> TArr (Maybe a) a
+maybeToFailure message = (isStrict &&& returnA) >>> arrIO f
+  where f (False, Nothing) = return def
+        f (True , Nothing) = 
+          case message of
+            Nothing  -> fail "Unexpected maybe in strict mode"
+            Just msg -> fail msg
+        f (_    , Just x)  = return x
+
+appendStatements :: TArr [Statement ()] ()
+appendStatements = (getUserState &&& returnA) >>>
+                   arr (\(state, addScript) -> 
+                         let ConsState grace baseURI cookies script = state in
+                         ConsState grace baseURI cookies (script++addScript)) >>>
+                   setUserState >>> arr (const ())
+
+appendScript :: TArr (JavaScript ()) ()
+appendScript = arr (\s -> let Script _ stmts = s in stmts) >>> appendStatements
+                   
+-- constructors
+-- | Constructs a new JavaScript element
+scriptElement :: ArrowXml ar => ar String XmlTree
+scriptElement = (mkElement (mkName "script") (sattr "type" "text/javascript")) $< arr txt
+
+-- Selectors
+-- | A selector for SCRIPT tags with JavaScript or empty type
+isAJavaScript :: ArrowXml ar => ar XmlTree XmlTree
+isAJavaScript = 
+  isElem >>> hasName "script" >>>
+  (((hasAttr "language" >>> hasAttrValue "language" (isInfixOf "javascript")) <+>
+    (hasAttr "type" >>> hasAttrValue "type" (isInfixOf "javascript"))) `orElse`
+   returnA)
+        
+-- | Selects the first child of a node
+firstChild :: (ArrowTree a, Tree t) => a (t b) (t b)
+firstChild = single getChildren
+  
+-- | Selects the last child fo a node
+lastChild :: (ArrowTree a, Tree t) => a (t b) (t b)
+lastChild = getChildren >>. (take 1 . reverse)
+
+-- | Selects the <html> tag
+html :: ArrowXml a => a XmlTree XmlTree
+html = deep $ hasName "html"
+
+-- | Selects <body> tag
+body :: ArrowXml a => a XmlTree XmlTree
+body = html /> hasName "body"
+  --deep $ hasName "html" /> hasName "body"
+
+selectTags :: ArrowXml a => [String] -> a XmlTree XmlTree
+selectTags = foldl (\arr tag -> arr <+> hasName tag) zeroArrow
+
+selectAttrValues :: ArrowXml a => [String] -> a XmlTree (String, String)
+selectAttrValues = foldl f zeroArrow
+  where f :: ArrowXml a => 
+             a XmlTree (String, String) -> String -> a XmlTree (String, String)
+        f a attr = a <+> (hasAttr attr >>> 
+                          (getAttrValue attr &&& arr (const attr)))
+                   
+hasAnyAttrs :: ArrowXml a => [String] -> a XmlTree XmlTree
+hasAnyAttrs = foldl f zeroArrow
+  where f :: ArrowXml a => a XmlTree XmlTree -> String -> a XmlTree XmlTree
+        f a attr = a <+> hasAttr attr
+                   
+removeAttributes :: ArrowXml a => [String] -> a XmlTree XmlTree
+removeAttributes = foldl f zeroArrow
+  where f :: ArrowXml a => a XmlTree XmlTree -> String -> a XmlTree XmlTree
+        f a attr = a >>> removeAttr attr
+  
+
+addIdIfNotPresent :: TArr XmlTree XmlTree
+addIdIfNotPresent = proc node -> do
+  idval <- getAttrValue "id" -< node
+  if null idval
+    then replaceChildren repFun -< node
+    else returnA -< node
+    where repFun = replaceChildren (genIdA >>> mkText)
+                   `when` (isAttr >>> hasName "id")
+
+-- | Selects the id of an element or adds a new one (and returns) if
+-- it's not present
+selectId :: TArr XmlTree String
+selectId = isElem >>> getAttrValue "id"
+                        
+genIdA :: ArrowIO ar => ar a String
+genIdA = arrIO $ const genId
+
+genId :: IO String
+genId = do firstLetter <- genLetter
+           return [firstLetter]
+           length <- getStdRandom $ randomR (minIdLength-1, maxIdLength-1)
+           restId <- mapM (const genLetter) [1..length] 
+           return $ firstLetter:restId
+  where minIdLength :: Int
+        minIdLength = 16
+        maxIdLength = 32
+        
+genLetter :: IO Char
+genLetter = do letter <- getStdRandom $ randomR (capitalACode, capitalZCode)
+               lettercase <- getStdRandom $ randomR (0,1)
+               return $ chr $ letter + lettercase * (lowercaseACode-capitalACode)
+  where capitalACode = 65
+        capitalZCode = 90
+        lowercaseACode = 97
+        
+swap (a,b) = (b,a)
diff --git a/test-data/cases/complex1.html b/test-data/cases/complex1.html
new file mode 100644
--- /dev/null
+++ b/test-data/cases/complex1.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>Merchant example</title>
+    <script src="http://chudnov.com/research/jsinline/examples/mashups/payment/iframe/ulj.js"></script>
+  </head>
+  <body>
+    <div id="order_summary">
+      <h1>Your order summary</h1>
+      <form id="cart_form" onsubmit="onCartUpdate()">
+	<table id="cart">
+	  <thead>
+	    <tr>
+	      <th>Item</th>
+	      <th>Price</th>
+	      <th>Qty</th>
+	    </tr>
+	  </thead>
+	  <tbody>
+	    <tr>
+	      <td>A bar of foo</td>
+	      <td>5</td>
+	      <td><input type="text" value="1"></td>
+	    </tr>
+	    <tr>
+	      <td>Sprocket 13-pin</td>
+	      <td>7</td>
+	      <td><input type="text" value="1"></td>
+	    </tr>
+	  </tbody>
+	</table>
+	<input type="submit" value="Update total">
+      </form>
+      <script type="text/javascript">
+        var key = 42;
+        window.onmessage = function (e) {
+           var confirmation;
+           try {
+              confirmation = JSON.parse(e.data);
+           } catch (e) {
+              alert("Received invalid confirmation from the payment service. Please try again");
+           }
+           if (confirmation) {
+              /*if (confirmation.transaction_id * confirmation.amount * key == 
+                  confirmation.signature) {*/
+              if (confirmation.signature == 42) {
+                     alert("Your order has been confirmed! Thank you!");
+              } else alert("Received invalid confirmation from the payment service. Please try again");
+           }
+        }
+        function onCartUpdate(e) {
+           var total = 0;
+           rows = document.getElementById("cart").tBodies[0].rows;
+           for (var i = 0; i < rows.length; i++) {
+               total += parseInt(rows[i].cells[1].firstChild.data)*
+                        parseInt(rows[i].cells[2].firstChild.value);
+           }
+           var w = document.getElementById("payment_frame").contentWindow;
+           w.postMessage(total.toString(), "http://chudnov.com");
+           if(e.preventDefault) e.preventDefault();
+           e.returnValue=false;
+        }, true);
+      </script>
+    </div>
+    <iframe src="http://chudnov.com/research/jsinline/examples/mashups/payment/iframe/payment.html"
+	    id="payment_frame">
+    </iframe>
+  </body>
+</html>
diff --git a/test-data/cases/form1.html b/test-data/cases/form1.html
new file mode 100644
--- /dev/null
+++ b/test-data/cases/form1.html
@@ -0,0 +1,6 @@
+<html>
+    <body>
+        <form>
+        </form>
+    </body>
+</html>
diff --git a/test-data/cases/iframe1.html b/test-data/cases/iframe1.html
new file mode 100644
--- /dev/null
+++ b/test-data/cases/iframe1.html
@@ -0,0 +1,8 @@
+<html>
+    <body>
+        <div>
+            <iframe>
+            </iframe>
+        </div>
+    </body>
+</html>
diff --git a/test-data/cases/scriptSimple1.html b/test-data/cases/scriptSimple1.html
new file mode 100644
--- /dev/null
+++ b/test-data/cases/scriptSimple1.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="UTF-8">
+        <title>MyTest</title>
+    </head>
+    <body>
+      <script>
+	foo(bar);
+      </script>
+      <span>Test</span>
+    </body>
+</html>
diff --git a/test-data/cases/trivial.html b/test-data/cases/trivial.html
new file mode 100644
--- /dev/null
+++ b/test-data/cases/trivial.html
@@ -0,0 +1,6 @@
+<html>
+    <head>
+    </head>
+    <body>
+    </body>
+</html>
diff --git a/test-data/expects/complex1.html b/test-data/expects/complex1.html
new file mode 100644
--- /dev/null
+++ b/test-data/expects/complex1.html
@@ -0,0 +1,68 @@
+<html>
+  <head>
+    <meta charset="UTF-8"/>
+    <title>Merchant example</title>
+    <!--Removed External Script: http://chudnov.com/research/jsinline/examples/mashups/payment/iframe/ulj.js-->
+  </head>
+  <body>
+    <div id="order_summary">
+      <h1>Your order summary</h1>
+      <form id="cart_form" onsubmit="onCartUpdate()">
+	<table id="cart">
+	  <thead>
+	    <tr>
+	      <th>Item</th>
+	      <th>Price</th>
+	      <th>Qty</th>
+	    </tr>
+	  </thead>
+	  <tbody>
+	    <tr>
+	      <td>A bar of foo</td>
+	      <td>5</td>
+	      <td><input type="text" value="1"/></td>
+	    </tr>
+	    <tr>
+	      <td>Sprocket 13-pin</td>
+	      <td>7</td>
+	      <td><input type="text" value="1"/></td>
+	    </tr>
+	  </tbody>
+	</table>
+	<input type="submit" value="Update total"/>
+      </form>
+      <script type="text/javascript">
+        var key = 42;
+        window.onmessage = function (e) {
+           var confirmation;
+           try {
+              confirmation = JSON.parse(e.data);
+           } catch (e) {
+              alert("Received invalid confirmation from the payment service. Please try again");
+           }
+           if (confirmation) {
+              /*if (confirmation.transaction_id * confirmation.amount * key == 
+                  confirmation.signature) {*/
+              if (confirmation.signature == 42) {
+                     alert("Your order has been confirmed! Thank you!");
+              } else alert("Received invalid confirmation from the payment service. Please try again");
+           }
+        }
+        function onCartUpdate(e) {
+           var total = 0;
+           rows = document.getElementById("cart").tBodies[0].rows;
+           for (var i = 0; i &lt; rows.length; i++) {
+               total += parseInt(rows[i].cells[1].firstChild.data)*
+                        parseInt(rows[i].cells[2].firstChild.value);
+           }
+           var w = document.getElementById("payment_frame").contentWindow;
+           w.postMessage(total.toString(), "http://chudnov.com");
+           if(e.preventDefault) e.preventDefault();
+           e.returnValue=false;
+        }, true);
+      </script>
+    </div>
+    <iframe id="payment_frame">
+    </iframe>
+  <script type="text/javascript"></script></body>
+</html>
diff --git a/test-data/expects/form1.html b/test-data/expects/form1.html
new file mode 100644
--- /dev/null
+++ b/test-data/expects/form1.html
@@ -0,0 +1,6 @@
+<html>
+    <body>
+        <form>
+        </form>
+    <script type="text/javascript"></script></body>
+</html>
diff --git a/test-data/expects/iframe1.html b/test-data/expects/iframe1.html
new file mode 100644
--- /dev/null
+++ b/test-data/expects/iframe1.html
@@ -0,0 +1,9 @@
+<html>
+    <body>
+        <div>
+            <iframe>
+            </iframe>
+        </div>
+	<script type="text/javascript"></script>
+    </body>
+</html>
diff --git a/test-data/expects/scriptSimple1.html b/test-data/expects/scriptSimple1.html
new file mode 100644
--- /dev/null
+++ b/test-data/expects/scriptSimple1.html
@@ -0,0 +1,13 @@
+<html>
+    <head>
+      <meta charset="UTF-8"/>
+      <title>MyTest</title>
+    </head>
+    <body>
+      <!--Removed Inline Script-->
+      <span>Test</span>
+      <script type="text/javascript">
+	foo(bar);
+      </script>
+    </body>
+</html>
diff --git a/test-data/expects/trivial.html b/test-data/expects/trivial.html
new file mode 100644
--- /dev/null
+++ b/test-data/expects/trivial.html
@@ -0,0 +1,7 @@
+<html>
+    <head>
+    </head>
+    <body>
+      <script type="text/javascript"></script>
+    </body>
+</html>
diff --git a/test/UnitTest.hs b/test/UnitTest.hs
new file mode 100644
--- /dev/null
+++ b/test/UnitTest.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE Rank2Types #-}
+
+module Main where
+
+import Test.Tasty
+import Test.Tasty.Golden.Advanced
+import System.Directory
+import qualified System.FilePath as FP
+import System.IO hiding (utf8)
+import Text.Html.Consolidate
+import Text.XML.HXT.Core
+import Text.XML.HXT.Arrow.XmlState.RunIOStateArrow
+import Text.XML.HXT.Arrow.WriteDocument
+import Control.Arrow
+import Control.Monad
+import Data.Char
+import Control.Monad.IO.Class
+
+main = do allCases <- getDirectoryContents casesDir
+          allExpects <- getDirectoryContents expectsDir
+          let validCases = getValid allCases
+          let validExpects = getValid allExpects
+          defaultMain $ testGroup "Tests" $
+            map genTest $ filter (`elem` validExpects) validCases
+            where getValid = filter $ \x -> FP.takeExtension x == ".js"
+
+casesDir = "test-data/cases"
+expectsDir = "test-data/expects"
+
+genTest :: FilePath -> TestTree
+genTest testFileName =
+  let caseFileName = casesDir `FP.combine` testFileName
+      expectFileName = expectsDir `FP.combine` testFileName
+      ns = initialConsState False Nothing []
+      state  = initialState ns
+      normalizedA :: String -> TArr XmlTree XmlTree
+      normalizedA s = single $ parseHTML s Nothing >>> consolidateArr
+      expectedA :: String -> TArr XmlTree XmlTree      
+      expectedA   s = single $ parseHTML s Nothing
+      runX :: forall r s. (String -> TArr XmlTree XmlTree) -> FilePath -> ValueGetter r String
+      runX a f = liftIO $ do
+          s <- readFile f
+          let arr :: TArr XmlTree String
+              arr = (a s) >>> writeDocumentToString [withOutputHTML, withOutputEncoding utf8]
+          liftM head $ runXIOState state $ arr
+      expectedValueAction = runX expectedA expectFileName
+      testValueAction     = runX normalizedA  caseFileName
+  in goldenTest testFileName expectedValueAction testValueAction verifyOutput (const $ return ())
+
+verifyOutput :: String -> String -> IO (Maybe String)
+verifyOutput expected actual = return $
+  let fs = filter (not . isSpace)
+      msg = "Failed to match expected output to normalized input:\n\
+            \Expected:\n" ++ expected ++ "\n\nSaw:\n" ++ actual ++ "\n"
+  in  if (fs expected == fs actual) then Nothing
+                                    else Just msg
