docvim-0.1.0.0: lib/Docvim/Printer/Vim.hs
module Docvim.Printer.Vim (vimHelp) where
import Control.Arrow ((***))
import Control.Monad (join)
import Control.Monad.Reader
import Control.Monad.State
import Data.Char (isSpace, toLower, toUpper)
import Data.List (intercalate, isSuffixOf, span, sort)
import Data.List.Split (splitOn)
import Data.Maybe (fromJust, fromMaybe)
import Data.Tuple (swap)
import Docvim.AST
import Docvim.Parse (rstrip)
import Docvim.Visitor.Plugin (getPluginName)
import Docvim.Visitor.Symbol (getSymbols)
-- TODO: add indentation here (using local, or just stick it in Context)
-- Instead of building up a [Char], we build up a list of operations, which
-- allows us a mechanism of implementing rollback and therefore hard-wrapping
-- (eg. append whitespace " ", then on next node, realize that we will exceed
-- line length limit, so rollback the " " and instead append "\n" etc).
data Operation = Append String
| Delete Int -- unconditional delete count of Char
| Slurp String -- delete string if present
data Metadata = Metadata { symbols :: [String]
, pluginName :: Maybe String
}
data Context = Context { lineBreak :: String
, partialLine :: String
}
type Env = ReaderT Metadata (State Context) [Operation]
textwidth :: Int
textwidth = 78
vimHelp :: Node -> String
vimHelp n = suppressTrailingWhitespace output ++ "\n"
where metadata = Metadata (getSymbols n) (getPluginName n)
context = Context defaultLineBreak ""
operations = evalState (runReaderT (node n) metadata) context
output = foldl reduce "" operations
reduce acc (Append atom) = acc ++ atom
reduce acc (Delete count) = take (length acc - count) acc
reduce acc (Slurp atom) = if atom `isSuffixOf` acc
then take (length acc - length atom) acc
else acc
suppressTrailingWhitespace str = rstrip $ intercalate "\n" (map rstrip (splitOn "\n" str))
-- | Helper function that appends and updates `partialLine` context,
-- hard-wrapping if necessary to remain under `textwidth`.
append :: String -> Env
append string = append' string textwidth
-- | Helper function that appends and updates `partialLine` context
-- uncontitionally (no hard-wrapping).
appendNoWrap :: String -> Env
appendNoWrap string = append' string (maxBound :: Int)
append' :: String -> Int -> Env
append' string width = do
context <- get
-- TODO obviously tidy this up
let (ops, line) = if renderedWidth (partialLine context) + renderedWidth leading >= width
then ( [ Delete (length $ snd $ hardwrap $ partialLine context)
, Slurp " "
, Append (lineBreak context)
, Append (snd $ hardwrap $ partialLine context)
, Append string
]
, lineBreak context ++ snd (hardwrap $ partialLine context) ++ string
)
else ([Append string], partialLine context ++ string)
put (Context (lineBreak context) (end line))
return ops
where
leading = takeWhile (/= '\n') string
trailing str = length $ takeWhile isSpace (reverse str)
end l = reverse $ takeWhile (/= '\n') (reverse l)
-- http://stackoverflow.com/a/9723976/2103996
mapTuple = join (***)
-- Given a string, hardwraps it into two parts by splitting it at the rightmost
-- whitespace.
hardwrap :: String -> (String, String)
hardwrap str = swap $ mapTuple reverse split
where
split = break isSpace (reverse str)
-- Helper function that deletes `count` elements from the end of the
--`partialLine` context.
delete :: Int -> Env
delete count = do
context <- get
put (Context (lineBreak context) (partial context))
return [Delete count]
where
partial context = take (length (partialLine context) - count) (partialLine context)
-- Helper function to conditionally remove a string if it appears at the end of
-- the output.
slurp :: String -> Env
slurp str = do
context <- get
put (Context (lineBreak context) (partial context))
return [Slurp str]
where
-- eg. (partialLine context) | str | result
-- ----------------------|------------|-------
-- "" | "\n" | ""
-- "foo" | "\n" | "foo"
-- "foo" | "bar" | "foo"
-- "abc" | "bc" | "a"
-- "abc" | "foo\nabc" | ""
--
-- Note: That last one is unsafe, because we can't guarantee that "foo" is
-- there. Caveat emptor!
partial context = if str `isSuffixOf` partialLine context
then take (length (partialLine context) - length str) (partialLine context)
else partialLine context
defaultLineBreak :: String
defaultLineBreak = "\n"
nodes :: [Node] -> Env
nodes ns = concat <$> mapM node ns
node :: Node -> Env
node n = case n of
Blockquote b -> blockquote b >>= nl >>= nl
BreakTag -> breaktag
Code c -> append $ "`" ++ c ++ "`"
CommandAnnotation {} -> command n
CommandsAnnotation -> heading "commands"
DocBlock d -> nodes d
Fenced f -> fenced f
FunctionsAnnotation -> heading "functions"
FunctionDeclaration {} -> nodes $ functionBody n
HeadingAnnotation h -> heading h
Link l -> append $ link l
LinkTargets l -> linkTargets l True
List ls -> nodes ls >>= nl
ListItem l -> listitem l
MappingAnnotation m -> mapping m
MappingsAnnotation -> heading "mappings"
OptionAnnotation {} -> option n
OptionsAnnotation -> heading "options"
Paragraph p -> nodes p >>= nl >>= nl
Plaintext p -> plaintext p
PluginAnnotation name desc -> plugin name desc
Project p -> nodes p
Separator -> append $ "---" ++ "\n\n"
SubheadingAnnotation s -> append $ s ++ " ~\n\n"
TOC t -> toc t
Unit u -> nodes u
Whitespace -> whitespace
_ -> append ""
-- TODO: add {name}.txt to the symbol table?
plugin :: String -> String -> Env
plugin name desc = append $
"*" ++ name ++ ".txt*" ++
" " ++ desc ++ " " ++
"*" ++ name ++ "*" ++ "\n\n"
-- | Append a newline.
nl :: [Operation] -> Env
nl os = liftM2 (++) (return os) (append "\n")
breaktag :: Env
breaktag = do
state <- get
append $ lineBreak state
listitem :: [Node] -> Env
listitem l = do
context <- get
-- TODO: consider using lenses to modify records
put (Context customLineBreak (partialLine context))
item <- liftM2 (++) (append "- ") (nodes l) >>= nl
put (Context defaultLineBreak (partialLine context))
return item
where
customLineBreak = "\n "
toc :: [String] -> Env
toc t = do
metadata <- ask
toc' $ fromJust $ pluginName metadata
where
toc' p = do
h <- heading "contents"
entries <- append $ intercalate "\n" format ++ "\n\n"
return (h ++ entries)
where
format = map pad numbered
longest = maximum (map (length . snd) numbered )
numbered = map prefix number
number = zip3 [1..] t (map (\x -> normalize $ p ++ "-" ++ x) t)
prefix (num, desc, l) = (show num ++ ". " ++ desc ++ " ", l)
pad (lhs, rhs) = lhs ++ replicate (longest - length lhs) ' ' ++ link rhs
-- TODO: consider doing this for markdown format too
command :: Node -> Env
command (CommandAnnotation name params) = do
lhs <- append $ concat [":", name, " ", fromMaybe "" params]
ws <- append " "
target <- linkTargets [":" ++ name] False
trailing <- append "\n"
return $ concat [lhs, ws, target, trailing]
-- TODO indent what follows until next annotation...
-- will require us to hoist it up inside CommandAnnotation
-- (and do similar for other sections)
-- once that is done, drop the extra newline above
mapping :: String -> Env
mapping name = linkTargets [name] True
option :: Node -> Env
option (OptionAnnotation n t d) = do
targets <- linkTargets [n] True
opt <- appendNoWrap $ link n
ws <- appendNoWrap " "
context <- get
meta <- appendNoWrap $ aligned context
return $ concat [targets, opt, ws, meta]
where
aligned context = rightAlign context rhs
rhs = t ++ " (default: " ++ fromMaybe "none" d ++ ")\n\n"
whitespace :: Env
whitespace = append " "
blockquote :: [Node] -> Env
blockquote ps = do
context <- get
put (Context customLineBreak (partialLine context))
ps' <- mapM paragraph ps
put (Context defaultLineBreak (partialLine context))
liftM2 (++) (append " ") (liftM2 intercalate customParagraphBreak (return ps'))
where
-- Strip off trailing newlines from each paragraph.
paragraph p = fmap trim (node p)
trim contents = take (length contents - 2) contents
customLineBreak = "\n "
customParagraphBreak = append "\n\n "
plaintext :: String -> Env
plaintext = append
fenced :: [String] -> Env
fenced f = do
cut <- slurp "\n"
prefix <- append ">\n"
body <- if null f
then append ""
else appendNoWrap $ " " ++ intercalate "\n " f ++ "\n"
suffix <- append "<\n"
return $ concat [cut, prefix, body, suffix]
heading :: String -> Env
heading h = do
metadata <- ask
heading' <- appendNoWrap $ map toUpper h ++ " "
target <- maybe (append "\n") (\x -> linkTargets [target x] False) (pluginName metadata)
trailing <- append "\n"
return $ concat [heading', target, trailing]
where
target x = normalize $ x ++ "-" ++ h
normalize :: String -> String
normalize = map (toLower . sanitize)
sanitize :: Char -> Char
sanitize x = if isSpace x then '-' else x
link :: String -> String
link l = "|" ++ l ++ "|"
-- TODO: be prepared to wrap these if there are a lot of them
-- TODO: fix code smell of passing in `wrap` bool here
linkTargets :: [String] -> Bool -> Env
linkTargets ls wrap = do
context <- get
if wrap
then append $ aligned context
else appendNoWrap $ aligned context
where
aligned context = rightAlign context (targets ++ "\n")
targets = unwords (map linkify $ sort ls)
linkify l = "*" ++ l ++ "*"
rightAlign :: Context -> String -> String
rightAlign context string = align (partialLine context)
where
align used = replicate (count used string) ' ' ++ string
count used xs = maximum [textwidth - renderedWidth xs - renderedWidth used, 0]
-- Crude approximation for calculating rendered width, that does so by not
-- counting the relatively rare |, *, ` and "\n" -- all of which usually get
-- concealed in the rendered output.
renderedWidth :: String -> Int
renderedWidth = foldr reduce 0
where reduce char acc = if char `elem` "\n|*`"
then acc
else acc + 1