├── .gitignore ├── ChangeLog.md ├── LICENSE ├── README.md ├── Setup.hs ├── app-ghci └── Main.hs ├── app └── Main.hs ├── lib └── Common.hs ├── package.yaml └── stack.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | latex-live-snippets.cabal 3 | .latex-live-snippets/ 4 | *~ 5 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Changelog for latex-live-snippets 2 | 3 | ## 0.1.0.0 -- 2018-07-11 4 | 5 | * First version. Released on an unsuspecting world. 6 | 7 | ## Unreleased changes 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Author name here (c) 2018 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Author name here nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # latex-live-snippets 2 | 3 | Install with `stack install`, and then in latex via: 4 | 5 | ```latex 6 | % set this to where your code is 7 | \newcommand{\srcdir}{.} 8 | 9 | \newcommand{\snip}[2]{\immediate\write18{latex-live-snippets \srcdir/#1.hs #2}\input{.latex-live-snippets/#1.#2.tex}} 10 | ``` 11 | 12 | Now, given a file `Test.hs`: 13 | 14 | ```haskell 15 | zoo :: Int 16 | zoo = 5 17 | 18 | test :: Bool -> Bool 19 | test True = id $ True 20 | test _ = True -- ! 1 21 | ``` 22 | 23 | we can call 24 | 25 | ```latex 26 | \snip{Test}{test} 27 | ``` 28 | 29 | which will result in: 30 | 31 | ```latex 32 | \begin{code} 33 | test :: Bool -> Bool 34 | test True = id $ True 35 | test _ = True !\annotate{1}! 36 | \end{code} 37 | ``` 38 | 39 | It will also find type families, data definitions. Custom snippet areas can be 40 | defined via comments of the form `-- # name`. 41 | 42 | 43 | # latex-live-snippets-ghci 44 | 45 | Put the following on your path as `latex-live-snippets-run-ghci` 46 | 47 | ```bash 48 | #!/usr/bin/bash 49 | 50 | response=$(mktemp /tmp/ghci-latex.XXXXXXXXXXX) 51 | 52 | echo ":l $1" | cat - $2 | stack exec ghci > $response 53 | latex-live-snippets-ghci $2 $response $3 54 | ``` 55 | 56 | and install in latex via 57 | 58 | ```latex 59 | \usepackage{fancyvrb} 60 | 61 | \makeatletter 62 | \newcommand*\ifcounter[1]{% 63 | \ifcsname c@#1\endcsname 64 | \expandafter\@firstoftwo 65 | \else 66 | \expandafter\@secondoftwo 67 | \fi 68 | } 69 | \makeatother 70 | 71 | \newcommand{\doreplparam}{} 72 | \newcommand{\doreplfile}{} 73 | \newenvironment{dorepl}[1]{\VerbatimEnvironment 74 | \renewcommand{\doreplparam}{#1} 75 | \renewcommand{\doreplfile}{\doreplparam-\arabic{\doreplparam}} 76 | \ifcounter{\doreplparam}{}{\newcounter{\doreplparam}} 77 | \begin{VerbatimOut}{/tmp/\doreplfile.aux}}{\end{VerbatimOut} 78 | \immediate\write18{latex-live-snippets-run-ghci \srcdir/\doreplparam.hs /tmp/\doreplfile.aux \doreplfile} 79 | \input{.latex-live-snippets/repl/\doreplfile.tex} 80 | \stepcounter{\doreplparam} 81 | } 82 | ``` 83 | 84 | Now you can run repl sessions: 85 | 86 | ```latex 87 | \begin{dorepl}{Test} 88 | :set -XDataKinds 89 | :t zoo 90 | take 3 $ iterate not False 91 | \end{dorepl} 92 | ``` 93 | 94 | results in 95 | 96 | ```latex 97 | \begin{repl} 98 | \ghcisilent{:set -XDataKinds} 99 | \ghci{:t zoo}{zoo :: Int} 100 | \ghci{take 3 $ iterate not False}{[False,True,False]]} 101 | \end{repl} 102 | ``` 103 | 104 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /app-ghci/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Common (escapeGHCILatexChars, getSub, runSub) 4 | import Control.Lens ((^.), _head, (%~)) 5 | import Control.Monad (guard, join) 6 | import Data.Bool (bool) 7 | import Data.Char (isSpace) 8 | import Data.List 9 | import Data.List.Utils (replace) 10 | import Data.Maybe (listToMaybe, isNothing, fromJust) 11 | import System.Directory 12 | import System.Environment 13 | import System.FilePath.Lens (basename) 14 | import System.FilePath.Posix 15 | 16 | 17 | import Debug.Trace 18 | 19 | main :: IO () 20 | main = do 21 | let dir = ".latex-live-snippets/repl" 22 | 23 | [samplefilename, responsefilename, uniqueid] <- getArgs 24 | sample <- readFile samplefilename 25 | response <- readFile responsefilename 26 | createDirectoryIfMissing True dir 27 | let filename' = dir uniqueid ++ ".tex" 28 | writeFile filename' $ interleave (lines sample) $ responses response 29 | 30 | 31 | interleave :: [String] -> [String] -> String 32 | interleave as 33 | = ("\\begin{repl}\\begin{lstlisting}\n" ++) 34 | . (++ "\\end{lstlisting}\\end{repl}\n") 35 | . intercalate "\n\n" 36 | . filter (not . null) 37 | . zipping (isSilent . fst) 38 | (\(a, f) -> 39 | let a' = runSub f a in 40 | if isReallySilent a' 41 | then "" 42 | else mconcat [ "> " 43 | , escapeGHCILatexChars a' 44 | ]) 45 | (\(a, f) b -> mconcat [ "> " 46 | , escapeGHCILatexChars $ runSub f a 47 | , "\n" 48 | , escapeGHCILatexChars . runSub f $ initNonEmpty b 49 | ]) 50 | (fmap (getSub . dropWhile isSpace) as) 51 | 52 | 53 | zipping :: (a -> Bool) -> (a -> c) -> (a -> b -> c) -> [a] -> [b] -> [c] 54 | zipping _ _ _ [] _ = [] 55 | zipping _ _ _ _ [] = [] 56 | zipping p d f (a:as) bs | p a = d a : zipping p d f as bs 57 | zipping p d f (a:as) (b:bs) = f a b : zipping p d f as bs 58 | 59 | 60 | initNonEmpty :: [a] -> [a] 61 | initNonEmpty [] = [] 62 | initNonEmpty a = init a 63 | 64 | 65 | responses :: String -> [String] 66 | responses 67 | = fmap unlines 68 | . fmap (_head %~ removeManyTags) 69 | . groupBy (\_ a -> not $ isResponse a) 70 | . drop 1 71 | . dropWhile (not . isPrefixOf "Ok, ") 72 | . drop 1 73 | . dropWhile (not . isPrefixOf "Ok, ") 74 | . lines 75 | 76 | 77 | removeTag :: String -> String 78 | removeTag = drop 2 . dropWhile (/= '>') 79 | 80 | 81 | removeManyTags :: String -> String 82 | removeManyTags ts = bool ts (removeManyTags $ removeTag ts) $ isResponse ts 83 | 84 | 85 | isResponse :: String -> Bool 86 | isResponse ('*':_) = True 87 | isResponse _ = False 88 | 89 | 90 | isSilent :: String -> Bool 91 | isSilent str 92 | | isPrefixOf ":set " str = True 93 | | isPrefixOf "let " str = True 94 | | isPrefixOf "type " str = True 95 | | isPrefixOf "import " str = True 96 | | isPrefixOf "default (" str = True 97 | | isPrefixOf "@" str = True 98 | | otherwise = False 99 | 100 | 101 | isReallySilent :: String -> Bool 102 | isReallySilent str 103 | | isPrefixOf "@" str = True 104 | | otherwise = False 105 | 106 | 107 | test :: String 108 | test = unlines 109 | [ "GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help" 110 | , "Prelude> [1 of 1] Compiling RankN ( code/RankN.hs, interpreted )" 111 | , "Ok, modules loaded: RankN." 112 | , "*RankN> Functor :: (* -> *) -> Constraint" 113 | , "= Functor" 114 | , "*RankN> Monad :: (* -> *) -> Constraint" 115 | , "= Monad" 116 | , "*RankN> Leaving GHCi." 117 | ] 118 | 119 | 120 | sample :: String 121 | sample = unlines 122 | [ ":set -XDataKinds" 123 | , ":kind! Functor" 124 | , ":kind! Monad" 125 | ] 126 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Common (escapeLatexChars) 4 | import Control.Lens ((^.)) 5 | import Control.Monad (guard) 6 | import Data.List 7 | import Data.List.Utils (replace) 8 | import Data.Maybe (listToMaybe, isNothing, fromJust, fromMaybe) 9 | import System.Directory 10 | import System.Environment 11 | import System.FilePath.Lens (basename) 12 | import System.FilePath.Posix 13 | 14 | 15 | main :: IO () 16 | main = do 17 | let dir = ".latex-live-snippets" 18 | 19 | filename <- head <$> getArgs 20 | decl <- listToMaybe . tail <$> getArgs 21 | file <- readFile filename 22 | createDirectoryIfMissing True dir 23 | let filename' = dir filename ^. basename ++ "." ++ fromMaybe "FILE" decl ++ ".tex" 24 | writeFile filename' $ getDefinition file decl 25 | 26 | 27 | matchDefinition :: String -> String -> Maybe ([String] -> [String]) 28 | matchDefinition decl line = 29 | listToMaybe $ do 30 | (form, f) <- [ ("", id) 31 | , ("-- # ", tail) 32 | , ("type family ", id) 33 | , ("data family ", id) 34 | , ("data ", id) 35 | , ("type ", id) 36 | , ("newtype ", id) 37 | , ("class ", id) 38 | ] 39 | guard $ isPrefixOf (form ++ decl) line 40 | pure f 41 | 42 | 43 | getDefinition :: String -> Maybe String -> String 44 | getDefinition file (Just decl) 45 | = unlines 46 | . ("\\begin{code}" :) 47 | . (++ ["\\end{code}"]) 48 | . fmap annotate 49 | . fmap escapeLatexChars 50 | . func 51 | $ ls 52 | where 53 | ls = takeWhile (not . null) 54 | . dropWhile (isNothing . matchDefinition decl) 55 | $ lines file 56 | func = fromJust . matchDefinition decl $ head ls 57 | getDefinition file Nothing 58 | = unlines 59 | . ("\\begin{code}" :) 60 | . (++ ["\\end{code}"]) 61 | . fmap annotate 62 | . fmap escapeLatexChars 63 | $ lines file 64 | 65 | 66 | 67 | annotate :: String -> String 68 | annotate [] = "" 69 | annotate ('-':'-':' ':'!':' ':zs) = "!\\annotate{" ++ zs ++ "}!" 70 | annotate (a:as) = a : annotate as 71 | 72 | -------------------------------------------------------------------------------- /lib/Common.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ViewPatterns #-} 2 | 3 | module Common where 4 | 5 | import Data.List.Utils (replace) 6 | 7 | escapeLatexChars :: String -> String 8 | escapeLatexChars 9 | = replace "~" "!\\tyeq!" 10 | 11 | 12 | escapeGHCILatexChars :: String -> String 13 | escapeGHCILatexChars 14 | = id 15 | 16 | -- escapeLatexChars 17 | -- . replace "!!!!!!!!!!" "\\" 18 | -- . replace "\\" "!!!!!!!!!!textbackslash{}" 19 | -- . replace "{" "!!!!!!!!!!{" 20 | -- . replace "}" "!!!!!!!!!!}" 21 | 22 | 23 | runSub :: Maybe (String -> String) -> String -> String 24 | runSub Nothing = id 25 | runSub (Just f) = f 26 | 27 | getSub :: String -> (String, Maybe (String -> String)) 28 | getSub ('/' : line) = 29 | let (rep, tail -> line') = span (/= '/') line 30 | (with, tail -> str') = span (/= '/') line' 31 | in (str', Just $ replace rep with) 32 | getSub str = (str, Nothing) 33 | 34 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: latex-live-snippets 2 | version: 0.1.0.0 3 | github: "isovector/latex-live-snippets" 4 | license: BSD3 5 | author: "Sandy Maguire" 6 | maintainer: "sandy@sandymaguire.me" 7 | copyright: "2018 Sandy Maguire" 8 | 9 | extra-source-files: 10 | - README.md 11 | - ChangeLog.md 12 | 13 | synopsis: Automatically inline Haskell snippets into LaTeX documents. 14 | category: System 15 | 16 | description: Please see the README on GitHub at 17 | 18 | dependencies: 19 | - base >= 4.7 && < 5 20 | - directory 21 | - MissingH 22 | - filepath 23 | - lens 24 | 25 | library: 26 | source-dirs: lib 27 | 28 | executables: 29 | latex-live-snippets: 30 | main: Main.hs 31 | source-dirs: app 32 | ghc-options: 33 | - -threaded 34 | - -rtsopts 35 | - -with-rtsopts=-N 36 | dependencies: 37 | - latex-live-snippets 38 | latex-live-snippets-ghci: 39 | main: Main.hs 40 | source-dirs: app-ghci 41 | ghc-options: 42 | - -threaded 43 | - -rtsopts 44 | - -with-rtsopts=-N 45 | dependencies: 46 | - latex-live-snippets 47 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 15 | # 16 | # The location of a snapshot can be provided as a file or url. Stack assumes 17 | # a snapshot provided as a file might change, whereas a url resource does not. 18 | # 19 | # resolver: ./custom-snapshot.yaml 20 | # resolver: https://example.com/snapshots/2018-01-01.yaml 21 | resolver: lts-11.9 22 | 23 | # User packages to be built. 24 | # Various formats can be used as shown in the example below. 25 | # 26 | # packages: 27 | # - some-directory 28 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 29 | # - location: 30 | # git: https://github.com/commercialhaskell/stack.git 31 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 32 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 33 | # subdirs: 34 | # - auto-update 35 | # - wai 36 | packages: 37 | - . 38 | # Dependency packages to be pulled from upstream that are not in the resolver 39 | # using the same syntax as the packages field. 40 | # (e.g., acme-missiles-0.3) 41 | # extra-deps: [] 42 | 43 | # Override default flag values for local packages and extra-deps 44 | # flags: {} 45 | 46 | # Extra package databases containing global packages 47 | # extra-package-dbs: [] 48 | 49 | # Control whether we use the GHC we find on the path 50 | # system-ghc: true 51 | # 52 | # Require a specific version of stack, using version ranges 53 | # require-stack-version: -any # Default 54 | # require-stack-version: ">=1.7" 55 | # 56 | # Override the architecture used by stack, especially useful on Windows 57 | # arch: i386 58 | # arch: x86_64 59 | # 60 | # Extra directories used by stack for building 61 | # extra-include-dirs: [/path/to/dir] 62 | # extra-lib-dirs: [/path/to/dir] 63 | # 64 | # Allow a newer minor version of GHC than the snapshot specifies 65 | # compiler-check: newer-minor --------------------------------------------------------------------------------