From 46393fee64540bae63958dd7900ce116ea323679 Mon Sep 17 00:00:00 2001 From: John MacFarlane Date: Fri, 22 Nov 2024 21:47:27 -0800 Subject: [PATCH] Add structure for skylighting-format-typst. Work towards #201. --- skylighting-format-typst/LICENSE | 30 ++++ skylighting-format-typst/README.md | 5 + .../skylighting-format-typst.cabal | 36 +++++ .../src/Skylighting/Format/Typst.hs | 152 ++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 skylighting-format-typst/LICENSE create mode 100644 skylighting-format-typst/README.md create mode 100644 skylighting-format-typst/skylighting-format-typst.cabal create mode 100644 skylighting-format-typst/src/Skylighting/Format/Typst.hs diff --git a/skylighting-format-typst/LICENSE b/skylighting-format-typst/LICENSE new file mode 100644 index 000000000..3911efee6 --- /dev/null +++ b/skylighting-format-typst/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2016-2018, John MacFarlane. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/skylighting-format-typst/README.md b/skylighting-format-typst/README.md new file mode 100644 index 000000000..84a6c8bc3 --- /dev/null +++ b/skylighting-format-typst/README.md @@ -0,0 +1,5 @@ +# skylighting-format-typst + +This package provides functions to render syntax-highlighting +as typst. + diff --git a/skylighting-format-typst/skylighting-format-typst.cabal b/skylighting-format-typst/skylighting-format-typst.cabal new file mode 100644 index 000000000..fc0273602 --- /dev/null +++ b/skylighting-format-typst/skylighting-format-typst.cabal @@ -0,0 +1,36 @@ +name: skylighting-format-typst +version: 0.1 +synopsis: Typst formatter for skylighting syntax highlighting library +description: This module allows tokens produced by skylighting-core + to be rendered as Typst. +homepage: https://github.com/jgm/skylighting +license: BSD3 +license-file: LICENSE +author: John MacFarlane +maintainer: jgm@berkeley.edu +copyright: (C) 2016-2022 John MacFarlane +category: Text +build-type: Simple +extra-source-files: README.md + +cabal-version: >=1.10 + +source-repository head + type: git + location: https://github.com/jgm/skylighting.git + +library + exposed-modules: Skylighting.Format.Typst + other-extensions: CPP + build-depends: base >= 4.8 && < 5.0, + skylighting-core, + text, + containers + hs-source-dirs: src + ghc-prof-options: -fprof-auto-exported + default-language: Haskell2010 + ghc-options: -Wall + if impl(ghc >= 8.4) + ghc-options: -fhide-source-paths + if impl(ghc >= 8.10) + ghc-options: -Wunused-packages diff --git a/skylighting-format-typst/src/Skylighting/Format/Typst.hs b/skylighting-format-typst/src/Skylighting/Format/Typst.hs new file mode 100644 index 000000000..354a10702 --- /dev/null +++ b/skylighting-format-typst/src/Skylighting/Format/Typst.hs @@ -0,0 +1,152 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +module Skylighting.Format.Typst ( + formatTypstInline + , formatTypstBlock + , styleToTypst + ) where + +import Control.Monad (mplus) +import Data.Char (isSpace) +import Data.List (sort) +import qualified Data.Map as Map +import Data.Text (Text) +import qualified Data.Text as Text +import Skylighting.Types +import Text.Printf +#if !MIN_VERSION_base(4,11,0) +import Data.Semigroup +#endif + +formatTypst :: Bool -> [SourceLine] -> Text +formatTypst inline = Text.intercalate (Text.singleton '\n') + . map (sourceLineToTypst inline) + +-- | Formats tokens as Typst using custom commands inside +-- @|@ characters. Assumes that @|@ is defined as a short verbatim +-- command by the macros produced by 'styleToTypst'. +-- A @KeywordTok@ is rendered using @\\KeywordTok{..}@, and so on. +formatTypstInline :: FormatOptions -> [SourceLine] -> Text +formatTypstInline _opts ls = "\\VERB|" <> formatTypst True ls <> "|" + +sourceLineToTypst :: Bool -> SourceLine -> Text +sourceLineToTypst inline = mconcat . map (tokenToTypst inline) + +tokenToTypst :: Bool -> Token -> Text +tokenToTypst inline (NormalTok, txt) + | Text.all isSpace txt = escapeTypst inline txt +tokenToTypst inline (toktype, txt) = Text.cons '\\' + (Text.pack (show toktype) <> "{" <> escapeTypst inline txt <> "}") + +escapeTypst :: Bool -> Text -> Text +escapeTypst inline = Text.concatMap escapeTypstChar + where escapeTypstChar c = + case c of + '\\' -> "\\textbackslash{}" + '{' -> "\\{" + '}' -> "\\}" + '|' | inline -> "\\VerbBar{}" -- used in inline verbatim + '_' -> "\\_" + '&' -> "\\&" + '%' -> "\\%" + '#' -> "\\#" + '`' -> "\\textasciigrave{}" + '\'' -> "\\textquotesingle{}" + '-' -> "{-}" -- prevent ligatures + '~' -> "\\textasciitilde{}" + '^' -> "\\^{}" + '>' -> "\\textgreater{}" + '<' -> "\\textless{}" + _ -> Text.singleton c + +-- Typst + +-- | Format tokens as a Typst @Highlighting@ environment inside a +-- @Shaded@ environment. @Highlighting@ and @Shaded@ are +-- defined by the macros produced by 'styleToTypst'. @Highlighting@ +-- is a verbatim environment using @fancyvrb@; @\\@, @{@, and @}@ +-- have their normal meanings inside this environment, so that +-- formatting commands work. @Shaded@ is either nothing +-- (if the style's background color is default) or a @snugshade@ +-- environment from @framed@, providing a background color +-- for the whole code block, even if it spans multiple pages. +formatTypstBlock :: FormatOptions -> [SourceLine] -> Text +formatTypstBlock opts ls = Text.unlines + ["\\begin{Shaded}" + ,"\\begin{Highlighting}[" <> + (if numberLines opts + then "numbers=left," <> + (if startNumber opts == 1 + then "" + else ",firstnumber=" <> + Text.pack (show (startNumber opts))) <> "," + else Text.empty) <> "]" + ,formatTypst False ls + ,"\\end{Highlighting}" + ,"\\end{Shaded}"] + +-- | Converts a 'Style' to a set of Typst macro definitions, +-- which should be placed in the document's preamble. +-- Note: default Typst setup doesn't allow boldface typewriter font. +-- To make boldface work in styles, you need to use a different typewriter +-- font. This will work for computer modern: +-- +-- > \DeclareFontShape{OT1}{cmtt}{bx}{n}{<5><6><7><8><9><10><10.95><12><14.4><17.28><20.74><24.88>cmttb10}{} +-- +-- Or, with xelatex: +-- +-- > \usepackage{fontspec} +-- > \setmainfont[SmallCapsFont={* Caps}]{Latin Modern Roman} +-- > \setsansfont{Latin Modern Sans} +-- > \setmonofont[SmallCapsFont={Latin Modern Mono Caps}]{Latin Modern Mono Light} +-- +styleToTypst :: Style -> Text +styleToTypst f = Text.unlines $ + [ "\\usepackage{color}" + , "\\usepackage{fancyvrb}" + , "\\newcommand{\\VerbBar}{|}" + , "\\newcommand{\\VERB}{\\Verb[commandchars=\\\\\\{\\}]}" + , "\\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\}}" + , "% Add ',fontsize=\\small' for more characters per line" + ] ++ + (case backgroundColor f of + Nothing -> ["\\newenvironment{Shaded}{}{}"] + Just (RGB r g b) -> ["\\usepackage{framed}" + ,Text.pack + (printf "\\definecolor{shadecolor}{RGB}{%d,%d,%d}" r g b) + ,"\\newenvironment{Shaded}{\\begin{snugshade}}{\\end{snugshade}}"]) + ++ sort (map (macrodef (defaultColor f) (Map.toList (tokenStyles f))) + (enumFromTo KeywordTok NormalTok)) + +macrodef :: Maybe Color -> [(TokenType, TokenStyle)] -> TokenType -> Text +macrodef defaultcol tokstyles tokt = "\\newcommand{\\" + <> Text.pack (show tokt) + <> "}[1]{" + <> Text.pack (co . ul . bf . it . bg $ "#1") + <> "}" + where tokf = case lookup tokt tokstyles of + Nothing -> defStyle + Just x -> x + ul x = if tokenUnderline tokf + then "\\underline{" <> x <> "}" + else x + it x = if tokenItalic tokf + then "\\textit{" <> x <> "}" + else x + bf x = if tokenBold tokf + then "\\textbf{" <> x <> "}" + else x + bcol = fromColor `fmap` tokenBackground tokf + :: Maybe (Double, Double, Double) + bg x = case bcol of + Nothing -> x + Just (r, g, b) -> + printf "\\colorbox[rgb]{%0.2f,%0.2f,%0.2f}{%s}" r g b x + col = fromColor `fmap` (tokenColor tokf `mplus` defaultcol) + :: Maybe (Double, Double, Double) + co x = case col of + Nothing -> x + Just (r, g, b) -> + printf "\\textcolor[rgb]{%0.2f,%0.2f,%0.2f}{%s}" r g b x +