@@ -50,6 +50,7 @@ import Data.Maybe ( fromMaybe )
50import Data.List ( isPrefixOf, isSuffixOf, intercalate )
50import Data.List ( isPrefixOf, isSuffixOf, intercalate )
51import Data.Char ( toLower, isAlphaNum )
51import Data.Char ( toLower, isAlphaNum )
52import Network.URI ( parseURIReference, URI (..) )
52import Network.URI ( parseURIReference, URI (..) )
53+import Control.Monad ( liftM )
54-- | Convert HTML-formatted string to 'Pandoc' document.
55-- | Convert HTML-formatted string to 'Pandoc' document.
55readHtml :: ParserState -- ^ Parser state
56readHtml :: ParserState -- ^ Parser state
@@ -71,7 +72,7 @@ inlineHtmlTags = ["a", "abbr", "acronym", "b", "basefont", "bdo", "big",
71 "br", "cite", "code", "dfn", "em", "font", "i", "img",
72 "br", "cite", "code", "dfn", "em", "font", "i", "img",
72 "input", "kbd", "label", "q", "s", "samp", "select",
73 "input", "kbd", "label", "q", "s", "samp", "select",
73 "small", "span", "strike", "strong", "sub", "sup",
74 "small", "span", "strike", "strong", "sub", "sup",
74− "textarea", "tt", "u", "var"] ++ eitherBlockOrInline
75+ "textarea", "tt", "u", "var"]
77blockHtmlTags :: [[Char]]
78blockHtmlTags :: [[Char]]
@@ -80,7 +81,7 @@ blockHtmlTags = ["address", "blockquote", "body", "center", "dir", "div",
80 "h5", "h6", "head", "hr", "html", "isindex", "menu", "noframes",
81 "h5", "h6", "head", "hr", "html", "isindex", "menu", "noframes",
81 "noscript", "ol", "p", "pre", "table", "ul", "dd",
82 "noscript", "ol", "p", "pre", "table", "ul", "dd",
82 "dt", "frameset", "li", "tbody", "td", "tfoot",
83 "dt", "frameset", "li", "tbody", "td", "tfoot",
83− "th", "thead", "tr", "script"] ++ eitherBlockOrInline
84+ "th", "thead", "tr", "script", "style"]
85sanitaryTags :: [[Char]]
86sanitaryTags :: [[Char]]
86sanitaryTags = ["a", "abbr", "acronym", "address", "area", "b", "big",
87sanitaryTags = ["a", "abbr", "acronym", "address", "area", "b", "big",
@@ -112,6 +113,40 @@ sanitaryAttributes = ["abbr", "accept", "accept-charset",
112 "summary", "tabindex", "target", "title", "type",
113 "summary", "tabindex", "target", "title", "type",
113 "usemap", "valign", "value", "vspace", "width"]
114 "usemap", "valign", "value", "vspace", "width"]
116+-- taken from HXT and extended
118+closes :: String -> String -> Bool
119+"EOF" `closes` _ = True
120+_ `closes` "body" = False
121+_ `closes` "html" = False
122+"a" `closes` "a" = True
123+"li" `closes` "li" = True
124+"th" `closes` t | t `elem` ["th","td"] = True
125+"td" `closes` t | t `elem` ["th","td"] = True
126+"tr" `closes` t | t `elem` ["th","td","tr"] = True
127+"dt" `closes` t | t `elem` ["dt","dd"] = True
128+"dd" `closes` t | t `elem` ["dt","dd"] = True
129+"hr" `closes` "p" = True
130+"p" `closes` "p" = True
131+"meta" `closes` "meta" = True
132+"colgroup" `closes` "colgroup" = True
133+"form" `closes` "form" = True
134+"label" `closes` "label" = True
135+"map" `closes` "map" = True
136+"object" `closes` "object" = True
137+_ `closes` t | t `elem` ["option","style","script","textarea","title"] = True
138+t `closes` "select" | t /= "option" = True
139+"thead" `closes` t | t `elem` ["colgroup"] = True
140+"tfoot" `closes` t | t `elem` ["thead","colgroup"] = True
141+"tbody" `closes` t | t `elem` ["tbody","tfoot","thead","colgroup"] = True
143+ t `elem` ["h1","h2","h3","h4","h5","h6","dl","ol","ul","table","div","p"] &&
144+ t2 `elem` ["h1","h2","h3","h4","h5","h6","p" ] = True -- not "div"
146+ t1 `elem` blockHtmlTags &&
147+ t2 `notElem` (blockHtmlTags ++ eitherBlockOrInline) = True
116-- HTML utility functions
151-- HTML utility functions
@@ -176,6 +211,19 @@ extractTagType ('<':rest) =
176 map toLower $ takeWhile isAlphaNum $ dropWhile isSpaceOrSlash rest
211 map toLower $ takeWhile isAlphaNum $ dropWhile isSpaceOrSlash rest
177extractTagType _ = ""
212extractTagType _ = ""
214+-- Parse any HTML tag (opening or self-closing) and return tag type
215+anyOpener :: GenParser Char ParserState [Char]
219+ tag <- many1 alphaNum
220+ skipMany htmlAttribute
222+ option "" (string "/")
225+ return $ map toLower tag
179-- | Parse any HTML tag (opening or self-closing) and return text of tag
227-- | Parse any HTML tag (opening or self-closing) and return text of tag
180anyHtmlTag :: GenParser Char ParserState [Char]
228anyHtmlTag :: GenParser Char ParserState [Char]
181anyHtmlTag = try $ do
229anyHtmlTag = try $ do
@@ -257,32 +305,30 @@ htmlRegularAttribute = try $ do
257 (content, quoteStr) <- choice [ (quoted '\''),
305 (content, quoteStr) <- choice [ (quoted '\''),
258 (quoted '"'),
306 (quoted '"'),
260− a <- many (alphaNum <|> (oneOf "-._:"))
308+ a <- many (noneOf " \t\n\r\"'<>")
261 return (a,"")) ]
309 return (a,"")) ]
262 return (name, content,
310 return (name, content,
263 (name ++ "=" ++ quoteStr ++ content ++ quoteStr))
311 (name ++ "=" ++ quoteStr ++ content ++ quoteStr))
265-- | Parse an end tag of type 'tag'
313-- | Parse an end tag of type 'tag'
266−htmlEndTag :: [Char] -> GenParser Char st [Char]
314+htmlEndTag :: [Char] -> GenParser Char ParserState [Char]
267htmlEndTag tag = try $ do
315htmlEndTag tag = try $ do
316+ closedByNext <- lookAhead $ option False $ liftM (`closes` tag) $
317+ anyOpener <|> (eof >> return "EOF")
268− char '<'
320+ else do char '<'
270− char '/'
322+ char '/'
272− stringAnyCase tag
324+ stringAnyCase tag
274− char '>'
326+ char '>'
275− return $ "</" ++ tag ++ ">"
327+ return $ "</" ++ tag ++ ">"
278−-- | Returns @True@ if the tag is (or can be) an inline tag.
279−isInline :: String -> Bool
280−isInline tag = (extractTagType tag) `elem` inlineHtmlTags
283-- | Returns @True@ if the tag is (or can be) a block tag.
329-- | Returns @True@ if the tag is (or can be) a block tag.
284isBlock :: String -> Bool
330isBlock :: String -> Bool
285−isBlock tag = (extractTagType tag) `elem` blockHtmlTags
331+isBlock tag = (extractTagType tag) `elem` (blockHtmlTags ++ eitherBlockOrInline)
287anyHtmlBlockTag :: GenParser Char ParserState [Char]
333anyHtmlBlockTag :: GenParser Char ParserState [Char]
288anyHtmlBlockTag = try $ do
334anyHtmlBlockTag = try $ do
@@ -298,18 +344,43 @@ anyHtmlInlineTag = try $ do
298-- Scripts must be treated differently, because they can contain '<>' etc.
344-- Scripts must be treated differently, because they can contain '<>' etc.
299htmlScript :: GenParser Char ParserState [Char]
345htmlScript :: GenParser Char ParserState [Char]
300htmlScript = try $ do
346htmlScript = try $ do
301− open <- string "<script"
347+ lookAhead $ htmlTag "script"
302− rest <- manyTill anyChar (htmlEndTag "script")
349+ rest <- liftM concat $ manyTill scriptChunk (htmlEndTag "script")
303 st <- getState
350 st <- getState
304 if stateSanitizeHTML st && not ("script" `elem` sanitaryTags)
351 if stateSanitizeHTML st && not ("script" `elem` sanitaryTags)
305 then return "<!-- unsafe HTML removed -->"
352 then return "<!-- unsafe HTML removed -->"
306 else return $ open ++ rest ++ "</script>"
353 else return $ open ++ rest ++ "</script>"
355+scriptChunk :: GenParser Char ParserState [Char]
356+scriptChunk = jsComment <|> jsString <|> jsChars
357+ where jsComment = jsEndlineComment <|> jsMultilineComment
358+ jsString = jsSingleQuoteString <|> jsDoubleQuoteString
359+ jsChars = many1 (noneOf "<\"'*/") <|> count 1 anyChar
360+ jsEndlineComment = try $ do
362+ res <- manyTill anyChar newline
363+ return ("//" ++ res)
364+ jsMultilineComment = try $ do
366+ res <- manyTill anyChar (try $ string "*/")
367+ return ("/*" ++ res ++ "*/")
368+ jsSingleQuoteString = stringwith '\''
369+ jsDoubleQuoteString = stringwith '"'
370+ charWithEsc escapable = try $
371+ (try $ char '\\' >> oneOf ('\\':escapable) >>= \x -> return ['\\',x])
373+ stringwith c = try $ do
375+ res <- liftM concat $ manyTill (charWithEsc [c]) (char c)
376+ return (c : (res ++ [c]))
308-- | Parses material between style tags.
378-- | Parses material between style tags.
309-- Style tags must be treated differently, because they can contain CSS
379-- Style tags must be treated differently, because they can contain CSS
310htmlStyle :: GenParser Char ParserState [Char]
380htmlStyle :: GenParser Char ParserState [Char]
311htmlStyle = try $ do
381htmlStyle = try $ do
312− open <- string "<style"
382+ lookAhead $ htmlTag "style"
313 rest <- manyTill anyChar (htmlEndTag "style")
384 rest <- manyTill anyChar (htmlEndTag "style")
314 st <- getState
385 st <- getState
315 if stateSanitizeHTML st && not ("style" `elem` sanitaryTags)
386 if stateSanitizeHTML st && not ("style" `elem` sanitaryTags)
@@ -404,6 +475,14 @@ bodyTitle = try $ do
404 _ -> fail "not title"
475 _ -> fail "not title"
405 inlinesTilEnd "h1"
476 inlinesTilEnd "h1"
478+endOfDoc :: GenParser Char ParserState ()
481+ optional (htmlEndTag "body")
483+ optional (htmlEndTag "html" >> many anyChar) -- ignore stuff after </html>
407parseHtml :: GenParser Char ParserState Pandoc
486parseHtml :: GenParser Char ParserState Pandoc
408parseHtml = do
487parseHtml = do
409 sepEndBy (choice [xmlDec, definition, htmlComment]) spaces
488 sepEndBy (choice [xmlDec, definition, htmlComment]) spaces
@@ -415,11 +494,7 @@ parseHtml = do
416 optional bodyTitle -- skip title in body, because it's represented in meta
495 optional bodyTitle -- skip title in body, because it's represented in meta
417 blocks <- parseBlocks
496 blocks <- parseBlocks
419− optional (htmlEndTag "body")
421− optional (htmlEndTag "html" >> many anyChar) -- ignore anything after </html>
423 return $ Pandoc meta blocks
498 return $ Pandoc meta blocks
@@ -438,6 +513,7 @@ block = choice [ codeBlock
440 , rawHtmlBlock'
515 , rawHtmlBlock'
516+ , notFollowedBy' endOfDoc >> char '<' >> return Null
441 ] <?> "block"
517 ] <?> "block"
@@ -586,6 +662,7 @@ inline = choice [ charRef
588 , rawHtmlInline
664 , rawHtmlInline
665+ , char '&' >> return (Str "&") -- common HTML error
589 ] <?> "inline"
666 ] <?> "inline"
591code :: GenParser Char ParserState Inline
668code :: GenParser Char ParserState Inline
@@ -599,7 +676,7 @@ code = try $ do
600rawHtmlInline :: GenParser Char ParserState Inline
677rawHtmlInline :: GenParser Char ParserState Inline
601rawHtmlInline = do
678rawHtmlInline = do
602− result <- htmlScript <|> htmlStyle <|> htmlComment <|> anyHtmlInlineTag
679+ result <- anyHtmlInlineTag <|> htmlComment
603 state <- getState
680 state <- getState
604 if stateParseRaw state then return (HtmlInline result) else return (Str "")
681 if stateParseRaw state then return (HtmlInline result) else return (Str "")
@@ -640,7 +717,7 @@ linebreak :: GenParser Char ParserState Inline
640linebreak = htmlTag "br" >> optional newline >> return LineBreak
717linebreak = htmlTag "br" >> optional newline >> return LineBreak
642str :: GenParser Char st Inline
719str :: GenParser Char st Inline
643−str = many1 (noneOf "<& \t\n") >>= return . Str
720+str = many1 (noneOf "< \t\n&") >>= return . Str
646-- links and images
723-- links and images