hanspell-0.2.3.0: src/Language/Hanspell/PnuSpellChecker.hs
{-# 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