2006-12-20 20:54:23 +00:00
|
|
|
{-
|
|
|
|
Copyright (C) 2006 John MacFarlane <jgm at berkeley dot edu>
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program; if not, write to the Free Software
|
|
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
-}
|
|
|
|
|
2006-12-20 06:50:14 +00:00
|
|
|
{- |
|
|
|
|
Module : Text.Pandoc.Readers.Markdown
|
|
|
|
Copyright : Copyright (C) 2006 John MacFarlane
|
|
|
|
License : GNU GPL, version 2 or above
|
|
|
|
|
|
|
|
Maintainer : John MacFarlane <jgm at berkeley dot edu>
|
2006-12-20 20:20:10 +00:00
|
|
|
Stability : alpha
|
2006-12-20 06:50:14 +00:00
|
|
|
Portability : portable
|
|
|
|
|
|
|
|
Conversion of markdown-formatted plain text to 'Pandoc' document.
|
|
|
|
-}
|
2006-10-17 14:22:29 +00:00
|
|
|
module Text.Pandoc.Readers.Markdown (
|
|
|
|
readMarkdown
|
|
|
|
) where
|
|
|
|
|
2006-12-19 23:13:03 +00:00
|
|
|
import Data.List ( findIndex, sortBy )
|
2006-10-17 14:22:29 +00:00
|
|
|
import Text.ParserCombinators.Pandoc
|
|
|
|
import Text.Pandoc.Definition
|
|
|
|
import Text.Pandoc.Readers.LaTeX ( rawLaTeXInline, rawLaTeXEnvironment )
|
|
|
|
import Text.Pandoc.Shared
|
2006-12-30 22:51:49 +00:00
|
|
|
import Text.Pandoc.Readers.HTML ( rawHtmlBlock,
|
|
|
|
anyHtmlBlockTag, anyHtmlInlineTag,
|
|
|
|
anyHtmlTag, anyHtmlEndTag,
|
|
|
|
htmlEndTag, extractTagType,
|
|
|
|
htmlBlockElement )
|
2007-01-02 00:40:12 +00:00
|
|
|
import Text.Pandoc.Entities ( decodeEntities )
|
2006-10-17 14:22:29 +00:00
|
|
|
import Text.Regex ( matchRegex, mkRegex )
|
|
|
|
import Text.ParserCombinators.Parsec
|
|
|
|
|
|
|
|
-- | Read markdown from an input string and return a Pandoc document.
|
|
|
|
readMarkdown :: ParserState -> String -> Pandoc
|
|
|
|
readMarkdown = readWith parseMarkdown
|
|
|
|
|
|
|
|
-- | Parse markdown string with default options and print result (for testing).
|
|
|
|
testString :: String -> IO ()
|
|
|
|
testString = testStringWith parseMarkdown
|
|
|
|
|
|
|
|
--
|
|
|
|
-- Constants and data structure definitions
|
|
|
|
--
|
|
|
|
|
|
|
|
spaceChars = " \t"
|
|
|
|
endLineChars = "\n"
|
|
|
|
labelStart = '['
|
|
|
|
labelEnd = ']'
|
|
|
|
labelSep = ':'
|
|
|
|
srcStart = '('
|
|
|
|
srcEnd = ')'
|
|
|
|
imageStart = '!'
|
|
|
|
noteStart = '^'
|
|
|
|
codeStart = '`'
|
|
|
|
codeEnd = '`'
|
|
|
|
emphStart = '*'
|
|
|
|
emphEnd = '*'
|
|
|
|
emphStartAlt = '_'
|
|
|
|
emphEndAlt = '_'
|
|
|
|
autoLinkStart = '<'
|
|
|
|
autoLinkEnd = '>'
|
|
|
|
mathStart = '$'
|
|
|
|
mathEnd = '$'
|
|
|
|
bulletListMarkers = "*+-"
|
2006-12-16 19:43:00 +00:00
|
|
|
orderedListDelimiters = ".)"
|
2006-10-17 14:22:29 +00:00
|
|
|
escapeChar = '\\'
|
|
|
|
hruleChars = "*-_"
|
|
|
|
quoteChars = "'\""
|
|
|
|
atxHChar = '#'
|
|
|
|
titleOpeners = "\"'("
|
|
|
|
setextHChars = ['=','-']
|
|
|
|
blockQuoteChar = '>'
|
|
|
|
hyphenChar = '-'
|
|
|
|
|
|
|
|
-- treat these as potentially non-text when parsing inline:
|
2006-12-20 06:50:14 +00:00
|
|
|
specialChars = [escapeChar, labelStart, labelEnd, emphStart, emphEnd,
|
|
|
|
emphStartAlt, emphEndAlt, codeStart, codeEnd, autoLinkEnd,
|
|
|
|
autoLinkStart, mathStart, mathEnd, imageStart, noteStart,
|
|
|
|
hyphenChar]
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- auxiliary functions
|
|
|
|
--
|
|
|
|
|
|
|
|
-- | Skip a single endline if there is one.
|
|
|
|
skipEndline = option Space endline
|
|
|
|
|
|
|
|
indentSpaces = do
|
|
|
|
state <- getState
|
|
|
|
let tabStop = stateTabStop state
|
|
|
|
oneOfStrings [ "\t", (replicate tabStop ' ') ] <?> "indentation"
|
|
|
|
|
|
|
|
skipNonindentSpaces = do
|
|
|
|
state <- getState
|
|
|
|
let tabStop = stateTabStop state
|
|
|
|
choice (map (\n -> (try (count n (char ' ')))) (reverse [0..(tabStop - 1)]))
|
|
|
|
|
2006-12-30 22:51:49 +00:00
|
|
|
-- | Fail if reader is in strict markdown syntax mode.
|
|
|
|
failIfStrict = do
|
|
|
|
state <- getState
|
|
|
|
if stateStrict state then fail "Strict markdown mode" else return ()
|
|
|
|
|
|
|
|
-- | Fail unless we're at beginning of a line.
|
|
|
|
failUnlessBeginningOfLine = do
|
|
|
|
pos <- getPosition
|
|
|
|
if sourceColumn pos == 1 then return () else fail "not beginning of line"
|
|
|
|
|
2006-10-17 14:22:29 +00:00
|
|
|
--
|
|
|
|
-- document structure
|
|
|
|
--
|
|
|
|
|
|
|
|
titleLine = try (do
|
|
|
|
char '%'
|
|
|
|
skipSpaces
|
|
|
|
line <- manyTill inline newline
|
|
|
|
return line)
|
|
|
|
|
|
|
|
authorsLine = try (do
|
|
|
|
char '%'
|
|
|
|
skipSpaces
|
|
|
|
authors <- sepEndBy (many1 (noneOf ",;\n")) (oneOf ",;")
|
|
|
|
newline
|
|
|
|
return (map removeLeadingTrailingSpace authors))
|
|
|
|
|
|
|
|
dateLine = try (do
|
|
|
|
char '%'
|
|
|
|
skipSpaces
|
|
|
|
date <- many (noneOf "\n")
|
|
|
|
newline
|
|
|
|
return (removeTrailingSpace date))
|
|
|
|
|
|
|
|
titleBlock = try (do
|
2006-12-30 22:51:49 +00:00
|
|
|
failIfStrict
|
2006-10-17 14:22:29 +00:00
|
|
|
title <- option [] titleLine
|
|
|
|
author <- option [] authorsLine
|
|
|
|
date <- option "" dateLine
|
|
|
|
option "" blanklines
|
|
|
|
return (title, author, date))
|
|
|
|
|
2006-12-19 23:13:03 +00:00
|
|
|
-- | Returns the number assigned to a Note block
|
|
|
|
numberOfNote :: Block -> Int
|
|
|
|
numberOfNote (Note ref _) = (read ref)
|
|
|
|
numberOfNote _ = 0
|
|
|
|
|
2006-10-17 14:22:29 +00:00
|
|
|
parseMarkdown = do
|
2006-12-20 06:50:14 +00:00
|
|
|
updateState (\state -> state { stateParseRaw = True })
|
|
|
|
-- need to parse raw HTML, since markdown allows it
|
2006-10-17 14:22:29 +00:00
|
|
|
(title, author, date) <- option ([],[],"") titleBlock
|
2006-12-30 22:51:49 +00:00
|
|
|
oldState <- getState
|
|
|
|
oldInput <- getInput
|
2006-12-31 17:34:06 +00:00
|
|
|
-- go through once just to get list of reference keys
|
|
|
|
manyTill (referenceKey <|> (do{anyLine; return Null})) eof
|
2006-12-30 22:51:49 +00:00
|
|
|
newState <- getState
|
|
|
|
let keysUsed = stateKeysUsed newState
|
|
|
|
setInput oldInput
|
|
|
|
setState (oldState { stateKeysUsed = keysUsed })
|
|
|
|
blocks <- parseBlocks -- go through again, for real
|
2006-12-19 23:13:03 +00:00
|
|
|
let blocks' = filter (/= Null) blocks
|
2006-10-17 14:22:29 +00:00
|
|
|
state <- getState
|
|
|
|
let keys = reverse $ stateKeyBlocks state
|
2006-12-19 23:13:03 +00:00
|
|
|
let notes = reverse $ stateNoteBlocks state
|
2006-12-20 06:50:14 +00:00
|
|
|
let sortedNotes = sortBy (\x y -> compare (numberOfNote x)
|
|
|
|
(numberOfNote y)) notes
|
2006-12-19 23:13:03 +00:00
|
|
|
return (Pandoc (Meta title author date) (blocks' ++ sortedNotes ++ keys))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- parsing blocks
|
|
|
|
--
|
|
|
|
|
2006-12-31 16:46:48 +00:00
|
|
|
parseBlocks = manyTill block eof
|
2006-10-17 14:22:29 +00:00
|
|
|
|
2006-12-20 06:50:14 +00:00
|
|
|
block = choice [ codeBlock, note, referenceKey, header, hrule, list,
|
2006-12-30 22:51:49 +00:00
|
|
|
blockQuote, htmlBlock, rawLaTeXEnvironment', para,
|
2006-12-20 06:50:14 +00:00
|
|
|
plain, blankBlock, nullBlock ] <?> "block"
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- header blocks
|
|
|
|
--
|
|
|
|
|
|
|
|
header = choice [ setextHeader, atxHeader ] <?> "header"
|
|
|
|
|
|
|
|
atxHeader = try (do
|
|
|
|
lead <- many1 (char atxHChar)
|
|
|
|
skipSpaces
|
2006-11-26 07:01:37 +00:00
|
|
|
txt <- manyTill inline atxClosing
|
2006-10-17 14:22:29 +00:00
|
|
|
return (Header (length lead) (normalizeSpaces txt)))
|
|
|
|
|
|
|
|
atxClosing = try (do
|
|
|
|
skipMany (char atxHChar)
|
|
|
|
skipSpaces
|
|
|
|
newline
|
|
|
|
option "" blanklines)
|
|
|
|
|
2006-12-20 06:50:14 +00:00
|
|
|
setextHeader = choice $
|
|
|
|
map (\x -> setextH x) (enumFromTo 1 (length setextHChars))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
setextH n = try (do
|
2006-12-30 22:51:49 +00:00
|
|
|
txt <- many1Till inline newline
|
2006-12-20 06:50:14 +00:00
|
|
|
many1 (char (setextHChars !! (n-1)))
|
|
|
|
skipSpaces
|
|
|
|
newline
|
|
|
|
option "" blanklines
|
|
|
|
return (Header n (normalizeSpaces txt)))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- hrule block
|
|
|
|
--
|
|
|
|
|
2006-12-20 06:50:14 +00:00
|
|
|
hruleWith chr = try (do
|
|
|
|
skipSpaces
|
|
|
|
char chr
|
|
|
|
skipSpaces
|
|
|
|
char chr
|
|
|
|
skipSpaces
|
|
|
|
char chr
|
|
|
|
skipMany (oneOf (chr:spaceChars))
|
|
|
|
newline
|
|
|
|
option "" blanklines
|
|
|
|
return HorizontalRule)
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
hrule = choice (map hruleWith hruleChars) <?> "hrule"
|
|
|
|
|
|
|
|
--
|
|
|
|
-- code blocks
|
|
|
|
--
|
|
|
|
|
|
|
|
indentedLine = try (do
|
2006-12-20 06:50:14 +00:00
|
|
|
indentSpaces
|
|
|
|
result <- manyTill anyChar newline
|
|
|
|
return (result ++ "\n"))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
-- two or more indented lines, possibly separated by blank lines
|
|
|
|
indentedBlock = try (do
|
|
|
|
res1 <- indentedLine
|
|
|
|
blanks <- many blankline
|
|
|
|
res2 <- choice [indentedBlock, indentedLine]
|
|
|
|
return (res1 ++ blanks ++ res2))
|
|
|
|
|
|
|
|
codeBlock = do
|
2006-12-20 06:50:14 +00:00
|
|
|
result <- choice [indentedBlock, indentedLine]
|
|
|
|
option "" blanklines
|
|
|
|
return (CodeBlock (stripTrailingNewlines result))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- note block
|
|
|
|
--
|
|
|
|
|
2006-12-19 07:30:36 +00:00
|
|
|
rawLine = try (do
|
2006-12-20 06:50:14 +00:00
|
|
|
notFollowedBy' blankline
|
|
|
|
notFollowedBy' noteMarker
|
|
|
|
contents <- many1 nonEndline
|
|
|
|
end <- option "" (do
|
|
|
|
newline
|
|
|
|
option "" indentSpaces
|
|
|
|
return "\n")
|
|
|
|
return (contents ++ end))
|
2006-12-19 07:30:36 +00:00
|
|
|
|
|
|
|
rawLines = do
|
|
|
|
lines <- many1 rawLine
|
|
|
|
return (concat lines)
|
|
|
|
|
2006-10-17 14:22:29 +00:00
|
|
|
note = try (do
|
2006-12-30 22:51:49 +00:00
|
|
|
failIfStrict
|
2006-12-20 06:50:14 +00:00
|
|
|
ref <- noteMarker
|
|
|
|
char ':'
|
|
|
|
skipSpaces
|
|
|
|
skipEndline
|
|
|
|
raw <- sepBy rawLines (try (do {blankline; indentSpaces}))
|
|
|
|
option "" blanklines
|
|
|
|
-- parse the extracted text, which may contain various block elements:
|
2006-12-21 09:02:06 +00:00
|
|
|
rest <- getInput
|
|
|
|
setInput $ (joinWithSep "\n" raw) ++ "\n\n"
|
|
|
|
contents <- parseBlocks
|
|
|
|
setInput rest
|
2006-12-20 06:50:14 +00:00
|
|
|
state <- getState
|
|
|
|
let identifiers = stateNoteIdentifiers state
|
|
|
|
case (findIndex (== ref) identifiers) of
|
|
|
|
Just n -> updateState (\s -> s {stateNoteBlocks =
|
2006-12-21 09:02:06 +00:00
|
|
|
(Note (show (n+1)) contents):(stateNoteBlocks s)})
|
2006-12-20 06:50:14 +00:00
|
|
|
Nothing -> updateState id
|
|
|
|
return Null)
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- block quotes
|
|
|
|
--
|
|
|
|
|
|
|
|
emacsBoxQuote = try (do
|
2006-12-30 22:51:49 +00:00
|
|
|
failIfStrict
|
2006-12-20 06:50:14 +00:00
|
|
|
string ",----"
|
|
|
|
manyTill anyChar newline
|
|
|
|
raw <- manyTill (try (do
|
|
|
|
char '|'
|
|
|
|
option ' ' (char ' ')
|
|
|
|
result <- manyTill anyChar newline
|
|
|
|
return result))
|
|
|
|
(string "`----")
|
|
|
|
manyTill anyChar newline
|
|
|
|
option "" blanklines
|
|
|
|
return raw)
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
emailBlockQuoteStart = try (do
|
|
|
|
skipNonindentSpaces
|
|
|
|
char blockQuoteChar
|
|
|
|
option ' ' (char ' ')
|
|
|
|
return "> ")
|
|
|
|
|
|
|
|
emailBlockQuote = try (do
|
2006-12-20 06:50:14 +00:00
|
|
|
emailBlockQuoteStart
|
|
|
|
raw <- sepBy (many (choice [nonEndline,
|
|
|
|
(try (do
|
|
|
|
endline
|
|
|
|
notFollowedBy' emailBlockQuoteStart
|
|
|
|
return '\n'))]))
|
|
|
|
(try (do {newline; emailBlockQuoteStart}))
|
|
|
|
newline <|> (do{ eof; return '\n' })
|
|
|
|
option "" blanklines
|
|
|
|
return raw)
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
blockQuote = do
|
2006-12-20 06:50:14 +00:00
|
|
|
raw <- choice [ emailBlockQuote, emacsBoxQuote ]
|
|
|
|
-- parse the extracted block, which may contain various block elements:
|
2006-12-21 09:02:06 +00:00
|
|
|
rest <- getInput
|
|
|
|
setInput $ (joinWithSep "\n" raw) ++ "\n\n"
|
|
|
|
contents <- parseBlocks
|
|
|
|
setInput rest
|
|
|
|
return (BlockQuote contents)
|
|
|
|
|
2006-10-17 14:22:29 +00:00
|
|
|
--
|
|
|
|
-- list blocks
|
|
|
|
--
|
|
|
|
|
|
|
|
list = choice [ bulletList, orderedList ] <?> "list"
|
|
|
|
|
2006-12-20 06:50:14 +00:00
|
|
|
bulletListStart = try (do
|
|
|
|
option ' ' newline -- if preceded by a Plain block in a list context
|
|
|
|
skipNonindentSpaces
|
|
|
|
notFollowedBy' hrule -- because hrules start out just like lists
|
|
|
|
oneOf bulletListMarkers
|
|
|
|
spaceChar
|
|
|
|
skipSpaces)
|
|
|
|
|
|
|
|
orderedListStart = try (do
|
|
|
|
option ' ' newline -- if preceded by a Plain block in a list context
|
|
|
|
skipNonindentSpaces
|
2006-12-30 22:51:49 +00:00
|
|
|
many1 digit <|> (do{failIfStrict; count 1 letter})
|
|
|
|
delim <- oneOf orderedListDelimiters
|
|
|
|
if delim /= '.' then failIfStrict else return ()
|
2006-12-20 06:50:14 +00:00
|
|
|
oneOf spaceChars
|
|
|
|
skipSpaces)
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
-- parse a line of a list item (start = parser for beginning of list item)
|
|
|
|
listLine start = try (do
|
|
|
|
notFollowedBy' start
|
|
|
|
notFollowedBy blankline
|
2006-12-20 06:50:14 +00:00
|
|
|
notFollowedBy' (do
|
|
|
|
indentSpaces
|
|
|
|
many (spaceChar)
|
|
|
|
choice [bulletListStart, orderedListStart])
|
2006-10-17 14:22:29 +00:00
|
|
|
line <- manyTill anyChar newline
|
|
|
|
return (line ++ "\n"))
|
|
|
|
|
|
|
|
-- parse raw text for one list item, excluding start marker and continuations
|
2006-12-20 06:50:14 +00:00
|
|
|
rawListItem start = try (do
|
|
|
|
start
|
|
|
|
result <- many1 (listLine start)
|
|
|
|
blanks <- many blankline
|
|
|
|
return ((concat result) ++ blanks))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
-- continuation of a list item - indented and separated by blankline
|
|
|
|
-- or (in compact lists) endline.
|
|
|
|
-- note: nested lists are parsed as continuations
|
2006-12-20 06:50:14 +00:00
|
|
|
listContinuation start = try (do
|
|
|
|
followedBy' indentSpaces
|
|
|
|
result <- many1 (listContinuationLine start)
|
|
|
|
blanks <- many blankline
|
|
|
|
return ((concat result) ++ blanks))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
listContinuationLine start = try (do
|
2006-12-20 06:50:14 +00:00
|
|
|
notFollowedBy' blankline
|
|
|
|
notFollowedBy' start
|
|
|
|
option "" indentSpaces
|
|
|
|
result <- manyTill anyChar newline
|
|
|
|
return (result ++ "\n"))
|
|
|
|
|
|
|
|
listItem start = try (do
|
|
|
|
first <- rawListItem start
|
2006-12-21 09:02:06 +00:00
|
|
|
continuations <- many (listContinuation start)
|
2006-12-20 06:50:14 +00:00
|
|
|
-- parsing with ListItemState forces markers at beginning of lines to
|
|
|
|
-- count as list item markers, even if not separated by blank space.
|
|
|
|
-- see definition of "endline"
|
|
|
|
state <- getState
|
2006-12-21 09:02:06 +00:00
|
|
|
let oldContext = stateParserContext state
|
|
|
|
setState $ state {stateParserContext = ListItemState}
|
|
|
|
-- parse the extracted block, which may contain various block elements:
|
|
|
|
rest <- getInput
|
|
|
|
let raw = concat (first:continuations)
|
|
|
|
setInput $ raw
|
|
|
|
contents <- parseBlocks
|
|
|
|
setInput rest
|
|
|
|
updateState (\st -> st {stateParserContext = oldContext})
|
|
|
|
return contents)
|
2006-12-20 06:50:14 +00:00
|
|
|
|
|
|
|
orderedList = try (do
|
|
|
|
items <- many1 (listItem orderedListStart)
|
|
|
|
let items' = compactify items
|
|
|
|
return (OrderedList items'))
|
|
|
|
|
|
|
|
bulletList = try (do
|
|
|
|
items <- many1 (listItem bulletListStart)
|
|
|
|
let items' = compactify items
|
|
|
|
return (BulletList items'))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- paragraph block
|
|
|
|
--
|
|
|
|
|
|
|
|
para = try (do
|
|
|
|
result <- many1 inline
|
|
|
|
newline
|
2006-12-30 22:51:49 +00:00
|
|
|
st <- getState
|
|
|
|
if stateStrict st
|
|
|
|
then choice [followedBy' blockQuote, followedBy' header,
|
|
|
|
(do{blanklines; return ()})]
|
|
|
|
else choice [followedBy' emacsBoxQuote,
|
|
|
|
(do{blanklines; return ()})]
|
2006-10-17 14:22:29 +00:00
|
|
|
let result' = normalizeSpaces result
|
|
|
|
return (Para result'))
|
|
|
|
|
|
|
|
plain = do
|
|
|
|
result <- many1 inline
|
|
|
|
let result' = normalizeSpaces result
|
|
|
|
return (Plain result')
|
|
|
|
|
|
|
|
--
|
|
|
|
-- raw html
|
|
|
|
--
|
|
|
|
|
2006-12-30 22:51:49 +00:00
|
|
|
htmlElement = choice [strictHtmlBlock,
|
|
|
|
htmlBlockElement] <?> "html element"
|
|
|
|
|
|
|
|
htmlBlock = do
|
|
|
|
st <- getState
|
|
|
|
if stateStrict st
|
|
|
|
then do
|
|
|
|
failUnlessBeginningOfLine
|
|
|
|
first <- htmlElement
|
|
|
|
finalSpace <- many (oneOf spaceChars)
|
|
|
|
finalNewlines <- many newline
|
|
|
|
return (RawHtml (first ++ finalSpace ++ finalNewlines))
|
|
|
|
else rawHtmlBlocks
|
|
|
|
|
|
|
|
-- True if tag is self-closing
|
|
|
|
selfClosing tag = case (matchRegex (mkRegex "\\/[[:space:]]*>$") tag) of
|
|
|
|
Just _ -> True
|
|
|
|
Nothing -> False
|
|
|
|
|
|
|
|
strictHtmlBlock = try (do
|
|
|
|
tag <- anyHtmlBlockTag
|
|
|
|
let tag' = extractTagType tag
|
|
|
|
if selfClosing tag || tag' == "hr"
|
|
|
|
then return tag
|
|
|
|
else do
|
|
|
|
contents <- many (do{notFollowedBy' (htmlEndTag tag');
|
|
|
|
htmlElement <|> (count 1 anyChar)})
|
|
|
|
end <- htmlEndTag tag'
|
|
|
|
return $ tag ++ (concat contents) ++ end)
|
|
|
|
|
2006-10-17 14:22:29 +00:00
|
|
|
rawHtmlBlocks = try (do
|
2006-12-20 06:50:14 +00:00
|
|
|
htmlBlocks <- many1 rawHtmlBlock
|
|
|
|
let combined = concatMap (\(RawHtml str) -> str) htmlBlocks
|
|
|
|
let combined' = if (last combined == '\n')
|
|
|
|
then init combined -- strip extra newline
|
|
|
|
else combined
|
|
|
|
return (RawHtml combined'))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- reference key
|
|
|
|
--
|
|
|
|
|
2006-12-20 06:50:14 +00:00
|
|
|
referenceKey = try (do
|
2006-12-31 17:34:06 +00:00
|
|
|
skipNonindentSpaces
|
2006-12-20 06:50:14 +00:00
|
|
|
label <- reference
|
|
|
|
char labelSep
|
|
|
|
skipSpaces
|
|
|
|
option ' ' (char autoLinkStart)
|
|
|
|
src <- many (noneOf (titleOpeners ++ [autoLinkEnd] ++ endLineChars))
|
|
|
|
option ' ' (char autoLinkEnd)
|
|
|
|
tit <- option "" title
|
|
|
|
blanklines
|
2006-12-30 22:51:49 +00:00
|
|
|
state <- getState
|
|
|
|
let keysUsed = stateKeysUsed state
|
|
|
|
updateState (\st -> st { stateKeysUsed = (label:keysUsed) })
|
|
|
|
return $ Key label (Src (removeTrailingSpace src) tit))
|
|
|
|
|
|
|
|
--
|
|
|
|
-- LaTeX
|
|
|
|
--
|
|
|
|
|
|
|
|
rawLaTeXEnvironment' = do
|
|
|
|
failIfStrict
|
|
|
|
rawLaTeXEnvironment
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- inline
|
|
|
|
--
|
|
|
|
|
2006-12-31 19:22:02 +00:00
|
|
|
text = choice [ math, strong, emph, code, str, linebreak, tabchar,
|
2006-10-17 14:22:29 +00:00
|
|
|
whitespace, endline ] <?> "text"
|
|
|
|
|
2006-12-30 22:51:49 +00:00
|
|
|
inline = choice [ rawLaTeXInline', escapedChar, special, hyphens, text,
|
2006-12-20 06:50:14 +00:00
|
|
|
ltSign, symbol ] <?> "inline"
|
2006-10-17 14:22:29 +00:00
|
|
|
|
2006-12-30 22:51:49 +00:00
|
|
|
special = choice [ noteRef, inlineNote, link, referenceLink, rawHtmlInline',
|
2006-12-20 06:50:14 +00:00
|
|
|
autoLink, image ] <?> "link, inline html, note, or image"
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
escapedChar = escaped anyChar
|
|
|
|
|
2006-11-26 07:01:37 +00:00
|
|
|
ltSign = try (do
|
2006-10-17 14:22:29 +00:00
|
|
|
notFollowedBy' rawHtmlBlocks -- don't return < if it starts html
|
|
|
|
char '<'
|
2006-11-26 07:01:37 +00:00
|
|
|
return (Str ['<']))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
specialCharsMinusLt = filter (/= '<') specialChars
|
|
|
|
|
|
|
|
symbol = do
|
|
|
|
result <- oneOf specialCharsMinusLt
|
|
|
|
return (Str [result])
|
|
|
|
|
|
|
|
hyphens = try (do
|
|
|
|
result <- many1 (char '-')
|
2006-12-20 06:50:14 +00:00
|
|
|
if (length result) == 1
|
|
|
|
then skipEndline -- don't want to treat endline after hyphen as a space
|
|
|
|
else do{ string ""; return Space }
|
2006-10-17 14:22:29 +00:00
|
|
|
return (Str result))
|
|
|
|
|
2006-12-31 19:22:02 +00:00
|
|
|
-- parses inline code, between n codeStarts and n codeEnds
|
|
|
|
code = try (do
|
|
|
|
starts <- many1 (char codeStart)
|
|
|
|
let num = length starts
|
|
|
|
result <- many1Till anyChar (try (count num (char codeEnd)))
|
2006-12-20 06:50:14 +00:00
|
|
|
-- get rid of any internal newlines
|
|
|
|
let result' = removeLeadingTrailingSpace $ joinWithSep " " $ lines result
|
|
|
|
return (Code result'))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
2006-12-20 06:50:14 +00:00
|
|
|
mathWord = many1 (choice [ (noneOf (" \t\n\\" ++ [mathEnd])),
|
|
|
|
(try (do
|
|
|
|
c <- char '\\'
|
|
|
|
notFollowedBy (char mathEnd)
|
|
|
|
return c))])
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
math = try (do
|
2006-12-30 22:51:49 +00:00
|
|
|
failIfStrict
|
2006-10-17 14:22:29 +00:00
|
|
|
char mathStart
|
|
|
|
notFollowedBy space
|
|
|
|
words <- sepBy1 mathWord (many1 space)
|
|
|
|
char mathEnd
|
|
|
|
return (TeX ("$" ++ (joinWithSep " " words) ++ "$")))
|
|
|
|
|
|
|
|
emph = do
|
|
|
|
result <- choice [ (enclosed (char emphStart) (char emphEnd) inline),
|
2006-12-20 06:50:14 +00:00
|
|
|
(enclosed (char emphStartAlt) (char emphEndAlt) inline) ]
|
2006-10-17 14:22:29 +00:00
|
|
|
return (Emph (normalizeSpaces result))
|
|
|
|
|
|
|
|
strong = do
|
2006-12-20 06:50:14 +00:00
|
|
|
result <- choice [ (enclosed (count 2 (char emphStart))
|
|
|
|
(count 2 (char emphEnd)) inline),
|
|
|
|
(enclosed (count 2 (char emphStartAlt))
|
|
|
|
(count 2 (char emphEndAlt)) inline) ]
|
2006-10-17 14:22:29 +00:00
|
|
|
return (Strong (normalizeSpaces result))
|
|
|
|
|
|
|
|
whitespace = do
|
|
|
|
many1 (oneOf spaceChars) <?> "whitespace"
|
|
|
|
return Space
|
|
|
|
|
|
|
|
tabchar = do
|
|
|
|
tab
|
|
|
|
return (Str "\t")
|
|
|
|
|
|
|
|
-- hard line break
|
|
|
|
linebreak = try (do
|
|
|
|
oneOf spaceChars
|
|
|
|
many1 (oneOf spaceChars)
|
|
|
|
endline
|
|
|
|
return LineBreak )
|
|
|
|
|
|
|
|
nonEndline = noneOf endLineChars
|
|
|
|
|
|
|
|
str = do
|
|
|
|
result <- many1 ((noneOf (specialChars ++ spaceChars ++ endLineChars)))
|
|
|
|
return (Str (decodeEntities result))
|
|
|
|
|
|
|
|
-- an endline character that can be treated as a space, not a structural break
|
2006-12-20 06:50:14 +00:00
|
|
|
endline = try (do
|
|
|
|
newline
|
|
|
|
notFollowedBy blankline
|
|
|
|
st <- getState
|
2006-12-30 22:51:49 +00:00
|
|
|
if stateStrict st
|
|
|
|
then do
|
|
|
|
notFollowedBy' emailBlockQuoteStart
|
|
|
|
notFollowedBy' header
|
|
|
|
else return ()
|
|
|
|
-- parse potential list-starts differently if in a list:
|
2006-12-20 06:50:14 +00:00
|
|
|
if (stateParserContext st) == ListItemState
|
2006-12-30 22:51:49 +00:00
|
|
|
then notFollowedBy' (orderedListStart <|> bulletListStart)
|
|
|
|
else return ()
|
2006-12-20 06:50:14 +00:00
|
|
|
return Space)
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
--
|
|
|
|
-- links
|
|
|
|
--
|
|
|
|
|
|
|
|
-- a reference label for a link
|
|
|
|
reference = do
|
|
|
|
char labelStart
|
2006-12-19 07:30:36 +00:00
|
|
|
notFollowedBy (char noteStart)
|
2006-12-30 22:51:49 +00:00
|
|
|
-- allow for embedded brackets:
|
|
|
|
label <- manyTill ((do{res <- reference;
|
|
|
|
return $ [Str "["] ++ res ++ [Str "]"]}) <|>
|
|
|
|
count 1 inline)
|
|
|
|
(char labelEnd)
|
|
|
|
return (normalizeSpaces (concat label))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
-- source for a link, with optional title
|
2006-12-20 06:50:14 +00:00
|
|
|
source = try (do
|
|
|
|
char srcStart
|
|
|
|
option ' ' (char autoLinkStart)
|
|
|
|
src <- many (noneOf ([srcEnd, autoLinkEnd] ++ titleOpeners))
|
|
|
|
option ' ' (char autoLinkEnd)
|
|
|
|
tit <- option "" title
|
|
|
|
skipSpaces
|
|
|
|
char srcEnd
|
|
|
|
return (Src (removeTrailingSpace src) tit))
|
|
|
|
|
|
|
|
titleWith startChar endChar = try (do
|
|
|
|
skipSpaces
|
|
|
|
skipEndline -- a title can be on the next line from the source
|
|
|
|
skipSpaces
|
|
|
|
char startChar
|
2006-12-30 22:51:49 +00:00
|
|
|
tit <- manyTill anyChar (try (do
|
|
|
|
char endChar
|
|
|
|
skipSpaces
|
|
|
|
followedBy' (char ')' <|> newline)))
|
2006-12-20 06:50:14 +00:00
|
|
|
let tit' = gsub "\"" """ tit
|
|
|
|
return tit')
|
|
|
|
|
|
|
|
title = choice [ titleWith '(' ')',
|
|
|
|
titleWith '"' '"',
|
|
|
|
titleWith '\'' '\''] <?> "title"
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
link = choice [explicitLink, referenceLink] <?> "link"
|
|
|
|
|
2006-12-20 06:50:14 +00:00
|
|
|
explicitLink = try (do
|
|
|
|
label <- reference
|
|
|
|
src <- source
|
|
|
|
return (Link label src))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
|
|
|
referenceLink = choice [referenceLinkDouble, referenceLinkSingle]
|
|
|
|
|
2006-12-30 22:51:49 +00:00
|
|
|
-- a link like [this][ref]
|
2006-12-20 06:50:14 +00:00
|
|
|
referenceLinkDouble = try (do
|
|
|
|
label <- reference
|
|
|
|
skipSpaces
|
|
|
|
skipEndline
|
|
|
|
skipSpaces
|
|
|
|
ref <- reference
|
2006-12-30 22:51:49 +00:00
|
|
|
let ref' = if null ref then label else ref
|
|
|
|
state <- getState
|
|
|
|
if ref' `elem` (stateKeysUsed state)
|
|
|
|
then return () else fail "no corresponding key"
|
|
|
|
return (Link label (Ref ref')))
|
2006-12-20 06:50:14 +00:00
|
|
|
|
|
|
|
-- a link like [this]
|
|
|
|
referenceLinkSingle = try (do
|
|
|
|
label <- reference
|
2006-12-30 22:51:49 +00:00
|
|
|
state <- getState
|
|
|
|
if label `elem` (stateKeysUsed state)
|
|
|
|
then return () else fail "no corresponding key"
|
|
|
|
return (Link label (Ref label)))
|
2006-12-20 06:50:14 +00:00
|
|
|
|
|
|
|
-- a link <like.this.com>
|
|
|
|
autoLink = try (do
|
|
|
|
notFollowedBy' anyHtmlBlockTag
|
|
|
|
src <- between (char autoLinkStart) (char autoLinkEnd)
|
|
|
|
(many (noneOf (spaceChars ++ endLineChars ++ [autoLinkEnd])))
|
|
|
|
case (matchRegex emailAddress src) of
|
|
|
|
Just _ -> return (Link [Str src] (Src ("mailto:" ++ src) ""))
|
|
|
|
Nothing -> return (Link [Str src] (Src src "")))
|
|
|
|
|
|
|
|
emailAddress =
|
|
|
|
mkRegex "([^@:/]+)@(([^.]+[.]?)*([^.]+))" -- presupposes no whitespace
|
|
|
|
|
|
|
|
image = try (do
|
|
|
|
char imageStart
|
|
|
|
(Link label src) <- link
|
|
|
|
return (Image label src))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
2006-12-19 23:13:03 +00:00
|
|
|
noteMarker = try (do
|
2006-12-20 06:50:14 +00:00
|
|
|
char labelStart
|
|
|
|
char noteStart
|
|
|
|
manyTill (noneOf " \t\n") (char labelEnd))
|
2006-12-19 23:13:03 +00:00
|
|
|
|
|
|
|
noteRef = try (do
|
2006-12-30 22:51:49 +00:00
|
|
|
failIfStrict
|
2006-12-20 06:50:14 +00:00
|
|
|
ref <- noteMarker
|
|
|
|
state <- getState
|
|
|
|
let identifiers = (stateNoteIdentifiers state) ++ [ref]
|
|
|
|
updateState (\st -> st {stateNoteIdentifiers = identifiers})
|
|
|
|
return (NoteRef (show (length identifiers))))
|
2006-12-19 23:13:03 +00:00
|
|
|
|
|
|
|
inlineNote = try (do
|
2006-12-30 22:51:49 +00:00
|
|
|
failIfStrict
|
2006-12-20 06:50:14 +00:00
|
|
|
char noteStart
|
|
|
|
char labelStart
|
|
|
|
contents <- manyTill inline (char labelEnd)
|
|
|
|
state <- getState
|
|
|
|
let identifiers = stateNoteIdentifiers state
|
|
|
|
let ref = show $ (length identifiers) + 1
|
|
|
|
let noteBlocks = stateNoteBlocks state
|
|
|
|
updateState (\st -> st {stateNoteIdentifiers = (identifiers ++ [ref]),
|
|
|
|
stateNoteBlocks =
|
|
|
|
(Note ref [Para contents]):noteBlocks})
|
|
|
|
return (NoteRef ref))
|
2006-10-17 14:22:29 +00:00
|
|
|
|
2006-12-30 22:51:49 +00:00
|
|
|
rawLaTeXInline' = do
|
|
|
|
failIfStrict
|
|
|
|
rawLaTeXInline
|
|
|
|
|
|
|
|
rawHtmlInline' = do
|
|
|
|
st <- getState
|
|
|
|
result <- if stateStrict st
|
|
|
|
then choice [htmlBlockElement, anyHtmlTag, anyHtmlEndTag]
|
|
|
|
else choice [htmlBlockElement, anyHtmlInlineTag]
|
|
|
|
return (HtmlInline result)
|
|
|
|
|