diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2018 Hyunrae Cho
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,153 @@
+# hanspell-hs
+`hanspell-hs`는 (주)다음과 부산대학교 인공지능연구실/(주)나라인포테크의 웹 서비스를 이용한 한글 맞춤법 검사기입니다.
+
+`hanspell-hs`는 자바스크립트로 작성한 [hanspell](https://github.com/9beach/hanspell)의
+[하스켈](https://www.haskell.org/) 포트입니다. 참고하세요.
+
+[![Build Status](https://travis-ci.org/9beach/hanspell-hs.svg?branch=master)](https://travis-ci.org/9beach/hanspell-hs)
+
+## 설치
+현재는 소스를 직접 빌드해서 설치해야 합니다.
+[해키지](https://hackage.haskell.org/)에는 곧 올릴 예정입니다. 
+
+[카발](https://www.haskell.org/cabal/)을 이용해서 아래와 같이 설치하세요.
+
+```sh
+$ git clone https://github.com/9beach/hanspell-hs.git
+$ cd hanspell-hs
+$ cabal new-build && cabal new-install
+$ # `~/.cabal/bin/hanspell` created
+```
+
+카발의 버전에 따라서 위의 명령 대신 `cabal new-build && cabal install` 또는
+`cabal build && cabal install`을 실행해야 할 수도 있습니다.
+
+[스택](https://docs.haskellstack.org)을 이용해서 다음과 같이 설치할 수도 있습니다.
+
+```sh
+$ git clone https://github.com/9beach/hanspell-hs.git
+$ cd hanspell-hs
+$ stack build && stack install
+$ # `~/.local/bin/hanspell` created
+```
+
+일부 배포본에서는 위의 명령을 실행하기 전에 다음을 먼저 실행해야 할 수도 있습니다.
+
+```sh
+$ stack upgrade
+$ sudo apt install libz-dev
+```
+
+## 명령어 사용법
+
+```
+$ hanspell -h
+사용법: hanspell [-d | -p | -a | -h] 
+
+옵션:
+  -d, --daum [default]	  다음 서비스를 이용해서 맞춤법을 교정합니다
+  -p, --pnu               부산대학교 서비스를 이용해서 맞춤법을 교정합니다
+  -a, --all 		  두 서비스의 모든 결과를 반영해서 맞춤법을 교정합니다
+  -h, --info              도움말을 출력합니다
+```
+
+문장을 직접 입력하거나 클립보드에서 복사해서 맞춤법을 교정할 수 있습니다. 다음은
+사용 예시입니다. <kbd>CTRL + D</kbd>는 줄을 바꾸고 맨 앞에서 입력해야 합니다.
+<pre>
+$ hanspell
+나는 차가운 모래속에 두 손을 넣고 검게 빛나는 바다를 바라본다.
+우주의 가장자리 같다.
+쇼코는 해변에 서 있으면 이세상의 변두리에 선 느낌이 든다고 말했었다.
+<kbd>CTRL + D</kbd>
+모래속에 <font color=grey>-></font> 모래 속에<font color=grey>
+띄어쓰기 오류입니다. 대치어를 참고하여 고쳐 쓰세요.</font>
+이세상의 <font color=grey>-></font> 이 세상의<font color=grey>
+관형사는 뒤에 오는 말과 띄어 쓰는 것이 옳습니다.
+...</font>
+</pre>
+
+![스크린샷](./hanspell-screenshot.png?raw=true "한스펠 스크린샷")
+
+파일의 맞춤법을 교정하려면 다음과 같이 명령합니다.
+```
+$ cat your-text | hanspell
+```
+로그는 생략한 채 교정된 문장만 보려면 다음과 같이 명령합니다.
+```
+$ cat your-text | hanspell 2> /dev/null
+나는 차가운 모래 속에 두 손을 넣고 검게 빛나는 바다를 바라본다.
+우주의 가장자리 같다.
+쇼코는 해변에 서 있으면 이 세상의 변두리에 선 느낌이 든다고 말했었다.
+```
+교정 제안만 보려면 다음과 같이 명령합니다.
+```
+$ cat your-text | hanspell 2>&1 > /dev/null | grep '->'
+```
+클립보드에 복사된 문장을 입력 없이 바로 교정하려면, 맥OS 사용자는 `pbpaste`, 
+X 윈도 시스템 사용자는 `xclip -o`, 마이크로소프트 윈도우 사용자는 
+`powershell.exe Get-Clipboard` 명령을 이용할 수 있습니다.
+```
+$ pbpaste | hanspell
+```
+`~/.hanspell-ignore` 파일에 교정 대상에서 제외할 문자열을 
+[글로브 패턴](https://ko.wikipedia.org/wiki/글로브_(프로그래밍))으로 지정할 수
+있습니다. "그로떼스끄"로 시작하는 문자열과 "빠이"를 교정 대상에서 제외하려면
+다음과 같이 설정하세요.
+```
+그로떼스끄*
+빠이
+```
+`~/.hanspell-history` 파일에 교정 내역이 기록됩니다.
+```
+$ sort < ~/.hanspell-history | uniq -c | sort -nr | head 
+  17 모래속에 -> 모래 속에
+  13 곤색이다 -> 감색이다
+  13 곤색의 -> 감색의
+  13 한바퀴 -> 한 바퀴
+  13 돌껀데 -> 돌 건데
+  10 리랜드는 -> 이랜드는
+   9 말했더만 -> 말했더구먼
+   7 주름투성이 -> 주름 투성이
+   7 암소여서도 -> 암소 여서도
+   7 열두살 -> 열두 살
+```
+
+## 터미널 및 파일 인코딩
+
+`hanspell`은 UTF-8으로 설정된 터미널에서만 테스트되었습니다.
+```
+$ cat your-text.utf-8 | hanspell
+```
+
+홈 디렉터리의 `.hanspell-ignore` 파일 또한 UTF-8 인코딩으로 저장해야 합니다.
+
+## 라이브러리 사용법
+
+`Language.Hanspell` 라이브러리는 `Typo` 자료구조와 관련 함수, 그리고 `spellCheckByDaum`, `spellCheckByPnu` 함수를 제공합니다. 다음은 사용 예입니다.
+```haskell
+module HanspellExample where
+
+import Language.Hanspell
+
+example = do
+    let sentence = "위에계신분, 잘들리세요?"
+    typos <- spellCheckByDaum sentence
+    mapM_ (putStrLn . typoToStringWithStyle True) typos
+```
+
+다음의 결과가 예상됩니다.
+```
+위에계신분, -> 위에 계신 분,
+뒤에 오는 명사를 수식하는 관형격 어미 ‘-ㄴ’, ‘-는’, ‘-던’, ‘-ㄹ’ 등과 의존명사는 띄어 쓰는 것이 옳습니다.
+(예)
+노력한 만큼 대가를 얻다.
+소문으로만 들었을 뿐이네.
+합격했다는 소리를 들으니 그저 기쁠 따름이다.
+
+잘들리세요? -> 잘 들리세요?
+'익숙하고 능란하게', '좋고 훌륭하게'라는 의미의 부사 '잘'은 띄어 쓰세요.
+(예)
+바둑을 잘 두다.
+옷을 잘 차려입고 나서니 딴사람 같구나.
+다음 대화를 잘 듣고 물음에 답하세요.
+```
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/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,81 @@
+module Main where
+
+import System.Directory
+import System.Environment
+import System.Exit
+import System.IO
+import System.Posix.IO
+import System.Posix.Terminal
+import Data.List
+import Control.Concurrent.Async
+
+import Language.Hanspell.Typo
+import Language.Hanspell.DaumSpellChecker
+import Language.Hanspell.PnuSpellChecker
+import Language.Hanspell.Glob
+import Language.Hanspell.TextLines
+
+data SpellChecker = DAUM | PNU | All
+
+main :: IO ()
+main = do
+    args <- getArgs
+    checker <- case args of
+        []          -> return DAUM
+        ["-d"]      -> return DAUM
+        ["--daum"]  -> return DAUM
+        ["-p"]      -> return PNU
+        ["--pnu"]   -> return PNU
+        ["-a"]      -> return All
+        ["--all"]   -> return All
+        _           -> putStrLn help >> exitFailure
+
+    -- Reads input and splits it to proper size.
+    sentences <- getContents
+    let splitted = case checker of
+            DAUM -> linesByLength daumSpellCheckerMaxChars sentences
+            _    -> linesByWordCount pnuSpellCheckerMaxWords sentences
+
+    -- Spell checks (gets typos) from the servers.
+    typos <- case checker of
+        DAUM -> concat <$> mapConcurrently spellCheckByDaum splitted
+        PNU  -> concat <$> mapConcurrently spellCheckByPnu splitted
+        All  -> (++) <$> (concat <$> mapConcurrently spellCheckByDaum splitted)
+                     <*> (concat <$> mapConcurrently spellCheckByPnu splitted)
+
+    -- Removes duplicated typos.
+    let typos' = rmdupTypos typos
+
+    -- Reads ignoring typos.
+    homeDir <- getEnv "HOME"
+    let ignorePath = homeDir ++ "/.hanspell-ignore"
+    exists <- doesFileExist ignorePath
+    ignoreds <- if exists
+                   then lines <$> readFile ignorePath
+                   else return []
+
+    -- Removes ignoring typos.
+    let typos'' = if not (null ignoreds)
+                     then filter (not . matchGlobs ignoreds . token) typos'
+                     else typos'
+
+    -- Prints typos and fixed sentences.
+    isTTY <- queryTerminal stdOutput
+    mapM_ (hPutStr stderr . typoToStringWithStyle isTTY) typos''
+    putStr $ fixTyposWithStyle isTTY sentences typos''
+
+    -- Writes history.
+    let logPath = homeDir ++ "/.hanspell-history"
+    let logs = concatMap (\t -> 
+               token t ++ " -> " ++ head (suggestions t) ++ "\n") typos''
+    appendFile logPath logs
+
+help :: String
+help = "\
+\사용법: hanspell [-d | -p | -a | -h]\n\
+\\n\
+\옵션:\n\
+\  -d, --daum              다음 서비스를 이용해서 맞춤법을 교정합니다\n\
+\  -p, --pnu               부산대학교 서비스를 이용해서 맞춤법을 교정합니다\n\
+\  -a, --all [default]     두 서비스의 모든 결과를 반영해서 맞춤법을 교정합니다\n\
+\  -h, --info              도움말을 출력합니다"
diff --git a/hanspell.cabal b/hanspell.cabal
new file mode 100644
--- /dev/null
+++ b/hanspell.cabal
@@ -0,0 +1,119 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: a15620cf968d00610db9e7355196e20f890261cecda759747cf0ccd6bfd1acea
+
+name:           hanspell
+version:        0.2.0.0
+synopsis:       Korean spell checker
+description:    This package includes @hanspell@, a simple command line spell check program and a library, @Language.Hanspell@. Both of them are based on DAUM spell check web service and Pusan National University spell check web service.
+category:       language,korean,spellchecker
+homepage:       https://github.com/9beach/hanspell-hs#readme
+bug-reports:    https://github.com/9beach/hanspell-hs/issues
+author:         Hyunrae Cho
+maintainer:     9beach@gmail.com
+copyright:      2020 Hyunrae Cho
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    test/long-paragraph-sample.txt
+
+source-repository head
+  type: git
+  location: https://github.com/9beach/hanspell-hs
+
+library
+  exposed-modules:
+      Language.Hanspell
+      Language.Hanspell.DaumSpellChecker
+      Language.Hanspell.Decoder
+      Language.Hanspell.Glob
+      Language.Hanspell.PnuSpellChecker
+      Language.Hanspell.TextLines
+      Language.Hanspell.Typo
+  other-modules:
+      Paths_hanspell
+  hs-source-dirs:
+      src
+  build-depends:
+      aeson >=1.4.7 && <1.5
+    , async >=2.2.2 && <2.3
+    , base >=4.7 && <5
+    , bytestring >=0.10.8 && <0.11
+    , directory >=1.3.0 && <1.4
+    , html-entities >=1.1.4 && <1.2
+    , http-client >=0.6.4 && <0.7
+    , http-client-tls >=0.3.5 && <0.4
+    , http-types >=0.12.3 && <0.13
+    , regex-compat-tdfa >=0.95.1 && <0.96
+    , split >=0.2.3 && <0.3
+    , text >=1.2.4 && <1.3
+    , transformers >=0.5.2 && <0.6
+    , unix >=2.7.2 && <2.8
+    , utf8-string >=1.0.1 && <1.1
+  default-language: Haskell2010
+
+executable hanspell
+  main-is: Main.hs
+  other-modules:
+      Paths_hanspell
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      aeson >=1.4.7 && <1.5
+    , async >=2.2.2 && <2.3
+    , base >=4.7 && <5
+    , bytestring >=0.10.8 && <0.11
+    , directory >=1.3.0 && <1.4
+    , hanspell
+    , html-entities >=1.1.4 && <1.2
+    , http-client >=0.6.4 && <0.7
+    , http-client-tls >=0.3.5 && <0.4
+    , http-types >=0.12.3 && <0.13
+    , regex-compat-tdfa >=0.95.1 && <0.96
+    , split >=0.2.3 && <0.3
+    , text >=1.2.4 && <1.3
+    , transformers >=0.5.2 && <0.6
+    , unix >=2.7.2 && <2.8
+    , utf8-string >=1.0.1 && <1.1
+  default-language: Haskell2010
+
+test-suite hanspell-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      DaumSpellCheckerSpec
+      HanspellExample
+      PnuSpellCheckerSpec
+      TextLinesSpec
+      TypoSpec
+      Paths_hanspell
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      QuickCheck
+    , aeson >=1.4.7 && <1.5
+    , async >=2.2.2 && <2.3
+    , base >=4.7 && <5
+    , bytestring >=0.10.8 && <0.11
+    , directory >=1.3.0 && <1.4
+    , hanspell
+    , hspec
+    , html-entities >=1.1.4 && <1.2
+    , http-client >=0.6.4 && <0.7
+    , http-client-tls >=0.3.5 && <0.4
+    , http-types >=0.12.3 && <0.13
+    , regex-compat-tdfa >=0.95.1 && <0.96
+    , split >=0.2.3 && <0.3
+    , text >=1.2.4 && <1.3
+    , transformers >=0.5.2 && <0.6
+    , unix >=2.7.2 && <2.8
+    , utf8-string >=1.0.1 && <1.1
+  default-language: Haskell2010
diff --git a/src/Language/Hanspell.hs b/src/Language/Hanspell.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Hanspell.hs
@@ -0,0 +1,5 @@
+module Language.Hanspell (module Hanspell) where
+
+import Language.Hanspell.PnuSpellChecker as Hanspell
+import Language.Hanspell.DaumSpellChecker as Hanspell
+import Language.Hanspell.Typo as Hanspell
diff --git a/src/Language/Hanspell/DaumSpellChecker.hs b/src/Language/Hanspell/DaumSpellChecker.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Hanspell/DaumSpellChecker.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# OPTIONS_HADDOCK hide #-}
+
+-- | Defines the interfaces for DAUM spell check service.
+module Language.Hanspell.DaumSpellChecker
+    ( DaumSpellChecker
+    , spellCheckByDaum
+    , daumSpellCheckerMaxChars
+    ) where
+
+import qualified Data.ByteString.UTF8 as BU
+import qualified Data.ByteString.Lazy.UTF8 as BLU
+
+import Data.List
+import Data.List.Split
+import Network.HTTP.Types.Status
+import Text.Regex
+import Debug.Trace
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Class
+import Network.HTTP.Client
+import Network.HTTP.Client.TLS
+
+import Language.Hanspell.Typo
+import Language.Hanspell.Decoder
+
+-- | Defines a class for 'spellCheckByDaum' function overloading.
+class Monad m => DaumSpellChecker m where
+
+    -- | Requests spell check to DAUM server, parses the responses,
+    -- and returns @m [Typo]@. @spellCheckByDaum@ has two return types.
+    -- One is @MaybeT IO [Typo]@, and the other is @IO [Typo]@. 
+    --
+    -- @
+    -- import Language.Hanspell 
+    --
+    -- example = do
+    --     let sentence = "위에계신분, 잘들리세요?"
+    --     typos <- spellCheckByDaum sentence
+    --     mapM_ (putStrLn . typoToStringWithStyle False) typos
+    -- @
+    --
+    -- The expected output is:
+    --
+    -- @
+    -- 위에계신분, -> 위에 계신 분,
+    -- 뒤에 오는 명사를 수식하는 관형격 어미 ‘-ㄴ’, ‘-는’, ‘-던’, ‘-ㄹ’ 등과 의존명사는 띄어 쓰는 것이 옳습니다.
+    -- (예)
+    -- 노력한 만큼 대가를 얻다.
+    -- 소문으로만 들었을 뿐이네.
+    -- 합격했다는 소리를 들으니 그저 기쁠 따름이다.
+    -- 
+    -- 잘들리세요? -> 잘 들리세요?
+    -- '익숙하고 능란하게', '좋고 훌륭하게'라는 의미의 부사 '잘'은 띄어 쓰세요.
+    -- (예)
+    -- 바둑을 잘 두다.
+    -- 옷을 잘 차려입고 나서니 딴사람 같구나.
+    -- 다음 대화를 잘 듣고 물음에 답하세요.
+    -- @ 
+    spellCheckByDaum :: String -> m [Typo]
+
+-- | Obssesive version returning @MaybeT IO [Typo]@.
+instance DaumSpellChecker (MaybeT IO) where
+    spellCheckByDaum text = htmlToTypos <$> requestToDaum text
+
+-- | Bold version returning @IO [Typo]@.
+instance DaumSpellChecker IO where
+    spellCheckByDaum text = do
+        maybe <- runMaybeT $ htmlToTypos <$> requestToDaum text
+        case maybe of 
+            Nothing -> return []
+            Just typos -> return typos
+
+-- | Official maximum character length for a 'spellCheckByDaum' request.
+-- Notice that DAUM server can handle more than maximum characters.
+daumSpellCheckerMaxChars :: Int
+daumSpellCheckerMaxChars = 1000
+ 
+-- 'spellCheckByDaum' prints the message below when the HTTP status code is 
+-- not 200. Mainly due to timeout.
+daumConnectError :: String
+daumConnectError = 
+     "-- 한스펠 오류: 다음 서버의 접속 오류로 일부 문장 교정에 실패했습니다."
+
+-- 'spellCheckByDaum' prints the message below when the response is not of 
+-- spell checker. Mainly due to the changes of service URL.
+invalidResponseFromDaum :: String
+invalidResponseFromDaum = 
+     "-- 한스펠 오류: 다음 서비스가 유효하지 않은 양식을 반환했습니다. (" 
+     ++ daumSpellCheckUrl ++ ")" 
+
+-- Daum spell checker URL.
+--
+-- Try `curl -H "Content-Type: application/x-www-form-urlencoded" \
+-- -X POST https://dic.daum.net/grammar_checker.do -d \
+-- "sentence=안녕 하세요"`.
+daumSpellCheckUrl :: String
+daumSpellCheckUrl = "https://dic.daum.net/grammar_checker.do"
+
+-- Requests spell check to the server, check the responses, and returns it. 
+-- When the status code is not 200, or the response is not of spell checker, 
+-- traces error message, and returns Nothing.
+requestToDaum :: String -> MaybeT IO String
+requestToDaum text = do
+    manager <- lift $ newManager tlsManagerSettings
+    let pair = [("sentence",BU.fromString text)]
+    initialRequest <- lift $ parseRequest daumSpellCheckUrl
+    let request = (urlEncodedBody pair initialRequest) { method = "POST" }
+    response <- lift $ httpLbs request manager
+    let errCode = statusCode (responseStatus response)
+    let daumResponseInfix = "=\"screen_out\">맞춤법 검사기 본문</h2>"
+    if errCode == 200
+       then let body = BLU.toString (responseBody response)
+             in if daumResponseInfix `isInfixOf` body
+                   then return body
+                   else trace invalidResponseFromDaum (MaybeT $ return Nothing)
+       else trace (daumConnectError ++ " ("++ show errCode ++ ")")
+            (MaybeT $ return Nothing)
+
+-- Parses the response HTML to [Typo]
+htmlToTypos :: String -> [Typo]
+htmlToTypos body =
+        -- Removes unuseds from body
+    let stripped = head . splitOn "<span class=\"info_byte\">" $ body
+        -- Splits the body for each typo
+        splitted = tail . splitOn "data-error-type" $ stripped
+     in map htmlToTypo splitted
+
+-- Parse a unit of response to Typo
+htmlToTypo :: String -> Typo
+htmlToTypo body = Typo { errorType    =  decodeEntity (splitted!!1)
+                       , token        =  decodeEntity (splitted!!3)
+                       , suggestions  = [decodeEntity (splitted!!5)]
+                       , context      =  decodeEntity (splitted!!7)
+                       , info         =  info'''
+                       } where
+    gsub from to text = subRegex (mkRegex from) text to
+    splitted = splitOn "\"" $ head (lines body)
+    info' = splitOn "<div>" body!!1
+    info'' = head (splitOn "<span class=\"info_byte\">" info')
+    info''' = gsub "^[ \n][ \n]*" ""
+            . gsub "<[^>]*>" ""
+            . gsub "<br[^>]*>" "\n"
+            . gsub "</span><span class.*\n" ""
+            . gsub "<a href=\"#none\".*\n" ""
+            . gsub "^<span>.*\n" ""
+            . gsub "<strong class.*\n" ""
+            . gsub ".*strong class=.tit_help.>예문</strong.*\n" "(예)"
+            . gsub "\t" ""
+            $ info''
diff --git a/src/Language/Hanspell/Decoder.hs b/src/Language/Hanspell/Decoder.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Hanspell/Decoder.hs
@@ -0,0 +1,13 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+module Language.Hanspell.Decoder (decodeEntity) where
+
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+
+import HTMLEntities.Decoder
+import Data.Text.Lazy.Builder
+
+-- HTML entity decoder.
+decodeEntity :: String -> String
+decodeEntity = T.unpack . TL.toStrict . toLazyText . htmlEncodedText . T.pack
diff --git a/src/Language/Hanspell/Glob.hs b/src/Language/Hanspell/Glob.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Hanspell/Glob.hs
@@ -0,0 +1,46 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+-- | Match Glob patterns by convertng them to Regular Expressions.
+-- Code is take from the book of "Real World Haskell".
+module Language.Hanspell.Glob (matchGlob, matchGlobs) where
+
+import Text.Regex
+import Data.Maybe
+
+-- | Checks if a string matches a glob pattern by converting that glob 
+-- pattern to a regular expression and matching using that.
+matchGlob :: String -> String -> Bool
+matchGlob glob string = isJust (matchRegex (mkRegex (globToRegex glob)) string)
+
+-- | Checks if a string matches any of glob patterns.
+matchGlobs :: [String] -> String -> Bool
+matchGlobs globs string = any (`matchGlob` string) globs
+
+-- Converts a Glob Expression into a Regular Expression, anchor it to the
+-- beginning and end of the line
+globToRegex :: String -> String
+globToRegex globex = '^' : globToRegex' globex ++ "$"
+
+-- Finds glob specific characters, and convert them to regex specific
+-- characters, escapes regex specific characters and verify that character
+-- classes are properly terminated
+globToRegex' :: String -> String
+globToRegex' ""             = ""
+globToRegex' ('*':cs)       = ".*" ++ globToRegex' cs
+globToRegex' ('?':cs)       = '.' : globToRegex' cs
+globToRegex' ('[':'!':c:cs) = "[^" ++ c : charClass cs
+globToRegex' ('[':c:cs)     = '[' : c : charClass cs
+globToRegex' ('[':_)        = error "unterminated character class"
+globToRegex' (c:cs)         = escape c ++ globToRegex' cs
+
+-- Escapes regex characters.
+escape :: Char -> String
+escape c | c `elem` regexChars = '\\' : [c]
+         | otherwise           = [c]
+    where regexChars = "\\+()^$.{}]"
+
+-- Verifies character classes are terminated.
+charClass :: String -> String
+charClass (']':cs) = ']' : globToRegex' cs
+charClass (c:cs)   = c : charClass cs
+charClass []       = error "unterminated character class"
diff --git a/src/Language/Hanspell/PnuSpellChecker.hs b/src/Language/Hanspell/PnuSpellChecker.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Hanspell/PnuSpellChecker.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE DeriveGeneric  #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# OPTIONS_HADDOCK hide #-}
+
+-- | Defines the interfaces for Pusan National University spell check service.
+module Language.Hanspell.PnuSpellChecker
+    ( PnuSpellChecker
+    , spellCheckByPnu
+    , pnuSpellCheckerMaxWords
+    ) where
+
+import qualified Data.ByteString.UTF8 as BU
+import qualified Data.ByteString.Lazy.UTF8 as BLU
+
+import Data.List
+import Data.List.Split
+import Network.HTTP.Types.Status
+import Text.Regex
+import Debug.Trace
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Class
+import Network.HTTP.Client
+import Network.HTTP.Client.TLS
+import Data.Aeson
+import GHC.Generics
+
+import Language.Hanspell.Typo
+import Language.Hanspell.Decoder
+
+-- | Defines a class for 'spellCheckByPnu' function overloading.
+class Monad m => PnuSpellChecker m where
+
+    -- | Requests spell check to PNU server, parses the responses, 
+    -- and returns @m [Typo]@. @spellCheckByPnu@ has two return types.
+    -- One is @MaybeT IO [Typo]@, and the other is @IO [Typo]@. 
+    --
+    -- @
+    -- import Language.Hanspell 
+    --
+    -- example = do
+    --     let sentence = "위에계신분, 잘들리세요?"
+    --     typos <- spellCheckByPnu sentence
+    --     mapM_ (putStrLn . typoToStringWithStyle False) typos
+    -- @
+    --
+    -- The expected output is:
+    --
+    -- @
+    -- 위에계신분 -> 위에 계신 분
+    -- '계신 분'으로 띄어 씁니다.
+    -- 
+    -- 잘들리세요 -> 잘 들리세요
+    -- 부사는 뒤의 말과 띄어 써야 합니다.
+    -- (예)
+    -- 곧돌아오마 (x)-> 곧 돌아오마 (o)
+    -- 부디건강해라(x) -> 부디 건강해라(o)
+    -- 어서오십시오(x) -> 어서 오십시오(o)
+    -- ...
+    -- @
+    spellCheckByPnu :: String -> m [Typo]
+
+-- | Obssesive version returning @MaybeT IO [Typo]@.
+instance PnuSpellChecker (MaybeT IO) where
+    spellCheckByPnu text = htmlToTypos <$> requestToPnu text
+
+-- | Bold version returning @IO [Typo]@.
+instance PnuSpellChecker IO where
+    spellCheckByPnu text = do
+        maybe <- runMaybeT $ htmlToTypos <$> requestToPnu text
+        case maybe of 
+            Nothing -> return []
+            Just typos -> return typos
+
+-- | Maximum words count for a 'spellCheckByPnu' request.
+pnuSpellCheckerMaxWords :: Int
+pnuSpellCheckerMaxWords = 295
+ 
+-- 'spellCheckByPnu' prints the message below when the HTTP status code is 
+-- not 200. Mainly due to timeout.
+pnuConnectError :: String
+pnuConnectError = 
+    "-- 한스펠 오류: 부산대 서버의 접속 오류로 일부 문장 교정에 실패했습니다."
+
+-- 'spellCheckByPnu' prints the message below when the response is not of 
+-- spell checker. Mainly due to the changes of service URL.
+invalidResponseFromPnu :: String
+invalidResponseFromPnu = 
+    "-- 한스펠 오류: 부산대 서비스가 유효하지 않은 양식을 반환했습니다. ("
+    ++ pnuSpellCheckUrl ++ ")"
+
+-- Pnu spell checker URL.
+--
+-- Try `curl -H "Content-Type: application/x-www-form-urlencoded" \
+-- -X POST http://speller.cs.pusan.ac.kr/results -d \
+-- --data-urlencode "text1=안녕 하세요"`.
+pnuSpellCheckUrl :: String
+pnuSpellCheckUrl = "http://speller.cs.pusan.ac.kr/results"
+
+-- For convenience
+gsub :: String -> String -> String -> String
+gsub from to text = subRegex (mkRegex from) text to
+
+-- Requests spell check to the server, check the responses, and returns it. 
+-- When the status code is not 200, or the response is not of spell checker, 
+-- traces error message, and returns Nothing.
+requestToPnu :: String -> MaybeT IO String
+requestToPnu text = if null (words text) then return "" else do
+    -- Walkaround for PNU server's weired logic
+    let text' = intercalate "\n " . splitOn "\n" $ text
+    manager <- lift $ newManager tlsManagerSettings
+    let pair = [("text1",BU.fromString text')]
+    initialRequest <- lift $ parseRequest pnuSpellCheckUrl
+    let request = (urlEncodedBody pair initialRequest) { method = "POST" }
+    response <- lift $ httpLbs request manager
+    let errCode = statusCode (responseStatus response)
+
+    let pnuResponseInfix = "<title>한국어 맞춤법/문법 검사기</title>"
+    if errCode == 200
+       then let body = BLU.toString (responseBody response)
+             in if pnuResponseInfix `isInfixOf` body
+                   then return body
+                   else trace invalidResponseFromPnu (MaybeT $ return Nothing)
+       else trace (pnuConnectError ++ " ("++ show errCode ++ ")")
+            (MaybeT $ return Nothing)
+
+-- PNU response format for a Typo.
+data PnuTypo = PnuTypo
+    { help :: String
+    , errorIdx :: Int
+    , correctMethod :: Int
+    , start :: Int
+    , end :: Int
+    , orgStr :: String
+    , candWord :: String
+    } deriving (Show, Generic, ToJSON, FromJSON)
+
+-- PNU response format for Typos.
+data PnuTypos = PnuTypos
+    { str :: String
+    , errInfo :: [PnuTypo]
+    , idx :: Int
+    } deriving (Show, Generic, ToJSON, FromJSON)
+
+-- Parses the response HTML to [Typo].
+htmlToTypos :: String -> [Typo]
+htmlToTypos body =
+    case matchRegex (mkRegex "^\tdata = (.*);$") body of 
+        Nothing -> []
+        Just [jsonText] -> map pnuTypoToTypo pnuTypos
+          where
+            Just pnuTyopsList = decode . BLU.fromString $ jsonText
+            pnuTypos = mconcat . map errInfo $ pnuTyopsList
+
+-- Converts PnuTypo (response JSON) to Typo.
+pnuTypoToTypo :: PnuTypo -> Typo
+pnuTypoToTypo pnuTypo = 
+    Typo { errorType = ""
+         , token = decodeEntity . orgStr $ pnuTypo
+         , suggestions = splitOn "|" . decodeEntity $ suggestions'
+         , context = ""
+         , info = decodeEntity . gsub "\n\\(예\\) " "(예)\n"
+                . gsub " *<br/> *" "\n" . (++ "\n") . help $ pnuTypo
+         } where
+    suggestions' = if null . candWord $ pnuTypo
+                      then orgStr pnuTypo
+                      else candWord pnuTypo
diff --git a/src/Language/Hanspell/TextLines.hs b/src/Language/Hanspell/TextLines.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Hanspell/TextLines.hs
@@ -0,0 +1,44 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+-- | Breaks a text up into a list of texts at newline chars. The maximum 
+-- length or word count of a element of the list is given by parameters.
+-- So the element may contain multiple newlines.
+module Language.Hanspell.TextLines
+    ( linesByLength
+    , linesByWordCount
+    ) where
+
+import Data.List
+
+-- | Line-wrapped break up to character count.
+linesByLength :: Int -> String -> [String]
+linesByLength maxChars =
+    map unlines . reverse . map reverse . merge . 
+        foldl' mergeLine (maxChars,0,[],[]) . lines
+  where
+    mergeLine :: (Int,Int,[String],[[String]])
+              -> String
+              -> (Int,Int,[String],[[String]])
+    mergeLine (maxChars,headsLength,heads,merged) aLine = 
+        if lineLength + headsLength > maxChars && not (null heads) 
+           then (maxChars,lineLength,[aLine],heads:merged)
+           else (maxChars,lineLength + headsLength,aLine:heads,merged)
+        where lineLength = length aLine + 1
+
+-- | Line-wrapped break up to word count.
+linesByWordCount :: Int -> String -> [String]
+linesByWordCount maxWords =
+    map unlines . reverse . map reverse . merge . 
+        foldl' mergeLine (maxWords,0,[],[]) . lines
+  where
+    mergeLine :: (Int,Int,[String],[[String]])
+              -> String
+              -> (Int,Int,[String],[[String]])
+    mergeLine (maxWords,headsWords,heads,merged) aLine = 
+        if xWords + headsWords > maxWords && not (null heads) 
+           then (maxWords,xWords,[aLine],heads:merged)
+           else (maxWords,xWords + headsWords,aLine:heads,merged)
+        where xWords = length (words aLine)
+
+-- Add the heads left to the merged heads list
+merge (_,_,heads,merged) = if null heads then merged else heads:merged
diff --git a/src/Language/Hanspell/Typo.hs b/src/Language/Hanspell/Typo.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Hanspell/Typo.hs
@@ -0,0 +1,69 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+-- | Defines Typo data structure and related utilities. Typo data carries the 
+-- information of a typo.
+module Language.Hanspell.Typo 
+    ( Typo(..)
+    , fixTyposWithStyle
+    , typoToStringWithStyle
+    , rmdupTypos
+    ) where
+
+import Data.Ord
+import Data.List
+import Data.List.Split
+
+-- | Carries the information of a typo.
+data Typo = Typo { errorType    ::  String
+                 , token        ::  String
+                 , suggestions  :: [String]
+                 , context      ::  String
+                 , info         ::  String
+                 } deriving (Show, Eq, Ord)
+
+-- If given @True@, makes console text style reversed.
+reversed :: Bool -> String -> String
+reversed isTTY text = if isTTY 
+                         then "\x1b[7m" ++ text ++ "\x1b[0m" 
+                         else text
+
+-- If given @True@, makes console text style grey.
+grey :: Bool -> String -> String
+grey isTTY text = if isTTY 
+                     then "\x1b[90m" ++ text ++ "\x1b[0m" 
+                     else text
+
+-- | Fixes typos of given sentences. If given @True@, the colors of fixed 
+-- words are inverted.
+fixTyposWithStyle :: Bool -> String -> [Typo] -> String
+fixTyposWithStyle isTTY = foldl' (fixTypo isTTY)
+  where
+    replace from to = intercalate to . splitOn from
+    fixTypo :: Bool -> String -> Typo -> String
+    fixTypo isTTY text aTypo = 
+        let aSuggestion = head (suggestions aTypo)
+         in if aSuggestion == token aTypo
+               then text
+               else replace (token aTypo) (reversed isTTY aSuggestion) text
+
+-- | Converts a 'Typo' to string. If given @True@, 'info' of the 'Typo' is 
+-- greyed out.
+typoToStringWithStyle :: Bool -> Typo -> String
+typoToStringWithStyle isTTY typo = token typo
+     ++ grey isTTY " -> "
+     ++ intercalate (grey isTTY ", ") (suggestions typo)
+     ++ "\n"
+     ++ grey isTTY (info typo)
+
+-- | Removes the 'Typo's whose 'token's are duplicated. Order preserving and 
+-- O(nlogn).
+rmdupTypos :: [Typo] -> [Typo]
+rmdupTypos =
+    map snd . sortBy compareOrder . rmdup . sortBy compareToken . zip [1..]
+  where
+    compareToken (n, t) (n', t') = compare (token t) (token t')
+    compareOrder (n, t) (n', t') = compare n n'
+    rmdup (a:b:typos) = if compareToken a b == EQ
+                           then rmdup (a:typos)
+                           else a:rmdup (b:typos)
+    rmdup typos = typos
diff --git a/test/DaumSpellCheckerSpec.hs b/test/DaumSpellCheckerSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/DaumSpellCheckerSpec.hs
@@ -0,0 +1,69 @@
+module DaumSpellCheckerSpec where
+
+import Test.Hspec
+import Test.QuickCheck
+
+import Data.List
+import Data.List.Split
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Class
+import Control.Monad.IO.Class
+import Control.Concurrent.Async
+
+import Language.Hanspell.Typo
+import Language.Hanspell.TextLines
+import Language.Hanspell.DaumSpellChecker
+
+fixTypos :: String -> [Typo] -> String
+fixTypos = foldl fixTypo where
+    replace from to = intercalate to . splitOn from
+    fixTypo :: String -> Typo -> String
+    fixTypo text aTypo = replace (token aTypo) (head (suggestions aTypo)) text
+
+sampleFile          = "test/long-paragraph-sample.txt"
+sampleText          = "안녕  하세요.자줏 빛 합니다.호호  하세요. 삐리릿!"
+sampleTextFixed     = "안녕하세요. 자주 빛 합니다. 호호하세요. 삐리릿!"
+sampleTextCorrect   = "안녕하세요."
+sampleTextLn        = "\n"
+
+spec :: Spec
+spec = do
+    describe "spellCheckByDaum correct sample text test" $
+        it "returns 0 typos" $ do
+            typos <- liftIO (spellCheckByDaum sampleTextCorrect)
+            length typos `shouldBe` 0
+
+            Just typos' <- runMaybeT $ spellCheckByDaum sampleTextCorrect
+            typos `shouldBe` typos'
+
+            typos <- liftIO (spellCheckByDaum sampleTextLn)
+            length typos `shouldBe` 0
+
+            Just typos' <- runMaybeT $ spellCheckByDaum sampleTextLn
+            typos `shouldBe` typos'
+
+    describe "spellCheckByDaum sample text test" $
+        it "returns 3 typos" $ do
+            typos <- liftIO (spellCheckByDaum sampleText)
+            length typos `shouldBe` 3
+
+            Just typos' <- runMaybeT $ spellCheckByDaum sampleText
+            typos `shouldBe` typos'
+
+            let fixed = fixTypos sampleText typos
+            fixed `shouldBe` sampleTextFixed
+
+    describe "spellCheckByDaum sample file test" $
+        it "returns more than 10 typos" $ do
+            content <- liftIO $ readFile sampleFile
+            let texts = linesByLength daumSpellCheckerMaxChars content
+            length texts `shouldBe` 3
+
+            -- typos :: [Typo]
+            typos <- liftIO $ concat <$> mapConcurrently spellCheckByDaum texts
+            length typos `shouldSatisfy` (>10)
+
+            -- typos' :: Maybe [[Typo]]
+            typos' <- liftIO $ mapConcurrently (runMaybeT . spellCheckByDaum) texts
+            let Just typos'' = sequenceA typos'
+            typos `shouldBe` concat typos''
diff --git a/test/HanspellExample.hs b/test/HanspellExample.hs
new file mode 100644
--- /dev/null
+++ b/test/HanspellExample.hs
@@ -0,0 +1,8 @@
+module HanspellExample where
+
+import Language.Hanspell
+
+example = do
+    let sentence = "위에계신분, 잘들리세요?"
+    typos <- spellCheckByDaum sentence
+    mapM_ (putStrLn . typoToStringWithStyle True) typos
diff --git a/test/PnuSpellCheckerSpec.hs b/test/PnuSpellCheckerSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/PnuSpellCheckerSpec.hs
@@ -0,0 +1,69 @@
+module PnuSpellCheckerSpec where
+
+import Test.Hspec
+import Test.QuickCheck
+
+import Data.List
+import Data.List.Split
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Class
+import Control.Monad.IO.Class
+import Control.Concurrent.Async
+
+import Language.Hanspell.Typo
+import Language.Hanspell.TextLines
+import Language.Hanspell.PnuSpellChecker
+
+fixTypos :: String -> [Typo] -> String
+fixTypos = foldl fixTypo where
+    replace from to = intercalate to . splitOn from
+    fixTypo :: String -> Typo -> String
+    fixTypo text aTypo = replace (token aTypo) (head (suggestions aTypo)) text
+
+sampleFile          = "test/long-paragraph-sample.txt"
+sampleText          = "안녕  하세요.자줏 빛 합니다.호호  하세요. 삐리릿!"
+sampleTextFixed     = "안녕  하세요. 자줏 빛 합니다.호호  하세요. 삐리릭!"
+sampleTextCorrect   = "안녕하세요."
+sampleTextLn        = "\n"
+
+spec :: Spec
+spec = do
+    describe "spellCheckByPnu correct sample text test" $
+        it "returns 0 typos" $ do
+            typos <- liftIO (spellCheckByPnu sampleTextCorrect)
+            length typos `shouldBe` 0
+
+            Just typos' <- runMaybeT $ spellCheckByPnu sampleTextCorrect
+            typos `shouldBe` typos'
+
+            typos <- liftIO (spellCheckByPnu sampleTextLn)
+            length typos `shouldBe` 0
+
+            Just typos' <- runMaybeT $ spellCheckByPnu sampleTextLn
+            [] `shouldBe` typos'
+
+    describe "spellCheckByPnu sample text test" $
+        it "returns 4 typos" $ do
+            typos <- liftIO (spellCheckByPnu sampleText)
+            length typos `shouldBe` 4
+
+            Just typos' <- runMaybeT $ spellCheckByPnu sampleText
+            typos `shouldBe` typos'
+
+            let fixed = fixTypos sampleText typos
+            fixed `shouldBe` sampleTextFixed
+
+    describe "spellCheckByPnu sample file test" $
+        it "returns more than 20 typos" $ do
+            content <- liftIO $ readFile sampleFile
+            let texts = linesByLength pnuSpellCheckerMaxWords content
+            length texts `shouldBe` 3
+
+            -- typos :: [Typo]
+            typos <- liftIO $ concat <$> mapConcurrently spellCheckByPnu texts
+            length typos `shouldSatisfy` (>20)
+
+            -- typos' :: Maybe [[Typo]]
+            typos' <- liftIO $ mapConcurrently (runMaybeT . spellCheckByPnu) texts
+            let Just typos'' = sequenceA typos'
+            typos `shouldBe` concat typos''
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,13 @@
+import Test.Hspec
+
+import TypoSpec
+import TextLinesSpec
+import DaumSpellCheckerSpec
+import PnuSpellCheckerSpec
+
+main :: IO ()
+main = hspec $ do
+    describe "TextLines"        TextLinesSpec.spec
+    describe "DaumSpellChecker" DaumSpellCheckerSpec.spec
+    describe "Typo"             TypoSpec.spec
+    describe "PnuSpellChecker"  PnuSpellCheckerSpec.spec
diff --git a/test/TextLinesSpec.hs b/test/TextLinesSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/TextLinesSpec.hs
@@ -0,0 +1,195 @@
+module TextLinesSpec where
+
+import Test.Hspec
+import Test.QuickCheck
+
+import Language.Hanspell.TextLines
+
+sampleText          = "12\t45\n12  56\n1234567"
+sampleTextLn        = "12\t45\n12  56\n1234567\n"
+sampleTextUtf8      = "안녕하세요.\n반갑습니다."
+sampleTextUtf8Ln    = "안녕하세요.\n반갑습니다.\n"
+
+spec :: Spec
+spec = do
+    describe "linesByLength properties" $ do
+        it "almost always returns 1" $
+            property $ \text ->
+                let inText = text
+                    inLength = length inText + 1
+                    outTexts = linesByLength inLength inText
+                 in length outTexts == 1 - if null text then 1 else 0
+        it "is almost always same" $
+            property $ \text len ->
+                let outText = concat $ linesByLength len text
+                 in outText == text || outText == (text ++ "\n")
+
+    describe "linesByLength basic tests" $ do
+        it "returns []" $
+            let inText = ""
+                outTexts = linesByLength 0 inText
+             in outTexts `shouldBe` []
+        it "returns []" $
+            let inText = ""
+                outTexts = linesByLength (-100) inText
+             in outTexts `shouldBe` []
+        it "returns 1 element" $
+            let inText = "\n"
+                outTexts = linesByLength (-1) inText
+             in outTexts `shouldBe` ["\n"]
+        it "returns 1 element" $
+            let inText = "\n"
+                outTexts = linesByLength 0 inText
+             in outTexts `shouldBe` ["\n"]
+        it "returns 1 element" $
+            let inText = "\n"
+                outTexts = linesByLength 1 inText
+             in outTexts `shouldBe` ["\n"]
+        it "returns 1 element" $
+            let inText = "\n\n"
+                outTexts = linesByLength 2 inText
+             in outTexts `shouldBe` ["\n\n"]
+        it "returns 2 elements" $
+            let inText = "\n\n"
+                outTexts = linesByLength 1 inText
+             in outTexts `shouldBe` ["\n","\n"]
+        it "returns 2 elements" $
+            let inText = "\n\n"
+                outTexts = linesByLength (-1) inText
+             in outTexts `shouldBe` ["\n","\n"]
+
+    describe "linesByLength sample tests" $ do
+        it "returns 3 elements" $
+            linesByWordCount 0 sampleText
+                `shouldBe` [ "12\t45\n"
+                           , "12  56\n"
+                           , "1234567\n"
+                           ]
+        it "returns 2 elements" $
+            linesByLength 13 sampleText
+                `shouldBe` [ "12\t45\n12  56\n"
+                           , "1234567\n"
+                           ]
+        it "returns 1 element" $
+            linesByLength 30 sampleText `shouldBe` [ sampleTextLn ]
+        it "returns 2 elements" $
+            linesByLength (length sampleText) sampleText
+                `shouldBe` [ "12\t45\n12  56\n"
+                           , "1234567\n"
+                           ]
+        it "returns 1 element" $
+            linesByLength (length sampleTextLn) sampleTextLn
+                `shouldBe` [ sampleTextLn ]
+        it "returns 1 element" $
+            linesByLength (length sampleText + 1) sampleText
+                `shouldBe` [ sampleTextLn ]
+
+    describe "linesByLength utf-8 sample tests" $ do
+        it "returns 2 elements" $
+            linesByWordCount 0 sampleTextUtf8
+                `shouldBe` [ "안녕하세요.\n"
+                           , "반갑습니다.\n"
+                           ]
+        it "returns 2 elements" $
+            linesByLength 7 sampleTextUtf8
+                `shouldBe` [ "안녕하세요.\n"
+                           , "반갑습니다.\n"
+                           ]
+        it "returns 2 elements" $
+            linesByLength 13 sampleTextUtf8
+                `shouldBe` [ "안녕하세요.\n"
+                           , "반갑습니다.\n"
+                           ]
+        it "returns 1 element" $
+            linesByLength (length sampleTextUtf8 + 1) sampleTextUtf8
+                `shouldBe` [ sampleTextUtf8Ln ]
+        it "returns 1 element" $
+            linesByLength 14 sampleTextUtf8 `shouldBe` [ sampleTextUtf8Ln ]
+        it "returns 1 element" $
+            linesByLength 30 sampleTextUtf8 `shouldBe` [ sampleTextUtf8Ln ]
+
+    describe "linesByWordCount properties" $ do
+        it "almost always returns 1" $
+            property $ \text ->
+                let inText = text
+                    wordCount = length (words text)
+                    outTexts = linesByWordCount wordCount inText
+                 in length outTexts == 1 - if null text then 1 else 0
+        it "is almost always same" $
+            property $ \text len ->
+                let outText = concat $ linesByWordCount len text
+                 in outText == text || outText == (text ++ "\n")
+
+    describe "linesByWordCount basic tests" $ do
+        it "returns []" $
+            let inText = ""
+                outTexts = linesByWordCount 0 inText
+             in outTexts `shouldBe` []
+        it "returns []" $
+            let inText = ""
+                outTexts = linesByWordCount (-100) inText
+             in outTexts `shouldBe` []
+        it "returns 1 element" $
+            let inText = "\n"
+                outTexts = linesByWordCount (-1) inText
+             in outTexts `shouldBe` ["\n"]
+        it "returns 1 element" $
+            let inText = "\n"
+                outTexts = linesByWordCount 0 inText
+             in outTexts `shouldBe` ["\n"]
+        it "returns 1 element" $
+            let inText = "\n"
+                outTexts = linesByWordCount 1 inText
+             in outTexts `shouldBe` ["\n"]
+        it "returns 1 element" $
+            let inText = "\n\n"
+                outTexts = linesByWordCount 0 inText
+             in outTexts `shouldBe` ["\n\n"]
+        it "returns 1 element" $
+            let inText = "\n\n"
+                outTexts = linesByWordCount 1 inText
+             in outTexts `shouldBe` ["\n\n"]
+        it "returns 1 element" $
+            let inText = "\n\n"
+                outTexts = linesByWordCount 2 inText
+             in outTexts `shouldBe` ["\n\n"]
+        it "returns 2 elements" $
+            let inText = "\n\n"
+                outTexts = linesByWordCount (-1) inText
+             in outTexts `shouldBe` ["\n","\n"]
+
+    describe "linesByWordCount sample tests" $ do
+        it "returns 3 elements" $
+            linesByWordCount 0 sampleText
+                `shouldBe` [ "12\t45\n"
+                           , "12  56\n"
+                           , "1234567\n"
+                           ]
+        it "returns 2 elements" $
+            linesByWordCount 3 sampleText
+                `shouldBe` [ "12\t45\n"
+                           , "12  56\n1234567\n"
+                           ]
+        it "returns 2 elements" $
+            linesByWordCount 4 sampleText
+                `shouldBe` [ "12\t45\n12  56\n"
+                           , "1234567\n"
+                           ]
+        it "returns 1 element" $
+            linesByWordCount (length (words sampleText)) sampleText
+                `shouldBe` [ sampleTextLn ]
+
+    describe "linesByWordCount utf-8 sample tests" $ do
+        it "returns 2 elements" $
+            linesByWordCount 0 sampleTextUtf8
+                `shouldBe` [ "안녕하세요.\n"
+                           , "반갑습니다.\n"
+                           ]
+        it "returns 2 elements" $
+            linesByWordCount 1 sampleTextUtf8
+                `shouldBe` [ "안녕하세요.\n"
+                           , "반갑습니다.\n"
+                           ]
+        it "returns 1 element" $
+            linesByWordCount 2 sampleTextUtf8
+                `shouldBe` [ sampleTextUtf8Ln ]
diff --git a/test/TypoSpec.hs b/test/TypoSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/TypoSpec.hs
@@ -0,0 +1,55 @@
+module TypoSpec where
+
+import qualified Data.Text as T
+import qualified Data.Text.IO as I
+
+import Test.Hspec
+import Test.QuickCheck
+import Control.Monad.IO.Class
+
+import Language.Hanspell
+
+info0 = "동작성이 있는 일부 명사나 어근 뒤에 붙어, 그 동작성을 ...\n"
+text0 = "사랑 합시다.\n여러분, 반갑 습니다.\n"
+
+token1 = "사랑 합시다"
+suggestion1 = "사랑합시다"
+typo1 = Typo "space" token1 [suggestion1] "" info0
+
+token2 = "안녕 하세요"
+suggestion2 = "안녕하세요"
+typo2 = Typo "cont_gramma" token2 [suggestion2] "" info0
+
+token3 = "반갑 습니다"
+suggestion3 = "반갑습니다"
+typo3 = Typo "space" token3 [suggestion3,suggestion1] "" info0
+typo3' = Typo "sp  ace" token3 [suggestion1] "" info0
+
+typos1 = [typo3,typo1,typo3',typo3,typo2]
+typos2 = [typo3,typo1,typo3',typo3,typo2,typo1]
+typos3 = [typo3,typo1,typo2]
+
+spec :: Spec
+spec = do
+    describe "rmdupTypos tests" $
+        it "returns 3" $ do
+            let typos1' = rmdupTypos typos1
+                typos2' = rmdupTypos typos2
+                typos3' = rmdupTypos typos3
+
+            length typos1' `shouldBe` 3
+            length typos2' `shouldBe` 3
+            typos1' `shouldBe` typos3
+            typos2' `shouldBe` typos3
+            typos3' `shouldBe` typos3
+
+    describe "typoToStringWithStyle tests" $
+        it "just prints results" $ do
+            liftIO . putStr . typoToStringWithStyle True $ typo1
+            liftIO . putStr . typoToStringWithStyle True $ typo3
+            shouldBe 1 1
+
+    describe "fixTyposWithStyle tests" $
+        it "just prints results" $ do
+            liftIO . putStr . fixTyposWithStyle True text0 $ [typo1,typo3]
+            shouldBe 1 1
diff --git a/test/long-paragraph-sample.txt b/test/long-paragraph-sample.txt
new file mode 100644
--- /dev/null
+++ b/test/long-paragraph-sample.txt
@@ -0,0 +1,3 @@
+그리고 바다에 도착하면 섬으로 가는 배들이 항구에 정박해 있고 그 배를 타기 위한 차들이 줄지어 서 있곤 했다. 폭풍이 불면 섬으로 가는 배가 출발하지 못했다. 그러면 몇 날이고 항구의 도시에서 기다리고 있어야 했다. 뒤숭숭한 뒷골목과 낯선 물건을 파는 기념품 상점과 사람들로 가득한 시장들. 항구에는 외국인들이 많았다. 물건을 흥정하는 아랍인들과 키예프에서 온 큰 체구의 우크라이나인들 사이로, 중국인들과 베트남인들 사이로, 우리는 돌아다닌곤 했다. 그리고 배가 고프면 언제나 빵가게에 들어가 빵을 골랐다. 빵은 도시마다 다르다. 아무것도 들어 있지 않은 바게뜨나 호밀빵을 골라 커피와 버터와 치즈와 함께 먹었다. 얇게 썬 호밀빵에 버터를 바르고 그 위에 크림치즈를 다시 발라 먹는다. 혹은 세서미크림 위에 샐러드용 야채를 얹어 먹어도 좋고 각설탕을 싸먹어도 된다. 자동차용 커피머신을 가지고 있었기 때문에 커피 가루만 슈퍼마켓에서 구할 수 있다면 좋았다. 우리는 길 위에서 수없이 많은 것을 샀다. 미네랄워터, 커피, 커피 거르는 거름종이, 각 도시의 여러 종류의 빵들, 깨끗하고 건조한 타월, 비누, 면도용 거품, 새 속옷, 빵과 함께 먹을 각설텅, 치즈와 버터와 일회용 포크와 나이프, 각 지방도로가 자세히 나와 있는, 그러나 대개는 틀린, 그런 지도, 치약, 밀감과 사과, 얼굴에 뿌린느 수분 스프레이, 바닷가의 햇볓을 가려주는, 시골 여인들이 쓰는 챙 넓은 모자, 창터에서 산 폭인 넒은 긴 스커트, 시골 우체국에서 파는 지방관광엽서(우리는 아무 곳에도 엽서를 보내지 않으면서 고집스럽게 이런 것들을 사모으곤 했다), 담배, 짠맛이 나는 비스킷, 두통약, 지방 소식이 실린 신문과 각 도시의 헌책방에서 산 포켓 사이즈 소설책들과 지나간 호의 <보그>잡지. 국도의 휴게소나 낯선 거리의 가로등 아래나 공원의 벤치에서 우리는 여행 틈틈이 이런 것들을 읽었다. 혹은 이바나를 불빛 아래 세워둔 채 차 안에서 읽기도 했다. 여름이면 열린 창으로 날벌레들이 들어와 우리들의 그 은밀한 페이지들 위로 내려앉곤 했다. 우리는 신중하게 그 벌레들을 잡아 다시 놓아주었다. 그런 식으로 그 해, 폭풍이 가라앉고 배가 다시 출항하기를 기다리는 지루한 시간들을 도스토예프스키와 톨스토이, 솔제니친을 읽으며 보냈다. 그러다가 우리의 마음은 변하여 섬이 아닌 다른 것으로 가기 위하여 항구도시를 떠나 내륙으로, 더 깊은 내륙으로 들어가곤 했다. 말라버린 분수와 폐쇄된 개인 박물관, 빈집들과 방치된 쓰레기 소각장이 있는 이름 없는 작은 도시들과 마을을 여행했다. 남부지방에서는 홍수를 만난 적도 있었고 저지대에서는 물에 갇혀버린 적도 있었다. 물에 갇힌다는 것은 상당히 묘한 느낌이었다. 마치 열대처럼 후덥지근한 열기가 우리의 피부를 남김 없이 붙들고 놓아주지 않았다. 어디에나 물이었고 흙과 나무와 책과 비스킷이 습기에 차 은은한 곰팡내를 풍겼다. 공기에서는 열병균 냄새를 맡을 수 있었다. 숨을 쉴 수 없을 정도로 지독한 냄새 속에서 우리는 땀을 흘렸다. 처음에 새것이었던 우리의 침낭은 낡고 더러워졌으며 담요는 세탁이 필요하고 겨울에는 뼈를 관통하는 듯한 냉기가 차 안에서 느껴졌다. 입김이 하얗게 공기 중에 얼어붙었고 들과 숲은 그지없이 황량해진다.
+
+우리는 수없이 많은 길과 길들을 여행했다. 길은 영원히 반복되어 끝없는 것처럼 보였고 우리는 이미 오래 전에 지나갔던 도시를 다시 여행했으며 이미 달렸던 고속도로와 국도를 다시 달렸으며 지도에 나와 있지도 않은 끊어진 도로와 파괴된 마을들을 여행했다. 비포장의 산길과 나환자들의 마을과 군사지역과 수몰 예정지의 스산하고 아름다운 마을들, 온 세상에 오직 섬광밖에 보이지 않는 번개 치는 한밤의 국도, 바다로 가는 길에 거짓말처럼 홀로 서 있던 당나귀, 지방도시의 작은 규모의 동물원들, 기타를 치고 있던 히치하이커, 산과 산 사이에 있는 그늘진 온천 휴양지, 그토록 길고 영원히 반복되는 길 위의 시간들. 지금도 나는 꿈에서 본다. 이바나와 함께 했던 시간들을. 그때 이바나는 단순한 자동차, 기능을 담당해주는 운송수단이 아니었다. 그것은 이미 영혼을 가진 하나의 낡은 존재였다. 달력조차 가지지 않고 했던 긴 여행들. 우리는 달력상의 날짜를 알 수 없었고 해가 지면 밤이 온다고 생각했고 잎이 떨어지는 숲에 다다르면 겨울이 온다고 생각하며 트렁크에서 스웨터를 꺼내어 입고 커피를 끓였다. 우리는 엄청나게 많은 커피를 마셨으며 담배를 피워대었고, 싫증날 때까지 레드 제플린의 <Going to California>를 들었다. 이바나의 플레이어는 최상품은 아니었다. 그러나 그때 우리에게 노래란 지하철의 환풍기에서 흘러나오는 허덕거리는 숨소리, 그런 정도에 불과해도 좋았다. 우리는 환각에 빠진 채 <Going to California>를 계속해서 들었다. 숲 가장자리에서 잠이 깬 어느 날 아침, 안개가 주변에 가득하고 갓 낳은 따뜻한 작은 새알의 온기가 느껴지고, 연기냄새가 나고 숲의 언저리에 바람이 불고 두 개의 스웨터를 입고 침낭에 깊숙이 들어가 있어도 추위가 느껴지고 어디선가 작은 짐승의 발자국 소리가 들린다. 야생오리들이 강으로 출발하고 커다란 날개를 가진 잿빛 새들이 절벽 위 둥지에서 날아오른다. 그런 아침, 밤 동안의 추위로 몸이 뻣뻣해진 채, 우리는 허밍과도 같은 기타 소리와 함께 시작되는 <Going to California>를 들었다. 우리는 그런 모든 날들을 이바나와 함께 했으며 그녀가 아닌 다른 자동차라면 도저히 느끼지 못했을 거라고 지금도 믿고 있는 교류를 나누었다.
