packages feed

scripths 0.5.1.0 → 0.5.2.0

raw patch · 9 files changed

+392/−123 lines, 9 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ ScriptHs.CLI.Types: Args :: Maybe FilePath -> Maybe FilePath -> [FilePath] -> Bool -> Bool -> Bool -> Bool -> CodeStyle -> OutputStyle -> Args
+ ScriptHs.CLI.Types: [argCodeStyle] :: Args -> CodeStyle
+ ScriptHs.CLI.Types: [argHelp] :: Args -> Bool
+ ScriptHs.CLI.Types: [argInPlace] :: Args -> Bool
+ ScriptHs.CLI.Types: [argNoLocalProject] :: Args -> Bool
+ ScriptHs.CLI.Types: [argOutputStyle] :: Args -> OutputStyle
+ ScriptHs.CLI.Types: [argOutput] :: Args -> Maybe FilePath
+ ScriptHs.CLI.Types: [argPackages] :: Args -> [FilePath]
+ ScriptHs.CLI.Types: [argScript] :: Args -> Maybe FilePath
+ ScriptHs.CLI.Types: [argVersion] :: Args -> Bool
+ ScriptHs.CLI.Types: data Args
+ ScriptHs.CLI.Types: emptyArgs :: Args
+ ScriptHs.CLI.Types: parseArgs :: [String] -> Either String Args
+ ScriptHs.Markdown: DisplayCode :: CodeStyle
+ ScriptHs.Markdown: OutputQuoted :: OutputStyle
+ ScriptHs.Markdown: OutputRaw :: OutputStyle
+ ScriptHs.Markdown: RemoveCode :: CodeStyle
+ ScriptHs.Markdown: RenderOptions :: CodeStyle -> OutputStyle -> RenderOptions
+ ScriptHs.Markdown: [renderCodeStyle] :: RenderOptions -> CodeStyle
+ ScriptHs.Markdown: [renderOutputStyle] :: RenderOptions -> OutputStyle
+ ScriptHs.Markdown: data CodeStyle
+ ScriptHs.Markdown: data OutputStyle
+ ScriptHs.Markdown: data RenderOptions
+ ScriptHs.Markdown: defaultCodeStyle :: CodeStyle
+ ScriptHs.Markdown: defaultOutputStyle :: OutputStyle
+ ScriptHs.Markdown: defaultRenderOptions :: RenderOptions
+ ScriptHs.Markdown: instance GHC.Classes.Eq ScriptHs.Markdown.CodeStyle
+ ScriptHs.Markdown: instance GHC.Classes.Eq ScriptHs.Markdown.OutputStyle
+ ScriptHs.Markdown: instance GHC.Classes.Eq ScriptHs.Markdown.RenderOptions
+ ScriptHs.Markdown: instance GHC.Classes.Ord ScriptHs.Markdown.CodeStyle
+ ScriptHs.Markdown: instance GHC.Classes.Ord ScriptHs.Markdown.OutputStyle
+ ScriptHs.Markdown: instance GHC.Show.Show ScriptHs.Markdown.CodeStyle
+ ScriptHs.Markdown: instance GHC.Show.Show ScriptHs.Markdown.OutputStyle
+ ScriptHs.Markdown: instance GHC.Show.Show ScriptHs.Markdown.RenderOptions
+ ScriptHs.Markdown: parseCodeStyle :: String -> Maybe CodeStyle
+ ScriptHs.Markdown: parseOutputStyle :: String -> Maybe OutputStyle
+ ScriptHs.Markdown: reassembleWith :: RenderOptions -> [Segment] -> Text
- ScriptHs.Notebook: executeCodeCells :: RunOptions -> FilePath -> CabalMeta -> IndexedSegments -> IndexedBlocks -> IO Text
+ ScriptHs.Notebook: executeCodeCells :: RenderOptions -> RunOptions -> FilePath -> CabalMeta -> IndexedSegments -> IndexedBlocks -> IO Text
- ScriptHs.Notebook: processNotebook :: RunOptions -> FilePath -> Text -> IO Text
+ ScriptHs.Notebook: processNotebook :: RenderOptions -> RunOptions -> FilePath -> Text -> IO Text
- ScriptHs.Notebook: runNotebook :: RunOptions -> FilePath -> Maybe FilePath -> IO ()
+ ScriptHs.Notebook: runNotebook :: RenderOptions -> RunOptions -> FilePath -> Maybe FilePath -> IO ()

Files

CHANGELOG.md view
@@ -1,6 +1,13 @@ # Revision history for scripths  +## 0.5.2.0 -- 2026-06-16+* **Styling with `RenderOptions`.** You can now have the output quoted vs unquoted+  and the code shown or not (code not shown is if you want to export the results only).+  Thanks to @tchoutri+* **Blank-line fix**: whitespace-only prose between two code fences no longer+  becomes a spurious empty prose segment.+ ## 0.4.1.0 -- 2056-05-31 * **Custom-prelude support**: auto print (our hook to rout evething but strings to shw)   now doesn't assume a prelude.
app/Main.hs view
@@ -1,7 +1,6 @@ module Main where  import Control.Monad (unless, when)-import Data.List (isPrefixOf) import Data.Maybe (isJust) import qualified Data.Text as T import qualified Data.Text.IO as TIO@@ -16,6 +15,8 @@ import System.FilePath (takeExtension) import System.IO (hPutStrLn, stderr) +import ScriptHs.CLI.Types+import ScriptHs.Markdown import ScriptHs.Notebook (runNotebook) import ScriptHs.Parser (parseScript) import ScriptHs.Run (RunOptions (..), defaultRunOptions, runScript)@@ -26,20 +27,6 @@     tagVersion,  ) --- | Parsed command-line options.-data Args = Args-    { argScript :: Maybe FilePath-    , argOutput :: Maybe FilePath-    , argPackages :: [FilePath]-    , argNoLocalProject :: Bool-    , argInPlace :: Bool-    , argHelp :: Bool-    , argVersion :: Bool-    }--emptyArgs :: Args-emptyArgs = Args Nothing Nothing [] False False False False- main :: IO () main = do     raw <- getArgs@@ -61,7 +48,12 @@                                 { roPackages = pkgs                                 , roEnclosingProject = not (argNoLocalProject a)                                 }-                    dispatch opts path outPath+                    let renderOpts =+                            RenderOptions+                                { renderCodeStyle = argCodeStyle a+                                , renderOutputStyle = argOutputStyle a+                                }+                    dispatch renderOpts opts path outPath  {- | Resolve the output destination, honouring @--in-place@: write back over the notebook itself. In-place is only meaningful for notebooks (the @.ghci@\/@.hs@@@ -80,43 +72,17 @@ isNotebook :: FilePath -> Bool isNotebook path = takeExtension path `elem` [".md", ".markdown"] -dispatch :: RunOptions -> FilePath -> Maybe FilePath -> IO ()-dispatch opts path outputPath =+dispatch ::+    RenderOptions -> RunOptions -> FilePath -> Maybe FilePath -> IO ()+dispatch renderOpts opts path outputPath =     case takeExtension path of-        ".md" -> runNotebook opts path outputPath-        ".markdown" -> runNotebook opts path outputPath+        ".md" -> runNotebook renderOpts opts path outputPath+        ".markdown" -> runNotebook renderOpts opts path outputPath         _ -> do             contents <- TIO.readFile path             let sf = parseScript contents             runScript opts path sf -{- | Parse args: @[-o FILE | --output=FILE] [-i | --in-place] [--package DIR |--p DIR | --package=DIR]... [--no-local-project] [-h | --help] <script>@. The-script is the sole non-flag argument.--}-parseArgs :: [String] -> Either String Args-parseArgs = go emptyArgs-  where-    go a [] = Right a-    go a ("-o" : f : rest) = go a{argOutput = Just f} rest-    go _ ["-o"] = Left "-o requires a filename"-    go a (tok : rest)-        | tok == "-p" || tok == "--package" = case rest of-            (d : rest') -> go a{argPackages = argPackages a ++ [d]} rest'-            [] -> Left (tok ++ " requires a directory")-        | Just f <- stripFlag "--output=" tok = go a{argOutput = Just f} rest-        | Just d <- stripFlag "--package=" tok =-            go a{argPackages = argPackages a ++ [d]} rest-        | tok == "--no-local-project" = go a{argNoLocalProject = True} rest-        | tok == "-i" || tok == "--in-place" = go a{argInPlace = True} rest-        | tok == "-h" || tok == "--help" = go a{argHelp = True} rest-        | tok == "-v" || tok == "--version" = go a{argVersion = True} rest-        | "-" `isPrefixOf` tok = Left ("unknown flag: " ++ tok)-        | otherwise = case argScript a of-            Nothing -> go a{argScript = Just tok} rest-            Just _ -> Left ("unexpected extra argument: " ++ tok)-    stripFlag p s = if p `isPrefixOf` s then Just (drop (length p) s) else Nothing- -- | Resolve a @--package@ dir to absolute, requiring a package root (a @.cabal@). resolvePackageDir :: FilePath -> IO FilePath resolvePackageDir dir = do@@ -171,6 +137,8 @@         , "  --no-local-project        do not auto-include the enclosing cabal project"         , "  -h, --help                show this help"         , "  -v, --version             show the scripths version"+        , "  --code-style=display|remove    control how code fences should appear after processing (default: display)"+        , "  --output-style=quoted|raw      control how evaluted code is outputed (default: quoted)"         , ""         , "Files may carry a first-line version tag recording the scripths that wrote"         , "them ('-- scripths: X' in scripts, '<!-- scripths: X -->' in notebooks); a"
scripths.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               scripths-version:            0.5.1.0+version:            0.5.2.0 synopsis:           GHCi scripts for standalone execution and Markdown documentation. description:        GHCi scripts for standalone execution (with dependency resolution) and Markdown documentation (produces inline output). homepage:           https://www.datahaskell.org/@@ -23,11 +23,12 @@  library     import:           warnings-    exposed-modules:  ScriptHs.Parser,+    exposed-modules:  ScriptHs.CLI.Types,                       ScriptHs.Compiled,-                      ScriptHs.Render,                       ScriptHs.Markdown,                       ScriptHs.Notebook,+                      ScriptHs.Parser,+                      ScriptHs.Render,                       ScriptHs.Repl,                       ScriptHs.Run,                       ScriptHs.Version
+ src/ScriptHs/CLI/Types.hs view
@@ -0,0 +1,72 @@+module ScriptHs.CLI.Types (+    Args (..),+    emptyArgs,+    parseArgs,+) where++import Data.List (isPrefixOf)+import ScriptHs.Markdown++-- | Parsed command-line options.+data Args = Args+    { argScript :: Maybe FilePath+    , argOutput :: Maybe FilePath+    , argPackages :: [FilePath]+    , argNoLocalProject :: Bool+    , argInPlace :: Bool+    , argHelp :: Bool+    , argVersion :: Bool+    , argCodeStyle :: CodeStyle+    , argOutputStyle :: OutputStyle+    }++emptyArgs :: Args+emptyArgs =+    Args+        { argScript = Nothing+        , argOutput = Nothing+        , argPackages = []+        , argNoLocalProject = False+        , argInPlace = False+        , argHelp = False+        , argVersion = False+        , argCodeStyle = DisplayCode+        , argOutputStyle = OutputQuoted+        }++{- | Parse args: @[-o FILE | --output=FILE] [-i | --in-place] [--package DIR |+-p DIR | --package=DIR]... [--no-local-project] [-h | --help] <script>@. The+script is the sole non-flag argument.+-}+parseArgs :: [String] -> Either String Args+parseArgs = go emptyArgs+  where+    go a [] = Right a+    go a ("-o" : f : rest) = go a{argOutput = Just f} rest+    go _ ["-o"] = Left "-o requires a filename"+    go a (tok : rest)+        | tok == "-p" || tok == "--package" = case rest of+            (d : rest') -> go a{argPackages = argPackages a ++ [d]} rest'+            [] -> Left (tok ++ " requires a directory")+        | Just f <- stripFlag "--output=" tok = go a{argOutput = Just f} rest+        | Just d <- stripFlag "--package=" tok =+            go a{argPackages = argPackages a ++ [d]} rest+        | tok == "--no-local-project" = go a{argNoLocalProject = True} rest+        | tok == "-i" || tok == "--in-place" = go a{argInPlace = True} rest+        | tok == "-h" || tok == "--help" = go a{argHelp = True} rest+        | tok == "-v" || tok == "--version" = go a{argVersion = True} rest+        | tok == "--code-style" = case rest of+            (d : rest') -> case parseCodeStyle d of+                Just codeStyle -> go a{argCodeStyle = codeStyle} rest'+                Nothing -> Left ("Could not parse code style: " ++ tok)+            [] -> Left (tok ++ " requires a code style")+        | tok == "--output-style" = case rest of+            (d : rest') -> case parseOutputStyle d of+                Just codeStyle -> go a{argOutputStyle = codeStyle} rest'+                Nothing -> Left ("Could not parse output style: " ++ tok)+            [] -> Left (tok ++ " requires a output style")+        | "-" `isPrefixOf` tok = Left ("unknown flag: " ++ tok)+        | otherwise = case argScript a of+            Nothing -> go a{argScript = Just tok} rest+            Just _ -> Left ("unexpected extra argument: " ++ tok)+    stripFlag p s = if p `isPrefixOf` s then Just (drop (length p) s) else Nothing
src/ScriptHs/Markdown.hs view
@@ -2,13 +2,63 @@     Segment (..),     parseMarkdown,     reassemble,+    reassembleWith,     MimeType (..),     CodeOutput (..),+    CodeStyle (..),+    parseCodeStyle,+    defaultCodeStyle,+    OutputStyle (..),+    parseOutputStyle,+    defaultOutputStyle,+    RenderOptions (..),+    defaultRenderOptions, ) where  import Data.Text (Text) import qualified Data.Text as T +{- |  Directs how the original code fences are styled+  in the processed output.+-}+data CodeStyle+    = DisplayCode+    | RemoveCode+    deriving (Eq, Ord, Show)++parseCodeStyle :: String -> Maybe CodeStyle+parseCodeStyle "display" = Just DisplayCode+parseCodeStyle "remove" = Just RemoveCode+parseCodeStyle _ = Nothing++defaultCodeStyle :: CodeStyle+defaultCodeStyle = DisplayCode++data OutputStyle+    = OutputQuoted+    | OutputRaw+    deriving (Eq, Ord, Show)++parseOutputStyle :: String -> Maybe OutputStyle+parseOutputStyle "quoted" = Just OutputQuoted+parseOutputStyle "raw" = Just OutputRaw+parseOutputStyle _ = Nothing++defaultOutputStyle :: OutputStyle+defaultOutputStyle = OutputQuoted++{- | Render-time presentation choices; kept out of the parsed 'CodeOutput'+content model. Consumed only by 'reassembleWith'.+-}+data RenderOptions = RenderOptions+    { renderCodeStyle :: CodeStyle+    , renderOutputStyle :: OutputStyle+    }+    deriving (Eq, Show)++defaultRenderOptions :: RenderOptions+defaultRenderOptions = RenderOptions defaultCodeStyle defaultOutputStyle+ data MimeType     = MimeHtml     | MimeMarkdown@@ -30,7 +80,7 @@ parseMarkdown = parseMarkdown' [] . T.lines  parseMarkdown' :: [Text] -> [Text] -> [Segment]-parseMarkdown' acc [] = [Prose prose | not (T.null prose)]+parseMarkdown' acc [] = [Prose prose | not (isBlank prose)]   where     prose = T.unlines acc parseMarkdown' acc (line : rest) = case fenceLang line of@@ -40,24 +90,24 @@             prose = T.unlines acc             (codeLines, rest') = fmap (drop 1) (break ((== fence) . T.strip) rest)             (output, rest'') = case dropWhile ((== "") . T.strip) rest' of-                (x : xs) ->-                    if not (isMimeMarkerLine x)-                        then (Nothing, rest')-                        else-                            let-                                (cOutput, afterOutput) = span (T.isPrefixOf "> ") xs-                                mType = mimeFromTag x-                             in-                                ( Just (CodeOutput mType (T.unlines (map (T.drop (T.length "> ")) cOutput)))-                                , afterOutput-                                )-                [] -> (Nothing, rest')+                (x : xs)+                    | isMimeMarkerLine x ->+                        let (body, afterOutput) = extractOutput x xs+                         in (Just (CodeOutput (mimeFromTag x) body), afterOutput)+                _ -> (Nothing, rest')             segments =-                [Prose prose | not (T.null prose)]+                [Prose prose | not (isBlank prose)]                     ++ [CodeBlock lang (T.unlines codeLines) output]          in             segments ++ parseMarkdown' [] rest'' +{- | A prose run that is empty or only whitespace carries no cell content;+the blank line(s) between two code fences parse to such a run and must not+become a spurious prose segment.+-}+isBlank :: Text -> Bool+isBlank = T.null . T.strip+ fence :: Text fence = "```" @@ -77,22 +127,52 @@ mimeMarker :: Text mimeMarker = "<!-- scripths:mime " --- | Does this line open a rendered-output block (either marker spelling)?+{- | Does this line open a rendered-output block? Matches either marker+spelling, quoted (@> @) or raw.+-} isMimeMarkerLine :: Text -> Bool isMimeMarkerLine x =-    T.isPrefixOf "> <!-- scripths:mime" x || T.isPrefixOf "> <!-- sabela:mime" x+    T.isPrefixOf "<!-- scripths:mime" bare || T.isPrefixOf "<!-- sabela:mime" bare+  where+    bare = if T.isPrefixOf "> " x then T.drop 2 x else x +{- | Closing marker that terminates a raw output block so it re-parses; quoted+blocks self-delimit via their @> @ prefix and need no terminator.+-}+endMarker :: Text+endMarker = "<!-- /scripths:mime -->"++isEndMarkerLine :: Text -> Bool+isEndMarkerLine l = T.strip l == endMarker++{- | Collect an output block's body and the lines after it: a quoted block+runs while lines keep the @> @ prefix; a raw block runs to 'endMarker'.+-}+extractOutput :: Text -> [Text] -> (Text, [Text])+extractOutput marker xs+    | T.isPrefixOf "> " marker =+        let (body, after) = span (T.isPrefixOf "> ") xs+         in (T.unlines (map (T.drop (T.length "> ")) body), after)+    | otherwise =+        let (body, after) = break isEndMarkerLine xs+         in (T.unlines body, drop 1 after)+ fenceCodeSegment :: Text -> Text -> Text fenceCodeSegment lang output     | T.null (T.strip output) = ""     | otherwise = T.unlines ["", fence <> lang, T.stripEnd output, fence, ""] -{- | Render segments back to markdown idempotently: the blank-line run at each-seam between segments collapses to one blank line and the document's leading/-trailing blanks are trimmed, so re-running (e.g. @--in-place@) adds no new lines.--}+-- | 'reassembleWith' under 'defaultRenderOptions'; the back-compatible default. reassemble :: [Segment] -> Text-reassemble = finalize . foldr (joinSeam . renderSegment) ""+reassemble = reassembleWith defaultRenderOptions++{- | Render segments back to markdown idempotently (a @--in-place@ re-run is a+fixed point): seam blank runs collapse to one and leading/trailing blanks are+trimmed. Output re-parses under every style — 'OutputRaw' is bounded by a+closing 'endMarker', and 'RemoveCode' leaves no fence so a re-run is a no-op.+-}+reassembleWith :: RenderOptions -> [Segment] -> Text+reassembleWith opts = finalize . foldr (joinSeam . renderSegment opts) ""   where     joinSeam "" acc = acc     joinSeam piece "" = piece@@ -106,19 +186,24 @@         let stripped = T.dropWhileEnd (== '\n') (T.dropWhile (== '\n') t)          in if T.null stripped then "" else stripped <> "\n" -renderSegment :: Segment -> Text-renderSegment (Prose t) = t-renderSegment (CodeBlock lang code Nothing) = fenceCodeSegment lang code-renderSegment (CodeBlock lang code (Just output)) = fenceCodeSegment lang code <> blockQuote output+renderSegment :: RenderOptions -> Segment -> Text+renderSegment _ (Prose t) = t+renderSegment _ (CodeBlock lang code Nothing) = fenceCodeSegment lang code+renderSegment opts (CodeBlock lang code (Just output)) =+    let codeFence = case renderCodeStyle opts of+            DisplayCode -> fenceCodeSegment lang code+            RemoveCode -> mempty+     in codeFence <> blockQuote (renderOutputStyle opts) output -blockQuote :: CodeOutput -> Text-blockQuote (CodeOutput mimeType t) =-    let-        ls = (mimeMarker <> mimeIndicator mimeType <> " -->") : T.lines t-        trimmed = reverse $ dropWhile T.null $ reverse ls-        quoted = T.unlines $ map (\l -> if T.null l then "> " else "> " <> l) trimmed-     in-        quoted <> "\n"+blockQuote :: OutputStyle -> CodeOutput -> Text+blockQuote outputStyle (CodeOutput mimeType t) =+    case outputStyle of+        OutputQuoted -> T.unlines (map quote (marker : body)) <> "\n"+        OutputRaw -> T.unlines (marker : body ++ [endMarker]) <> "\n"+  where+    marker = mimeMarker <> mimeIndicator mimeType <> " -->"+    body = reverse $ dropWhile T.null $ reverse (T.lines t)+    quote l = if T.null l then "> " else "> " <> l  mimeIndicator :: MimeType -> Text mimeIndicator m = case m of
src/ScriptHs/Notebook.hs view
@@ -14,9 +14,10 @@ import ScriptHs.Markdown (     CodeOutput (..),     MimeType (..),+    RenderOptions,     Segment (..),     parseMarkdown,-    reassemble,+    reassembleWith,  ) import ScriptHs.Parser (     CabalMeta (..),@@ -37,10 +38,11 @@ clean; output written to a file (@-o@ or @--in-place@) is stamped with the current scripths version tag at the top. -}-runNotebook :: RunOptions -> FilePath -> Maybe FilePath -> IO ()-runNotebook opts path outputPath = do+runNotebook ::+    RenderOptions -> RunOptions -> FilePath -> Maybe FilePath -> IO ()+runNotebook renderOpts opts path outputPath = do     contents <- stripBom <$> TIO.readFile path-    outputMd <- processNotebook opts path contents+    outputMd <- processNotebook renderOpts opts path contents     case outputPath of         Nothing -> TIO.putStr outputMd         Just output -> TIO.writeFile output (stampVersion NotebookTag outputMd)@@ -49,22 +51,31 @@ stripBom :: Text -> Text stripBom t = fromMaybe t (T.stripPrefix "\65279" t) -processNotebook :: RunOptions -> FilePath -> Text -> IO Text-processNotebook opts notebookPath contents = do+processNotebook ::+    RenderOptions -> RunOptions -> FilePath -> Text -> IO Text+processNotebook renderOpts opts notebookPath contents = do     let indexedSegments = zip [0 ..] (parseMarkdown contents)         (metas, indexedCodeBlocks) = parseBlocks indexedSegments     if null indexedCodeBlocks         then pure contents-        else executeCodeCells opts notebookPath metas indexedSegments indexedCodeBlocks+        else+            executeCodeCells+                renderOpts+                opts+                notebookPath+                metas+                indexedSegments+                indexedCodeBlocks  executeCodeCells ::+    RenderOptions ->     RunOptions ->     FilePath ->     CabalMeta ->     IndexedSegments ->     IndexedBlocks ->     IO Text-executeCodeCells opts notebookPath meta allSegments codeBlocks = do+executeCodeCells renderOpts opts notebookPath meta allSegments codeBlocks = do     let ghciScript0 = generatedMarkedScript "" codeBlocks     nonce <- makeNonce ghciScript0     let ghciScript = generatedMarkedScript nonce codeBlocks@@ -77,7 +88,7 @@                 (fmap (scrubCellOutput nonce indices))                 (splitByMarkers nonce rawOutput indices)         blocksWithOutput = addOutputToSegments outputs allSegments-    pure $ reassemble blocksWithOutput+    pure $ reassembleWith renderOpts blocksWithOutput  {- | A per-run hex nonce woven into every cell-end marker so a cell's own output cannot spoof a marker and misattribute another cell's output. Mixes the process@@ -104,11 +115,13 @@   where     stripMarkers t = foldr (\i acc -> T.replace (mkMarker nonce i) "" acc) t indices -addOutputToSegments :: [(Int, Text)] -> IndexedSegments -> [Segment]+addOutputToSegments ::+    [(Int, Text)] -> IndexedSegments -> [Segment] addOutputToSegments outputs = map addOutput   where     addOutput :: (Int, Segment) -> Segment-    addOutput (i, CodeBlock lang code _) = CodeBlock lang code (fmap (CodeOutput MimePlain) (lookup i outputs))+    addOutput (i, CodeBlock lang code _) =+        CodeBlock lang code (fmap (CodeOutput MimePlain) (lookup i outputs))     addOutput (_, seg) = seg  mkIndexedCodeSegments :: IndexedSegments -> IndexedSegments
test/Test/Integration.hs view
@@ -5,6 +5,7 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, testCase) +import ScriptHs.Markdown import ScriptHs.Notebook (processNotebook) import ScriptHs.Run (defaultRunOptions) @@ -34,7 +35,12 @@                                 , "\"rendered-ok\" :: String"                                 , "```"                                 ]-                    out <- processNotebook defaultRunOptions "/tmp/scripths-integration.md" nb+                    out <-+                        processNotebook+                            defaultRenderOptions+                            defaultRunOptions+                            "/tmp/scripths-integration.md"+                            nb                     assertBool "renders the cell value" ("rendered-ok" `T.isInfixOf` out)                     assertBool                         "no ScripthsInternal leak"
test/Test/Markdown.hs view
@@ -10,13 +10,7 @@  import Data.Text (Text) import qualified Data.Text as T-import ScriptHs.Markdown (-    CodeOutput (..),-    MimeType (..),-    Segment (CodeBlock, Prose),-    parseMarkdown,-    reassemble,- )+import ScriptHs.Markdown  markdownTests :: TestTree markdownTests =@@ -44,6 +38,28 @@                         lang @?= "haskell"                         assertBool "has print 42" (T.isInfixOf "print 42" code)                     other -> assertFailure $ "expected [CodeBlock], got: " ++ show other+            , testCase "adjacent code blocks emit no spurious prose between them" $ do+                let input =+                        T.unlines+                            [ "```haskell"+                            , "A"+                            , "```"+                            , ""+                            , "```haskell"+                            , "B"+                            , "```"+                            ]+                let segs = parseMarkdown input+                case segs of+                    [CodeBlock{}, CodeBlock{}] -> pure ()+                    other ->+                        assertFailure $+                            "expected [CodeBlock, CodeBlock], got: " ++ show other+            , testCase "whitespace-only trailing lines are not a prose segment" $ do+                let segs = parseMarkdown (T.unlines ["```haskell", "A", "```", "", "  "])+                case segs of+                    [CodeBlock{}] -> pure ()+                    other -> assertFailure $ "expected [CodeBlock], got: " ++ show other             , testCase "four backticks is not a code fence" $ do                 let input =                         T.unlines@@ -196,7 +212,13 @@             ]         , testGroup             "reassemble"-            [ testCase "prose without output" $ do+            [ testCase "reassemble == reassembleWith defaultRenderOptions" $ do+                let segs =+                        [ CodeBlock "hs" "x\n" (Just $ CodeOutput MimePlain "hi")+                        , Prose "p\n"+                        ]+                reassemble segs @?= reassembleWith defaultRenderOptions segs+            , testCase "prose without output" $ do                 let result = reassemble [Prose "Hello\n"]                 result @?= "Hello\n"             , testCase "code block without output" $ do@@ -206,21 +228,37 @@                 assertBool "no blockquote" (not $ T.isInfixOf "> " result)             , testCase "code block with output" $ do                 let result =-                        reassemble [CodeBlock "haskell" "print 42\n" (Just $ CodeOutput MimePlain "42")]+                        reassemble+                            [ CodeBlock+                                "haskell"+                                "print 42\n"+                                (Just $ CodeOutput MimePlain "42")+                            ]                 assertBool "has fence" (T.isInfixOf "```haskell" result)                 assertBool "has code" (T.isInfixOf "print 42" result)                 assertBool "has blockquote" (T.isInfixOf "> 42" result)             , testCase "code block with multi-line output" $ do                 let result =-                        reassemble [CodeBlock "hs" "print [1,2]\n" (Just $ CodeOutput MimePlain "1\n2")]+                        reassemble+                            [ CodeBlock+                                "hs"+                                "print [1,2]\n"+                                (Just $ CodeOutput MimePlain "1\n2")+                            ]                 assertBool "has > 1" (T.isInfixOf "> 1" result)                 assertBool "has > 2" (T.isInfixOf "> 2" result)             , testCase "full document roundtrip" $ do                 let segs =                         [ Prose "# Title\n\n"-                        , CodeBlock "haskell" "print 42\n" (Just $ CodeOutput MimePlain "42")+                        , CodeBlock+                            "haskell"+                            "print 42\n"+                            (Just $ CodeOutput MimePlain "42")                         , Prose "\nSome text.\n"-                        , CodeBlock "haskell" "print 99\n" (Just $ CodeOutput MimePlain "99")+                        , CodeBlock+                            "haskell"+                            "print 99\n"+                            (Just $ CodeOutput MimePlain "99")                         ]                 let result = reassemble segs @@ -238,17 +276,44 @@                 assertBool "middle before code2" (middleIdx < code2Idx)             ]         , testGroup-            "blockquote"-            [ testCase "single line" $ do-                let result = reassemble [CodeBlock "hs" "x\n" (Just $ CodeOutput MimePlain "hello")]-                assertBool "blockquoted" (T.isInfixOf "> hello" result)-            , testCase "empty lines in output get bare >" $ do-                let result = reassemble [CodeBlock "hs" "x\n" (Just $ CodeOutput MimePlain "a\n\nb")]-                assertBool "has bare >" (T.isInfixOf "\n> \n" result)-            , testCase "renders the scripths:mime marker (not the legacy sabela)" $ do-                let result = reassemble [CodeBlock "hs" "x\n" (Just $ CodeOutput MimePlain "y")]-                assertBool "uses scripths:mime" (T.isInfixOf "scripths:mime" result)-                assertBool "no sabela:mime" (not (T.isInfixOf "sabela:mime" result))+            "Output style"+            [ testGroup+                "raw"+                [ testCase "raw output drops the blockquote prefix" $ do+                    let result =+                            reassembleWith+                                (RenderOptions defaultCodeStyle OutputRaw)+                                [CodeBlock "hs" "x\n" (Just $ CodeOutput MimePlain "hello")]+                    assertBool "has raw output" (T.isInfixOf "hello" result)+                    assertBool "not quoted" (not (T.isInfixOf "> hello" result))+                , testCase "remove-code + raw drops both the fence and the prefix" $ do+                    let result =+                            reassembleWith+                                (RenderOptions RemoveCode OutputRaw)+                                [CodeBlock "hs" "x = 1\n" (Just $ CodeOutput MimePlain "hello")]+                    assertBool "has raw output" (T.isInfixOf "hello" result)+                    assertBool "no code fence" (not (T.isInfixOf "```" result))+                    assertBool "no blockquote prefix" (not (T.isInfixOf "> " result))+                ]+            , testGroup+                "blockquote"+                [ testCase "single line" $ do+                    let result =+                            reassemble+                                [CodeBlock "hs" "x\n" (Just $ CodeOutput MimePlain "hello")]+                    assertBool "blockquoted" (T.isInfixOf "> hello" result)+                , testCase "empty lines in output get bare >" $ do+                    let result =+                            reassemble+                                [CodeBlock "hs" "x\n" (Just $ CodeOutput MimePlain "a\n\nb")]+                    assertBool "has bare >" (T.isInfixOf "\n> \n" result)+                , testCase "renders the scripths:mime marker (not the legacy sabela)" $ do+                    let result =+                            reassemble+                                [CodeBlock "hs" "x\n" (Just $ CodeOutput MimePlain "y")]+                    assertBool "uses scripths:mime" (T.isInfixOf "scripths:mime" result)+                    assertBool "no sabela:mime" (not (T.isInfixOf "sabela:mime" result))+                ]             ]         , testGroup             "idempotency (no extra blank lines on re-run)"@@ -256,6 +321,18 @@                 let once = roundTrip docWithOutput                     twice = roundTrip once                 twice @?= once+            , testCase "raw output re-parses as output and is a fixed point" $ do+                let raw = reassembleWith (RenderOptions DisplayCode OutputRaw)+                    segs =+                        [CodeBlock "haskell" "x = 1\n" (Just $ CodeOutput MimePlain "a\n\nb")]+                    once = raw segs+                case parseMarkdown once of+                    [CodeBlock _ _ (Just (CodeOutput MimePlain body))] ->+                        assertBool "output recovered, not orphaned as prose" $+                            T.isInfixOf "a" body && T.isInfixOf "b" body+                    other ->+                        assertFailure $ "expected one CodeBlock with output, got: " ++ show other+                raw (parseMarkdown once) @?= once             , testCase "newline count does not grow across re-runs" $ do                 let once = roundTrip docWithOutput                     twice = roundTrip once
test/Test/Notebook.hs view
@@ -5,7 +5,15 @@  import qualified Data.Text as T -import ScriptHs.Markdown (CodeOutput (..), MimeType (..), Segment (..))+import ScriptHs.Markdown (+    CodeOutput (..),+    CodeStyle (..),+    MimeType (..),+    RenderOptions (..),+    Segment (..),+    defaultOutputStyle,+    defaultRenderOptions,+ ) import ScriptHs.Notebook (     addOutputToSegments,     generatedMarkedScript,@@ -164,8 +172,15 @@                 result !! 4 @?= Prose "outro\n"                  result !! 1-                    @?= CodeBlock "haskell" "print 1\n" (Just $ CodeOutput MimePlain "out-1")-                result !! 3 @?= CodeBlock "hs" "print 3\n" (Just $ CodeOutput MimePlain "out-3")+                    @?= CodeBlock+                        "haskell"+                        "print 1\n"+                        (Just $ CodeOutput MimePlain "out-1")+                result !! 3+                    @?= CodeBlock+                        "hs"+                        "print 3\n"+                        (Just $ CodeOutput MimePlain "out-3")                  result !! 2 @?= CodeBlock "python" "print('x')\n" Nothing             ]@@ -231,7 +246,8 @@             "processNotebook"             [ testCase "no code blocks => returns input unchanged" $ do                 let input = T.unlines ["# Title", "", "Just prose.", ""]-                out <- processNotebook defaultRunOptions "" input+                out <-+                    processNotebook defaultRenderOptions defaultRunOptions "" input                 out @?= input             , testCase "non-haskell code blocks only => returns input unchanged" $ do                 let input =@@ -244,7 +260,8 @@                             , ""                             , "more prose"                             ]-                out <- processNotebook defaultRunOptions "" input+                out <-+                    processNotebook defaultRenderOptions defaultRunOptions "" input                 out @?= input             ]         , testGroup@@ -264,5 +281,28 @@             , testCase "a marker whose index is NOT a real cell is left intact" $ do                 let out = "real data: " <> mkMarker tn 5                 scrubCellOutput tn [0] out @?= out+            ]+        , testGroup+            "Code style"+            [ testCase "Removed code in final output" $ do+                let input =+                        T.unlines+                            [ "# Title"+                            , "```haskell"+                            , "1 + 1"+                            , "```"+                            ]+                result <-+                    processNotebook+                        (RenderOptions RemoveCode defaultOutputStyle)+                        defaultRunOptions+                        ""+                        input+                result+                    @?= T.unlines+                        [ "# Title"+                        , "> <!-- scripths:mime text/plain -->"+                        , "> 2"+                        ]             ]         ]