module Skylighting.Format.HTML (
formatHtmlInline
, formatHtmlBlock
, styleToCss
) where
import Data.List (intersperse, sort)
import qualified Data.Map as Map
import Data.Monoid ((<>))
import Data.String (fromString)
import qualified Data.Text as Text
import Skylighting.Types
import Text.Blaze.Html
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
formatHtmlInline :: FormatOptions -> [SourceLine] -> Html
formatHtmlInline opts = wrapCode opts
. mconcat . intersperse (toHtml "\n")
. map (mapM_ (tokenToHtml opts))
formatHtmlBlock :: FormatOptions -> [SourceLine] -> Html
formatHtmlBlock opts ls =
H.div ! A.class_ (toValue "sourceCode") $
H.pre ! A.class_ (toValue $ Text.unwords classes)
$ wrapCode opts
$ mconcat . intersperse (toHtml "\n")
$ zipWith (sourceLineToHtml opts) [startNum..] ls
where classes = Text.pack "sourceCode" :
[Text.pack "numberSource" | numberLines opts] ++
[x | x <- containerClasses opts
, x /= Text.pack "sourceCode"]
startNum = LineNo $ startNumber opts
wrapCode :: FormatOptions -> Html -> Html
wrapCode opts h = H.code ! A.class_ (toValue $ Text.unwords
$ Text.pack "sourceCode"
: codeClasses opts) $ h
sourceLineToHtml :: FormatOptions -> LineNo -> SourceLine -> Html
sourceLineToHtml opts lno cont =
(if lineAnchors opts
then H.a ! A.class_ sourceLine
! A.id lineNum
! A.href lineRef
! dataAttrib
else H.a ! A.class_ sourceLine
! A.id lineNum
! dataAttrib) $ mapM_ (tokenToHtml opts) cont
where sourceLine = toValue "sourceLine"
lineNum = toValue prefixedLineNo
lineRef = toValue ('#':prefixedLineNo)
prefixedLineNo = Text.unpack (lineIdPrefix opts) <> show (lineNo lno)
dataAttrib = H.dataAttribute (fromString "line-number")
(toValue (show (lineNo lno)))
tokenToHtml :: FormatOptions -> Token -> Html
tokenToHtml _ (NormalTok, txt) = toHtml txt
tokenToHtml opts (toktype, txt) =
if titleAttributes opts
then sp ! A.title (toValue $ show toktype)
else sp
where sp = H.span ! A.class_ (toValue $ short toktype) $ toHtml txt
short :: TokenType -> String
short KeywordTok = "kw"
short DataTypeTok = "dt"
short DecValTok = "dv"
short BaseNTok = "bn"
short FloatTok = "fl"
short CharTok = "ch"
short StringTok = "st"
short CommentTok = "co"
short OtherTok = "ot"
short AlertTok = "al"
short FunctionTok = "fu"
short RegionMarkerTok = "re"
short ErrorTok = "er"
short ConstantTok = "cn"
short SpecialCharTok = "sc"
short VerbatimStringTok = "vs"
short SpecialStringTok = "ss"
short ImportTok = "im"
short DocumentationTok = "do"
short AnnotationTok = "an"
short CommentVarTok = "cv"
short VariableTok = "va"
short ControlFlowTok = "cf"
short OperatorTok = "op"
short BuiltInTok = "bu"
short ExtensionTok = "ex"
short PreprocessorTok = "pp"
short AttributeTok = "at"
short InformationTok = "in"
short WarningTok = "wa"
short NormalTok = ""
styleToCss :: Style -> String
styleToCss f = unlines $
divspec ++ numberspec ++ colorspec ++ linkspec ++
sort (map toCss (Map.toList (tokenStyles f)))
where colorspec = [
"div.sourceCode\n { "
++ case (defaultColor f, backgroundColor f) of
(Nothing, Nothing) -> ""
(Just c, Nothing) -> "color: " ++ fromColor c ++ ";"
(Nothing, Just c) -> "background-color: " ++ fromColor c ++ ";"
(Just c1, Just c2) -> "color: " ++ fromColor c1
++ "; background-color: " ++ fromColor c2 ++ ";"
++ " }"]
numberspec = [
"pre.numberSource a.sourceLine"
, " { position: relative; left: -4em; }"
, "pre.numberSource a.sourceLine::before"
, " { content: attr(data-line-number);"
, " position: relative; left: -1em; text-align: right; vertical-align: baseline;"
, " border: none; pointer-events: all; display: inline-block;"
, " -webkit-touch-callout: none; -webkit-user-select: none;"
, " -khtml-user-select: none; -moz-user-select: none;"
, " -ms-user-select: none; user-select: none;"
, " padding: 0 4px; width: 4em;"
, maybe "" (\c -> " background-color: " ++ fromColor c ++ ";\n")
(lineNumberBackgroundColor f) ++
maybe "" (\c -> " color: " ++ fromColor c ++ ";\n")
(lineNumberColor f) ++
" }"
, "pre.numberSource { margin-left: 3em; " ++
maybe "" (\c -> "border-left: 1px solid " ++ fromColor c ++ "; ") (lineNumberColor f) ++
" padding-left: 4px; }"
]
divspec = [
"a.sourceLine { display: inline-block; line-height: 1.25; }"
, "a.sourceLine { pointer-events: none; color: inherit; text-decoration: inherit; }"
, "a.sourceLine:empty { height: 1.2em; }"
, ".sourceCode { overflow: visible; }"
, "code.sourceCode { white-space: pre; position: relative; }"
, "div.sourceCode { margin: 1em 0; }"
, "pre.sourceCode { margin: 0; }"
, "@media screen {"
, "div.sourceCode { overflow: auto; }"
, "}"
, "@media print {"
, "code.sourceCode { white-space: pre-wrap; }"
, "a.sourceLine { text-indent: -1em; padding-left: 1em; }"
, "}"
]
linkspec = [ "@media screen {"
, "a.sourceLine::before { text-decoration: underline; }"
, "}"
]
toCss :: (TokenType, TokenStyle) -> String
toCss (t,tf) = "code span." ++ short t ++ " { "
++ colorspec ++ backgroundspec ++ weightspec ++ stylespec
++ decorationspec ++ "} /* " ++ showTokenType t ++ " */"
where colorspec = maybe "" (\col -> "color: " ++ fromColor col ++ "; ") $ tokenColor tf
backgroundspec = maybe "" (\col -> "background-color: " ++ fromColor col ++ "; ") $ tokenBackground tf
weightspec = if tokenBold tf then "font-weight: bold; " else ""
stylespec = if tokenItalic tf then "font-style: italic; " else ""
decorationspec = if tokenUnderline tf then "text-decoration: underline; " else ""
showTokenType t' = case reverse (show t') of
'k':'o':'T':xs -> reverse xs
_ -> ""