org-parser-0.1.0.0: src/Org/Parser/Objects.hs
-- | Parsers for Org objects.
module Org.Parser.Objects
( -- * Sets of markup
minimalSet
, standardSet
-- * Marked parsers
, Marked (..)
, markupContext
, plainMarkupContext
-- * General purpose parsers
, markup
, rawMarkup
-- * Objects
, code
, verbatim
, italic
, underline
, bold
, strikethrough
, singleQuoted
, doubleQuoted
, entity
, latexFragment
, texMathFragment
, exportSnippet
, citation
, inlBabel
, inlSrc
, linebreak
, angleLink
, regularLink
, target
, suscript
, macro
, footnoteReference
, timestamp
, statisticCookie
-- * Auxiliary
, linkToTarget
, parseTimestamp
)
where
import Data.Set qualified as Set
import Data.Text qualified as T
import Org.Builder qualified as B
import Org.Data.Entities (defaultEntitiesNames)
import Org.Parser.Common
import Org.Parser.Definitions
import Org.Parser.MarkupContexts
import Prelude hiding (many, some)
-- * Sets of objects
minimalSet :: Marked OrgParser OrgObjects
minimalSet =
mconcat
[ endline
, code
, verbatim
, italic
, underline
, bold
, strikethrough
, entity
, latexFragment
, texMathFragment
, singleQuoted
, doubleQuoted
, suscript
, statisticCookie
, macro
]
standardSet :: Marked OrgParser OrgObjects
standardSet =
mconcat
[ minimalSet
, regularLink
, footnoteReference
, timestamp
, exportSnippet
, inlBabel
, inlSrc
, linebreak
, target
, angleLink
, citation
]
{- | Parse inside a "plain context", i.e., plain text not matched by any parsers
gets converted to 'Plain' objects.
@
'plainMarkupContext' = 'markupContext' 'B.plain'
@
-}
plainMarkupContext :: Marked OrgParser OrgObjects -> OrgParser OrgObjects
plainMarkupContext = markupContext B.plain
newlineAndClear :: OrgParser Char
newlineAndClear = newline <* clearLastChar
emphasisPreChars :: Set Char
emphasisPreChars = fromList "-('\"{"
emphPreChar :: Char -> Bool
emphPreChar c = isSpace c || c `Set.member` emphasisPreChars
emphasisPre :: Char -> OrgParser ()
emphasisPre s = try $ do
lchar <- gets orgStateLastChar
for_ lchar $ guard . emphPreChar
_ <- char s
notFollowedBy spaceChar
emphasisPostChars :: Set Char
emphasisPostChars = fromList "-.,;:!?'\")}\\["
emphPostChar :: Char -> Bool
emphPostChar c = isSpace c || c `Set.member` emphasisPostChars
emphasisPost :: Char -> OrgParser ()
emphasisPost e = try $ do
lchar <- gets orgStateLastChar
for_ lchar $ guard . not . isSpace
_ <- char e
putLastChar e
lookAhead (eof <|> void (satisfy emphPostChar))
emphasisSkip :: Char -> OrgParser ()
emphasisSkip s = try $ do
putLastChar =<< anySingle
t <- takeWhileP Nothing (/= s)
setLastChar (snd <$> T.unsnoc t)
markup ::
(OrgObjects -> OrgObjects) ->
Char ->
Marked OrgParser OrgObjects
markup f c = Marked [c] $
try $ do
emphasisPre c
st <- getFullState
s <- anySingle
(t, _) <- skipManyTill' (emphasisSkip c) (emphasisPost c)
f <$> parseFromText st (T.cons s t) (plainMarkupContext standardSet)
rawMarkup ::
(Text -> OrgObjects) ->
Char ->
Marked OrgParser OrgObjects
rawMarkup f d = Marked [d] $
try $ do
emphasisPre d
f . fst <$> skipManyTill' (emphasisSkip d) (emphasisPost d)
-- | Parse a code object.
code :: Marked OrgParser OrgObjects
code = rawMarkup B.code '~'
-- | Parse a verbatim object.
verbatim :: Marked OrgParser OrgObjects
verbatim = rawMarkup B.verbatim '='
-- | Parse an italic object.
italic :: Marked OrgParser OrgObjects
italic = markup B.italic '/'
-- | Parse an underline object.
underline :: Marked OrgParser OrgObjects
underline = markup B.underline '_'
-- | Parse a bold object.
bold :: Marked OrgParser OrgObjects
bold = markup B.bold '*'
-- | Parse a strikethrough object.
strikethrough :: Marked OrgParser OrgObjects
strikethrough = markup B.strikethrough '+'
-- | Parse a single-quoted object.
singleQuoted :: Marked OrgParser OrgObjects
singleQuoted = markup B.singleQuoted '\''
-- | Parse a double-quoted object.
doubleQuoted :: Marked OrgParser OrgObjects
doubleQuoted = markup B.doubleQuoted '"'
-- | An endline character that can be treated as a space, not a line break.
endline :: Marked OrgParser OrgObjects
endline =
Marked "\n" $
try $
newlineAndClear
*> hspace
$> B.plain "\n"
-- * Entities and LaTeX fragments
-- | Parse an entity object.
entity :: Marked OrgParser OrgObjects
entity = Marked "\\" $ try $ do
_ <- char '\\'
name <- choice (map string defaultEntitiesNames)
void (string "{}") <|> notFollowedBy asciiAlpha
pure $ B.entity name
-- | Parse a LaTeX fragment object.
latexFragment :: Marked OrgParser OrgObjects
latexFragment = Marked "\\" $ try do
_ <- char '\\'
mathFragment <|> rawFragment
where
mathFragment = try do
inline <-
char '(' $> True
<|> char '[' $> False
(str, _) <-
findSkipping
(/= '\\')
(try $ char '\\' *> char if inline then ')' else ']')
pure $
if inline
then B.inlMath str
else B.dispMath str
rawFragment :: MonadParser m => m OrgObjects
rawFragment = try $ do
name <- someAsciiAlpha
text <- (name <>) <$> option "" brackets
pure $ B.fragment ("\\" <> text)
brackets :: MonadParser m => m Text
brackets = try $ do
open <- satisfy (\c -> c == '{' || c == '[')
let close = if open == '{' then '}' else ']'
str <- takeWhileP Nothing (\c -> c /= close && c /= '\n')
_ <- char close
pure $ open `T.cons` str `T.snoc` close
-- | Parse a TeX math fragment object.
texMathFragment :: Marked OrgParser OrgObjects
texMathFragment = Marked "$" $ try $ display <|> inline
where
display = try $ do
_ <- string "$$"
(str, _) <-
findSkipping
(/= '$')
(string "$$")
pure $ B.dispMath str
post = do
_ <- char '$'
eof
<|> ( void . lookAhead $
satisfy (\x -> isPunctuation x || isSpace x || x == '"')
)
inline = try $ do
lchar <- gets orgStateLastChar
for_ lchar $ guard . (/= '$')
_ <- char '$'
str <- singleChar <|> moreChars
pure $ B.inlMath str
moreChars = try $ do
str <- takeWhile1P (Just "inside of inline math") (/= '$')
guard $ border1 (T.head str) && border2 (T.last str)
post
pure str
singleChar = try $ do
c <- satisfy (\x -> not (isSpace x) && x `notElem` disallowedCharsSet)
post
pure $ one c
disallowedCharsSet :: [Char]
disallowedCharsSet = ['.', ',', '?', ';', '"']
border1 c = not (isSpace c) && c `notElem` (".,;$" :: String)
border2 c = not (isSpace c) && c `notElem` (".,$" :: String)
-- * Export snippets
-- | Parse an export snippet object.
exportSnippet :: Marked OrgParser OrgObjects
exportSnippet = Marked "@" $
try $ do
_ <- string "@@"
backend <-
takeWhile1P
(Just "export snippet backend")
(\c -> isAsciiAlpha c || isDigit c || c == '-')
_ <- char ':'
B.exportSnippet backend . fst
<$> findSkipping (/= '@') (string "@@")
-- * Citations
-- The following code for org-cite citations was adapted and improved upon pandoc's.
-- | Parse a citation object.
citation :: Marked OrgParser OrgObjects
citation =
Marked "[" $
B.citation <$> withBalancedContext '[' ']' (const True) orgCite
-- | A citation in org-cite style
orgCite :: OrgParser Citation
orgCite = try $ do
_ <- string "cite"
(style, variant) <- citeStyle
_ <- char ':'
space
globalPrefix <- option mempty (try (citeSuffix <* char ';'))
items <- citeItems
globalSuffix <- option mempty (try (char ';' *> citePrefix))
space
eof
return
Citation
{ citationStyle = style
, citationVariant = variant
, citationPrefix = toList globalPrefix
, citationSuffix = toList globalSuffix
, citationReferences = items
}
citeStyle :: OrgParser (Tokens Text, Tokens Text)
citeStyle = do
sty <- option "" $ try style
vars <- option "" $ try variants
return (sty, vars)
where
style =
char '/'
*> takeWhileP
(Just "alphaNum, '_' or '-' characters")
(\c -> isAlphaNum c || c == '_' || c == '-')
variants =
char '/'
*> takeWhileP
(Just "alphaNum, '_', '-' or '/' characters")
(\c -> isAlphaNum c || c == '_' || c == '-' || c == '/')
citeItems :: OrgParser [CiteReference]
citeItems = citeItem `sepBy1'` char ';'
where
sepBy1' p sep = (:) <$> p <*> many (try $ sep >> p)
citeItem :: OrgParser CiteReference
citeItem = do
pref <- option mempty citePrefix
itemKey <- orgCiteKey
suff <- option mempty citeSuffix
return
CiteReference
{ refId = itemKey
, refPrefix = toList pref
, refSuffix = toList suff
}
citePrefix :: OrgParser OrgObjects
citePrefix = try $ do
clearLastChar
withContext
(takeWhile1P Nothing (/= '@'))
(eof <|> void (lookAhead $ char '@'))
(plainMarkupContext minimalSet)
citeSuffix :: OrgParser OrgObjects
citeSuffix = try $ do
clearLastChar
withContext
(takeWhile1P Nothing (/= ';'))
(eof <|> void (lookAhead $ char ';'))
(plainMarkupContext minimalSet)
orgCiteKey :: OrgParser Text
orgCiteKey = do
_ <- char '@'
takeWhile1P (Just "citation key allowed chars") orgCiteKeyChar
orgCiteKeyChar :: Char -> Bool
orgCiteKeyChar c =
isAlphaNum c || c `elem` (".:?!`\'/*@+|(){}<>&_^$#%~-" :: String)
-- * Inline Babel calls
-- | Parse an inline babel call object.
inlBabel :: Marked OrgParser OrgObjects
inlBabel = Marked "c" . try $ do
_ <- string "call_"
name <-
takeWhile1P
(Just "babel call name")
(\c -> not (isSpace c) && c `notElem` ['[', ']', '(', ')'])
header1 <- option "" header
args <- arguments
header2 <- option "" header
return $ B.inlBabel name header1 header2 args
where
header = withBalancedContext '[' ']' (/= '\n') getInput
arguments = withBalancedContext '(' ')' (/= '\n') getInput
-- * Inline source blocks
-- | Parse an inline source object.
inlSrc :: Marked OrgParser OrgObjects
inlSrc = Marked "s" . try $ do
_ <- string "src_"
name <-
takeWhile1P
(Just "babel call name")
(\c -> not (isSpace c) && c /= '{' && c /= '[')
headers <- option "" header
B.inlSrc name headers <$> body
where
header = withBalancedContext '[' ']' (/= '\n') getInput
body = withBalancedContext '{' '}' (/= '\n') getInput
-- * Line breaks
-- | Parse a linebreak object.
linebreak :: Marked OrgParser OrgObjects
linebreak =
Marked "\\" . try $
B.linebreak <$ string "\\\\" <* blankline' <* clearLastChar
-- * Links
-- | Parse a angle link object.
angleLink :: Marked OrgParser OrgObjects
angleLink = Marked "<" . try $ do
_ <- char '<'
protocol <- manyAsciiAlpha
_ <- char ':'
tgt <- fix $ \search -> do
partial <-
takeWhile1P
(Just "angle link target")
(\c -> c /= '\n' && c /= '>')
char '>' $> partial
<|> newline *> hspace *> ((T.stripEnd partial <>) <$> search)
return $ B.uriLink protocol tgt (B.plain $ protocol <> ":" <> tgt)
-- | Parse a regular link object.
regularLink :: Marked OrgParser OrgObjects
regularLink =
Marked "[" . try $
do
_ <- string "[["
str <- linkTarget
descr <- linkDescr <|> char ']' $> mempty
putLastChar ']'
return $ B.link (linkToTarget str) descr
where
linkTarget :: MonadParser m => m Text
linkTarget = fix $ \rest -> do
partial <-
takeWhileP
(Just "link target")
(\c -> c /= ']' && c /= '[' && c /= '\\' && c /= '\n')
oneOf ['[', ']'] $> partial
<|> char '\\' *> liftA2 T.cons (option '\\' $ oneOf ['[', ']']) rest
<|> newline *> hspace *> ((T.stripEnd partial `T.snoc` ' ' <>) <$> rest)
linkDescr :: OrgParser OrgObjects
linkDescr = try $ do
_ <- char '['
-- FIXME this is not the right set but... whatever
withContext skip (string "]]") (plainMarkupContext standardSet)
where
skip = anySingle >> takeWhileP Nothing (/= ']')
-- TODO this will probably be replaced by the AST annotations.
-- | Transform the link text into a link target.
linkToTarget :: Text -> LinkTarget
linkToTarget link
| any (`T.isPrefixOf` link) ["/", "./", "../"] =
let link' = toText (toString link)
in URILink "file" link'
| (prot, rest) <- T.break (== ':') link
, Just (_, uri) <- T.uncons rest =
URILink prot uri
| otherwise = UnresolvedLink link
-- * Targets and radio targets
-- | Parse a target object.
target :: Marked OrgParser OrgObjects
target = Marked "<" $ try do
_ <- string "<<"
str <- takeWhile1P (Just "dedicated target") (\c -> c /= '<' && c /= '>' && c /= '\n')
guard (not (isSpace $ T.head str))
guard (not (isSpace $ T.last str))
_ <- string ">>"
return $ B.target "" str
-- * Subscripts and superscripts
-- | Parse a subscript or a superscript object.
suscript :: Marked OrgParser OrgObjects
suscript = Marked "_^" $ try do
lchar <- gets orgStateLastChar
for_ lchar $ guard . not . isSpace
start <- satisfy \c -> c == '_' || c == '^'
contents <- asterisk <|> balanced <|> plain
pure $
if start == '_'
then B.subscript contents
else B.superscript contents
where
asterisk = B.plain . one <$> char '*'
balanced =
withBalancedContext '{' '}' (const True) $
plainMarkupContext minimalSet
sign = option mempty (B.plain . one <$> oneOf ['+', '-'])
plain =
liftA2 (<>) sign $
withMContext (const True) isAlphaNum plainEnd $
plainMarkupContext (entity <> latexFragment)
plainEnd :: OrgParser ()
plainEnd = try do
lookAhead $
eof
<|> try (some (oneOf [',', '.', '\\']) *> notFollowedBy (satisfy isAlphaNum))
<|> void (noneOf [',', '.', '\\'])
-- * Macros
-- | Parse a macro object.
macro :: Marked OrgParser OrgObjects
macro = Marked "{" $ try do
_ <- string "{{{"
_ <- lookAhead $ satisfy isAsciiAlpha
key <- takeWhile1P Nothing allowedKeyChar
args <-
(string "}}}" $> []) <|> do
_ <- char '('
t <- fst <$> findSkipping (/= ')') (string ")}}}")
return $ T.split (== ',') t
return $ B.macro key args
where
allowedKeyChar c = isAsciiAlpha c || isDigit c || c == '-' || c == '_'
-- * Footnote references
-- | Parse a footnote reference object.
footnoteReference :: Marked OrgParser OrgObjects
footnoteReference = Marked "[" $
withBalancedContext '[' ']' (const True) do
_ <- string "fn:"
lbl <-
optional $
takeWhile1P
(Just "footnote ref label")
(\c -> isAlphaNum c || c == '-' || c == '_')
def <-
optional $ try do
_ <- char ':'
plainMarkupContext standardSet
case (lbl, def) of
(Nothing, Nothing) -> empty
(Just lbl', Nothing) ->
return $ B.footnoteLabel lbl'
(_, Just def') ->
return $ B.footnoteInlDef lbl def'
-- * Timestamps
-- | Parse a timestamp object.
timestamp :: Marked OrgParser OrgObjects
timestamp = Marked "<[" $ B.timestamp <$> parseTimestamp
-- | Parse a timestamp.
parseTimestamp :: OrgParser TimestampData
parseTimestamp = try $ do
openChar <- lookAhead $ satisfy (\c -> c == '<' || c == '[')
let isActive = openChar == '<'
closeChar = if isActive then '>' else ']'
delims = (openChar, closeChar)
(d1, t1, r1, w1) <- component delims
optional (try $ string "--" *> component delims)
>>= \case
Just (d2, t2, r2, w2) ->
pure $ TimestampRange isActive (d1, fst <$> t1, r1, w1) (d2, fst <$> t2, r2, w2)
Nothing -> case t1 of
Just (t1', Just t1'') ->
pure $ TimestampRange isActive (d1, Just t1', r1, w1) (d1, Just t1'', r1, w1)
_ ->
pure $ TimestampData isActive (d1, fst <$> t1, r1, w1)
where
component delims = do
_ <- char (fst delims)
date <- parseDate
time <- optional . try $ do
hspace1
startTime <- parseTime
endTime <- optional . try $ char '-' *> parseTime
pure (startTime, endTime)
repeater <- optional (try $ hspace1 *> repeaterMark)
warning <- optional (try $ hspace1 *> warningMark)
hspace
_ <- char (snd delims)
pure (date, time, repeater, warning)
parseDate :: OrgParser Date
parseDate = do
year <- number 4 <* char '-'
month <- number 2 <* char '-'
day <- number 2
dayName <- optional $ try do
hspace1
takeWhile1P (Just "dayname characters") isLetter
pure (year, month, day, dayName)
repeaterMark = tsmark ["++", ".+", "+"]
warningMark = tsmark ["--", "-"]
tsmark :: [Text] -> OrgParser TimestampMark
tsmark marks = do
mtype <- (,,) <$> choice (map string marks)
mtype <$> integer <*> oneOf ['h', 'd', 'w', 'm', 'y']
-- * Statistic Cookies
-- | Parse a statistic cookie object.
statisticCookie :: Marked OrgParser OrgObjects
statisticCookie = Marked "[" $ try do
_ <- char '['
res <- Left <$> fra <|> Right <$> pct
_ <- char ']'
return $ B.statisticCookie res
where
fra = try $ liftA2 (,) integer (char '/' *> integer)
pct = try $ integer <* char '%'