git.iko.soy/typeable/pandoc
on master help sign in
Added Text.Pandoc.Readers.Groff.

This is an internal module that exports a tokenizer for groff formats. Closes #4998.

commit6c71100fcf0abc609dda323a76c78b0838234044 authorJohn MacFarlane <jgm@berkeley.edu> dateWed 24 Oct 2018, 17:38 (8 years ago) parent1233817
3 files changed+531−465
pandoc.cabal+1 src/Text/Pandoc/Readers/Groff.hs+524 src/Text/Pandoc/Readers/Man.hs+6−465
pandoc.cabal+1
@@ -534,6 +534,7 @@ library
534534 Text.Pandoc.Readers.Org.ParserState,
535535 Text.Pandoc.Readers.Org.Parsing,
536536 Text.Pandoc.Readers.Org.Shared,
537+ Text.Pandoc.Readers.Groff,
537538 Text.Pandoc.Writers.Groff,
538539 Text.Pandoc.Writers.Powerpoint.Presentation,
539540 Text.Pandoc.Writers.Powerpoint.Output,
src/Text/Pandoc/Readers/Groff.hsnew file+524
@@ -0,0 +1,524 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+{-
4+ Copyright (C) 2018 Yan Pashkovsky <yanp.bugz@gmail.com>
5+ and John MacFarlane
6+
7+This program is free software; you can redistribute it and/or modify
8+it under the terms of the GNU General Public License as published by
9+the Free Software Foundation; either version 2 of the License, or
10+(at your option) any later version.
11+
12+This program is distributed in the hope that it will be useful,
13+but WITHOUT ANY WARRANTY; without even the implied warranty of
14+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+GNU General Public License for more details.
16+
17+You should have received a copy of the GNU General Public License
18+along with this program; if not, write to the Free Software
19+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20+
21+-}
22+
23+{- |
24+ Module : Text.Pandoc.Readers.Groff
25+ Copyright : Copyright (C) 2018 Yan Pashkovsky and John MacFarlane
26+ License : GNU GPL, version 2 or above
27+
28+ Maintainer : Yan Pashkovsky <yanp.bugz@gmail.com>
29+ Stability : WIP
30+ Portability : portable
31+
32+Tokenizer for groff formats (man, ms).
33+-}
34+module Text.Pandoc.Readers.Groff
35+ ( FontKind(..)
36+ , Font
37+ , MacroKind
38+ , LinePart(..)
39+ , Arg
40+ , ManToken(..)
41+ , ManTokens(..)
42+ , singleTok
43+ , RoffState(..)
44+ , ManLexer
45+ , manToken
46+ , linePartsToString
47+ )
48+where
49+
50+import Prelude
51+import Control.Monad (void, mzero, guard, when)
52+import Text.Pandoc.Class
53+ (getResourcePath, readFileFromDirs, PandocMonad(..), report)
54+import Data.Char (isHexDigit, chr, ord, isAscii, isAlphaNum, isSpace)
55+import Data.Default (Default)
56+import qualified Data.Map as M
57+import Data.Set (Set)
58+import qualified Data.Set as S
59+import Data.List (intercalate, isSuffixOf)
60+import qualified Data.Text as T
61+import Text.Pandoc.Builder as B
62+import Text.Pandoc.Logging (LogMessage(..))
63+import Text.Pandoc.Options
64+import Text.Pandoc.Parsing
65+import Text.Pandoc.Shared (safeRead)
66+import Text.Parsec hiding (tokenPrim)
67+import qualified Text.Parsec as Parsec
68+import Text.Pandoc.GroffChar (characterCodes, combiningAccents)
69+import qualified Data.Sequence as Seq
70+import qualified Data.Foldable as Foldable
71+import qualified Data.Text.Normalize as Normalize
72+
73+-- import Debug.Trace (traceShowId)
74+
75+--
76+-- Data Types
77+--
78+data FontKind = Bold | Italic | Monospace | Regular deriving (Show, Eq, Ord)
79+
80+type MacroKind = String
81+
82+type Font = Set FontKind
83+
84+data LinePart = RoffStr (String, Font)
85+ | MacroArg Int
86+ deriving Show
87+
88+type Arg = [LinePart]
89+
90+-- TODO parse tables (see man tbl)
91+data ManToken = MLine [LinePart]
92+ | MEmptyLine
93+ | MMacro MacroKind [Arg] SourcePos
94+ | MTable [Alignment] ManTokens [ManTokens] [[ManTokens]]
95+ deriving Show
96+
97+newtype ManTokens = ManTokens { unManTokens :: Seq.Seq ManToken }
98+ deriving (Show, Semigroup, Monoid)
99+
100+singleTok :: ManToken -> ManTokens
101+singleTok t = ManTokens (Seq.singleton t)
102+
103+data RoffState = RoffState { fontKind :: Font
104+ , customMacros :: M.Map String ManTokens
105+ } deriving Show
106+
107+instance Default RoffState where
108+ def = RoffState { customMacros = M.fromList
109+ $ map (\(n, s) ->
110+ (n, singleTok
111+ (MLine [RoffStr (s, mempty)])))
112+ [ ("Tm", "\x2122")
113+ , ("lq", "\x201C")
114+ , ("rq", "\x201D")
115+ , ("R", "\x00AE") ]
116+ , fontKind = S.singleton Regular }
117+
118+type ManLexer m = ParserT [Char] RoffState m
119+
120+--
121+-- Lexer: String -> ManToken
122+--
123+
124+eofline :: Stream s m Char => ParsecT s u m ()
125+eofline = void newline <|> eof
126+
127+spacetab :: Stream s m Char => ParsecT s u m Char
128+spacetab = char ' ' <|> char '\t'
129+
130+characterCodeMap :: M.Map String Char
131+characterCodeMap =
132+ M.fromList $ map (\(x,y) -> (y,x)) characterCodes
133+
134+combiningAccentsMap :: M.Map String Char
135+combiningAccentsMap =
136+ M.fromList $ map (\(x,y) -> (y,x)) combiningAccents
137+
138+escapeLexer :: PandocMonad m => ManLexer m String
139+escapeLexer = try $ do
140+ char '\\'
141+ c <- noneOf ['*','$'] -- see escStar, macroArg
142+ case c of
143+ '(' -> twoCharGlyph
144+ '[' -> bracketedGlyph
145+ 'f' -> escFont
146+ 's' -> escFontSize
147+ '"' -> mempty <$ skipMany (satisfy (/='\n')) -- line comment
148+ '#' -> mempty <$ manyTill anyChar newline
149+ '%' -> return mempty
150+ '{' -> return mempty
151+ '}' -> return mempty
152+ '&' -> return mempty
153+ '\n' -> return mempty
154+ ':' -> return mempty
155+ '0' -> return mempty
156+ 'c' -> return mempty
157+ '-' -> return "-"
158+ '_' -> return "_"
159+ ' ' -> return " "
160+ '\\' -> return "\\"
161+ 't' -> return "\t"
162+ 'e' -> return "\\"
163+ '`' -> return "`"
164+ '^' -> return " "
165+ '|' -> return " "
166+ '\'' -> return "`"
167+ '.' -> return "`"
168+ '~' -> return "\160" -- nonbreaking space
169+ _ -> escUnknown ['\\',c] "\xFFFD"
170+
171+ where
172+
173+ twoCharGlyph = do
174+ cs <- count 2 anyChar
175+ case M.lookup cs characterCodeMap of
176+ Just c -> return [c]
177+ Nothing -> escUnknown ('\\':'(':cs) "\xFFFD"
178+
179+ bracketedGlyph = unicodeGlyph <|> charGlyph
180+
181+ charGlyph = do
182+ cs <- manyTill (noneOf ['[',']','\n']) (char ']')
183+ (case words cs of
184+ [] -> mzero
185+ [s] -> case M.lookup s characterCodeMap of
186+ Nothing -> mzero
187+ Just c -> return [c]
188+ (s:ss) -> do
189+ basechar <- case M.lookup cs characterCodeMap of
190+ Nothing ->
191+ case s of
192+ [ch] | isAscii ch && isAlphaNum ch ->
193+ return ch
194+ _ -> mzero
195+ Just c -> return c
196+ let addAccents [] xs = return $ T.unpack .
197+ Normalize.normalize Normalize.NFC .
198+ T.pack $ reverse xs
199+ addAccents (a:as) xs =
200+ case M.lookup a combiningAccentsMap of
201+ Just x -> addAccents as (x:xs)
202+ Nothing -> mzero
203+ addAccents ss [basechar])
204+ <|> escUnknown ("\\[" ++ cs ++ "]") "\xFFFD"
205+
206+ unicodeGlyph = try $ ucharCode `sepBy1` (char '_') <* char ']'
207+
208+ ucharCode = try $ do
209+ char 'u'
210+ cs <- many1 (satisfy isHexDigit)
211+ let lcs = length cs
212+ guard $ lcs >= 4 && lcs <= 6
213+ case chr <$> safeRead ('0':'x':cs) of
214+ Nothing -> mzero
215+ Just c -> return c
216+
217+ -- \s-1 \s0 -- we ignore these
218+ escFontSize :: PandocMonad m => ManLexer m String
219+ escFontSize = do
220+ pos <- getPosition
221+ pm <- option "" $ count 1 (oneOf "+-")
222+ ds <- many1 digit
223+ report $ SkippedContent ("\\s" ++ pm ++ ds) pos
224+ return mempty
225+
226+ escFont :: PandocMonad m => ManLexer m String
227+ escFont = do
228+ font <- choice
229+ [ S.singleton <$> letterFontKind
230+ , char '(' >> anyChar >> anyChar >> return (S.singleton Regular)
231+ , char 'S' >> return (S.singleton Regular)
232+ , try lettersFont
233+ , digit >> return (S.singleton Regular)
234+ ]
235+ modifyState (\r -> r {fontKind = font})
236+ return mempty
237+
238+ lettersFont :: PandocMonad m => ManLexer m Font
239+ lettersFont = do
240+ char '['
241+ fs <- many letterFontKind
242+ skipMany letter
243+ char ']'
244+ return $ S.fromList fs
245+
246+ letterFontKind :: PandocMonad m => ManLexer m FontKind
247+ letterFontKind = choice [
248+ oneOf ['B','b'] >> return Bold
249+ , oneOf ['I','i'] >> return Italic
250+ , oneOf ['C','c'] >> return Monospace
251+ , oneOf ['P','p','R','r'] >> return Regular
252+ ]
253+
254+ escUnknown :: PandocMonad m => String -> a -> ManLexer m a
255+ escUnknown s x = do
256+ pos <- getPosition
257+ report $ SkippedContent ("Unknown escape sequence " ++ s) pos
258+ return x
259+
260+currentFont :: PandocMonad m => ManLexer m Font
261+currentFont = fontKind <$> getState
262+
263+-- separate function from lexMacro since real man files sometimes do not follow the rules
264+lexComment :: PandocMonad m => ManLexer m ManTokens
265+lexComment = do
266+ try $ string ".\\\""
267+ many Parsec.space
268+ skipMany $ noneOf "\n"
269+ char '\n'
270+ return mempty
271+
272+lexMacro :: PandocMonad m => ManLexer m ManTokens
273+lexMacro = do
274+ pos <- getPosition
275+ char '.' <|> char '\''
276+ many spacetab
277+ macroName <- many (satisfy (not . isSpace))
278+ case macroName of
279+ "nop" -> return mempty
280+ "ie" -> lexConditional
281+ "if" -> lexConditional
282+ "el" -> skipConditional
283+ "TS" -> lexTable
284+
285+ _ -> do
286+ args <- lexArgs
287+ case macroName of
288+ "" -> return mempty
289+ "\\\"" -> return mempty
290+ "\\#" -> return mempty
291+ "de" -> lexMacroDef args
292+ "de1" -> lexMacroDef args
293+ "ds" -> lexStringDef args
294+ "ds1" -> lexStringDef args
295+ "sp" -> return $ singleTok MEmptyLine
296+ "so" -> lexIncludeFile args
297+ _ -> resolveMacro macroName args pos
298+
299+-- | TODO placeholder
300+lexTable :: PandocMonad m => ManLexer m ManTokens
301+lexTable = do
302+ pos <- getPosition
303+ manyTill anyLine (try (string ".TE" >> many spacetab >> eofline))
304+ report $ SkippedContent "table" pos
305+ return mempty
306+
307+-- We don't fully handle the conditional. But we do
308+-- include everything under '.ie n', which occurs commonly
309+-- in man pages. We always skip the '.el' part.
310+lexConditional :: PandocMonad m => ManLexer m ManTokens
311+lexConditional = do
312+ skipMany spacetab
313+ parseNCond <|> skipConditional
314+
315+-- n means nroff mode
316+parseNCond :: PandocMonad m => ManLexer m ManTokens
317+parseNCond = do
318+ char '\n'
319+ many1 spacetab
320+ lexGroup <|> manToken
321+
322+lexGroup :: PandocMonad m => ManLexer m ManTokens
323+lexGroup = do
324+ groupstart
325+ mconcat <$> manyTill manToken groupend
326+ where
327+ groupstart = try $ string "\\{\\" >> newline
328+ groupend = try $ string "\\}" >> eofline
329+
330+skipConditional :: PandocMonad m => ManLexer m ManTokens
331+skipConditional = do
332+ rest <- anyLine
333+ when ("\\{\\" `isSuffixOf` rest) $
334+ void $ manyTill anyChar (try (string "\\}"))
335+ return mempty
336+
337+lexIncludeFile :: PandocMonad m => [Arg] -> ManLexer m ManTokens
338+lexIncludeFile args = do
339+ pos <- getPosition
340+ case args of
341+ (f:_) -> do
342+ let fp = linePartsToString f
343+ dirs <- getResourcePath
344+ result <- readFileFromDirs dirs fp
345+ case result of
346+ Nothing -> report $ CouldNotLoadIncludeFile fp pos
347+ Just s -> getInput >>= setInput . (s ++)
348+ return mempty
349+ [] -> return mempty
350+
351+resolveMacro :: PandocMonad m
352+ => String -> [Arg] -> SourcePos -> ManLexer m ManTokens
353+resolveMacro macroName args pos = do
354+ macros <- customMacros <$> getState
355+ case M.lookup macroName macros of
356+ Nothing -> return $ singleTok $ MMacro macroName args pos
357+ Just ts -> do
358+ let fillLP (RoffStr (x,y)) zs = RoffStr (x,y) : zs
359+ fillLP (MacroArg i) zs =
360+ case drop (i - 1) args of
361+ [] -> zs
362+ (ys:_) -> ys ++ zs
363+ let fillMacroArg (MLine lineparts) =
364+ MLine (foldr fillLP [] lineparts)
365+ fillMacroArg x = x
366+ return $ ManTokens . fmap fillMacroArg . unManTokens $ ts
367+
368+lexStringDef :: PandocMonad m => [Arg] -> ManLexer m ManTokens
369+lexStringDef args = do -- string definition
370+ case args of
371+ [] -> fail "No argument to .ds"
372+ (x:ys) -> do
373+ let ts = singleTok $ MLine (intercalate [RoffStr (" ", mempty)] ys)
374+ let stringName = linePartsToString x
375+ modifyState $ \st ->
376+ st{ customMacros = M.insert stringName ts (customMacros st) }
377+ return mempty
378+
379+lexMacroDef :: PandocMonad m => [Arg] -> ManLexer m ManTokens
380+lexMacroDef args = do -- macro definition
381+ (macroName, stopMacro) <-
382+ case args of
383+ (x : y : _) -> return (linePartsToString x, linePartsToString y)
384+ -- optional second arg
385+ (x:_) -> return (linePartsToString x, ".")
386+ [] -> fail "No argument to .de"
387+ let stop = try $ do
388+ char '.' <|> char '\''
389+ many spacetab
390+ string stopMacro
391+ _ <- lexArgs
392+ return ()
393+ ts <- mconcat <$> manyTill manToken stop
394+ modifyState $ \st ->
395+ st{ customMacros = M.insert macroName ts (customMacros st) }
396+ return mempty
397+
398+lexArgs :: PandocMonad m => ManLexer m [Arg]
399+lexArgs = do
400+ args <- many $ try oneArg
401+ skipMany spacetab
402+ eofline
403+ return args
404+
405+ where
406+
407+ oneArg :: PandocMonad m => ManLexer m [LinePart]
408+ oneArg = do
409+ skipMany $ try $ string "\\\n" -- continuation line
410+ try quotedArg <|> plainArg
411+ -- try, because there are some erroneous files, e.g. linux/bpf.2
412+
413+ plainArg :: PandocMonad m => ManLexer m [LinePart]
414+ plainArg = do
415+ skipMany spacetab
416+ mconcat <$> many1
417+ (macroArg <|> esc <|> regularText <|> unescapedQuote <|> escStar)
418+ where
419+ unescapedQuote = do
420+ char '"'
421+ fonts <- currentFont
422+ return [RoffStr ("\"", fonts)]
423+
424+
425+ quotedArg :: PandocMonad m => ManLexer m [LinePart]
426+ quotedArg = do
427+ skipMany spacetab
428+ char '"'
429+ xs <- mconcat <$>
430+ many (macroArg <|> esc <|> escStar <|> regularText
431+ <|> spaceTabChar <|> escapedQuote)
432+ char '"'
433+ return xs
434+ where
435+ escapedQuote = try $ do
436+ char '"'
437+ char '"'
438+ fonts <- currentFont
439+ return [RoffStr ("\"", fonts)]
440+
441+escStar :: PandocMonad m => ManLexer m [LinePart]
442+escStar = try $ do
443+ pos <- getPosition
444+ char '\\'
445+ char '*'
446+ c <- anyChar
447+ case c of
448+ '(' -> do
449+ cs <- count 2 anyChar
450+ resolveString cs pos
451+ '[' -> do
452+ cs <- many (noneOf "\t\n\r ]")
453+ char ']'
454+ resolveString cs pos
455+ 'S' -> return mempty -- switch back to default font size
456+ _ -> resolveString [c] pos
457+
458+ where
459+
460+ -- strings and macros share namespace
461+ resolveString stringname pos = do
462+ ManTokens ts <- resolveMacro stringname [] pos
463+ case Foldable.toList ts of
464+ [MLine xs] -> return xs
465+ _ -> do
466+ report $ SkippedContent ("unknown string " ++ stringname) pos
467+ return mempty
468+
469+lexLine :: PandocMonad m => ManLexer m ManTokens
470+lexLine = do
471+ lnparts <- mconcat <$> many1 linePart
472+ eofline
473+ go lnparts
474+ where -- return empty line if we only have empty strings;
475+ -- this can happen if the line just contains \f[C], for example.
476+ go [] = return mempty
477+ go (RoffStr ("",_):xs) = go xs
478+ go xs = return $ singleTok $ MLine xs
479+
480+linePart :: PandocMonad m => ManLexer m [LinePart]
481+linePart = macroArg <|> esc <|> escStar <|>
482+ regularText <|> quoteChar <|> spaceTabChar
483+
484+macroArg :: PandocMonad m => ManLexer m [LinePart]
485+macroArg = try $ do
486+ string "\\\\$"
487+ x <- digit
488+ return [MacroArg $ ord x - ord '0']
489+
490+esc :: PandocMonad m => ManLexer m [LinePart]
491+esc = do
492+ s <- escapeLexer
493+ font <- currentFont
494+ return [RoffStr (s, font)]
495+
496+regularText :: PandocMonad m => ManLexer m [LinePart]
497+regularText = do
498+ s <- many1 $ noneOf "\n\r\t \\\""
499+ font <- currentFont
500+ return [RoffStr (s, font)]
501+
502+quoteChar :: PandocMonad m => ManLexer m [LinePart]
503+quoteChar = do
504+ char '"'
505+ font <- currentFont
506+ return [RoffStr ("\"", font)]
507+
508+spaceTabChar :: PandocMonad m => ManLexer m [LinePart]
509+spaceTabChar = do
510+ c <- spacetab
511+ font <- currentFont
512+ return [RoffStr ([c], font)]
513+
514+lexEmptyLine :: PandocMonad m => ManLexer m ManTokens
515+lexEmptyLine = char '\n' >> return (singleTok MEmptyLine)
516+
517+manToken :: PandocMonad m => ManLexer m ManTokens
518+manToken = lexComment <|> lexMacro <|> lexLine <|> lexEmptyLine
519+
520+linePartsToString :: [LinePart] -> String
521+linePartsToString = mconcat . map go
522+ where
523+ go (RoffStr (s, _)) = s
524+ go _ = mempty
src/Text/Pandoc/Readers/Man.hs+6−465
@@ -18,8 +18,6 @@ You should have received a copy of the GNU General Public License
1818along with this program; if not, write to the Free Software
1919Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2020
21−
22−
2321-}
2422
2523{- |
@@ -36,76 +34,25 @@ Conversion of man to 'Pandoc' document.
3634module Text.Pandoc.Readers.Man (readMan) where
3735
3836import Prelude
39−import Control.Monad (liftM, void, mzero, guard, when)
40−import Control.Monad.Except (throwError)
41−import Text.Pandoc.Class
42− (getResourcePath, readFileFromDirs, PandocMonad(..), report)
43−import Data.Char (isHexDigit, chr, ord, isAscii, isAlphaNum, isSpace)
4437import Data.Default (Default)
38+import Control.Monad (liftM, mzero, guard)
39+import Control.Monad.Except (throwError)
40+import Text.Pandoc.Class (PandocMonad(..), report)
4541import Data.Maybe (catMaybes)
46−import qualified Data.Map as M
47−import Data.Set (Set)
4842import qualified Data.Set as S
49−import Data.List (intersperse, intercalate, isSuffixOf)
43+import Data.List (intersperse, intercalate)
5044import qualified Data.Text as T
5145import Text.Pandoc.Builder as B
5246import Text.Pandoc.Error (PandocError (PandocParsecError))
5347import Text.Pandoc.Logging (LogMessage(..))
5448import Text.Pandoc.Options
5549import Text.Pandoc.Parsing
56−import Text.Pandoc.Shared (crFilter, safeRead)
50+import Text.Pandoc.Shared (crFilter)
51+import Text.Pandoc.Readers.Groff -- TODO explicit imports
5752import Text.Parsec hiding (tokenPrim)
5853import qualified Text.Parsec as Parsec
5954import Text.Parsec.Pos (updatePosString)
60−import Text.Pandoc.GroffChar (characterCodes, combiningAccents)
61−import qualified Data.Sequence as Seq
6255import qualified Data.Foldable as Foldable
63−import qualified Data.Text.Normalize as Normalize
64−
65−-- import Debug.Trace (traceShowId)
66−
67−--
68−-- Data Types
69−--
70−data FontKind = Bold | Italic | Monospace | Regular deriving (Show, Eq, Ord)
71−
72−type MacroKind = String
73−
74−type Font = Set FontKind
75−
76−data LinePart = RoffStr (String, Font)
77− | MacroArg Int
78− deriving Show
79−
80−type Arg = [LinePart]
81−
82−-- TODO parse tables (see man tbl)
83−data ManToken = MLine [LinePart]
84− | MEmptyLine
85− | MMacro MacroKind [Arg] SourcePos
86− | MTable [Alignment] ManTokens [ManTokens] [[ManTokens]]
87− deriving Show
88−
89−newtype ManTokens = ManTokens { unManTokens :: Seq.Seq ManToken }
90− deriving (Show, Semigroup, Monoid)
91−
92−singleTok :: ManToken -> ManTokens
93−singleTok t = ManTokens (Seq.singleton t)
94−
95−data RoffState = RoffState { fontKind :: Font
96− , customMacros :: M.Map String ManTokens
97− } deriving Show
98−
99−instance Default RoffState where
100− def = RoffState { customMacros = M.fromList
101− $ map (\(n, s) ->
102− (n, singleTok
103− (MLine [RoffStr (s, mempty)])))
104− [ ("Tm", "\x2122")
105− , ("lq", "\x201C")
106− , ("rq", "\x201D")
107− , ("R", "\x00AE") ]
108− , fontKind = S.singleton Regular }
10956
11057data ManState = ManState { readerOptions :: ReaderOptions
11158 , metadata :: Meta
@@ -115,7 +62,6 @@ instance Default ManState where
11562 def = ManState { readerOptions = def
11663 , metadata = nullMeta }
11764
118−type ManLexer m = ParserT [Char] RoffState m
11965type ManParser m = ParserT [ManToken] ManState m
12066
12167
@@ -147,405 +93,6 @@ readMan opts txt = do
14793 mapLeft f (Left x) = Left $ f x
14894 mapLeft _ (Right r) = Right r
14995
150−--
151−-- Lexer: String -> ManToken
152−--
153−
154−eofline :: Stream s m Char => ParsecT s u m ()
155−eofline = void newline <|> eof
156−
157−spacetab :: Stream s m Char => ParsecT s u m Char
158−spacetab = char ' ' <|> char '\t'
159−
160−characterCodeMap :: M.Map String Char
161−characterCodeMap =
162− M.fromList $ map (\(x,y) -> (y,x)) characterCodes
163−
164−combiningAccentsMap :: M.Map String Char
165−combiningAccentsMap =
166− M.fromList $ map (\(x,y) -> (y,x)) combiningAccents
167−
168−escapeLexer :: PandocMonad m => ManLexer m String
169−escapeLexer = try $ do
170− char '\\'
171− c <- noneOf ['*','$'] -- see escStar, macroArg
172− case c of
173− '(' -> twoCharGlyph
174− '[' -> bracketedGlyph
175− 'f' -> escFont
176− 's' -> escFontSize
177− '"' -> mempty <$ skipMany (satisfy (/='\n')) -- line comment
178− '#' -> mempty <$ manyTill anyChar newline
179− '%' -> return mempty
180− '{' -> return mempty
181− '}' -> return mempty
182− '&' -> return mempty
183− '\n' -> return mempty
184− ':' -> return mempty
185− '0' -> return mempty
186− 'c' -> return mempty
187− '-' -> return "-"
188− '_' -> return "_"
189− ' ' -> return " "
190− '\\' -> return "\\"
191− 't' -> return "\t"
192− 'e' -> return "\\"
193− '`' -> return "`"
194− '^' -> return " "
195− '|' -> return " "
196− '\'' -> return "`"
197− '.' -> return "`"
198− '~' -> return "\160" -- nonbreaking space
199− _ -> escUnknown ['\\',c] "\xFFFD"
200−
201− where
202−
203− twoCharGlyph = do
204− cs <- count 2 anyChar
205− case M.lookup cs characterCodeMap of
206− Just c -> return [c]
207− Nothing -> escUnknown ('\\':'(':cs) "\xFFFD"
208−
209− bracketedGlyph = unicodeGlyph <|> charGlyph
210−
211− charGlyph = do
212− cs <- manyTill (noneOf ['[',']','\n']) (char ']')
213− (case words cs of
214− [] -> mzero
215− [s] -> case M.lookup s characterCodeMap of
216− Nothing -> mzero
217− Just c -> return [c]
218− (s:ss) -> do
219− basechar <- case M.lookup cs characterCodeMap of
220− Nothing ->
221− case s of
222− [ch] | isAscii ch && isAlphaNum ch ->
223− return ch
224− _ -> mzero
225− Just c -> return c
226− let addAccents [] xs = return $ T.unpack .
227− Normalize.normalize Normalize.NFC .
228− T.pack $ reverse xs
229− addAccents (a:as) xs =
230− case M.lookup a combiningAccentsMap of
231− Just x -> addAccents as (x:xs)
232− Nothing -> mzero
233− addAccents ss [basechar])
234− <|> escUnknown ("\\[" ++ cs ++ "]") "\xFFFD"
235−
236− unicodeGlyph = try $ ucharCode `sepBy1` (char '_') <* char ']'
237−
238− ucharCode = try $ do
239− char 'u'
240− cs <- many1 (satisfy isHexDigit)
241− let lcs = length cs
242− guard $ lcs >= 4 && lcs <= 6
243− case chr <$> safeRead ('0':'x':cs) of
244− Nothing -> mzero
245− Just c -> return c
246−
247− -- \s-1 \s0 -- we ignore these
248− escFontSize :: PandocMonad m => ManLexer m String
249− escFontSize = do
250− pos <- getPosition
251− pm <- option "" $ count 1 (oneOf "+-")
252− ds <- many1 digit
253− report $ SkippedContent ("\\s" ++ pm ++ ds) pos
254− return mempty
255−
256− escFont :: PandocMonad m => ManLexer m String
257− escFont = do
258− font <- choice
259− [ S.singleton <$> letterFontKind
260− , char '(' >> anyChar >> anyChar >> return (S.singleton Regular)
261− , char 'S' >> return (S.singleton Regular)
262− , try lettersFont
263− , digit >> return (S.singleton Regular)
264− ]
265− modifyState (\r -> r {fontKind = font})
266− return mempty
267−
268− lettersFont :: PandocMonad m => ManLexer m Font
269− lettersFont = do
270− char '['
271− fs <- many letterFontKind
272− skipMany letter
273− char ']'
274− return $ S.fromList fs
275−
276− letterFontKind :: PandocMonad m => ManLexer m FontKind
277− letterFontKind = choice [
278− oneOf ['B','b'] >> return Bold
279− , oneOf ['I','i'] >> return Italic
280− , oneOf ['C','c'] >> return Monospace
281− , oneOf ['P','p','R','r'] >> return Regular
282− ]
283−
284− escUnknown :: PandocMonad m => String -> a -> ManLexer m a
285− escUnknown s x = do
286− pos <- getPosition
287− report $ SkippedContent ("Unknown escape sequence " ++ s) pos
288− return x
289−
290−currentFont :: PandocMonad m => ManLexer m Font
291−currentFont = fontKind <$> getState
292−
293−-- separate function from lexMacro since real man files sometimes do not follow the rules
294−lexComment :: PandocMonad m => ManLexer m ManTokens
295−lexComment = do
296− try $ string ".\\\""
297− many Parsec.space
298− skipMany $ noneOf "\n"
299− char '\n'
300− return mempty
301−
302−lexMacro :: PandocMonad m => ManLexer m ManTokens
303−lexMacro = do
304− pos <- getPosition
305− char '.' <|> char '\''
306− many spacetab
307− macroName <- many (satisfy (not . isSpace))
308− case macroName of
309− "nop" -> return mempty
310− "ie" -> lexConditional
311− "if" -> lexConditional
312− "el" -> skipConditional
313− "TS" -> lexTable
314−
315− _ -> do
316− args <- lexArgs
317− case macroName of
318− "" -> return mempty
319− "\\\"" -> return mempty
320− "\\#" -> return mempty
321− "de" -> lexMacroDef args
322− "de1" -> lexMacroDef args
323− "ds" -> lexStringDef args
324− "ds1" -> lexStringDef args
325− "sp" -> return $ singleTok MEmptyLine
326− "so" -> lexIncludeFile args
327− _ -> resolveMacro macroName args pos
328−
329−-- | TODO placeholder
330−lexTable :: PandocMonad m => ManLexer m ManTokens
331−lexTable = do
332− pos <- getPosition
333− manyTill anyLine (try (string ".TE" >> many spacetab >> eofline))
334− report $ SkippedContent "table" pos
335− return mempty
336−
337−-- We don't fully handle the conditional. But we do
338−-- include everything under '.ie n', which occurs commonly
339−-- in man pages. We always skip the '.el' part.
340−lexConditional :: PandocMonad m => ManLexer m ManTokens
341−lexConditional = do
342− skipMany spacetab
343− parseNCond <|> skipConditional
344−
345−-- n means nroff mode
346−parseNCond :: PandocMonad m => ManLexer m ManTokens
347−parseNCond = do
348− char '\n'
349− many1 spacetab
350− lexGroup <|> manToken
351−
352−lexGroup :: PandocMonad m => ManLexer m ManTokens
353−lexGroup = do
354− groupstart
355− mconcat <$> manyTill manToken groupend
356− where
357− groupstart = try $ string "\\{\\" >> newline
358− groupend = try $ string "\\}" >> eofline
359−
360−skipConditional :: PandocMonad m => ManLexer m ManTokens
361−skipConditional = do
362− rest <- anyLine
363− when ("\\{\\" `isSuffixOf` rest) $
364− void $ manyTill anyChar (try (string "\\}"))
365− return mempty
366−
367−lexIncludeFile :: PandocMonad m => [Arg] -> ManLexer m ManTokens
368−lexIncludeFile args = do
369− pos <- getPosition
370− case args of
371− (f:_) -> do
372− let fp = linePartsToString f
373− dirs <- getResourcePath
374− result <- readFileFromDirs dirs fp
375− case result of
376− Nothing -> report $ CouldNotLoadIncludeFile fp pos
377− Just s -> getInput >>= setInput . (s ++)
378− return mempty
379− [] -> return mempty
380−
381−resolveMacro :: PandocMonad m
382− => String -> [Arg] -> SourcePos -> ManLexer m ManTokens
383−resolveMacro macroName args pos = do
384− macros <- customMacros <$> getState
385− case M.lookup macroName macros of
386− Nothing -> return $ singleTok $ MMacro macroName args pos
387− Just ts -> do
388− let fillLP (RoffStr (x,y)) zs = RoffStr (x,y) : zs
389− fillLP (MacroArg i) zs =
390− case drop (i - 1) args of
391− [] -> zs
392− (ys:_) -> ys ++ zs
393− let fillMacroArg (MLine lineparts) =
394− MLine (foldr fillLP [] lineparts)
395− fillMacroArg x = x
396− return $ ManTokens . fmap fillMacroArg . unManTokens $ ts
397−
398−lexStringDef :: PandocMonad m => [Arg] -> ManLexer m ManTokens
399−lexStringDef args = do -- string definition
400− case args of
401− [] -> fail "No argument to .ds"
402− (x:ys) -> do
403− let ts = singleTok $ MLine (intercalate [RoffStr (" ", mempty)] ys)
404− let stringName = linePartsToString x
405− modifyState $ \st ->
406− st{ customMacros = M.insert stringName ts (customMacros st) }
407− return mempty
408−
409−lexMacroDef :: PandocMonad m => [Arg] -> ManLexer m ManTokens
410−lexMacroDef args = do -- macro definition
411− (macroName, stopMacro) <-
412− case args of
413− (x : y : _) -> return (linePartsToString x, linePartsToString y)
414− -- optional second arg
415− (x:_) -> return (linePartsToString x, ".")
416− [] -> fail "No argument to .de"
417− let stop = try $ do
418− char '.' <|> char '\''
419− many spacetab
420− string stopMacro
421− _ <- lexArgs
422− return ()
423− ts <- mconcat <$> manyTill manToken stop
424− modifyState $ \st ->
425− st{ customMacros = M.insert macroName ts (customMacros st) }
426− return mempty
427−
428−lexArgs :: PandocMonad m => ManLexer m [Arg]
429−lexArgs = do
430− args <- many $ try oneArg
431− skipMany spacetab
432− eofline
433− return args
434−
435− where
436−
437− oneArg :: PandocMonad m => ManLexer m [LinePart]
438− oneArg = do
439− skipMany $ try $ string "\\\n" -- continuation line
440− try quotedArg <|> plainArg
441− -- try, because there are some erroneous files, e.g. linux/bpf.2
442−
443− plainArg :: PandocMonad m => ManLexer m [LinePart]
444− plainArg = do
445− skipMany spacetab
446− mconcat <$> many1
447− (macroArg <|> esc <|> regularText <|> unescapedQuote <|> escStar)
448− where
449− unescapedQuote = do
450− char '"'
451− fonts <- currentFont
452− return [RoffStr ("\"", fonts)]
453−
454−
455− quotedArg :: PandocMonad m => ManLexer m [LinePart]
456− quotedArg = do
457− skipMany spacetab
458− char '"'
459− xs <- mconcat <$>
460− many (macroArg <|> esc <|> escStar <|> regularText
461− <|> spaceTabChar <|> escapedQuote)
462− char '"'
463− return xs
464− where
465− escapedQuote = try $ do
466− char '"'
467− char '"'
468− fonts <- currentFont
469− return [RoffStr ("\"", fonts)]
470−
471−escStar :: PandocMonad m => ManLexer m [LinePart]
472−escStar = try $ do
473− pos <- getPosition
474− char '\\'
475− char '*'
476− c <- anyChar
477− case c of
478− '(' -> do
479− cs <- count 2 anyChar
480− resolveString cs pos
481− '[' -> do
482− cs <- many (noneOf "\t\n\r ]")
483− char ']'
484− resolveString cs pos
485− 'S' -> return mempty -- switch back to default font size
486− _ -> resolveString [c] pos
487−
488− where
489−
490− -- strings and macros share namespace
491− resolveString stringname pos = do
492− ManTokens ts <- resolveMacro stringname [] pos
493− case Foldable.toList ts of
494− [MLine xs] -> return xs
495− _ -> do
496− report $ SkippedContent ("unknown string " ++ stringname) pos
497− return mempty
498−
499−lexLine :: PandocMonad m => ManLexer m ManTokens
500−lexLine = do
501− lnparts <- mconcat <$> many1 linePart
502− eofline
503− go lnparts
504− where -- return empty line if we only have empty strings;
505− -- this can happen if the line just contains \f[C], for example.
506− go [] = return mempty
507− go (RoffStr ("",_):xs) = go xs
508− go xs = return $ singleTok $ MLine xs
509−
510−linePart :: PandocMonad m => ManLexer m [LinePart]
511−linePart = macroArg <|> esc <|> escStar <|>
512− regularText <|> quoteChar <|> spaceTabChar
513−
514−macroArg :: PandocMonad m => ManLexer m [LinePart]
515−macroArg = try $ do
516− string "\\\\$"
517− x <- digit
518− return [MacroArg $ ord x - ord '0']
519−
520−esc :: PandocMonad m => ManLexer m [LinePart]
521−esc = do
522− s <- escapeLexer
523− font <- currentFont
524− return [RoffStr (s, font)]
525−
526−regularText :: PandocMonad m => ManLexer m [LinePart]
527−regularText = do
528− s <- many1 $ noneOf "\n\r\t \\\""
529− font <- currentFont
530− return [RoffStr (s, font)]
531−
532−quoteChar :: PandocMonad m => ManLexer m [LinePart]
533−quoteChar = do
534− char '"'
535− font <- currentFont
536− return [RoffStr ("\"", font)]
537−
538−spaceTabChar :: PandocMonad m => ManLexer m [LinePart]
539−spaceTabChar = do
540− c <- spacetab
541− font <- currentFont
542− return [RoffStr ([c], font)]
543−
544−lexEmptyLine :: PandocMonad m => ManLexer m ManTokens
545−lexEmptyLine = char '\n' >> return (singleTok MEmptyLine)
546−
547−manToken :: PandocMonad m => ManLexer m ManTokens
548−manToken = lexComment <|> lexMacro <|> lexLine <|> lexEmptyLine
54996
55097parseMan :: PandocMonad m => ManParser m Pandoc
55198parseMan = do
@@ -656,12 +203,6 @@ linePartsToInlines = go
656203 isItalic (RoffStr (_,f)) = Italic `S.member` f
657204 isItalic _ = False
658205
659−linePartsToString :: [LinePart] -> String
660−linePartsToString = mconcat . map go
661− where
662− go (RoffStr (s, _)) = s
663− go _ = mempty
664−
665206parsePara :: PandocMonad m => ManParser m Blocks
666207parsePara = para . trimInlines <$> parseInlines
667208