├── test ├── snapshots │ ├── .gitattributes │ ├── DodoAlignCurrentColumnIdempotent.output │ ├── DodoWithPosition.output │ ├── DodoLocally.output │ ├── DodoFlexSelect.output │ ├── DodoFlexGroupIndent.output │ ├── DodoAlignCurrentColumn.output │ ├── DodoAnsi.output │ ├── DodoLongLineSmallWidth.output │ ├── DodoFlexPrefixOptimization.output │ ├── DodoFlexSelectHanging.output │ ├── DodoAlignCurrentColumnIdempotent.purs │ ├── DodoAlignCurrentColumn.purs │ ├── DodoLocally.purs │ ├── DodoFlexGroupIndent.purs │ ├── DodoWithPosition.purs │ ├── DodoLongLineSmallWidth.purs │ ├── DodoFlexSelect.purs │ ├── DodoAnsi.purs │ ├── DodoTextParagraph.purs │ ├── DodoRibbonRatio.output │ ├── DodoRibbonRatio.purs │ ├── DodoTextParagraph.output │ ├── DodoFlexPrefixOptimization.purs │ ├── DodoExampleJson.output │ ├── DodoExampleJson.purs │ ├── DodoFlexSelectHanging.purs │ ├── DodoBox.purs │ └── DodoBox.output ├── Bench.purs ├── Main.purs └── Snapshot.purs ├── .gitignore ├── .tidyrc.json ├── package.json ├── .github └── workflows │ └── ci.yml ├── src ├── Dodo │ ├── Internal │ │ └── Buffer.purs │ ├── Common.purs │ ├── Internal.purs │ ├── Ansi.purs │ └── Box.purs └── Dodo.purs ├── spago.yaml ├── LICENSE ├── README.md └── spago.lock /test/snapshots/.gitattributes: -------------------------------------------------------------------------------- 1 | *.output -merge -text 2 | -------------------------------------------------------------------------------- /test/snapshots/DodoAlignCurrentColumnIdempotent.output: -------------------------------------------------------------------------------- 1 | 1234 2 | 1234 3 | -------------------------------------------------------------------------------- /test/snapshots/DodoWithPosition.output: -------------------------------------------------------------------------------- 1 | abcedefg 0 9 0 40 80 2 | abcedefg 3 | 1 5 5 3 10 4 | -------------------------------------------------------------------------------- /test/snapshots/DodoLocally.output: -------------------------------------------------------------------------------- 1 | hello 2 | indented 3 | undented 4 | undented indent 5 | indented again 6 | -------------------------------------------------------------------------------- /test/snapshots/DodoFlexSelect.output: -------------------------------------------------------------------------------- 1 | hello true 2 | first 3 | hello false 4 | second 5 | hello whatever 6 | first 7 | hello whatever 8 | second 9 | -------------------------------------------------------------------------------- /test/snapshots/DodoFlexGroupIndent.output: -------------------------------------------------------------------------------- 1 | a b c 2 | a 3 | b 4 | c 5 | a b c 6 | a 7 | b 8 | c 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | /.vscode 12 | -------------------------------------------------------------------------------- /.tidyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "importSort": "ide", 3 | "importWrap": "source", 4 | "indent": 2, 5 | "operatorsFile": null, 6 | "ribbon": 1, 7 | "typeArrowPlacement": "first", 8 | "unicode": "never", 9 | "width": null 10 | } 11 | -------------------------------------------------------------------------------- /test/snapshots/DodoAlignCurrentColumn.output: -------------------------------------------------------------------------------- 1 | 111 a b c d 2 | e f g 3 | 111111 a b c 4 | d e f 5 | g 6 | 111111111 a 7 | b 8 | c 9 | d 10 | e 11 | f 12 | g 13 | -------------------------------------------------------------------------------- /test/snapshots/DodoAnsi.output: -------------------------------------------------------------------------------- 1 | This is bold text. The end. 2 | Line with no style 3 | Strikethrough 4 | Red 5 | Lines 6 | And bold 7 | The end. 8 | -------------------------------------------------------------------------------- /test/snapshots/DodoLongLineSmallWidth.output: -------------------------------------------------------------------------------- 1 | [ [ [], [ 0 ], [ 0 ], [ 11111111111111111111111111111111111111111111111111111111111111111111 ] ] ] 2 | [ [ [] 3 | , [ 0 ] 4 | , [ 0 ] 5 | , [ 11111111111111111111111111111111111111111111111111111111111111111111 6 | ] 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-dodo-printer", 3 | "type": "module", 4 | "scripts": { 5 | "build": "spago build", 6 | "test": "spago test", 7 | "format": "purs-tidy format-in-place src test" 8 | }, 9 | "devDependencies": { 10 | "purescript": "^0.15.15", 11 | "purs-tidy": "^0.10.1", 12 | "spago": "^0.93.42" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/snapshots/DodoFlexPrefixOptimization.output: -------------------------------------------------------------------------------- 1 | out: a 2 | out: b 3 | out: c 4 | out: d 5 | abcd 6 | out: a 7 | out: b 8 | out: c 9 | out: e 10 | abce 11 | out: a 12 | out: b 13 | out: b 14 | out: c 15 | out: d 16 | abcd 17 | out: a 18 | out: b 19 | out: c 20 | out: e 21 | abce 22 | out: a 23 | out: b 24 | out: c 25 | out: d 26 | out: e 27 | out: b 28 | out: c 29 | out: d 30 | out: e 31 | out: e 32 | out: f 33 | out: g 34 | * a b c d 35 | e f g 36 | -------------------------------------------------------------------------------- /test/snapshots/DodoFlexSelectHanging.output: -------------------------------------------------------------------------------- 1 | foo bar { prop = 42 } baz \a -> a 2 | foo bar { prop = 42 } baz \a -> 3 | a 4 | foo bar { prop = 42 } 5 | baz 6 | \a -> a 7 | foo 8 | bar { prop = 42 } 9 | baz 10 | \a -> a 11 | foo 12 | bar 13 | { prop = 42 14 | } 15 | baz 16 | \a -> a 17 | foo 18 | bar 19 | { prop = 20 | 42 21 | } 22 | baz 23 | \a -> a 24 | foo 25 | bar 26 | { prop = 27 | 42 28 | } 29 | baz 30 | \a -> 31 | a 32 | -------------------------------------------------------------------------------- /test/snapshots/DodoAlignCurrentColumnIdempotent.purs: -------------------------------------------------------------------------------- 1 | module DodoAlignCurrentColumnIdempotent where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, alignCurrentColumn, flexGroup, fourSpaces, plainText, print, softBreak, text) 6 | import Effect (Effect) 7 | import Effect.Class.Console as Console 8 | 9 | test :: forall a. Doc a 10 | test = text "1234" <> aligns 11 | where 12 | aligns = flexGroup 13 | $ alignCurrentColumn 14 | $ alignCurrentColumn 15 | $ alignCurrentColumn 16 | $ softBreak <> text "1234" 17 | 18 | main :: Effect Unit 19 | main = Console.log $ print plainText (fourSpaces { pageWidth = 0 }) test 20 | -------------------------------------------------------------------------------- /test/snapshots/DodoAlignCurrentColumn.purs: -------------------------------------------------------------------------------- 1 | module DodoAlignCurrentColumn where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, alignCurrentColumn, fourSpaces, lines, plainText, print, text, textParagraph, words) 6 | import Effect (Effect) 7 | import Effect.Class.Console as Console 8 | 9 | test1 :: forall a. Doc a 10 | test1 = lines 11 | [ words [ text "111", hanging ] 12 | , words [ text "111111", hanging ] 13 | , words [ text "111111111", hanging ] 14 | ] 15 | where 16 | hanging = alignCurrentColumn $ textParagraph "a b c d e f g" 17 | 18 | main :: Effect Unit 19 | main = do 20 | Console.log $ print plainText (fourSpaces { pageWidth = 12 }) test1 21 | -------------------------------------------------------------------------------- /test/snapshots/DodoLocally.purs: -------------------------------------------------------------------------------- 1 | module DodoLocally where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, fourSpaces, indent, lines, locally, plainText, print, text) 6 | import Effect (Effect) 7 | import Effect.Class.Console as Console 8 | 9 | test1 :: forall a. Doc a 10 | test1 = lines 11 | [ text "hello" 12 | , indent $ lines 13 | [ text "indented" 14 | , locally (_ { indent = 2, indentSpaces = " " }) do 15 | lines 16 | [ text "undented" 17 | , indent $ text "undented indent" 18 | ] 19 | , text "indented again" 20 | ] 21 | ] 22 | 23 | main :: Effect Unit 24 | main = do 25 | Console.log $ print plainText fourSpaces test1 26 | -------------------------------------------------------------------------------- /test/snapshots/DodoFlexGroupIndent.purs: -------------------------------------------------------------------------------- 1 | module DodoFlexGroupIndent where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, align, flexGroup, fourSpaces, indent, paragraph, plainText, print, text) 6 | import Effect (Effect) 7 | import Effect.Class.Console as Console 8 | 9 | test1 :: forall a. Doc a 10 | test1 = flexGroup $ indent $ paragraph 11 | [ text "a" 12 | , text "b" 13 | , text "c" 14 | ] 15 | 16 | test2 :: forall a. Doc a 17 | test2 = flexGroup $ align 20 $ paragraph 18 | [ text "a" 19 | , text "b" 20 | , text "c" 21 | ] 22 | 23 | main :: Effect Unit 24 | main = do 25 | Console.log $ print plainText fourSpaces test1 26 | Console.log $ print plainText (fourSpaces { pageWidth = 0 }) test1 27 | Console.log $ print plainText fourSpaces test2 28 | Console.log $ print plainText (fourSpaces { pageWidth = 0 }) test2 29 | -------------------------------------------------------------------------------- /test/snapshots/DodoWithPosition.purs: -------------------------------------------------------------------------------- 1 | module DodoWithPosition where 2 | 3 | import Prelude 4 | 5 | import Data.Foldable (intercalate) 6 | import Dodo (Doc, align, paragraph, plainText, print, text, twoSpaces, withPosition) 7 | import Effect (Effect) 8 | import Effect.Class.Console as Console 9 | 10 | test1 :: forall a. Doc a 11 | test1 = 12 | paragraph 13 | [ text "abcedefg" 14 | , align 5 $ withPosition \pos -> 15 | text $ intercalate " " 16 | [ show pos.line 17 | , show pos.column 18 | , show pos.indent 19 | , show pos.ribbonWidth 20 | , show pos.pageWidth 21 | ] 22 | ] 23 | 24 | main :: Effect Unit 25 | main = do 26 | Console.log $ print plainText (twoSpaces { ribbonRatio = 0.5 }) test1 27 | Console.log $ print plainText (twoSpaces { ribbonRatio = 0.5, pageWidth = 10 }) test1 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: purescript-contrib/setup-purescript@main 16 | with: 17 | purescript: "0.15.15" 18 | purs-tidy: "latest" 19 | spago: "unstable" 20 | 21 | - name: Cache PureScript dependencies 22 | uses: actions/cache@v4 23 | with: 24 | key: ${{ runner.os }}-spago-${{ hashFiles('**/spago.lock') }} 25 | path: | 26 | .spago 27 | output 28 | 29 | - name: Build source 30 | run: spago build --pure 31 | 32 | - name: Run tests 33 | run: spago test --pure 34 | 35 | - name: Verify formatting 36 | run: purs-tidy check src test 37 | -------------------------------------------------------------------------------- /test/snapshots/DodoLongLineSmallWidth.purs: -------------------------------------------------------------------------------- 1 | module DodoLongLineSmallWidth where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, foldWithSeparator, indent, plainText, print, text, twoSpaces) 6 | import Dodo.Common (leadingComma, pursSquares) 7 | import Effect (Effect) 8 | import Effect.Class.Console as Console 9 | 10 | test1 :: forall a. Doc a 11 | test1 = do 12 | let array = pursSquares <<< foldWithSeparator leadingComma 13 | array 14 | [ indent $ array 15 | [ indent $ array [] 16 | , indent $ array [ indent $ text "0" ] 17 | , indent $ array [ indent $ text "0" ] 18 | , indent $ array [ indent $ text "11111111111111111111111111111111111111111111111111111111111111111111" ] 19 | ] 20 | ] 21 | 22 | main :: Effect Unit 23 | main = do 24 | Console.log $ print plainText (twoSpaces { pageWidth = 200 }) test1 25 | Console.log $ print plainText (twoSpaces { pageWidth = 20 }) test1 26 | -------------------------------------------------------------------------------- /test/snapshots/DodoFlexSelect.purs: -------------------------------------------------------------------------------- 1 | module DodoFlexSelect where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, break, flexAlt, flexSelect, fourSpaces, indent, plainText, print, text, words) 6 | import Effect (Effect) 7 | import Effect.Class.Console as Console 8 | 9 | test1 :: forall a. Doc a 10 | test1 = words 11 | [ text "hello" 12 | , indent $ flexSelect 13 | (flexAlt (text "true") (text "false")) 14 | (break <> text "first") 15 | (break <> text "second") 16 | ] 17 | 18 | test2 :: forall a. Doc a 19 | test2 = words 20 | [ text "hello" 21 | , indent $ flexSelect 22 | (text "whatever") 23 | (break <> text "first") 24 | (break <> text "second") 25 | ] 26 | 27 | main :: Effect Unit 28 | main = do 29 | Console.log $ print plainText fourSpaces test1 30 | Console.log $ print plainText (fourSpaces { pageWidth = 1 }) test1 31 | Console.log $ print plainText fourSpaces test2 32 | Console.log $ print plainText (fourSpaces { pageWidth = 1 }) test2 33 | -------------------------------------------------------------------------------- /test/Bench.purs: -------------------------------------------------------------------------------- 1 | module Test.Bench where 2 | 3 | import Prelude 4 | 5 | import Data.Monoid (power) 6 | import Dodo (Doc, align, break, plainText, print, text, textParagraph, twoSpaces) 7 | import Effect (Effect) 8 | import Performance.Minibench (benchWith) 9 | 10 | test :: forall a. Doc a 11 | test = point latin <> break 12 | where 13 | point = append (text "* ") <<< align 2 <<< textParagraph 14 | latin = 15 | """ 16 | Quisque finibus tellus non molestie porta. In non posuere metus, vitae 17 | tincidunt enim. Nam quis elit pharetra, elementum elit lacinia, efficitur 18 | nibh. Cras lobortis neque sed ante ornare rutrum. Maecenas sed urna nisl. 19 | Phasellus aliquam finibus ex vitae iaculis. Vestibulum ante ipsum primis 20 | in faucibus orci luctus et ultrices posuere cubilia curae; Suspendisse 21 | eget tortor eget sapien tincidunt vestibulum eu a velit. Pellentesque eu 22 | tortor ut lectus sodales ornare. 23 | """ 24 | 25 | main :: Effect Unit 26 | main = do 27 | benchWith 3000 \_ -> print plainText twoSpaces (power test 10) 28 | -------------------------------------------------------------------------------- /src/Dodo/Internal/Buffer.purs: -------------------------------------------------------------------------------- 1 | module Dodo.Internal.Buffer where 2 | 3 | import Prelude 4 | 5 | import Data.List (List(..), drop, foldr, null, (:)) 6 | 7 | newtype Buffer b = Buffer 8 | { buffer :: b 9 | , queue :: List (List (b -> b)) 10 | } 11 | 12 | branch :: forall b. Buffer b -> Buffer b 13 | branch (Buffer b) = Buffer b { queue = Nil : b.queue } 14 | 15 | commit :: forall b. Buffer b -> Buffer b 16 | commit (Buffer b) = Buffer 17 | { buffer: foldr (flip (foldr ($))) b.buffer b.queue 18 | , queue: Nil 19 | } 20 | 21 | revert :: forall b. Buffer b -> Buffer b 22 | revert (Buffer b) = Buffer b { queue = drop 1 b.queue } 23 | 24 | modify :: forall b. (b -> b) -> Buffer b -> Buffer b 25 | modify f (Buffer b) = case b.queue of 26 | fs : queue -> 27 | Buffer b { queue = (f : fs) : queue } 28 | _ -> 29 | Buffer b { buffer = f b.buffer } 30 | 31 | get :: forall b. Buffer b -> b 32 | get = commit >>> \(Buffer b) -> b.buffer 33 | 34 | new :: forall b. b -> Buffer b 35 | new buffer = Buffer { buffer, queue: Nil } 36 | 37 | isBranching :: forall b. Buffer b -> Boolean 38 | isBranching (Buffer b) = not $ null b.queue 39 | -------------------------------------------------------------------------------- /spago.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: dodo-printer 3 | description: An adequate printer. 4 | publish: 5 | version: 2.2.3 6 | license: MIT 7 | location: 8 | githubOwner: natefaubion 9 | githubRepo: purescript-dodo-printer 10 | build: 11 | pedanticPackages: true 12 | strict: true 13 | dependencies: 14 | - ansi: ">=7.0.0 <8.0.0" 15 | - either: ">=6.1.0 <7.0.0" 16 | - foldable-traversable: ">=6.0.0 <7.0.0" 17 | - integers: ">=6.0.0 <7.0.0" 18 | - lists: ">=7.0.0 <8.0.0" 19 | - maybe: ">=6.0.0 <7.0.0" 20 | - newtype: ">=5.0.0 <6.0.0" 21 | - partial: ">=4.0.0 <5.0.0" 22 | - prelude: ">=6.0.1 <7.0.0" 23 | - safe-coerce: ">=2.0.0 <3.0.0" 24 | - strings: ">=6.0.1 <7.0.0" 25 | - tuples: ">=7.0.0 <8.0.0" 26 | test: 27 | main: Test.Main 28 | dependencies: 29 | - aff 30 | - avar 31 | - console 32 | - effect 33 | - exceptions 34 | - minibench 35 | - node-buffer 36 | - node-child-process 37 | - node-fs 38 | - node-path 39 | - node-process 40 | - node-streams 41 | - parallel 42 | workspace: {} 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Nathan Faubion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/snapshots/DodoAnsi.purs: -------------------------------------------------------------------------------- 1 | module DodoAnsi where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, indent, lines, print, text, twoSpaces, words) 6 | import Dodo.Ansi as Ansi 7 | import Effect (Effect) 8 | import Effect.Class.Console as Console 9 | 10 | test1 :: Doc Ansi.GraphicsParam 11 | test1 = 12 | Ansi.foreground Ansi.Red $ Ansi.strikethrough $ words 13 | [ text "This is" 14 | , Ansi.foreground Ansi.Blue $ Ansi.bold $ text "bold" 15 | , Ansi.reset $ Ansi.underline $ text "text." 16 | , text "The end." 17 | ] 18 | 19 | test2 :: Doc Ansi.GraphicsParam 20 | test2 = 21 | lines 22 | [ text "Line with no style" 23 | , Ansi.background Ansi.Yellow $ Ansi.bold $ lines 24 | [ Ansi.strikethrough $ text "Strikethrough" 25 | , indent $ Ansi.foreground Ansi.Red $ lines 26 | [ text "Red" 27 | , Ansi.reset $ text "Lines" 28 | , text "And bold" 29 | ] 30 | , text "The end." 31 | ] 32 | ] 33 | 34 | main :: Effect Unit 35 | main = do 36 | Console.log $ print Ansi.ansiGraphics twoSpaces test1 37 | Console.log $ print Ansi.ansiGraphics twoSpaces test2 38 | -------------------------------------------------------------------------------- /test/snapshots/DodoTextParagraph.purs: -------------------------------------------------------------------------------- 1 | module DodoTextParagraph where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, align, break, plainText, print, text, textParagraph, twoSpaces) 6 | import Effect (Effect) 7 | import Effect.Class.Console as Console 8 | 9 | test :: forall a. Doc a 10 | test = point latin <> break 11 | where 12 | point = append (text "* ") <<< align 2 <<< textParagraph 13 | latin = 14 | """ 15 | Quisque finibus tellus non molestie porta. In non posuere metus, vitae 16 | tincidunt enim. Nam quis elit pharetra, elementum elit lacinia, efficitur 17 | nibh. Cras lobortis neque sed ante ornare rutrum. Maecenas sed urna nisl. 18 | Phasellus aliquam finibus ex vitae iaculis. Vestibulum ante ipsum primis 19 | in faucibus orci luctus et ultrices posuere cubilia curae; Suspendisse 20 | eget tortor eget sapien tincidunt vestibulum eu a velit. Pellentesque eu 21 | tortor ut lectus sodales ornare. 22 | """ 23 | 24 | main :: Effect Unit 25 | main = do 26 | Console.log $ print plainText (twoSpaces { pageWidth = 160 }) test 27 | Console.log $ print plainText (twoSpaces { pageWidth = 80 }) test 28 | Console.log $ print plainText (twoSpaces { pageWidth = 40 }) test 29 | -------------------------------------------------------------------------------- /test/snapshots/DodoRibbonRatio.output: -------------------------------------------------------------------------------- 1 | 1234567890---------- 2 | abcdefghij 3 | klmnopqrst 4 | uvwxyz 5 | 12345678-------- 6 | abcdefgh 7 | ijklmnop 8 | qrstuvwx 9 | yz 10 | 123456------ 11 | abcdef 12 | ghijkl 13 | mnopqr 14 | stuvwx 15 | yz 16 | 1234---- 17 | abcd 18 | efgh 19 | ijkl 20 | mnop 21 | qrst 22 | uvwx 23 | yz 24 | 12-- 25 | ab 26 | cd 27 | ef 28 | gh 29 | ij 30 | kl 31 | mn 32 | op 33 | qr 34 | st 35 | uv 36 | wx 37 | yz 38 | 123456789012345678901234567890---------- 39 | abcdefghijklmnopqrstuvwxyz 40 | 123456789012345678901234567--------- 41 | abcdefghijklmnopqrstuvwxyz 42 | 123456789012345678901234-------- 43 | abcdefghijklmnopqrstuvwx 44 | yz 45 | 123456789012345678901------- 46 | abcdefghijklmnopqrstu 47 | vwxyz 48 | 123456789012345678------ 49 | abcdefghijklmnopqr 50 | stuvwxyz 51 | -------------------------------------------------------------------------------- /test/snapshots/DodoRibbonRatio.purs: -------------------------------------------------------------------------------- 1 | module DodoRibbonRatio where 2 | 3 | import Prelude 4 | 5 | import Data.Foldable (foldl) 6 | import Data.Int as Int 7 | import Data.Monoid (power) 8 | import Data.String (Pattern(..)) 9 | import Data.String as String 10 | import Data.String.CodeUnits as SCU 11 | import Dodo (Doc, flexGroup, fourSpaces, indent, lines, plainText, print, softBreak, text, withPosition) 12 | import Effect (Effect) 13 | import Effect.Class.Console as Console 14 | 15 | pageArea :: forall a. Doc a 16 | pageArea = withPosition \pos -> do 17 | let restWidth = pos.pageWidth - pos.ribbonWidth - pos.indent 18 | let printable = SCU.take pos.ribbonWidth $ power "1234567890" $ Int.ceil (Int.toNumber pos.ribbonWidth / 10.0) 19 | let rest = power "-" restWidth 20 | text (printable <> rest) 21 | 22 | test :: forall a. Doc a 23 | test = lines 24 | [ chunk 25 | , indent chunk 26 | , indent $ indent chunk 27 | , indent $ indent $ indent chunk 28 | , indent $ indent $ indent $ indent chunk 29 | ] 30 | where 31 | chunk = 32 | lines [ pageArea, letters ] 33 | 34 | letters = 35 | foldl appendSoftBreak mempty 36 | $ map text 37 | $ String.split (Pattern "") 38 | $ "abcdefghijklmnopqrstuvwxyz" 39 | 40 | appendSoftBreak a b = 41 | a <> flexGroup (softBreak <> b) 42 | 43 | main :: Effect Unit 44 | main = do 45 | Console.log $ print plainText (fourSpaces { pageWidth = 20, ribbonRatio = 0.5 }) test 46 | Console.log $ print plainText (fourSpaces { pageWidth = 40, ribbonRatio = 0.75 }) test 47 | -------------------------------------------------------------------------------- /test/snapshots/DodoTextParagraph.output: -------------------------------------------------------------------------------- 1 | * Quisque finibus tellus non molestie porta. In non posuere metus, vitae tincidunt enim. Nam quis elit pharetra, elementum elit lacinia, efficitur nibh. Cras 2 | lobortis neque sed ante ornare rutrum. Maecenas sed urna nisl. Phasellus aliquam finibus ex vitae iaculis. Vestibulum ante ipsum primis in faucibus orci 3 | luctus et ultrices posuere cubilia curae; Suspendisse eget tortor eget sapien tincidunt vestibulum eu a velit. Pellentesque eu tortor ut lectus sodales 4 | ornare. 5 | 6 | * Quisque finibus tellus non molestie porta. In non posuere metus, vitae 7 | tincidunt enim. Nam quis elit pharetra, elementum elit lacinia, efficitur 8 | nibh. Cras lobortis neque sed ante ornare rutrum. Maecenas sed urna nisl. 9 | Phasellus aliquam finibus ex vitae iaculis. Vestibulum ante ipsum primis in 10 | faucibus orci luctus et ultrices posuere cubilia curae; Suspendisse eget 11 | tortor eget sapien tincidunt vestibulum eu a velit. Pellentesque eu tortor ut 12 | lectus sodales ornare. 13 | 14 | * Quisque finibus tellus non molestie 15 | porta. In non posuere metus, vitae 16 | tincidunt enim. Nam quis elit 17 | pharetra, elementum elit lacinia, 18 | efficitur nibh. Cras lobortis neque 19 | sed ante ornare rutrum. Maecenas sed 20 | urna nisl. Phasellus aliquam finibus 21 | ex vitae iaculis. Vestibulum ante 22 | ipsum primis in faucibus orci luctus 23 | et ultrices posuere cubilia curae; 24 | Suspendisse eget tortor eget sapien 25 | tincidunt vestibulum eu a velit. 26 | Pellentesque eu tortor ut lectus 27 | sodales ornare. 28 | 29 | -------------------------------------------------------------------------------- /test/snapshots/DodoFlexPrefixOptimization.purs: -------------------------------------------------------------------------------- 1 | module DodoFlexPrefixOptimization where 2 | 3 | import Prelude 4 | 5 | import Data.Foldable (fold) 6 | import Dodo (Doc, align, flexAlt, flexGroup, fourSpaces, paragraph, plainText, print, text, withPosition) 7 | import Effect (Effect) 8 | import Effect.Class.Console as Console 9 | import Effect.Unsafe (unsafePerformEffect) 10 | 11 | logText :: forall a. String -> Doc a 12 | logText str = withPosition \_ -> 13 | unsafePerformEffect do 14 | Console.log $ "out: " <> str 15 | pure $ text str 16 | 17 | test1 :: forall a. Doc a 18 | test1 = flexGroup $ fold 19 | [ logText "a" 20 | , logText "b" 21 | , logText "c" 22 | , flexAlt (logText "e") (logText "d") 23 | ] 24 | 25 | test2 :: forall a. Doc a 26 | test2 = flexGroup $ fold 27 | [ logText "a" 28 | , flexGroup $ fold 29 | [ logText "b" 30 | , flexGroup $ fold 31 | [ logText "c" 32 | , flexAlt (logText "e") (logText "d") 33 | ] 34 | ] 35 | ] 36 | 37 | -- Bad output due to aggressive optimization: 38 | -- * a 39 | -- b c d e 40 | -- f g 41 | test3 :: forall a. Doc a 42 | test3 = flexGroup $ text "* " <> align 2 letters 43 | where 44 | letters = 45 | paragraph $ map logText 46 | [ "a" 47 | , "b" 48 | , "c" 49 | , "d" 50 | , "e" 51 | , "f" 52 | , "g" 53 | ] 54 | 55 | main :: Effect Unit 56 | main = do 57 | Console.log $ print plainText (fourSpaces { pageWidth = 1 }) test1 58 | Console.log $ print plainText fourSpaces test1 59 | Console.log $ print plainText (fourSpaces { pageWidth = 1 }) test2 60 | Console.log $ print plainText fourSpaces test2 61 | Console.log $ print plainText (fourSpaces { pageWidth = 10 }) test3 62 | -------------------------------------------------------------------------------- /test/snapshots/DodoExampleJson.output: -------------------------------------------------------------------------------- 1 | {"bool": true, "string": "bar", "number": 1234.0, "array": [null, "two", ["three"]], "object": {}, "wideObject": {"key": "1111111111111111111111111111111111111111"}} 2 | {"bool": true, "string": "bar", "number": 1234.0, "array": [null, "two", ["three"]], "object": {}, "wideObject": {"key": "1111111111111111111111111111111111111111"}} 3 | { 4 | "bool": true, 5 | "string": "bar", 6 | "number": 1234.0, 7 | "array": [null, "two", ["three"]], 8 | "object": {}, 9 | "wideObject": {"key": "1111111111111111111111111111111111111111"} 10 | } 11 | { 12 | "bool": true, 13 | "string": "bar", 14 | "number": 1234.0, 15 | "array": [null, "two", ["three"]], 16 | "object": {}, 17 | "wideObject": {"key": "1111111111111111111111111111111111111111"} 18 | } 19 | { 20 | "bool": true, 21 | "string": "bar", 22 | "number": 1234.0, 23 | "array": [ 24 | null, 25 | "two", 26 | ["three"] 27 | ], 28 | "object": {}, 29 | "wideObject": { 30 | "key": "1111111111111111111111111111111111111111" 31 | } 32 | } 33 | { 34 | "bool": true, 35 | "string": "bar", 36 | "number": 1234.0, 37 | "array": [ 38 | null, 39 | "two", 40 | ["three"] 41 | ], 42 | "object": {}, 43 | "wideObject": { 44 | "key": "1111111111111111111111111111111111111111" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Dodo/Common.purs: -------------------------------------------------------------------------------- 1 | module Dodo.Common where 2 | 3 | import Prelude 4 | 5 | import Dodo (Doc, break, enclose, encloseEmptyAlt, flexAlt, flexGroup, indent, text) 6 | 7 | leadingComma :: forall a. Doc a 8 | leadingComma = flexAlt (text ", ") (break <> text ", ") 9 | 10 | trailingComma :: forall a. Doc a 11 | trailingComma = flexAlt (text ", ") (text "," <> break) 12 | 13 | pursCurlies :: forall a. Doc a -> Doc a 14 | pursCurlies = flexGroup <<< encloseEmptyAlt open close (text "{}") 15 | where 16 | open = text "{ " 17 | close = flexAlt (text " }") (break <> text "}") 18 | 19 | pursSquares :: forall a. Doc a -> Doc a 20 | pursSquares = flexGroup <<< encloseEmptyAlt open close (text "[]") 21 | where 22 | open = text "[ " 23 | close = flexAlt (text " ]") (break <> text "]") 24 | 25 | pursParens :: forall a. Doc a -> Doc a 26 | pursParens = flexGroup <<< encloseEmptyAlt open close (text "()") 27 | where 28 | open = flexAlt (text "(") (text "( ") 29 | close = flexAlt (text ")") (break <> text ")") 30 | 31 | pursParensExpr :: forall a. Doc a -> Doc a 32 | pursParensExpr = flexGroup <<< enclose open close <<< indent 33 | where 34 | open = text "(" 35 | close = text ")" 36 | 37 | jsCurlies :: forall a. Doc a -> Doc a 38 | jsCurlies = flexGroup <<< encloseEmptyAlt open close (text "{}") <<< indent 39 | where 40 | open = flexAlt (text "{") (text "{" <> break) 41 | close = flexAlt (text "}") (break <> text "}") 42 | 43 | jsSquares :: forall a. Doc a -> Doc a 44 | jsSquares = flexGroup <<< encloseEmptyAlt open close (text "[]") <<< indent 45 | where 46 | open = flexAlt (text "[") (text "[" <> break) 47 | close = flexAlt (text "]") (break <> text "]") 48 | 49 | jsParens :: forall a. Doc a -> Doc a 50 | jsParens = flexGroup <<< encloseEmptyAlt open close (text "()") <<< indent 51 | where 52 | open = flexAlt (text "(") (text "(" <> break) 53 | close = flexAlt (text ")") (break <> text ")") 54 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Ansi.Output (foreground, withGraphics) 6 | import Data.Foldable (any, findMap, for_) 7 | import Data.String (Pattern(..)) 8 | import Data.String as String 9 | import Dodo.Ansi (Color(..)) 10 | import Effect (Effect) 11 | import Effect.Aff (launchAff_) 12 | import Effect.Class (liftEffect) 13 | import Effect.Class.Console as Console 14 | import Effect.Exception as Error 15 | import Node.Process as Process 16 | import Test.Snapshot (SnapshotResult(..), isBad, snapshotMainOutput) 17 | 18 | main :: Effect Unit 19 | main = do 20 | args <- Process.argv 21 | let accept = any (eq "--accept" || eq "-a") args 22 | let printOutput = any (eq "--print-output" || eq "-p") args 23 | let filter = Pattern <$> findMap (String.stripPrefix (Pattern "--filter=")) args 24 | launchAff_ do 25 | results <- snapshotMainOutput "./test/snapshots" accept filter 26 | for_ results \{ name, output, result } -> case result of 27 | Passed -> do 28 | Console.log $ withGraphics (foreground Green) "✓" <> " " <> name <> " passed." 29 | when printOutput $ Console.log output 30 | Saved -> do 31 | Console.log $ withGraphics (foreground Yellow) "✓" <> " " <> name <> " saved." 32 | when printOutput $ Console.log output 33 | Accepted -> do 34 | Console.log $ withGraphics (foreground Yellow) "✓" <> " " <> name <> " accepted." 35 | when printOutput $ Console.log output 36 | Failed diff -> do 37 | Console.log $ withGraphics (foreground Red) "✗" <> " " <> name <> " failed." 38 | Console.log diff 39 | when printOutput $ Console.log output 40 | ErrorRunningTest err -> do 41 | Console.log $ withGraphics (foreground Red) "✗" <> " " <> name <> " failed due to an error." 42 | Console.log $ Error.message err 43 | when (any (isBad <<< _.result) results) do 44 | liftEffect $ Process.exit' 1 45 | -------------------------------------------------------------------------------- /src/Dodo/Internal.purs: -------------------------------------------------------------------------------- 1 | module Dodo.Internal where 2 | 3 | import Prelude 4 | 5 | import Data.Tuple (Tuple) 6 | 7 | -- | Document lines and columns are 0-based offsets. 8 | type Position = 9 | { line :: Int 10 | , column :: Int 11 | , indent :: Int 12 | , nextIndent :: Int 13 | , pageWidth :: Int 14 | , ribbonWidth :: Int 15 | } 16 | 17 | -- | Documents are built using `<>` as horizontal, line-wise concatenation. 18 | -- | The functions in this module let you build documents that respond well 19 | -- | to width constraints (such as `flexGroup` and `flexAlt`). 20 | data Doc a 21 | = Append (Doc a) (Doc a) 22 | | Indent (Doc a) 23 | | Align Int (Doc a) 24 | | Annotate a (Doc a) 25 | | FlexSelect (Doc a) (Doc a) (Doc a) 26 | | FlexAlt (Doc a) (Doc a) 27 | | WithPosition (Position -> Doc a) 28 | | Local (LocalOptions -> Tuple LocalOptions (Doc a)) 29 | | Text Int String 30 | | Break 31 | | Empty 32 | 33 | type LocalOptions = 34 | { indent :: Int 35 | , indentSpaces :: String 36 | , indentUnit :: String 37 | , indentWidth :: Int 38 | , pageWidth :: Int 39 | , ribbonRatio :: Number 40 | } 41 | 42 | derive instance functorDoc :: Functor Doc 43 | 44 | instance semigroupDoc :: Semigroup (Doc a) where 45 | append = bothNotEmpty case _, _ of 46 | Text n1 str1, Text n2 str2 -> Text (n1 + n2) (str1 <> str2) 47 | a, b -> Append a b 48 | 49 | instance monoidDoc :: Monoid (Doc a) where 50 | mempty = Empty 51 | 52 | -- | Only applies the provided function if both documents are 53 | -- | non-empty, otherwise just yields whichever is non-empty. 54 | bothNotEmpty :: forall a. (Doc a -> Doc a -> Doc a) -> Doc a -> Doc a -> Doc a 55 | bothNotEmpty f = case _, _ of 56 | Empty, b -> b 57 | a, Empty -> a 58 | a, b -> f a b 59 | 60 | -- | Only applies the provided function if the document is non-empty. 61 | notEmpty :: forall a. (Doc a -> Doc a) -> Doc a -> Doc a 62 | notEmpty f = case _ of 63 | Empty -> Empty 64 | b -> f b 65 | 66 | -- | Checks whether the document is empty. 67 | isEmpty :: forall a. Doc a -> Boolean 68 | isEmpty = case _ of 69 | Empty -> true 70 | _ -> false 71 | -------------------------------------------------------------------------------- /test/snapshots/DodoExampleJson.purs: -------------------------------------------------------------------------------- 1 | module DodoExampleJson where 2 | 3 | import Prelude 4 | 5 | import Ansi.Codes (Color(..), GraphicsParam) 6 | import Data.Foldable (fold) 7 | import Data.Tuple (Tuple(..)) 8 | import Dodo (Doc, foldWithSeparator, plainText, print, text, twoSpaces) 9 | import Dodo.Ansi (ansiGraphics, dim, foreground) 10 | import Dodo.Common (jsCurlies, jsSquares, trailingComma) 11 | import Effect (Effect) 12 | import Effect.Class.Console as Console 13 | 14 | data Json 15 | = JNull 16 | | JString String 17 | | JNumber Number 18 | | JBool Boolean 19 | | JArray (Array Json) 20 | | JObject (Array (Tuple String Json)) 21 | 22 | printJson :: Json -> Doc GraphicsParam 23 | printJson = case _ of 24 | JNull -> dim $ text "null" 25 | JString str -> foreground Red $ text $ show str 26 | JNumber num -> foreground Green $ text $ show num 27 | JBool bool -> foreground Blue $ text $ show bool 28 | JArray arr -> jsSquares $ foldWithSeparator trailingComma $ map printJson arr 29 | JObject obj -> jsCurlies $ foldWithSeparator trailingComma $ map printKeyValue obj 30 | where 31 | printKeyValue (Tuple key value) = 32 | fold 33 | [ foreground Yellow $ text $ show key 34 | , text ": " 35 | , printJson value 36 | ] 37 | 38 | infix 0 Tuple as : 39 | 40 | exampleJson :: Json 41 | exampleJson = 42 | JObject 43 | [ "bool" : JBool true 44 | , "string" : JString "bar" 45 | , "number" : JNumber 1234.0 46 | , "array" : JArray 47 | [ JNull 48 | , JString "two" 49 | , JArray [ JString "three" ] 50 | ] 51 | , "object" : JObject 52 | [] 53 | , "wideObject" : JObject 54 | [ "key" : JString "1111111111111111111111111111111111111111" 55 | ] 56 | ] 57 | 58 | main :: Effect Unit 59 | main = do 60 | let json = printJson exampleJson 61 | Console.log $ print plainText (twoSpaces { pageWidth = 200 }) json 62 | Console.log $ print ansiGraphics (twoSpaces { pageWidth = 200 }) json 63 | Console.log $ print plainText (twoSpaces { pageWidth = 80 }) json 64 | Console.log $ print ansiGraphics (twoSpaces { pageWidth = 80 }) json 65 | Console.log $ print plainText (twoSpaces { pageWidth = 20 }) json 66 | Console.log $ print ansiGraphics (twoSpaces { pageWidth = 20 }) json 67 | -------------------------------------------------------------------------------- /test/snapshots/DodoFlexSelectHanging.purs: -------------------------------------------------------------------------------- 1 | module DodoFlexSelectHanging where 2 | 3 | import Prelude 4 | 5 | import Data.Foldable (fold, foldr) 6 | import Data.Tuple (Tuple(..), fst, snd) 7 | import Dodo (Doc, break, flexGroup, flexSelect, indent, plainText, print, space, spaceBreak, text, twoSpaces) 8 | import Dodo.Common (pursCurlies) 9 | import Effect (Effect) 10 | import Effect.Class.Console as Console 11 | 12 | data Hanging a 13 | = NoHang (Doc a) 14 | | Hang (Doc a) (Doc a) 15 | 16 | arg1 :: forall a. Hanging a 17 | arg1 = 18 | Hang (text "bar") $ pursCurlies $ flexGroup $ fold 19 | [ text "prop" 20 | , space 21 | , text "=" 22 | , spaceBreak 23 | , indent $ text "42" 24 | ] 25 | 26 | arg2 :: forall a. Hanging a 27 | arg2 = NoHang $ text "baz" 28 | 29 | arg3 :: forall a. Hanging a 30 | arg3 = Hang (text "\\a ->") (text "a") 31 | 32 | joinArgs :: forall a. Array (Hanging a) -> Hanging a -> Doc a 33 | joinArgs args last = fst $ foldr go start args 34 | where 35 | start = case last of 36 | Hang a b -> 37 | Tuple 38 | ( flexSelect 39 | (spaceBreak <> a) 40 | (flexGroup (spaceBreak <> b)) 41 | (flexGroup (spaceBreak <> indent b)) 42 | ) 43 | (break <> a <> flexGroup (spaceBreak <> indent b)) 44 | NoHang a -> 45 | Tuple 46 | (flexGroup (spaceBreak <> a)) 47 | (break <> a) 48 | 49 | go hdoc next = do 50 | let 51 | doc = case hdoc of 52 | NoHang a -> 53 | a 54 | Hang a b -> 55 | a <> flexGroup (spaceBreak <> indent b) 56 | Tuple 57 | ( flexSelect 58 | (spaceBreak <> doc) 59 | (fst next) 60 | (snd next) 61 | ) 62 | (break <> doc <> snd next) 63 | 64 | test :: forall a. Doc a 65 | test = text "foo" <> indent (joinArgs [ arg1, arg2 ] arg3) 66 | 67 | main :: Effect Unit 68 | main = do 69 | Console.log $ print plainText twoSpaces test 70 | Console.log $ print plainText (twoSpaces { pageWidth = 32 }) test 71 | Console.log $ print plainText (twoSpaces { pageWidth = 24 }) test 72 | Console.log $ print plainText (twoSpaces { pageWidth = 20 }) test 73 | Console.log $ print plainText (twoSpaces { pageWidth = 16 }) test 74 | Console.log $ print plainText (twoSpaces { pageWidth = 10 }) test 75 | Console.log $ print plainText (twoSpaces { pageWidth = 1 }) test 76 | -------------------------------------------------------------------------------- /test/Snapshot.purs: -------------------------------------------------------------------------------- 1 | module Test.Snapshot where 2 | 3 | import Prelude 4 | 5 | import Control.Alternative (guard) 6 | import Control.Parallel (parTraverse) 7 | import Data.Array (mapMaybe) 8 | import Data.Array as Array 9 | import Data.Either (Either(..)) 10 | import Data.Foldable (for_) 11 | import Data.Maybe (Maybe(..)) 12 | import Data.String (Pattern(..)) 13 | import Data.String as String 14 | import Data.String.CodeUnits (stripSuffix) 15 | import Effect (Effect) 16 | import Effect.AVar as EffectAVar 17 | import Effect.Aff (Aff, Error, catchError, effectCanceler, makeAff, throwError, try) 18 | import Effect.Aff.AVar as AVar 19 | import Effect.Class (liftEffect) 20 | import Node.Buffer (Buffer, freeze) 21 | import Node.Buffer as Buffer 22 | import Node.Buffer.Immutable as ImmutableBuffer 23 | import Node.ChildProcess (ExecResult) 24 | import Node.ChildProcess as ChildProcess 25 | import Node.Encoding (Encoding(..)) 26 | import Node.Errors.SystemError as SystemError 27 | import Node.FS.Aff (readFile, readdir, writeFile) 28 | import Node.Path (basename) 29 | import Node.Path as Path 30 | import Node.Stream as Stream 31 | 32 | data SnapshotResult 33 | = Passed 34 | | Saved 35 | | Accepted 36 | | Failed String 37 | | ErrorRunningTest Error 38 | 39 | type SnapshotTest = 40 | { name :: String 41 | , output :: String 42 | , result :: SnapshotResult 43 | } 44 | 45 | isBad :: SnapshotResult -> Boolean 46 | isBad = case _ of 47 | Failed _ -> true 48 | ErrorRunningTest _ -> true 49 | _ -> false 50 | 51 | snapshotMainOutput :: String -> Boolean -> Maybe Pattern -> Aff (Array SnapshotTest) 52 | snapshotMainOutput directory accept mbPattern = do 53 | paths <- readdir directory 54 | block <- AVar.empty 55 | for_ (Array.range 1 4) \_ -> do 56 | liftEffect $ EffectAVar.put unit block mempty 57 | flip parTraverse (pursPaths paths) \name -> do 58 | AVar.take block 59 | result <- runSnapshot name 60 | _ <- liftEffect $ EffectAVar.put unit block mempty 61 | pure result 62 | where 63 | pursPaths = 64 | mapMaybe (filterPath <=< stripSuffix (Pattern ".purs") <<< basename) 65 | 66 | filterPath = case mbPattern of 67 | Just pat -> \path -> guard (String.contains pat path) $> path 68 | Nothing -> pure 69 | 70 | makeErrorResult :: String -> Error -> Aff SnapshotTest 71 | makeErrorResult name err = pure { name, output: "", result: ErrorRunningTest err } 72 | 73 | runSnapshot :: String -> Aff SnapshotTest 74 | runSnapshot name = flip catchError (makeErrorResult name) do 75 | result <- exec $ "node --input-type=module -e 'import { main } from \"./output/" <> name <> "/index.js\";main()'" 76 | case result of 77 | { error: Just err } -> 78 | throwError (SystemError.toError err) 79 | { stdout } -> do 80 | output <- liftEffect $ bufferToUTF8 stdout 81 | let 82 | outputFile = Path.concat [ directory, name <> ".output" ] 83 | acceptOutput = do 84 | writeFile outputFile =<< liftEffect (Buffer.fromString output UTF8) 85 | savedOutput <- try $ readFile outputFile 86 | case savedOutput of 87 | Left _ -> do 88 | acceptOutput 89 | pure { name, output, result: Saved } 90 | Right buffer -> do 91 | savedOutput' <- liftEffect $ bufferToUTF8 buffer 92 | if output == savedOutput' then 93 | pure { name, output, result: Passed } 94 | else if accept then do 95 | acceptOutput 96 | pure { name, output, result: Accepted } 97 | else do 98 | { stdout: diffOutput } <- execWithStdin ("diff " <> outputFile <> " -") output 99 | diffOutput' <- liftEffect $ bufferToUTF8 diffOutput 100 | pure { name, output, result: Failed diffOutput' } 101 | 102 | exec :: String -> Aff ExecResult 103 | exec command = makeAff \k -> do 104 | childProc <- ChildProcess.exec' command identity (k <<< Right) 105 | pure $ effectCanceler $ void (ChildProcess.kill childProc) 106 | 107 | execWithStdin :: String -> String -> Aff ExecResult 108 | execWithStdin command input = makeAff \k -> do 109 | childProc <- ChildProcess.exec' command identity (k <<< Right) 110 | _ <- Stream.writeString (ChildProcess.stdin childProc) UTF8 input 111 | Stream.end (ChildProcess.stdin childProc) 112 | pure $ effectCanceler $ void (ChildProcess.kill childProc) 113 | 114 | bufferToUTF8 :: Buffer -> Effect String 115 | bufferToUTF8 = map (ImmutableBuffer.toString UTF8) <<< freeze 116 | -------------------------------------------------------------------------------- /test/snapshots/DodoBox.purs: -------------------------------------------------------------------------------- 1 | module DodoBox where 2 | 3 | import Prelude 4 | 5 | import Ansi.Codes (GraphicsParam) 6 | import Data.Array (intersperse) 7 | import Data.Array as Array 8 | import Data.Maybe (fromMaybe) 9 | import Dodo (Doc, plainText, print, textParagraph, twoSpaces) 10 | import Dodo as Dodo 11 | import Dodo.Ansi (ansiGraphics) 12 | import Dodo.Ansi as Ansi 13 | import Dodo.Box (Align(..), DocBox, docBox, fill, halign, horizontal, hpadding, resize, sizeOf, valign, vertical) 14 | import Dodo.Box as Box 15 | import DodoExampleJson (exampleJson, printJson) 16 | import Effect (Effect) 17 | import Effect.Class.Console as Console 18 | 19 | para2 :: forall a. Doc a 20 | para2 = textParagraph 21 | """ 22 | Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere 23 | cubilia curae; Suspendisse eget tortor. 24 | """ 25 | 26 | textBox :: forall a. Int -> Doc a -> DocBox a 27 | textBox pageWidth = print docBox (twoSpaces { pageWidth = pageWidth }) 28 | 29 | heading :: DocBox GraphicsParam -> DocBox GraphicsParam -> DocBox GraphicsParam 30 | heading head body = 31 | vertical 32 | [ head 33 | , fill (Ansi.dim (Dodo.text "-")) 34 | { width: max (sizeOf head).width (sizeOf body).width 35 | , height: 1 36 | } 37 | , body 38 | ] 39 | 40 | test :: Doc GraphicsParam 41 | test = Box.toDoc do 42 | heading 43 | (textBox 40 (Ansi.bold (Dodo.text "Example JSON"))) 44 | ( vertical 45 | [ fill (Ansi.dim (Dodo.text "*")) { width: 120, height: 1 } 46 | , halign Middle $ horizontal $ intersperse (hpadding 4) 47 | [ textBox 40 (printJson exampleJson) 48 | , valign Middle $ vertical 49 | [ halign Middle $ textBox 40 (Ansi.bold (Dodo.text "NOTE")) 50 | , textBox 40 (Ansi.italic para2) 51 | ] 52 | ] 53 | ] 54 | ) 55 | 56 | table 57 | :: forall a 58 | . { headers :: Array (DocBox a) 59 | , rows :: Array (Array (DocBox a)) 60 | } 61 | -> DocBox a 62 | table { headers, rows } = 63 | vertical 64 | [ rowSep 65 | , vertical $ Array.intersperse rowSep $ map columns $ Array.cons headers rows 66 | , rowSep 67 | ] 68 | where 69 | joint = 70 | fill (Dodo.text "+") { width: 1, height: 1 } 71 | 72 | rowSep = 73 | horizontal 74 | [ joint 75 | , horizontal $ Array.intersperse joint $ map 76 | ( \width -> 77 | fill (Dodo.text "-") 78 | { width: width + 2 79 | , height: 1 80 | } 81 | ) 82 | widths 83 | , joint 84 | ] 85 | 86 | columns cols = do 87 | let 88 | height = 89 | Array.foldr (max <<< _.height <<< Box.sizeOf) 0 cols 90 | 91 | colBoxes = Array.mapWithIndex 92 | ( \ix col -> 93 | horizontal 94 | [ hpadding 1 95 | , resize 96 | { width: fromMaybe 0 (Array.index widths ix) 97 | , height 98 | } 99 | col 100 | , hpadding 1 101 | ] 102 | ) 103 | cols 104 | 105 | sep = fill (Dodo.text "|") { width: 1, height } 106 | 107 | horizontal 108 | [ sep 109 | , horizontal $ Array.intersperse sep colBoxes 110 | , sep 111 | ] 112 | 113 | widths = Array.mapWithIndex 114 | ( \ix hd -> 115 | Array.foldr 116 | ( flip Array.index ix 117 | >>> map (_.width <<< Box.sizeOf) 118 | >>> fromMaybe 0 119 | >>> max 120 | ) 121 | (Box.sizeOf hd).width 122 | rows 123 | ) 124 | headers 125 | 126 | testTable :: Doc GraphicsParam 127 | testTable = Box.toDoc $ table 128 | { headers: 129 | [ valign Middle $ halign Middle $ textBox 20 $ Dodo.text "Example" 130 | , valign Middle $ halign Middle $ textBox 20 $ Dodo.text "Comment" 131 | ] 132 | , rows: 133 | [ [ textBox 40 (printJson exampleJson) 134 | , valign Middle $ halign Middle $ textBox 40 (Ansi.italic para2) 135 | ] 136 | , [ textBox 120 (printJson exampleJson) 137 | , valign Middle $ textBox 60 (Ansi.italic para2) 138 | ] 139 | , [ textBox 120 (printJson exampleJson) 140 | , textBox 60 (Ansi.italic para2) 141 | ] 142 | ] 143 | } 144 | 145 | main :: Effect Unit 146 | main = do 147 | Console.log $ print plainText (twoSpaces { pageWidth = top }) test 148 | Console.log $ print ansiGraphics (twoSpaces { pageWidth = top }) test 149 | Console.log $ print plainText (twoSpaces { pageWidth = top }) testTable 150 | -------------------------------------------------------------------------------- /test/snapshots/DodoBox.output: -------------------------------------------------------------------------------- 1 | Example JSON 2 | ------------------------------------------------------------------------------------------------------------------------ 3 | ************************************************************************************************************************ 4 | { 5 | "bool": true, 6 | "string": "bar", 7 | "number": 1234.0, NOTE 8 | "array": [null, "two", ["three"]], Vestibulum ante ipsum primis in faucibus 9 | "object": {}, orci luctus et ultrices posuere cubilia 10 | "wideObject": { curae; Suspendisse eget tortor. 11 | "key": "1111111111111111111111111111111111111111" 12 | } 13 | } 14 | Example JSON 15 | ------------------------------------------------------------------------------------------------------------------------ 16 | ************************************************************************************************************************ 17 | { 18 | "bool": true, 19 | "string": "bar", 20 | "number": 1234.0, NOTE 21 | "array": [null, "two", ["three"]], Vestibulum ante ipsum primis in faucibus 22 | "object": {}, orci luctus et ultrices posuere cubilia 23 | "wideObject": { curae; Suspendisse eget tortor. 24 | "key": "1111111111111111111111111111111111111111" 25 | } 26 | } 27 | +---------------------------------------------------------------------+----------------------------------------------------------+ 28 | | Example | Comment | 29 | +---------------------------------------------------------------------+----------------------------------------------------------+ 30 | | { | | 31 | | "bool": true, | | 32 | | "string": "bar", | | 33 | | "number": 1234.0, | Vestibulum ante ipsum primis in faucibus | 34 | | "array": [null, "two", ["three"]], | orci luctus et ultrices posuere cubilia | 35 | | "object": {}, | curae; Suspendisse eget tortor. | 36 | | "wideObject": { | | 37 | | "key": "1111111111111111111111111111111111111111" | | 38 | | } | | 39 | | } | | 40 | +---------------------------------------------------------------------+----------------------------------------------------------+ 41 | | { | | 42 | | "bool": true, | | 43 | | "string": "bar", | | 44 | | "number": 1234.0, | Vestibulum ante ipsum primis in faucibus orci luctus et | 45 | | "array": [null, "two", ["three"]], | ultrices posuere cubilia curae; Suspendisse eget tortor. | 46 | | "object": {}, | | 47 | | "wideObject": {"key": "1111111111111111111111111111111111111111"} | | 48 | | } | | 49 | +---------------------------------------------------------------------+----------------------------------------------------------+ 50 | | { | Vestibulum ante ipsum primis in faucibus orci luctus et | 51 | | "bool": true, | ultrices posuere cubilia curae; Suspendisse eget tortor. | 52 | | "string": "bar", | | 53 | | "number": 1234.0, | | 54 | | "array": [null, "two", ["three"]], | | 55 | | "object": {}, | | 56 | | "wideObject": {"key": "1111111111111111111111111111111111111111"} | | 57 | | } | | 58 | +---------------------------------------------------------------------+----------------------------------------------------------+ 59 | -------------------------------------------------------------------------------- /src/Dodo/Ansi.purs: -------------------------------------------------------------------------------- 1 | -- | This module provides functions printing with cascading ANSI styles. 2 | -- | ANSI annotations closer to the root will cascade down to child nodes, 3 | -- | where styles closer to the leaves take precedence. Indentation is 4 | -- | never printed with ANSI styles, only the text elements of the document. 5 | module Dodo.Ansi 6 | ( module Dodo.Ansi 7 | , module Exports 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Ansi.Codes (Color(..), GraphicsParam) as Exports 13 | import Ansi.Codes as Ansi 14 | import Data.List (List) 15 | import Data.List as List 16 | import Data.List.NonEmpty as NonEmptyList 17 | import Data.List.Types (NonEmptyList) 18 | import Data.Maybe (Maybe(..), fromMaybe, isNothing) 19 | import Dodo (Doc, Printer(..), annotate) 20 | 21 | -- | Resets all cascading styles for a document so that outer styles won't 22 | -- | interfere with inner styles. 23 | reset :: Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 24 | reset = annotate Ansi.Reset 25 | 26 | -- | Prints a document with bold styling. 27 | bold :: Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 28 | bold = annotate (Ansi.PMode Ansi.Bold) 29 | 30 | -- | Prints a document with dim styling. 31 | dim :: Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 32 | dim = annotate (Ansi.PMode Ansi.Dim) 33 | 34 | -- | Prints a document with italic styling. 35 | italic :: Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 36 | italic = annotate (Ansi.PMode Ansi.Italic) 37 | 38 | -- | Prints a document with underline styling. 39 | underline :: Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 40 | underline = annotate (Ansi.PMode Ansi.Underline) 41 | 42 | -- | Prints a document with inverse styling. 43 | inverse :: Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 44 | inverse = annotate (Ansi.PMode Ansi.Inverse) 45 | 46 | -- | Prints a document with strikethrough styling. 47 | strikethrough :: Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 48 | strikethrough = annotate (Ansi.PMode Ansi.Strikethrough) 49 | 50 | -- | Prints a document with a specific foreground color. 51 | foreground :: Ansi.Color -> Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 52 | foreground color = annotate (Ansi.PForeground color) 53 | 54 | -- | Prints a document with a specific background color. 55 | background :: Ansi.Color -> Doc Ansi.GraphicsParam -> Doc Ansi.GraphicsParam 56 | background color = annotate (Ansi.PBackground color) 57 | 58 | newtype AnsiBuffer = AnsiBuffer 59 | { output :: String 60 | , pending :: Maybe (NonEmptyList Ansi.GraphicsParam) 61 | , current :: List Ansi.GraphicsParam 62 | , previous :: List Ansi.GraphicsParam 63 | } 64 | 65 | ansiGraphics :: Printer AnsiBuffer Ansi.GraphicsParam String 66 | ansiGraphics = Printer 67 | { emptyBuffer 68 | , writeText 69 | , writeIndent 70 | , writeBreak 71 | , enterAnnotation 72 | , leaveAnnotation 73 | , flushBuffer 74 | } 75 | where 76 | emptyBuffer :: AnsiBuffer 77 | emptyBuffer = 78 | AnsiBuffer 79 | { output: "" 80 | , pending: Nothing 81 | , current: List.Nil 82 | , previous: List.Nil 83 | } 84 | 85 | resetCode :: String 86 | resetCode = Ansi.escapeCodeToString $ Ansi.Graphics $ pure Ansi.Reset 87 | 88 | writeText :: Int -> String -> AnsiBuffer -> AnsiBuffer 89 | writeText _ text output = do 90 | let AnsiBuffer buffer = writePendingGraphics output 91 | AnsiBuffer buffer 92 | { output = buffer.output <> text 93 | , previous = List.Nil 94 | } 95 | 96 | writeIndent :: Int -> String -> AnsiBuffer -> AnsiBuffer 97 | writeIndent _ text (AnsiBuffer buffer) = 98 | AnsiBuffer buffer 99 | { output = buffer.output <> text 100 | } 101 | 102 | writeBreak :: AnsiBuffer -> AnsiBuffer 103 | writeBreak (AnsiBuffer buffer) = do 104 | let pending = getPendingGraphics buffer.current 105 | let resetOrEmpty = if isNothing pending && List.null buffer.previous then "" else resetCode 106 | AnsiBuffer buffer 107 | { output = buffer.output <> resetOrEmpty <> "\n" 108 | , pending = pending 109 | } 110 | 111 | enterAnnotation :: Ansi.GraphicsParam -> List Ansi.GraphicsParam -> AnsiBuffer -> AnsiBuffer 112 | enterAnnotation a as (AnsiBuffer buffer) = do 113 | let current = getCurrentGraphics (List.Cons a as) 114 | AnsiBuffer buffer 115 | { pending = getPendingGraphics current 116 | , current = current 117 | , previous = buffer.current 118 | } 119 | 120 | leaveAnnotation :: Ansi.GraphicsParam -> List Ansi.GraphicsParam -> AnsiBuffer -> AnsiBuffer 121 | leaveAnnotation _ as (AnsiBuffer buffer) = do 122 | let current = getCurrentGraphics as 123 | AnsiBuffer buffer 124 | { pending = Just $ fromMaybe (pure Ansi.Reset) $ getPendingGraphics current 125 | , current = current 126 | , previous = buffer.current 127 | } 128 | 129 | flushBuffer :: AnsiBuffer -> String 130 | flushBuffer ansiBuffer = do 131 | let AnsiBuffer buffer = writePendingGraphics ansiBuffer 132 | buffer.output 133 | 134 | writePendingGraphics :: AnsiBuffer -> AnsiBuffer 135 | writePendingGraphics (AnsiBuffer buffer) = 136 | case buffer.pending of 137 | Nothing -> 138 | AnsiBuffer buffer 139 | Just gfx -> 140 | AnsiBuffer buffer 141 | { output = buffer.output <> Ansi.escapeCodeToString (Ansi.Graphics gfx) 142 | , pending = Nothing 143 | } 144 | 145 | getCurrentGraphics :: List Ansi.GraphicsParam -> List Ansi.GraphicsParam 146 | getCurrentGraphics = 147 | List.takeWhile (_ /= Ansi.Reset) 148 | >>> List.nubByEq graphicsConflict 149 | >>> List.reverse 150 | 151 | getPendingGraphics :: List Ansi.GraphicsParam -> Maybe (NonEmptyList Ansi.GraphicsParam) 152 | getPendingGraphics = 153 | NonEmptyList.fromList 154 | >>> map (NonEmptyList.cons Ansi.Reset) 155 | 156 | graphicsConflict :: Ansi.GraphicsParam -> Ansi.GraphicsParam -> Boolean 157 | graphicsConflict = case _, _ of 158 | Ansi.Reset, Ansi.Reset -> true 159 | Ansi.PForeground _, Ansi.PForeground _ -> true 160 | Ansi.PBackground _, Ansi.PBackground _ -> true 161 | Ansi.PMode a, Ansi.PMode b -> a == b 162 | _, _ -> false 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-dodo-printer 2 | 3 | An adequate printer. 4 | 5 | This library implements the equivalent functions of many other pretty-printers in 6 | the Wadler/Leijen style but with all the names gratuitously changed. 7 | 8 | It also provides nice ANSI integration. 9 | 10 | ## Getting Started 11 | 12 | The core type of `Dodo` is `Doc`, which represents a document. It has one 13 | type parameter which represents an annotation. Annotations let you mark parts 14 | of your document tree with things such as styles and colors (e.g. `Dodo.Ansi` 15 | for ANSI graphics). `Doc`s which don't use annotations can use a type 16 | variable or `Void`. The most primitive ways to construct and manipulate 17 | `Doc`s are via `text` and `Monoid`. 18 | 19 | ```purescript 20 | import Prelude 21 | import Dodo (Doc, text) 22 | 23 | hello :: forall a. Doc a 24 | hello = text "Hello, " <> text "World!" 25 | ``` 26 | 27 | Using `(<>)` (or `append`, or `fold`, or any other `Monoid` function) will put 28 | text next to each other on a line. In order to render this, we need to choose 29 | a particular printer interface and provide some print options. 30 | 31 | The most basic printer is `plainText`, which only renders plain text, 32 | ignoring all annotations. Print options let you configure things like 33 | indentation and page width. There are several presets for print options, so 34 | we will just pick one for now (`twoSpaces`). 35 | 36 | ```purescript 37 | import Prelude 38 | import Effect (Effect) 39 | import Effect.Console as Console 40 | import Dodo (Doc, text, plainText, twoSpaces, print) 41 | 42 | hello :: forall a. Doc a 43 | hello = text "Hello, " <> text "World!" 44 | 45 | main :: Effect Unit 46 | main = Console.log $ print plainText twoSpaces hello 47 | ``` 48 | ``` 49 | Hello, World! 50 | ``` 51 | 52 | ### Indentation 53 | 54 | Dodo treats indentation as a separate configurable thing. Indentation is only 55 | ever a line prefix, and printed on demand after lines breaks and before text. 56 | There are two primitive functions for manipulating indentation: 57 | 58 | * `indent` which increases the indentation level by one indent unit 59 | (configurable with print options). 60 | * `align` which increases the indentation level by a certain number of spaces 61 | (not configurable). 62 | 63 | Let's try indenting the second word in our example: 64 | 65 | ```purescript 66 | hello :: forall a. Doc a 67 | hello = text "Hello, " <> indent (text "World!") 68 | ``` 69 | 70 | Printing this, however, will yield an identical result. That's because we 71 | already printed text, and indentation only affects the line prefix. We can 72 | insert a `break`: 73 | 74 | ```purescript 75 | hello :: forall a. Doc a 76 | hello = text "Hello, " <> indent (break <> text "World!") 77 | ``` 78 | ``` 79 | Hello, 80 | World! 81 | ``` 82 | 83 | We can also align to a certain number of spaces: 84 | 85 | ```purescript 86 | hello :: forall a. Doc a 87 | hello = text "Hello, " <> align 10 (break <> text "World!") 88 | ``` 89 | ``` 90 | Hello, 91 | World! 92 | ``` 93 | 94 | Or even align to the current column: 95 | 96 | ```purescript 97 | hello :: forall a. Doc a 98 | hello = text "Hello, " <> alignCurrentColumn (break <> text "World!") 99 | ``` 100 | ``` 101 | Hello, 102 | World! 103 | ``` 104 | 105 | ### Flexible Layouts 106 | 107 | We've learned how to append text, insert line breaks, and indent, but often 108 | times we want our layout to be a little more flexible. If we have plenty of 109 | space left on the line, we may want to print our document on a single line, 110 | and only insert breaks as needed. There are two primitives for building 111 | flexible layouts: 112 | 113 | * `flexGroup` which specifies a flexible region of the document and may 114 | need to respond to page width constraints. 115 | * `flexAlt` which specifies an alternative document to try and render while 116 | in a flex group, falling back to a default if it doesn't fit. 117 | 118 | When Dodo enters a flex group, it will use the first argument to any flex 119 | alternatives, rendering until content either extends past the configured 120 | page width or until there is a line break. If it encounters either of these 121 | conditions, it will back up and use the second arguments to any flex 122 | alternatives. 123 | 124 | One of the most basic flexible documents is `spaceBreak`, which will either 125 | insert a space or a line break. `spaceBreak` is defined as: 126 | 127 | ```purescript 128 | spaceBreak :: forall a. Doc a 129 | spaceBreak = flexAlt (text " ") break 130 | ``` 131 | 132 | If we substitute `break` for `spaceBreak` in our example: 133 | 134 | ```purescript 135 | hello :: forall a. Doc a 136 | hello = text "Hello," <> indent (spaceBreak <> text "World!") 137 | ``` 138 | 139 | Nothing will change! Without a surrounding flex group, flex alternatives 140 | always render the second argument. 141 | 142 | ```purescript 143 | hello :: forall a. Doc a 144 | hello = flexGroup (text "Hello," <> indent (spaceBreak <> text "World!")) 145 | ``` 146 | ``` 147 | Hello, World! 148 | ``` 149 | 150 | Since there was available width on the line, it rendered with a space. If we 151 | configure our page width to something really small though: 152 | 153 | ```purescript 154 | main :: Effect Unit 155 | main = Console.log $ print plainText (twoSpaces { pageWidth = 10 }) hello 156 | ``` 157 | ``` 158 | Hello, 159 | World! 160 | ``` 161 | 162 | Dodo inserts the break and indent. "Hello, World!" on a single line would 163 | exceed the maximum page width, so it uses the flex default (`break`) instead. 164 | 165 | ### Two-Dimensional Layouts 166 | 167 | Dodo also supports two-dimensional layouts through the `Dodo.Box` interface. 168 | Boxes can be joined and aligned both vertically and horizontally to create 169 | complex layouts such as tables or grids. 170 | 171 | ## Examples 172 | 173 | * Colorful, flexible JSON printer ([code](test/snapshots/DodoExampleJson.purs), [output](test/snapshots/DodoExampleJson.output)) 174 | * Text paragraphs ([code](test/snapshots/DodoTextParagraph.purs), [output](test/snapshots/DodoTextParagraph.output)) 175 | * 2D layout ([code](test/snapshots/DodoBox.purs), [output](test/snapshots/DodoBox.output)) 176 | -------------------------------------------------------------------------------- /src/Dodo/Box.purs: -------------------------------------------------------------------------------- 1 | module Dodo.Box 2 | ( DocBox 3 | , DocBoxBuffer 4 | , DocAnnStk 5 | , Vertical(..) 6 | , Horizontal(..) 7 | , Align(..) 8 | , BoxSize 9 | , valign 10 | , halign 11 | , vappend 12 | , happend 13 | , vertical 14 | , verticalWithAlign 15 | , horizontal 16 | , horizontalWithAlign 17 | , resize 18 | , fill 19 | , vpadding 20 | , hpadding 21 | , sizeOf 22 | , isEmpty 23 | , empty 24 | , toDoc 25 | , docBox 26 | ) where 27 | 28 | import Prelude 29 | 30 | import Data.Either (Either(..), isLeft) 31 | import Data.Foldable (class Foldable, foldl, foldr) 32 | import Data.Int as Int 33 | import Data.List (List, (:)) 34 | import Data.List as List 35 | import Data.Maybe (Maybe(..)) 36 | import Data.Monoid (power) 37 | import Data.Newtype (class Newtype, under) 38 | import Dodo (Doc, Printer(..), annotate) 39 | import Dodo as Dodo 40 | import Dodo.Internal as Internal 41 | import Partial.Unsafe (unsafeCrashWith) 42 | import Safe.Coerce (coerce) 43 | 44 | data Align 45 | = Start 46 | | Middle 47 | | End 48 | 49 | derive instance Eq Align 50 | 51 | type BoxSize = 52 | { width :: Int 53 | , height :: Int 54 | } 55 | 56 | -- | Unlike `Doc`s, which can only be joined along a line, `DocBox`es 57 | -- | are two-dimensional units which can be stacked vertically and 58 | -- | horizontally (eg. for tables). 59 | -- | 60 | -- | `Doc`s can be lifted into `DocBox` by using the `doxBox` printer. 61 | -- | ```purescript 62 | -- | example = Dodo.print docBox twoSpaces myDoc 63 | -- | ```` 64 | data DocBox a 65 | = DocLine (Doc a) Int 66 | | DocVApp (DocBox a) (DocBox a) BoxSize 67 | | DocHApp (DocBox a) (DocBox a) BoxSize 68 | | DocAlign Align Align (DocBox a) 69 | | DocPad BoxSize 70 | | DocEmpty 71 | 72 | derive instance Functor DocBox 73 | 74 | -- | A newtype whose Semigroup instance stacks DocBoxes vertically. 75 | newtype Vertical a = Vertical (DocBox a) 76 | 77 | derive instance Newtype (Vertical a) _ 78 | 79 | derive newtype instance Functor Vertical 80 | 81 | instance Semigroup (Vertical a) where 82 | append = coerce (vappend :: DocBox a -> _ -> _) 83 | 84 | instance Monoid (Vertical a) where 85 | mempty = Vertical DocEmpty 86 | 87 | -- | A newtype whose Semigroup instance stacks DocBoxes horizontally. 88 | newtype Horizontal a = Horizontal (DocBox a) 89 | 90 | derive instance Newtype (Horizontal a) _ 91 | 92 | derive newtype instance Functor Horizontal 93 | 94 | instance Semigroup (Horizontal a) where 95 | append = coerce (happend :: DocBox a -> _ -> _) 96 | 97 | instance Monoid (Horizontal a) where 98 | mempty = Horizontal DocEmpty 99 | 100 | -- | Joins DocBoxes in a vertical run. 101 | vertical :: forall f a. Foldable f => f (DocBox a) -> DocBox a 102 | vertical = foldr vappend DocEmpty 103 | 104 | -- | Joins DocBoxes in a vertical run with uniform horizontal alignment. 105 | verticalWithAlign :: forall f a. Foldable f => Align -> f (DocBox a) -> DocBox a 106 | verticalWithAlign align = foldr (\a b -> halign align a `vappend` b) DocEmpty 107 | 108 | -- | Joins DocBoxes in a horizontal run. 109 | horizontal :: forall f a. Foldable f => f (DocBox a) -> DocBox a 110 | horizontal = foldr happend DocEmpty 111 | 112 | -- | Joins DocBoxes in a horizontal run with uniform vertical alignment. 113 | horizontalWithAlign :: forall f a. Foldable f => Align -> f (DocBox a) -> DocBox a 114 | horizontalWithAlign align = foldr (\a b -> valign align a `happend` b) DocEmpty 115 | 116 | -- | Joins two DocBoxes vertically on top of each other. 117 | vappend :: forall a. DocBox a -> DocBox a -> DocBox a 118 | vappend = case _, _ of 119 | DocEmpty, b -> b 120 | a, DocEmpty -> a 121 | DocPad sizea, DocPad sizeb -> 122 | DocPad (scale sizea sizeb) 123 | a, b -> 124 | DocVApp a b (scale (sizeOf a) (sizeOf b)) 125 | where 126 | scale sizea sizeb = 127 | { width: max sizea.width sizeb.width 128 | , height: sizea.height + sizeb.height 129 | } 130 | 131 | -- | Joins two DocBoxes horizontally next to each other. 132 | happend :: forall a. DocBox a -> DocBox a -> DocBox a 133 | happend = case _, _ of 134 | DocEmpty, b -> b 135 | a, DocEmpty -> a 136 | DocPad sizea, DocPad sizeb -> 137 | DocPad (scale sizea sizeb) 138 | a, b -> do 139 | DocHApp a b (scale (sizeOf a) (sizeOf b)) 140 | where 141 | scale sizea sizeb = 142 | { width: sizea.width + sizeb.width 143 | , height: max sizea.height sizeb.height 144 | } 145 | 146 | -- | Pads a DocBox vertically to fit the tallest box within a 147 | -- | horizontal run. 148 | -- | ```purescript 149 | -- | example = 150 | -- | valign Middle shortDoc 151 | -- | `happend` tallDoc 152 | -- | ``` 153 | valign :: forall a. Align -> DocBox a -> DocBox a 154 | valign a = case _ of 155 | DocAlign _ b doc 156 | | Start <- a, Start <- b -> doc 157 | | otherwise -> 158 | DocAlign a b doc 159 | other -> 160 | DocAlign a Start other 161 | 162 | -- | Pads a DocBox horizontally to fit the widest line within a 163 | -- | vertical run. 164 | -- | ```purescript 165 | -- | example = 166 | -- | halign Middle skinnyDoc 167 | -- | `vappend` wideDoc 168 | -- | ``` 169 | halign :: forall a. Align -> DocBox a -> DocBox a 170 | halign b = case _ of 171 | DocAlign a _ doc 172 | | Start <- a, Start <- b -> doc 173 | | otherwise -> 174 | DocAlign a b doc 175 | other -> 176 | DocAlign Start b other 177 | 178 | -- | Resizes a box to a larger or equivalent size, positioning its content 179 | -- | according to its alignment. 180 | resize :: forall a. BoxSize -> DocBox a -> DocBox a 181 | resize newSize box = vdoc 182 | where 183 | box' = case box of 184 | DocAlign _ _ b -> b 185 | _ -> box 186 | size = sizeOf box 187 | hpad = newSize.width - size.width 188 | vpad = newSize.height - size.height 189 | hdoc 190 | | hpad <= 0 = valign Start box' 191 | | otherwise = padWithAlign happend hpadding hpad box (halignOf box) 192 | vdoc 193 | | vpad <= 0 = halign Start hdoc 194 | | otherwise = padWithAlign vappend vpadding vpad hdoc (valignOf box) 195 | 196 | padWithAlign :: forall a. (a -> a -> a) -> (Int -> a) -> Int -> a -> Align -> a 197 | padWithAlign appendFn paddingFn padWidth doc = case _ of 198 | Start -> 199 | doc `appendFn` paddingFn padWidth 200 | Middle -> do 201 | let mid = Int.toNumber padWidth / 2.0 202 | paddingFn (Int.floor mid) 203 | `appendFn` doc 204 | `appendFn` paddingFn (Int.ceil mid) 205 | End -> 206 | paddingFn padWidth `appendFn` doc 207 | 208 | -- | Fills a box to a given size with a Doc. The Doc is assumed 209 | -- | to be 1x1. Providing a Doc of a different size will result 210 | -- | in incorrect layouts. 211 | -- | ``` 212 | -- | example = 213 | -- | fill (Ansi.dim (Dodo.text "-")) 214 | -- | { width: 100 215 | -- | , height: 1 216 | -- | } 217 | -- | ``` 218 | fill :: forall a. Doc a -> BoxSize -> DocBox a 219 | fill ch { width, height } = under Vertical (flip power height) line 220 | where 221 | line = case ch of 222 | Internal.Annotate a doc -> 223 | DocLine (annotate a (power doc width)) width 224 | _ -> 225 | DocLine (power ch width) width 226 | 227 | -- | Vertical padding of a specific height. 228 | vpadding :: forall a. Int -> DocBox a 229 | vpadding height 230 | | height <= 0 = DocEmpty 231 | | otherwise = DocPad { height, width: 0 } 232 | 233 | -- | Horizontal padding of a specific width. 234 | hpadding :: forall a. Int -> DocBox a 235 | hpadding width 236 | | width <= 0 = DocEmpty 237 | | otherwise = DocPad { height: 1, width } 238 | 239 | -- | Returns the size of a DocBox. 240 | sizeOf :: forall a. DocBox a -> BoxSize 241 | sizeOf = case _ of 242 | DocLine _ width -> { width, height: 1 } 243 | DocVApp _ _ size -> size 244 | DocHApp _ _ size -> size 245 | DocAlign _ _ doc -> sizeOf doc 246 | DocPad size -> size 247 | DocEmpty -> { width: 0, height: 0 } 248 | 249 | valignOf :: forall a. DocBox a -> Align 250 | valignOf = case _ of 251 | DocAlign v _ _ -> v 252 | _ -> Start 253 | 254 | halignOf :: forall a. DocBox a -> Align 255 | halignOf = case _ of 256 | DocAlign _ h _ -> h 257 | _ -> Start 258 | 259 | -- | The identity DocBox. 260 | empty :: forall a. DocBox a 261 | empty = DocEmpty 262 | 263 | -- | Checks whether a DocBox is empty. 264 | isEmpty :: forall a. DocBox a -> Boolean 265 | isEmpty = case _ of 266 | DocEmpty -> true 267 | _ -> false 268 | 269 | -- | Converts a DocBox back into Doc for printing. 270 | toDoc :: forall a. DocBox a -> Doc a 271 | toDoc = go1 <<< resume <<< build AsIs StpDone 272 | where 273 | go1 = case _ of 274 | Nothing -> mempty 275 | Just { line, next } -> 276 | go2 (formatLine line) (resume next) 277 | 278 | go2 acc = case _ of 279 | Nothing -> acc 280 | Just { line, next } -> 281 | go2 (acc <> Dodo.break <> formatLine line) (resume next) 282 | 283 | formatLine :: forall a. DocLine a -> Doc a 284 | formatLine = go mempty <<< List.singleton 285 | where 286 | go acc = case _ of 287 | List.Nil -> 288 | acc 289 | line : lines -> 290 | case line of 291 | LinePad w 292 | | Dodo.isEmpty acc -> 293 | go acc lines 294 | | otherwise -> 295 | go (power Dodo.space w <> acc) lines 296 | LineDoc doc -> 297 | go (doc <> acc) lines 298 | LineAppend a b -> 299 | go acc (b : a : lines) 300 | 301 | data DocBoxStep a 302 | = StpDone 303 | | StpLine (Doc a) (DocBoxStep a) 304 | | StpPad Int Int (DocBoxStep a) 305 | | StpHorz (DocBoxStep a) (DocBoxStep a) (DocBoxStep a) 306 | 307 | data DocBuildSize 308 | = FullHeight Int 309 | | FullWidth Int 310 | | AsIs 311 | 312 | data DocBuildCmd a 313 | = BuildEnter DocBuildSize (DocBoxStep a) (DocBox a) 314 | | BuildLeave (DocBoxStep a) 315 | 316 | data DocBuildStk a 317 | = BuildVAppR Int (DocBox a) (DocBuildStk a) 318 | | BuildHAppR Int (DocBox a) (DocBoxStep a) (DocBuildStk a) 319 | | BuildHAppH (DocBoxStep a) (DocBoxStep a) (DocBuildStk a) 320 | | BuildNil 321 | 322 | build :: forall a. DocBuildSize -> DocBoxStep a -> DocBox a -> DocBoxStep a 323 | build = (\size next box -> go (BuildEnter size next box) BuildNil) 324 | where 325 | go cmd stack = case cmd of 326 | BuildEnter size next box -> 327 | case size of 328 | FullHeight height -> 329 | case box of 330 | DocHApp a b _ -> 331 | go (BuildEnter size StpDone b) (BuildHAppR height a next stack) 332 | _ -> 333 | go (BuildEnter AsIs next (resize { width: 0, height } box)) stack 334 | FullWidth width -> 335 | case box of 336 | DocVApp a b _ -> 337 | go (BuildEnter size next b) (BuildVAppR width a stack) 338 | _ -> 339 | go (BuildEnter AsIs next (resize { width, height: 0 } box)) stack 340 | AsIs -> 341 | case box of 342 | DocVApp a b { width } -> 343 | go (BuildEnter (FullWidth width) next b) (BuildVAppR width a stack) 344 | DocHApp a b { height } -> 345 | go (BuildEnter (FullHeight height) StpDone b) (BuildHAppR height a next stack) 346 | DocAlign _ _ a -> 347 | go (BuildEnter size next a) stack 348 | DocLine line _ -> 349 | go (BuildLeave (StpLine line next)) stack 350 | DocPad padSize -> 351 | go (BuildLeave (StpPad padSize.width padSize.height next)) stack 352 | DocEmpty -> 353 | go (BuildLeave StpDone) stack 354 | BuildLeave step -> 355 | case stack of 356 | BuildVAppR width boxa stk -> 357 | go (BuildEnter (FullWidth width) step boxa) stk 358 | BuildHAppR height boxa next stk -> 359 | go (BuildEnter (FullHeight height) StpDone boxa) (BuildHAppH step next stk) 360 | BuildHAppH stepb next stk -> 361 | go (BuildLeave (StpHorz step stepb next)) stk 362 | BuildNil -> 363 | step 364 | 365 | data DocLine a 366 | = LinePad Int 367 | | LineDoc (Doc a) 368 | | LineAppend (DocLine a) (DocLine a) 369 | 370 | type DocProducer a = 371 | { line :: DocLine a 372 | , next :: DocBoxStep a 373 | } 374 | 375 | data DocResumeCmd a 376 | = ResumeEnter (DocBoxStep a) 377 | | ResumeLeave (Maybe (DocProducer a)) 378 | 379 | data DocResumeStk a 380 | = ResumeHorzR (DocBoxStep a) (DocBoxStep a) (DocResumeStk a) 381 | | ResumeHorzH (Maybe (DocProducer a)) (DocBoxStep a) (DocResumeStk a) 382 | | ResumeNil 383 | 384 | resume :: forall a. DocBoxStep a -> Maybe (DocProducer a) 385 | resume = flip go ResumeNil <<< ResumeEnter 386 | where 387 | go cmd stack = case cmd of 388 | ResumeEnter step -> 389 | case step of 390 | StpDone -> 391 | go (ResumeLeave Nothing) stack 392 | StpLine doc next -> do 393 | go (ResumeLeave $ Just { line: LineDoc doc, next }) stack 394 | StpPad width height next -> 395 | if height == 0 then 396 | go (ResumeEnter next) stack 397 | else 398 | go 399 | ( ResumeLeave $ Just 400 | { line: LinePad width 401 | , next: StpPad width (height - 1) next 402 | } 403 | ) 404 | stack 405 | StpHorz a b next -> 406 | go (ResumeEnter b) (ResumeHorzR a next stack) 407 | ResumeLeave prod -> 408 | case stack of 409 | ResumeHorzR stepa next stk -> 410 | go (ResumeEnter stepa) (ResumeHorzH prod next stk) 411 | ResumeHorzH prodb next stk -> 412 | case prod, prodb of 413 | Just a, Just b -> 414 | go 415 | ( ResumeLeave $ Just 416 | { line: LineAppend a.line b.line 417 | , next: StpHorz a.next b.next next 418 | } 419 | ) 420 | stk 421 | _, _ -> 422 | go (ResumeEnter next) stk 423 | ResumeNil -> 424 | prod 425 | 426 | type DocAnnStk a = List (Either a (Doc a)) 427 | 428 | newtype DocBoxBuffer a = DocBoxBuffer 429 | { currentIndent :: Doc a 430 | , currentLine :: DocAnnStk a 431 | , currentWidth :: Int 432 | , lines :: DocBox a 433 | } 434 | 435 | -- | A printer which can lift a Doc into DocBox. It is assumed that 436 | -- | the Doc's annotations respect a distributive law: 437 | -- | ``` purescript 438 | -- | annotate ann (a <> b) = annotate ann a <> annotate ann b 439 | -- | ```` 440 | docBox :: forall a. Printer (DocBoxBuffer a) a (DocBox a) 441 | docBox = Printer 442 | { emptyBuffer 443 | , writeText 444 | , writeIndent 445 | , writeBreak 446 | , enterAnnotation 447 | , leaveAnnotation 448 | , flushBuffer 449 | } 450 | where 451 | emptyBuffer :: DocBoxBuffer a 452 | emptyBuffer = DocBoxBuffer 453 | { currentIndent: mempty 454 | , currentLine: List.Nil 455 | , currentWidth: 0 456 | , lines: DocEmpty 457 | } 458 | 459 | writeText :: Int -> String -> DocBoxBuffer a -> DocBoxBuffer a 460 | writeText width text (DocBoxBuffer buff) = do 461 | let 462 | doc' = Internal.Text width text 463 | line = case buff.currentLine of 464 | Right doc : rest -> 465 | Right (doc <> doc') : rest 466 | rest -> 467 | Right doc' : rest 468 | DocBoxBuffer buff 469 | { currentLine = line 470 | , currentWidth = buff.currentWidth + width 471 | } 472 | 473 | writeIndent :: Int -> String -> DocBoxBuffer a -> DocBoxBuffer a 474 | writeIndent width text (DocBoxBuffer buff) = do 475 | let doc = Internal.Text width text 476 | DocBoxBuffer buff 477 | { currentIndent = buff.currentIndent <> doc 478 | , currentWidth = buff.currentWidth + width 479 | } 480 | 481 | writeBreak :: DocBoxBuffer a -> DocBoxBuffer a 482 | writeBreak (DocBoxBuffer buff) = do 483 | let line = stkToDoc buff.currentLine 484 | DocBoxBuffer buff 485 | { currentIndent = mempty 486 | , currentLine = List.filter isLeft buff.currentLine 487 | , currentWidth = 0 488 | , lines = buff.lines `vappend` DocLine (buff.currentIndent <> line) buff.currentWidth 489 | } 490 | 491 | enterAnnotation :: a -> List a -> DocBoxBuffer a -> DocBoxBuffer a 492 | enterAnnotation ann _ (DocBoxBuffer buff) = 493 | DocBoxBuffer buff 494 | { currentLine = Left ann : buff.currentLine 495 | } 496 | 497 | leaveAnnotation :: a -> List a -> DocBoxBuffer a -> DocBoxBuffer a 498 | leaveAnnotation _ _ (DocBoxBuffer buff) = do 499 | let 500 | line = case buff.currentLine of 501 | Right doc : Left ann : rest -> 502 | Right (annotate ann doc) : rest 503 | Left _ : rest -> 504 | rest 505 | _ -> 506 | unsafeCrashWith "leaveAnnotation: docs and annotations must be interleaved" 507 | DocBoxBuffer buff 508 | { currentLine = line 509 | } 510 | 511 | flushBuffer :: DocBoxBuffer a -> DocBox a 512 | flushBuffer (DocBoxBuffer buff) 513 | | isEmpty buff.lines && List.null buff.currentLine = 514 | DocEmpty 515 | | otherwise = do 516 | let line = stkToDoc buff.currentLine 517 | buff.lines `vappend` DocLine (buff.currentIndent <> line) buff.currentWidth 518 | 519 | stkToDoc :: DocAnnStk a -> Doc a 520 | stkToDoc = foldl 521 | ( \doc -> case _ of 522 | Left ann -> 523 | annotate ann doc 524 | Right doc' -> 525 | doc' <> doc 526 | ) 527 | mempty 528 | -------------------------------------------------------------------------------- /src/Dodo.purs: -------------------------------------------------------------------------------- 1 | module Dodo 2 | ( module Exports 3 | , indent 4 | , align 5 | , alignCurrentColumn 6 | , annotate 7 | , withPosition 8 | , text 9 | , break 10 | , spaceBreak 11 | , softBreak 12 | , space 13 | , lines 14 | , words 15 | , (<%>) 16 | , (<+>) 17 | , () 18 | , appendBreak 19 | , appendSpace 20 | , appendSpaceBreak 21 | , flexAlt 22 | , flexGroup 23 | , flexSelect 24 | , paragraph 25 | , textParagraph 26 | , enclose 27 | , encloseEmptyAlt 28 | , encloseWithSeparator 29 | , foldWithSeparator 30 | , foldWith 31 | , locally 32 | , withLocalOptions 33 | , print 34 | , Printer(..) 35 | , plainText 36 | , PrintOptions 37 | , twoSpaces 38 | , fourSpaces 39 | , tabs 40 | ) where 41 | 42 | import Prelude 43 | 44 | import Data.Foldable (class Foldable, foldl, foldr) 45 | import Data.Int as Int 46 | import Data.List (List, (:)) 47 | import Data.List as List 48 | import Data.Monoid (power) 49 | import Data.String as String 50 | import Data.String.Regex as Regex 51 | import Data.String.Regex.Flags (global) 52 | import Data.String.Regex.Unsafe (unsafeRegex) 53 | import Data.Tuple (Tuple(..)) 54 | import Dodo.Internal (Doc(..), LocalOptions, Position, bothNotEmpty, isEmpty, notEmpty) 55 | import Dodo.Internal (Doc, Position, bothNotEmpty, isEmpty, notEmpty) as Exports 56 | import Dodo.Internal.Buffer (Buffer) 57 | import Dodo.Internal.Buffer as Buffer 58 | 59 | -- | Increases the indentation level by one indent. 60 | indent :: forall a. Doc a -> Doc a 61 | indent = notEmpty Indent 62 | 63 | -- | Increases the indentation level by the number of spaces (for alignment purposes). 64 | align :: forall a. Int -> Doc a -> Doc a 65 | align n doc 66 | | n > 0 = notEmpty (Align n) doc 67 | | otherwise = doc 68 | 69 | -- | Increases the indentation level so that it aligns to the current column. 70 | alignCurrentColumn :: forall a. Doc a -> Doc a 71 | alignCurrentColumn = notEmpty \doc -> withPosition \pos -> align (pos.column - pos.nextIndent) doc 72 | 73 | -- | Adds an annotation to a document. Printers can interpret annotations to style 74 | -- | their output, eg. ANSI colors. 75 | annotate :: forall a. a -> Doc a -> Doc a 76 | annotate = notEmpty <<< Annotate 77 | 78 | -- | Attempts to layout the document with flex alternatives, falling back 79 | -- | to defaults if it doesn't fit the page width. 80 | flexGroup :: forall a. Doc a -> Doc a 81 | flexGroup = notEmpty case _ of 82 | doc@(FlexSelect _ a b) | isEmpty a && isEmpty b -> doc 83 | doc -> FlexSelect doc Empty Empty 84 | 85 | -- | Attempts to layout the first document with flex alternatives, falling 86 | -- | back to defaults if it doesn't fit the page width. If the flex alternatives 87 | -- | are used then the second document will be appended, otherwise the third 88 | -- | document will be appended. 89 | flexSelect :: forall a. Doc a -> Doc a -> Doc a -> Doc a 90 | flexSelect doc1 doc2 doc3 91 | | isEmpty doc1 = doc2 92 | | otherwise = FlexSelect doc1 doc2 doc3 93 | 94 | -- | Attempts to layout the first document when in a flex group, falling back 95 | -- | to the second as a default. 96 | flexAlt :: forall a. Doc a -> Doc a -> Doc a 97 | flexAlt = FlexAlt 98 | 99 | -- | Build a document based on the current layout position. 100 | withPosition :: forall a. (Position -> Doc a) -> Doc a 101 | withPosition = WithPosition 102 | 103 | -- | The most basic document leaf. This should not contain newlines. If it does 104 | -- | your document will look very funny. 105 | text :: forall a. String -> Doc a 106 | text = case _ of 107 | "" -> Empty 108 | str -> Text (String.length str) str 109 | 110 | -- | Inserts a hard line break. 111 | break :: forall a. Doc a 112 | break = Break 113 | 114 | -- | Inserts a space when in a flex group, otherwise inserts a break. 115 | spaceBreak :: forall a. Doc a 116 | spaceBreak = flexAlt space break 117 | 118 | -- | Inserts nothing when in a flex group, otherwise inserts a break. 119 | softBreak :: forall a. Doc a 120 | softBreak = flexAlt mempty break 121 | 122 | -- | A singe space character. 123 | space :: forall a. Doc a 124 | space = text " " 125 | 126 | -- | Appends documents with a break in between them. 127 | lines :: forall f a. Foldable f => f (Doc a) -> Doc a 128 | lines = foldr appendBreak Empty 129 | 130 | -- | Appends documents with a space in between them. 131 | words :: forall f a. Foldable f => f (Doc a) -> Doc a 132 | words = foldr appendSpace Empty 133 | 134 | -- | Appends documents with a space-break in between them. 135 | paragraph :: forall f a. Foldable f => f (Doc a) -> Doc a 136 | paragraph = foldl appendSpaceBreak Empty 137 | 138 | -- | Constructs a wrapping paragraph from a blob of text. Ignores newlines and 139 | -- | multiple spaces. 140 | textParagraph :: forall a. String -> Doc a 141 | textParagraph = paragraph <<< map text <<< Regex.split spaceRegex <<< String.trim 142 | where 143 | spaceRegex = unsafeRegex """[\s\n]+""" global 144 | 145 | -- | Appends two documents with a break between them. 146 | appendBreak :: forall a. Doc a -> Doc a -> Doc a 147 | appendBreak = bothNotEmpty \a b -> a <> (break <> b) 148 | 149 | infixr 1 appendBreak as <%> 150 | 151 | -- | Appends two documents with a space between them. 152 | appendSpace :: forall a. Doc a -> Doc a -> Doc a 153 | appendSpace = bothNotEmpty \a b -> a <> (space <> b) 154 | 155 | infixr 2 appendSpace as <+> 156 | 157 | -- | Appends two documents with a space between them, falling back to a 158 | -- | break if that does not fit. 159 | appendSpaceBreak :: forall a. Doc a -> Doc a -> Doc a 160 | appendSpaceBreak = bothNotEmpty \a b -> a <> flexGroup (spaceBreak <> b) 161 | 162 | infixl 2 appendSpaceBreak as 163 | 164 | -- | Uses an opening and closing document to wrap another document. 165 | -- | ```purescript 166 | -- | example = enclose (text "(") (text ")") (text "inner") 167 | -- | ``` 168 | enclose :: forall a. Doc a -> Doc a -> Doc a -> Doc a 169 | enclose open close inner = open <> (inner <> close) 170 | 171 | -- | Uses an opening and closing document to wrap another document, falling 172 | -- | back when the inner document is empty. 173 | -- | ```purescript 174 | -- | example = encloseEmptyAlt (text "[ ") (text " ]") (text "[]") mempty 175 | -- | ``` 176 | encloseEmptyAlt :: forall a. Doc a -> Doc a -> Doc a -> Doc a -> Doc a 177 | encloseEmptyAlt open close default inner 178 | | isEmpty inner = default 179 | | otherwise = open <> (inner <> close) 180 | 181 | -- | Uses an opening and closing document, as a well as a separator, to render 182 | -- | a series of documents. 183 | -- | ```purescript` 184 | -- | example = encloseWithSeparator (text "[") (text "]") (",") [ text "one", text "two" ] 185 | -- | ``` 186 | encloseWithSeparator :: forall f a. Foldable f => Doc a -> Doc a -> Doc a -> f (Doc a) -> Doc a 187 | encloseWithSeparator open close separator inner = 188 | open <> (foldWithSeparator separator inner <> close) 189 | 190 | -- | Appends a series of documents together with a separator in between them. 191 | foldWithSeparator :: forall f a. Foldable f => Doc a -> f (Doc a) -> Doc a 192 | foldWithSeparator separator = foldWith (\a b -> a <> (separator <> b)) 193 | 194 | -- | Appends a series of documents together with a given append function. This 195 | -- | is notable because it ignores empty documents. 196 | foldWith :: forall f a. Foldable f => (Doc a -> Doc a -> Doc a) -> f (Doc a) -> Doc a 197 | foldWith f = foldr (bothNotEmpty f) mempty 198 | 199 | -- | *EXPERIMENTAL:* modifies printing state and options locally for a document. 200 | -- | This may change or be removed at any time. 201 | locally :: forall a. (LocalOptions -> LocalOptions) -> Doc a -> Doc a 202 | locally k doc = Local \options -> Tuple (k options) doc 203 | 204 | -- | *EXPERIMENTAL:* modifies printing state and options locally for a document. 205 | -- | This may change or be removed at any time. Differs from `locally` in that the 206 | -- | document can be responsive to options. 207 | withLocalOptions :: forall a. (LocalOptions -> Tuple LocalOptions (Doc a)) -> Doc a 208 | withLocalOptions = Local 209 | 210 | -- | Custom printers can be used to render richer documents than just plain 211 | -- | text. 212 | -- | * `emptyBuffer` - The initial buffer. 213 | -- | * `writeText` - Should write a string with the given width to the buffer. 214 | -- | * `writeIndent` - Should write indentation with the given width to the buffer. 215 | -- | * `writeBreak` - Should write a line break to the buffer. 216 | -- | * `enterAnnotation` - Called when entering a new annotated region. Provides the full annotation stack. 217 | -- | * `leaveAnnotation` - Called when leaving an annotated region. Provides the full annotation stack. 218 | -- | * `flushBuffer` - Called at the end of the document to get the final result. 219 | newtype Printer buff ann res = Printer 220 | { emptyBuffer :: buff 221 | , writeText :: Int -> String -> buff -> buff 222 | , writeIndent :: Int -> String -> buff -> buff 223 | , writeBreak :: buff -> buff 224 | , enterAnnotation :: ann -> List ann -> buff -> buff 225 | , leaveAnnotation :: ann -> List ann -> buff -> buff 226 | , flushBuffer :: buff -> res 227 | } 228 | 229 | -- | A plain text printer. Can be used with any document. 230 | plainText :: forall a. Printer String a String 231 | plainText = Printer 232 | { emptyBuffer: "" 233 | , writeText: \_ str buff -> buff <> str 234 | , writeIndent: \_ str buff -> buff <> str 235 | , writeBreak: \buff -> buff <> "\n" 236 | , enterAnnotation: \_ _ buff -> buff 237 | , leaveAnnotation: \_ _ buff -> buff 238 | , flushBuffer: \buff -> buff 239 | } 240 | 241 | -- | Configuration options for the printer. 242 | -- | * `pageWidth` - The printer will try not to exceed this width on any given line. 243 | -- | * `ribbonRatio` - Ratio between 0.0 and 1.0, defaults to 1.0. The printer will 244 | -- | use this ratio to calculate the printable area between the current indentation 245 | -- | level and the `pageWidth`. 246 | -- | * `indentUnit` - The string used for a single indent. 247 | -- | * `indentWidth` - The assumed character width of a single `indentUnit`. 248 | type PrintOptions = 249 | { pageWidth :: Int 250 | , ribbonRatio :: Number 251 | , indentUnit :: String 252 | , indentWidth :: Int 253 | } 254 | 255 | -- | Prints 2-space indents, with a default 80-column page width. 256 | twoSpaces :: PrintOptions 257 | twoSpaces = { pageWidth: 80, ribbonRatio: 1.0, indentUnit: " ", indentWidth: 2 } 258 | 259 | -- | Prints 4-space indents, with a default 120-column page width. 260 | fourSpaces :: PrintOptions 261 | fourSpaces = { pageWidth: 120, ribbonRatio: 1.0, indentUnit: " ", indentWidth: 4 } 262 | 263 | -- | Prints tab indents (4-wide), with a default 120-column page width. 264 | tabs :: PrintOptions 265 | tabs = { pageWidth: 120, ribbonRatio: 1.0, indentUnit: "\t", indentWidth: 4 } 266 | 267 | data DocCmd a 268 | = Doc (Doc a) 269 | | Dedent String Int 270 | | LeaveAnnotation a (List a) 271 | | LeaveFlexGroup (Doc a) (Doc a) 272 | | LeaveLocal LocalOptions 273 | 274 | data FlexGroupStatus b a 275 | = NoFlexGroup 276 | | FlexGroupPending 277 | | FlexGroupReset (FlexGroupState b a) 278 | 279 | type FlexGroupState b a = 280 | { position :: Position 281 | , buffer :: Buffer b 282 | , annotations :: List a 283 | , indentSpaces :: String 284 | , stack :: List (DocCmd a) 285 | , options :: PrintOptions 286 | } 287 | 288 | type DocState b a = 289 | { position :: Position 290 | , buffer :: Buffer b 291 | , annotations :: List a 292 | , indentSpaces :: String 293 | , flexGroup :: FlexGroupStatus b a 294 | , options :: PrintOptions 295 | } 296 | 297 | resetState :: forall a b. FlexGroupState b a -> DocState b a 298 | resetState { position, buffer, annotations, indentSpaces, options } = 299 | { position, buffer, annotations, indentSpaces, flexGroup: NoFlexGroup, options } 300 | 301 | storeState :: forall a b. List (DocCmd a) -> DocState b a -> FlexGroupState b a 302 | storeState stack { position, buffer, annotations, indentSpaces, options } = 303 | { position, buffer, annotations, indentSpaces, stack, options } 304 | 305 | storeOptions :: forall a b. Int -> LocalOptions -> DocState b a -> DocState b a 306 | storeOptions prevIndent localOptions state = do 307 | let 308 | newOptions = 309 | { indentUnit: localOptions.indentUnit 310 | , indentWidth: localOptions.indentWidth 311 | , pageWidth: localOptions.pageWidth 312 | , ribbonRatio: localOptions.ribbonRatio 313 | } 314 | state 315 | { indentSpaces = localOptions.indentSpaces 316 | , options = newOptions 317 | , position 318 | { pageWidth = newOptions.pageWidth 319 | , ribbonWidth = calcRibbonWidth newOptions prevIndent 320 | , nextIndent = localOptions.indent 321 | } 322 | } 323 | 324 | calcRibbonWidth :: PrintOptions -> Int -> Int 325 | calcRibbonWidth { pageWidth, ribbonRatio } n = 326 | max 0 $ Int.ceil $ mul ribbonRatio $ Int.toNumber $ pageWidth - n 327 | 328 | -- | Prints a documents given a printer and print options. 329 | -- | 330 | -- | ```purescript 331 | -- | print plainText twoSpaces myDoc 332 | -- | ``` 333 | -- | 334 | -- | This will use full line-lookahead from the start of a flex group. If it 335 | -- | encounters a break or content overflows the page-width, it will layout 336 | -- | the group using flex alternative defaults instead. 337 | print :: forall b a r. Printer b a r -> PrintOptions -> Doc a -> r 338 | print (Printer printer) opts = flip go initState <<< pure <<< Doc 339 | where 340 | initOptions :: PrintOptions 341 | initOptions = opts { ribbonRatio = max 0.0 (min 1.0 opts.ribbonRatio) } 342 | 343 | initState :: DocState b a 344 | initState = 345 | { position: 346 | { line: 0 347 | , column: 0 348 | , indent: 0 349 | , nextIndent: 0 350 | , pageWidth: initOptions.pageWidth 351 | , ribbonWidth: calcRibbonWidth initOptions 0 352 | } 353 | , buffer: Buffer.new printer.emptyBuffer 354 | , annotations: List.Nil 355 | , indentSpaces: "" 356 | , flexGroup: NoFlexGroup 357 | , options: initOptions 358 | } 359 | 360 | go :: List (DocCmd a) -> DocState b a -> r 361 | go stack state = case stack of 362 | List.Nil -> 363 | printer.flushBuffer $ Buffer.get state.buffer 364 | cmd : stk -> case cmd of 365 | Doc doc -> case doc of 366 | Append doc1 doc2 -> 367 | go (Doc doc1 : Doc doc2 : stk) state 368 | Text len str 369 | | state.position.column == 0 && state.position.indent > 0 -> 370 | go stack state 371 | { position { column = state.position.indent } 372 | , buffer = Buffer.modify (printer.writeIndent state.position.indent state.indentSpaces) state.buffer 373 | } 374 | | state.position.column + len <= state.position.indent + state.position.ribbonWidth -> 375 | go stk state 376 | { position { column = state.position.column + len } 377 | , buffer = Buffer.modify (printer.writeText len str) state.buffer 378 | } 379 | | otherwise -> case state.flexGroup of 380 | FlexGroupReset frame -> 381 | go frame.stack $ resetState frame 382 | _ -> 383 | go stk state 384 | { position { column = state.position.column + len } 385 | , flexGroup = NoFlexGroup 386 | , buffer = Buffer.modify (printer.writeText len str) state.buffer 387 | } 388 | Break -> case state.flexGroup of 389 | FlexGroupReset frame -> 390 | go frame.stack $ resetState frame 391 | _ -> 392 | go stk state 393 | { position 394 | { line = state.position.line + 1 395 | , column = 0 396 | , indent = state.position.nextIndent 397 | , ribbonWidth = calcRibbonWidth state.options state.position.nextIndent 398 | } 399 | , buffer = Buffer.modify printer.writeBreak state.buffer 400 | , flexGroup = NoFlexGroup 401 | } 402 | Indent doc1 403 | | state.position.column == 0 -> 404 | go (Doc doc1 : Dedent state.indentSpaces state.position.nextIndent : stk) state 405 | { position 406 | { indent = state.position.nextIndent + opts.indentWidth 407 | , nextIndent = state.position.nextIndent + opts.indentWidth 408 | , ribbonWidth = calcRibbonWidth state.options (state.position.nextIndent + opts.indentWidth) 409 | } 410 | , indentSpaces = state.indentSpaces <> opts.indentUnit 411 | } 412 | | otherwise -> 413 | go (Doc doc1 : Dedent state.indentSpaces state.position.nextIndent : stk) state 414 | { position { nextIndent = state.position.nextIndent + opts.indentWidth } 415 | , indentSpaces = state.indentSpaces <> opts.indentUnit 416 | } 417 | Align width doc1 418 | | state.position.column == 0 -> 419 | go (Doc doc1 : Dedent state.indentSpaces state.position.nextIndent : stk) state 420 | { position 421 | { indent = state.position.nextIndent + width 422 | , nextIndent = state.position.nextIndent + width 423 | , ribbonWidth = calcRibbonWidth state.options (state.position.nextIndent + width) 424 | } 425 | , indentSpaces = state.indentSpaces <> power " " width 426 | } 427 | | otherwise -> 428 | go (Doc doc1 : Dedent state.indentSpaces state.position.nextIndent : stk) state 429 | { position { nextIndent = state.position.nextIndent + width } 430 | , indentSpaces = state.indentSpaces <> power " " width 431 | } 432 | FlexSelect doc1 doc2 doc3 -> case state.flexGroup of 433 | NoFlexGroup -> 434 | go (Doc doc1 : LeaveFlexGroup doc2 doc3 : stk) state 435 | { flexGroup = FlexGroupPending 436 | } 437 | FlexGroupPending | state.position.ribbonWidth > 0 -> 438 | go (Doc doc1 : Doc doc2 : stk) state 439 | { flexGroup = FlexGroupReset $ storeState stack state 440 | , buffer = Buffer.branch state.buffer 441 | } 442 | _ -> 443 | go (Doc doc1 : Doc doc2 : stk) state 444 | FlexAlt flexDoc doc1 -> case state.flexGroup of 445 | FlexGroupReset _ -> 446 | go (Doc flexDoc : stk) state 447 | FlexGroupPending | state.position.ribbonWidth > 0 -> 448 | go (Doc flexDoc : stk) state 449 | { flexGroup = FlexGroupReset $ storeState (Doc doc1 : stk) state 450 | , buffer = Buffer.branch state.buffer 451 | } 452 | _ -> 453 | go (Doc doc1 : stk) state 454 | WithPosition k 455 | | state.position.column == 0 && state.position.nextIndent > 0 -> 456 | go (Doc (k state.position { column = state.position.nextIndent }) : stk) state 457 | | otherwise -> 458 | go (Doc (k state.position) : stk) state 459 | Annotate ann doc1 -> 460 | go (Doc doc1 : LeaveAnnotation ann state.annotations : stk) state 461 | { annotations = ann : state.annotations 462 | , buffer = Buffer.modify (printer.enterAnnotation ann state.annotations) state.buffer 463 | } 464 | Local k -> do 465 | let 466 | prevOptions = 467 | { indent: state.position.indent 468 | , indentSpaces: state.indentSpaces 469 | , indentUnit: state.options.indentUnit 470 | , indentWidth: state.options.indentWidth 471 | , pageWidth: state.options.pageWidth 472 | , ribbonRatio: state.options.ribbonRatio 473 | } 474 | Tuple localOptions doc1 = k prevOptions 475 | go (Doc doc1 : LeaveLocal prevOptions : stk) $ storeOptions state.position.indent localOptions state 476 | Empty -> 477 | go stk state 478 | LeaveFlexGroup doc1 doc2 -> case state.flexGroup of 479 | NoFlexGroup -> 480 | go (Doc doc2 : stk) state 481 | { buffer = Buffer.commit state.buffer 482 | } 483 | _ -> 484 | go (Doc doc1 : stk) state 485 | { flexGroup = NoFlexGroup 486 | , buffer = Buffer.commit state.buffer 487 | } 488 | Dedent indSpaces ind -> 489 | go stk state 490 | { position { nextIndent = ind } 491 | , indentSpaces = indSpaces 492 | } 493 | LeaveAnnotation ann anns -> 494 | go stk state 495 | { annotations = anns 496 | , buffer = Buffer.modify (printer.leaveAnnotation ann anns) state.buffer 497 | } 498 | LeaveLocal prevOptions -> 499 | go stk $ storeOptions state.position.indent prevOptions state 500 | -------------------------------------------------------------------------------- /spago.lock: -------------------------------------------------------------------------------- 1 | { 2 | "workspace": { 3 | "packages": { 4 | "dodo-printer": { 5 | "path": "./", 6 | "core": { 7 | "dependencies": [ 8 | { 9 | "ansi": ">=7.0.0 <8.0.0" 10 | }, 11 | { 12 | "either": ">=6.1.0 <7.0.0" 13 | }, 14 | { 15 | "foldable-traversable": ">=6.0.0 <7.0.0" 16 | }, 17 | { 18 | "integers": ">=6.0.0 <7.0.0" 19 | }, 20 | { 21 | "lists": ">=7.0.0 <8.0.0" 22 | }, 23 | { 24 | "maybe": ">=6.0.0 <7.0.0" 25 | }, 26 | { 27 | "newtype": ">=5.0.0 <6.0.0" 28 | }, 29 | { 30 | "partial": ">=4.0.0 <5.0.0" 31 | }, 32 | { 33 | "prelude": ">=6.0.1 <7.0.0" 34 | }, 35 | { 36 | "safe-coerce": ">=2.0.0 <3.0.0" 37 | }, 38 | { 39 | "strings": ">=6.0.1 <7.0.0" 40 | }, 41 | { 42 | "tuples": ">=7.0.0 <8.0.0" 43 | } 44 | ], 45 | "build_plan": [ 46 | "ansi", 47 | "arrays", 48 | "bifunctors", 49 | "const", 50 | "contravariant", 51 | "control", 52 | "distributive", 53 | "effect", 54 | "either", 55 | "enums", 56 | "exists", 57 | "foldable-traversable", 58 | "functions", 59 | "functors", 60 | "gen", 61 | "identity", 62 | "integers", 63 | "invariant", 64 | "lazy", 65 | "lists", 66 | "maybe", 67 | "newtype", 68 | "nonempty", 69 | "numbers", 70 | "orders", 71 | "partial", 72 | "prelude", 73 | "profunctor", 74 | "refs", 75 | "safe-coerce", 76 | "st", 77 | "strings", 78 | "tailrec", 79 | "tuples", 80 | "type-equality", 81 | "unfoldable", 82 | "unsafe-coerce" 83 | ] 84 | }, 85 | "test": { 86 | "dependencies": [ 87 | "aff", 88 | "avar", 89 | "console", 90 | "effect", 91 | "exceptions", 92 | "minibench", 93 | "node-buffer", 94 | "node-child-process", 95 | "node-fs", 96 | "node-path", 97 | "node-process", 98 | "node-streams", 99 | "parallel" 100 | ], 101 | "build_plan": [ 102 | "aff", 103 | "arraybuffer-types", 104 | "arrays", 105 | "avar", 106 | "bifunctors", 107 | "console", 108 | "const", 109 | "contravariant", 110 | "control", 111 | "datetime", 112 | "distributive", 113 | "effect", 114 | "either", 115 | "enums", 116 | "exceptions", 117 | "exists", 118 | "foldable-traversable", 119 | "foreign", 120 | "foreign-object", 121 | "functions", 122 | "functors", 123 | "gen", 124 | "identity", 125 | "integers", 126 | "invariant", 127 | "js-date", 128 | "lazy", 129 | "lists", 130 | "maybe", 131 | "minibench", 132 | "newtype", 133 | "node-buffer", 134 | "node-child-process", 135 | "node-event-emitter", 136 | "node-fs", 137 | "node-os", 138 | "node-path", 139 | "node-process", 140 | "node-streams", 141 | "nonempty", 142 | "now", 143 | "nullable", 144 | "numbers", 145 | "ordered-collections", 146 | "orders", 147 | "parallel", 148 | "partial", 149 | "posix-types", 150 | "prelude", 151 | "profunctor", 152 | "refs", 153 | "safe-coerce", 154 | "st", 155 | "strings", 156 | "tailrec", 157 | "transformers", 158 | "tuples", 159 | "type-equality", 160 | "typelevel-prelude", 161 | "unfoldable", 162 | "unsafe-coerce" 163 | ] 164 | } 165 | } 166 | }, 167 | "extra_packages": {} 168 | }, 169 | "packages": { 170 | "aff": { 171 | "type": "registry", 172 | "version": "7.1.0", 173 | "integrity": "sha256-7hOC6uQO9XBAI5FD8F33ChLjFAiZVfd4BJMqlMh7TNU=", 174 | "dependencies": [ 175 | "arrays", 176 | "bifunctors", 177 | "control", 178 | "datetime", 179 | "effect", 180 | "either", 181 | "exceptions", 182 | "foldable-traversable", 183 | "functions", 184 | "maybe", 185 | "newtype", 186 | "parallel", 187 | "prelude", 188 | "refs", 189 | "tailrec", 190 | "transformers", 191 | "unsafe-coerce" 192 | ] 193 | }, 194 | "ansi": { 195 | "type": "registry", 196 | "version": "7.0.0", 197 | "integrity": "sha256-ZMB6HD+q9CXvn9fRCmJ8dvuDrOVHcjombL3oNOerVnE=", 198 | "dependencies": [ 199 | "foldable-traversable", 200 | "lists", 201 | "strings" 202 | ] 203 | }, 204 | "arraybuffer-types": { 205 | "type": "registry", 206 | "version": "3.0.2", 207 | "integrity": "sha256-mQKokysYVkooS4uXbO+yovmV/s8b138Ws3zQvOwIHRA=", 208 | "dependencies": [] 209 | }, 210 | "arrays": { 211 | "type": "registry", 212 | "version": "7.3.0", 213 | "integrity": "sha256-tmcklBlc/muUtUfr9RapdCPwnlQeB3aSrC4dK85gQlc=", 214 | "dependencies": [ 215 | "bifunctors", 216 | "control", 217 | "foldable-traversable", 218 | "functions", 219 | "maybe", 220 | "nonempty", 221 | "partial", 222 | "prelude", 223 | "safe-coerce", 224 | "st", 225 | "tailrec", 226 | "tuples", 227 | "unfoldable", 228 | "unsafe-coerce" 229 | ] 230 | }, 231 | "avar": { 232 | "type": "registry", 233 | "version": "5.0.0", 234 | "integrity": "sha256-e7hf0x4hEpcygXP0LtvfvAQ49Bbj2aWtZT3gqM///0A=", 235 | "dependencies": [ 236 | "aff", 237 | "effect", 238 | "either", 239 | "exceptions", 240 | "functions", 241 | "maybe" 242 | ] 243 | }, 244 | "bifunctors": { 245 | "type": "registry", 246 | "version": "6.0.0", 247 | "integrity": "sha256-/gZwC9YhNxZNQpnHa5BIYerCGM2jeX9ukZiEvYxm5Nw=", 248 | "dependencies": [ 249 | "const", 250 | "either", 251 | "newtype", 252 | "prelude", 253 | "tuples" 254 | ] 255 | }, 256 | "console": { 257 | "type": "registry", 258 | "version": "6.1.0", 259 | "integrity": "sha256-CxmAzjgyuGDmt9FZW51VhV6rBPwR6o0YeKUzA9rSzcM=", 260 | "dependencies": [ 261 | "effect", 262 | "prelude" 263 | ] 264 | }, 265 | "const": { 266 | "type": "registry", 267 | "version": "6.0.0", 268 | "integrity": "sha256-tNrxDW8D8H4jdHE2HiPzpLy08zkzJMmGHdRqt5BQuTc=", 269 | "dependencies": [ 270 | "invariant", 271 | "newtype", 272 | "prelude" 273 | ] 274 | }, 275 | "contravariant": { 276 | "type": "registry", 277 | "version": "6.0.0", 278 | "integrity": "sha256-TP+ooAp3vvmdjfQsQJSichF5B4BPDHp3wAJoWchip6c=", 279 | "dependencies": [ 280 | "const", 281 | "either", 282 | "newtype", 283 | "prelude", 284 | "tuples" 285 | ] 286 | }, 287 | "control": { 288 | "type": "registry", 289 | "version": "6.0.0", 290 | "integrity": "sha256-sH7Pg9E96JCPF9PIA6oQ8+BjTyO/BH1ZuE/bOcyj4Jk=", 291 | "dependencies": [ 292 | "newtype", 293 | "prelude" 294 | ] 295 | }, 296 | "datetime": { 297 | "type": "registry", 298 | "version": "6.1.0", 299 | "integrity": "sha256-g/5X5BBegQWLpI9IWD+sY6mcaYpzzlW5lz5NBzaMtyI=", 300 | "dependencies": [ 301 | "bifunctors", 302 | "control", 303 | "either", 304 | "enums", 305 | "foldable-traversable", 306 | "functions", 307 | "gen", 308 | "integers", 309 | "lists", 310 | "maybe", 311 | "newtype", 312 | "numbers", 313 | "ordered-collections", 314 | "partial", 315 | "prelude", 316 | "tuples" 317 | ] 318 | }, 319 | "distributive": { 320 | "type": "registry", 321 | "version": "6.0.0", 322 | "integrity": "sha256-HTDdmEnzigMl+02SJB88j+gAXDx9VKsbvR4MJGDPbOQ=", 323 | "dependencies": [ 324 | "identity", 325 | "newtype", 326 | "prelude", 327 | "tuples", 328 | "type-equality" 329 | ] 330 | }, 331 | "effect": { 332 | "type": "registry", 333 | "version": "4.0.0", 334 | "integrity": "sha256-eBtZu+HZcMa5HilvI6kaDyVX3ji8p0W9MGKy2K4T6+M=", 335 | "dependencies": [ 336 | "prelude" 337 | ] 338 | }, 339 | "either": { 340 | "type": "registry", 341 | "version": "6.1.0", 342 | "integrity": "sha256-6hgTPisnMWVwQivOu2PKYcH8uqjEOOqDyaDQVUchTpY=", 343 | "dependencies": [ 344 | "control", 345 | "invariant", 346 | "maybe", 347 | "prelude" 348 | ] 349 | }, 350 | "enums": { 351 | "type": "registry", 352 | "version": "6.0.1", 353 | "integrity": "sha256-HWaD73JFLorc4A6trKIRUeDMdzE+GpkJaEOM1nTNkC8=", 354 | "dependencies": [ 355 | "control", 356 | "either", 357 | "gen", 358 | "maybe", 359 | "newtype", 360 | "nonempty", 361 | "partial", 362 | "prelude", 363 | "tuples", 364 | "unfoldable" 365 | ] 366 | }, 367 | "exceptions": { 368 | "type": "registry", 369 | "version": "6.1.0", 370 | "integrity": "sha256-K0T89IHtF3vBY7eSAO7eDOqSb2J9kZGAcDN5+IKsF8E=", 371 | "dependencies": [ 372 | "effect", 373 | "either", 374 | "maybe", 375 | "prelude" 376 | ] 377 | }, 378 | "exists": { 379 | "type": "registry", 380 | "version": "6.0.0", 381 | "integrity": "sha256-A0JQHpTfo1dNOj9U5/Fd3xndlRSE0g2IQWOGor2yXn8=", 382 | "dependencies": [ 383 | "unsafe-coerce" 384 | ] 385 | }, 386 | "foldable-traversable": { 387 | "type": "registry", 388 | "version": "6.0.0", 389 | "integrity": "sha256-fLeqRYM4jUrZD5H4WqcwUgzU7XfYkzO4zhgtNc3jcWM=", 390 | "dependencies": [ 391 | "bifunctors", 392 | "const", 393 | "control", 394 | "either", 395 | "functors", 396 | "identity", 397 | "maybe", 398 | "newtype", 399 | "orders", 400 | "prelude", 401 | "tuples" 402 | ] 403 | }, 404 | "foreign": { 405 | "type": "registry", 406 | "version": "7.0.0", 407 | "integrity": "sha256-1ORiqoS3HW+qfwSZAppHPWy4/6AQysxZ2t29jcdUMNA=", 408 | "dependencies": [ 409 | "either", 410 | "functions", 411 | "identity", 412 | "integers", 413 | "lists", 414 | "maybe", 415 | "prelude", 416 | "strings", 417 | "transformers" 418 | ] 419 | }, 420 | "foreign-object": { 421 | "type": "registry", 422 | "version": "4.1.0", 423 | "integrity": "sha256-q24okj6mT+yGHYQ+ei/pYPj5ih6sTbu7eDv/WU56JVo=", 424 | "dependencies": [ 425 | "arrays", 426 | "foldable-traversable", 427 | "functions", 428 | "gen", 429 | "lists", 430 | "maybe", 431 | "prelude", 432 | "st", 433 | "tailrec", 434 | "tuples", 435 | "typelevel-prelude", 436 | "unfoldable" 437 | ] 438 | }, 439 | "functions": { 440 | "type": "registry", 441 | "version": "6.0.0", 442 | "integrity": "sha256-adMyJNEnhGde2unHHAP79gPtlNjNqzgLB8arEOn9hLI=", 443 | "dependencies": [ 444 | "prelude" 445 | ] 446 | }, 447 | "functors": { 448 | "type": "registry", 449 | "version": "5.0.0", 450 | "integrity": "sha256-zfPWWYisbD84MqwpJSZFlvM6v86McM68ob8p9s27ywU=", 451 | "dependencies": [ 452 | "bifunctors", 453 | "const", 454 | "contravariant", 455 | "control", 456 | "distributive", 457 | "either", 458 | "invariant", 459 | "maybe", 460 | "newtype", 461 | "prelude", 462 | "profunctor", 463 | "tuples", 464 | "unsafe-coerce" 465 | ] 466 | }, 467 | "gen": { 468 | "type": "registry", 469 | "version": "4.0.0", 470 | "integrity": "sha256-f7yzAXWwr+xnaqEOcvyO3ezKdoes8+WXWdXIHDBCAPI=", 471 | "dependencies": [ 472 | "either", 473 | "foldable-traversable", 474 | "identity", 475 | "maybe", 476 | "newtype", 477 | "nonempty", 478 | "prelude", 479 | "tailrec", 480 | "tuples", 481 | "unfoldable" 482 | ] 483 | }, 484 | "identity": { 485 | "type": "registry", 486 | "version": "6.0.0", 487 | "integrity": "sha256-4wY0XZbAksjY6UAg99WkuKyJlQlWAfTi2ssadH0wVMY=", 488 | "dependencies": [ 489 | "control", 490 | "invariant", 491 | "newtype", 492 | "prelude" 493 | ] 494 | }, 495 | "integers": { 496 | "type": "registry", 497 | "version": "6.0.0", 498 | "integrity": "sha256-sf+sK26R1hzwl3NhXR7WAu9zCDjQnfoXwcyGoseX158=", 499 | "dependencies": [ 500 | "maybe", 501 | "numbers", 502 | "prelude" 503 | ] 504 | }, 505 | "invariant": { 506 | "type": "registry", 507 | "version": "6.0.0", 508 | "integrity": "sha256-RGWWyYrz0Hs1KjPDA+87Kia67ZFBhfJ5lMGOMCEFoLo=", 509 | "dependencies": [ 510 | "control", 511 | "prelude" 512 | ] 513 | }, 514 | "js-date": { 515 | "type": "registry", 516 | "version": "8.0.0", 517 | "integrity": "sha256-6TVF4DWg5JL+jRAsoMssYw8rgOVALMUHT1CuNZt8NRo=", 518 | "dependencies": [ 519 | "datetime", 520 | "effect", 521 | "exceptions", 522 | "foreign", 523 | "integers", 524 | "now" 525 | ] 526 | }, 527 | "lazy": { 528 | "type": "registry", 529 | "version": "6.0.0", 530 | "integrity": "sha256-lMsfFOnlqfe4KzRRiW8ot5ge6HtcU3Eyh2XkXcP5IgU=", 531 | "dependencies": [ 532 | "control", 533 | "foldable-traversable", 534 | "invariant", 535 | "prelude" 536 | ] 537 | }, 538 | "lists": { 539 | "type": "registry", 540 | "version": "7.0.0", 541 | "integrity": "sha256-EKF15qYqucuXP2lT/xPxhqy58f0FFT6KHdIB/yBOayI=", 542 | "dependencies": [ 543 | "bifunctors", 544 | "control", 545 | "foldable-traversable", 546 | "lazy", 547 | "maybe", 548 | "newtype", 549 | "nonempty", 550 | "partial", 551 | "prelude", 552 | "tailrec", 553 | "tuples", 554 | "unfoldable" 555 | ] 556 | }, 557 | "maybe": { 558 | "type": "registry", 559 | "version": "6.0.0", 560 | "integrity": "sha256-5cCIb0wPwbat2PRkQhUeZO0jcAmf8jCt2qE0wbC3v2Q=", 561 | "dependencies": [ 562 | "control", 563 | "invariant", 564 | "newtype", 565 | "prelude" 566 | ] 567 | }, 568 | "minibench": { 569 | "type": "registry", 570 | "version": "4.0.1", 571 | "integrity": "sha256-7jyxcklZI49q/otYvMV4f9YnJwEqQ3Me5buhDwAOydw=", 572 | "dependencies": [ 573 | "console", 574 | "effect", 575 | "integers", 576 | "numbers", 577 | "partial", 578 | "prelude", 579 | "refs" 580 | ] 581 | }, 582 | "newtype": { 583 | "type": "registry", 584 | "version": "5.0.0", 585 | "integrity": "sha256-gdrQu8oGe9eZE6L3wOI8ql/igOg+zEGB5ITh2g+uttw=", 586 | "dependencies": [ 587 | "prelude", 588 | "safe-coerce" 589 | ] 590 | }, 591 | "node-buffer": { 592 | "type": "registry", 593 | "version": "9.0.0", 594 | "integrity": "sha256-PWE2DJ5ruBLCmeA/fUiuySEFmUJ/VuRfyrnCuVZBlu4=", 595 | "dependencies": [ 596 | "arraybuffer-types", 597 | "effect", 598 | "maybe", 599 | "nullable", 600 | "st", 601 | "unsafe-coerce" 602 | ] 603 | }, 604 | "node-child-process": { 605 | "type": "registry", 606 | "version": "11.1.0", 607 | "integrity": "sha256-vioMNgk8p+CGwlb6T3I3TIir27el85Yg4satLE/I89w=", 608 | "dependencies": [ 609 | "exceptions", 610 | "foreign", 611 | "foreign-object", 612 | "functions", 613 | "node-event-emitter", 614 | "node-fs", 615 | "node-os", 616 | "node-streams", 617 | "nullable", 618 | "posix-types", 619 | "unsafe-coerce" 620 | ] 621 | }, 622 | "node-event-emitter": { 623 | "type": "registry", 624 | "version": "3.0.0", 625 | "integrity": "sha256-Qw0MjsT4xRH2j2i4K8JmRjcMKnH5z1Cw39t00q4LE4w=", 626 | "dependencies": [ 627 | "effect", 628 | "either", 629 | "functions", 630 | "maybe", 631 | "nullable", 632 | "prelude", 633 | "unsafe-coerce" 634 | ] 635 | }, 636 | "node-fs": { 637 | "type": "registry", 638 | "version": "9.2.0", 639 | "integrity": "sha256-Sg0vkXycEzkEerX6hLccz21Ygd9w1+QSk1thotRZPGI=", 640 | "dependencies": [ 641 | "datetime", 642 | "effect", 643 | "either", 644 | "enums", 645 | "exceptions", 646 | "functions", 647 | "integers", 648 | "js-date", 649 | "maybe", 650 | "node-buffer", 651 | "node-path", 652 | "node-streams", 653 | "nullable", 654 | "partial", 655 | "prelude", 656 | "strings", 657 | "unsafe-coerce" 658 | ] 659 | }, 660 | "node-os": { 661 | "type": "registry", 662 | "version": "5.1.0", 663 | "integrity": "sha256-K3gcu9AXanN1+qtk1900+Fi+CuO0s3/H/RMNRNgIzso=", 664 | "dependencies": [ 665 | "arrays", 666 | "bifunctors", 667 | "console", 668 | "control", 669 | "datetime", 670 | "effect", 671 | "either", 672 | "exceptions", 673 | "foldable-traversable", 674 | "foreign", 675 | "foreign-object", 676 | "functions", 677 | "maybe", 678 | "node-buffer", 679 | "nullable", 680 | "partial", 681 | "posix-types", 682 | "prelude", 683 | "unsafe-coerce" 684 | ] 685 | }, 686 | "node-path": { 687 | "type": "registry", 688 | "version": "5.0.1", 689 | "integrity": "sha256-ePOElFamHkffhwJcS0Ozq4A14rflnkasFU6X2B8/yXs=", 690 | "dependencies": [ 691 | "effect" 692 | ] 693 | }, 694 | "node-process": { 695 | "type": "registry", 696 | "version": "11.2.0", 697 | "integrity": "sha256-+2MQDYChjGbVbapCyJtuWYwD41jk+BntF/kcOTKBMVs=", 698 | "dependencies": [ 699 | "effect", 700 | "foreign", 701 | "foreign-object", 702 | "maybe", 703 | "node-event-emitter", 704 | "node-streams", 705 | "posix-types", 706 | "prelude", 707 | "unsafe-coerce" 708 | ] 709 | }, 710 | "node-streams": { 711 | "type": "registry", 712 | "version": "9.0.0", 713 | "integrity": "sha256-2n6dq7YWleTDmD1Kur/ul7Cn08IvWrScgPf+0PgX2TQ=", 714 | "dependencies": [ 715 | "aff", 716 | "effect", 717 | "either", 718 | "exceptions", 719 | "node-buffer", 720 | "node-event-emitter", 721 | "nullable", 722 | "prelude" 723 | ] 724 | }, 725 | "nonempty": { 726 | "type": "registry", 727 | "version": "7.0.0", 728 | "integrity": "sha256-54ablJZUHGvvlTJzi3oXyPCuvY6zsrWJuH/dMJ/MFLs=", 729 | "dependencies": [ 730 | "control", 731 | "foldable-traversable", 732 | "maybe", 733 | "prelude", 734 | "tuples", 735 | "unfoldable" 736 | ] 737 | }, 738 | "now": { 739 | "type": "registry", 740 | "version": "6.0.0", 741 | "integrity": "sha256-xZ7x37ZMREfs6GCDw/h+FaKHV/3sPWmtqBZRGTxybQY=", 742 | "dependencies": [ 743 | "datetime", 744 | "effect" 745 | ] 746 | }, 747 | "nullable": { 748 | "type": "registry", 749 | "version": "6.0.0", 750 | "integrity": "sha256-yiGBVl3AD+Guy4kNWWeN+zl1gCiJK+oeIFtZtPCw4+o=", 751 | "dependencies": [ 752 | "effect", 753 | "functions", 754 | "maybe" 755 | ] 756 | }, 757 | "numbers": { 758 | "type": "registry", 759 | "version": "9.0.1", 760 | "integrity": "sha256-/9M6aeMDBdB4cwYDeJvLFprAHZ49EbtKQLIJsneXLIk=", 761 | "dependencies": [ 762 | "functions", 763 | "maybe" 764 | ] 765 | }, 766 | "ordered-collections": { 767 | "type": "registry", 768 | "version": "3.2.0", 769 | "integrity": "sha256-o9jqsj5rpJmMdoe/zyufWHFjYYFTTsJpgcuCnqCO6PM=", 770 | "dependencies": [ 771 | "arrays", 772 | "foldable-traversable", 773 | "gen", 774 | "lists", 775 | "maybe", 776 | "partial", 777 | "prelude", 778 | "st", 779 | "tailrec", 780 | "tuples", 781 | "unfoldable" 782 | ] 783 | }, 784 | "orders": { 785 | "type": "registry", 786 | "version": "6.0.0", 787 | "integrity": "sha256-nBA0g3/ai0euH8q9pSbGqk53W2q6agm/dECZTHcoink=", 788 | "dependencies": [ 789 | "newtype", 790 | "prelude" 791 | ] 792 | }, 793 | "parallel": { 794 | "type": "registry", 795 | "version": "6.0.0", 796 | "integrity": "sha256-VJbkGD0rAKX+NUEeBJbYJ78bEKaZbgow+QwQEfPB6ko=", 797 | "dependencies": [ 798 | "control", 799 | "effect", 800 | "either", 801 | "foldable-traversable", 802 | "functors", 803 | "maybe", 804 | "newtype", 805 | "prelude", 806 | "profunctor", 807 | "refs", 808 | "transformers" 809 | ] 810 | }, 811 | "partial": { 812 | "type": "registry", 813 | "version": "4.0.0", 814 | "integrity": "sha256-fwXerld6Xw1VkReh8yeQsdtLVrjfGiVuC5bA1Wyo/J4=", 815 | "dependencies": [] 816 | }, 817 | "posix-types": { 818 | "type": "registry", 819 | "version": "6.0.0", 820 | "integrity": "sha256-ZfFz8RR1lee/o/Prccyeut3Q+9tYd08mlR72sIh6GzA=", 821 | "dependencies": [ 822 | "maybe", 823 | "prelude" 824 | ] 825 | }, 826 | "prelude": { 827 | "type": "registry", 828 | "version": "6.0.1", 829 | "integrity": "sha256-o8p6SLYmVPqzXZhQFd2hGAWEwBoXl1swxLG/scpJ0V0=", 830 | "dependencies": [] 831 | }, 832 | "profunctor": { 833 | "type": "registry", 834 | "version": "6.0.1", 835 | "integrity": "sha256-E58hSYdJvF2Qjf9dnWLPlJKh2Z2fLfFLkQoYi16vsFk=", 836 | "dependencies": [ 837 | "control", 838 | "distributive", 839 | "either", 840 | "exists", 841 | "invariant", 842 | "newtype", 843 | "prelude", 844 | "tuples" 845 | ] 846 | }, 847 | "refs": { 848 | "type": "registry", 849 | "version": "6.0.0", 850 | "integrity": "sha256-Vgwne7jIbD3ZMoLNNETLT8Litw6lIYo3MfYNdtYWj9s=", 851 | "dependencies": [ 852 | "effect", 853 | "prelude" 854 | ] 855 | }, 856 | "safe-coerce": { 857 | "type": "registry", 858 | "version": "2.0.0", 859 | "integrity": "sha256-a1ibQkiUcbODbLE/WAq7Ttbbh9ex+x33VCQ7GngKudU=", 860 | "dependencies": [ 861 | "unsafe-coerce" 862 | ] 863 | }, 864 | "st": { 865 | "type": "registry", 866 | "version": "6.2.0", 867 | "integrity": "sha256-z9X0WsOUlPwNx9GlCC+YccCyz8MejC8Wb0C4+9fiBRY=", 868 | "dependencies": [ 869 | "partial", 870 | "prelude", 871 | "tailrec", 872 | "unsafe-coerce" 873 | ] 874 | }, 875 | "strings": { 876 | "type": "registry", 877 | "version": "6.0.1", 878 | "integrity": "sha256-WssD3DbX4OPzxSdjvRMX0yvc9+pS7n5gyPv5I2Trb7k=", 879 | "dependencies": [ 880 | "arrays", 881 | "control", 882 | "either", 883 | "enums", 884 | "foldable-traversable", 885 | "gen", 886 | "integers", 887 | "maybe", 888 | "newtype", 889 | "nonempty", 890 | "partial", 891 | "prelude", 892 | "tailrec", 893 | "tuples", 894 | "unfoldable", 895 | "unsafe-coerce" 896 | ] 897 | }, 898 | "tailrec": { 899 | "type": "registry", 900 | "version": "6.1.0", 901 | "integrity": "sha256-Xx19ECVDRrDWpz9D2GxQHHV89vd61dnXxQm0IcYQHGk=", 902 | "dependencies": [ 903 | "bifunctors", 904 | "effect", 905 | "either", 906 | "identity", 907 | "maybe", 908 | "partial", 909 | "prelude", 910 | "refs" 911 | ] 912 | }, 913 | "transformers": { 914 | "type": "registry", 915 | "version": "6.1.0", 916 | "integrity": "sha256-3Bm+Z6tsC/paG888XkywDngJ2JMos+JfOhRlkVfb7gI=", 917 | "dependencies": [ 918 | "control", 919 | "distributive", 920 | "effect", 921 | "either", 922 | "exceptions", 923 | "foldable-traversable", 924 | "identity", 925 | "lazy", 926 | "maybe", 927 | "newtype", 928 | "prelude", 929 | "st", 930 | "tailrec", 931 | "tuples", 932 | "unfoldable" 933 | ] 934 | }, 935 | "tuples": { 936 | "type": "registry", 937 | "version": "7.0.0", 938 | "integrity": "sha256-1rXgTomes9105BjgXqIw0FL6Fz1lqqUTLWOumhWec1M=", 939 | "dependencies": [ 940 | "control", 941 | "invariant", 942 | "prelude" 943 | ] 944 | }, 945 | "type-equality": { 946 | "type": "registry", 947 | "version": "4.0.1", 948 | "integrity": "sha256-Hs9D6Y71zFi/b+qu5NSbuadUQXe5iv5iWx0226vOHUw=", 949 | "dependencies": [] 950 | }, 951 | "typelevel-prelude": { 952 | "type": "registry", 953 | "version": "7.0.0", 954 | "integrity": "sha256-uFF2ph+vHcQpfPuPf2a3ukJDFmLhApmkpTMviHIWgJM=", 955 | "dependencies": [ 956 | "prelude", 957 | "type-equality" 958 | ] 959 | }, 960 | "unfoldable": { 961 | "type": "registry", 962 | "version": "6.0.0", 963 | "integrity": "sha256-JtikvJdktRap7vr/K4ITlxUX1QexpnqBq0G/InLr6eg=", 964 | "dependencies": [ 965 | "foldable-traversable", 966 | "maybe", 967 | "partial", 968 | "prelude", 969 | "tuples" 970 | ] 971 | }, 972 | "unsafe-coerce": { 973 | "type": "registry", 974 | "version": "6.0.0", 975 | "integrity": "sha256-IqIYW4Vkevn8sI+6aUwRGvd87tVL36BBeOr0cGAE7t0=", 976 | "dependencies": [] 977 | } 978 | } 979 | } 980 | --------------------------------------------------------------------------------