packages feed

BNFC-2.9.6.3: src/BNFC/Backend/Pygments.hs

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}

{- Generates a Pygments lexer from a BNF grammar.
 -
 - Resources:
 - * Pygments: http://pygments.org/
 - * Lexer development: http://pygments.org/docs/lexerdevelopment/
 - * Token types: http://pygments.org/docs/tokens/
 - -}
module BNFC.Backend.Pygments where

import Prelude hiding ((<>))

import BNFC.Abs (Reg(..))
import BNFC.Backend.Base (mkfile, Backend)
import BNFC.CF
import BNFC.Lexing
import BNFC.Options hiding (Backend)
import BNFC.Utils
import BNFC.PrettyPrint

makePygments :: SharedOptions -> CF -> Backend
makePygments opts cf = do
    let lexerfile = render (lowerCase name <> "/__init__.py")
        setupfile = "setup.py"
    mkfile lexerfile comment (lexer name cf)
    mkfile setupfile comment (setup name)
  where name = lang opts

comment :: String -> String
comment = ("# " ++)

setup :: String -> Doc
setup name = vcat
    [ "from setuptools import setup, find_packages"
    , "setup" <> parens (fsep (punctuate ","
        [ "name" <=> quotes ("pygment-"<>lowerCase name)
        , "version" <=> "0.1"
        , "packages" <=> brackets (quotes moduleName)
        , "entry_points" <=> entryPoints
        , "install_requires = ['pygments']"
        ]))
    ]
  where
    className = camelCase name <> "Lexer"
    moduleName = lowerCase name
    entryPoints =
        braces( "'pygments.lexers':"
              <> doubleQuotes (moduleName <> "=" <> moduleName <> ":" <> className))

lexer :: String -> CF -> Doc
lexer name cf = vcat
    -- Import statments
    [ "import pygments.lexer"
    , "from pygments.token import *"
    -- Declare our lexer
    , "__all__" <=> brackets (doubleQuotes className)
    -- define lexer
    , "class" <+> className <> parens "pygments.lexer.RegexLexer" <> ":"
    , indent
        [ "name" <=> quotes (text name)
        , "aliases" <=> brackets (quotes (lowerCase name))
        -- filenames = ['*.cf', '*lbnf']
        , "KEYWORDS" <=> brackets keywords
        -- We override the get_tokens_unprocessed method to filter keywords
        -- from identifiers
        , "def get_tokens_unprocessed(self, text):"
        , indent
            [ "for index, token, value in super(" <> className <> ",self).get_tokens_unprocessed(text):"
            , indent
                [ "if token is Name and value in self.KEYWORDS:"
                , indent [ "yield index, Keyword, value" ]
                , "else:"
                , indent [ "yield index, token, value" ]
                ]
            ]
        -- The token is defined using regex
        , "tokens = {"
        , indent
            [ "'root': ["
            , indent (map prLexRule (mkLexer cf) ++ ["(r'\\s+', Token.Space)"])
            , "]"
            ]
        , "}"
        ]
    ]
  where
    className = camelCase name <> "Lexer"
    keywords = fsep (punctuate "," (map (quotes . text) (reservedWords cf)))
    indent = nest 4 . vcat
    prLexRule (reg,ltype) =
        parens ("r" <> quotes (pyRegex reg) <> "," <+> pyToken ltype) <> ","
    pyToken LexComment = "Comment"
    pyToken LexSymbols = "Operator"
    pyToken (LexToken "Integer") = "Number.Integer"
    pyToken (LexToken "Double") = "Number.Float"
    pyToken (LexToken "Char") = "String.Char"
    pyToken (LexToken "String") = "String.Double"
    pyToken (LexToken _) = "Name"



-- | Convert a Reg to a python regex
-- >>> pyRegex (RSeqs "abc")
-- abc
-- >>> pyRegex (RAlt (RSeqs "::=") (RChar '.'))
-- ::=|\.
-- >>> pyRegex (RChar '=')
-- =
-- >>> pyRegex RAny
-- .
-- >>> pyRegex (RStar RAny)
-- .*
-- >>> pyRegex (RPlus (RSeqs "xxx"))
-- (xxx)+
-- >>> pyRegex (ROpt (RSeqs "abc"))
-- (abc)?
-- >>> pyRegex (RSeq (RSeqs "--") (RSeq (RStar RAny) (RChar '\n')))
-- --.*\n
-- >>> pyRegex (RStar (RSeq (RSeqs "abc") (RChar '*')))
-- (abc\*)*
-- >>> pyRegex REps
-- <BLANKLINE>
-- >>> pyRegex (RAlts "abc[].")
-- [abc\[\]\.]
-- >>> pyRegex RDigit
-- \d
-- >>> pyRegex RLetter
-- [a-zA-Z]
-- >>> pyRegex RUpper
-- [A-Z]
-- >>> pyRegex RLower
-- [a-z]
-- >>> pyRegex (RMinus RAny RDigit)
-- (.)(?<!\d)
-- >>> pyRegex (RSeq (RAlt (RChar 'a') RAny) (RAlt (RChar 'b') (RChar 'c')))
-- (a|.)(b|c)
-- >>> pyRegex (RSeq (RChar '^') (RChar '$'))
-- \^\$
pyRegex :: Reg -> Doc
pyRegex reg = case reg of
    RSeqs s       -> text (concatMap escape s)
    RAlt r1 r2    -> pyRegex r1 <> "|" <> pyRegex r2
    RChar c       -> text (escape c)
    RAny          -> char '.'
    RStar RAny    -> ".*"
    RStar re      -> parens (pyRegex re) <> char '*'
    RPlus re      -> parens (pyRegex re) <> char '+'
    ROpt re       -> parens (pyRegex re) <> char '?'
    RSeq r1 r2    -> pyRegex' r1 <> pyRegex' r2
    REps          -> empty
    RAlts cs      -> brackets (hcat (map (pyRegex . RChar) cs))
    RDigit        -> "\\d"
    RUpper        -> "[A-Z]"
    RLower        -> "[a-z]"
    RLetter       -> "[a-zA-Z]"
    RMinus r1 r2  -> parens (pyRegex r1) <> parens ("?<!" <> pyRegex r2)
  where
    escape '\n' = "\\n"
    escape '\t' = "\\t"
    escape c | c `elem` (".'[]()|*+?{}\\^$" :: String) = ['\\',c]
    escape c = [c]
    pyRegex' r@(RAlt{}) = parens (pyRegex r)
    pyRegex' r = pyRegex r