Simplified Fill In The Gaps

This commit is contained in:
Mark Eibes 2020-04-14 21:01:21 +02:00
parent 21162af38f
commit d37fadc1ff
6 changed files with 177 additions and 234 deletions

1
blog/.psc-ide-port Normal file
View File

@ -0,0 +1 @@
15613

View File

@ -1,14 +1,17 @@
module PSLayout where
import Prelude
import Data.Array as A
import Data.Array as Array
import Data.Array.NonEmpty as NEA
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Nullable (Nullable)
import Data.Nullable as Nullable
import Data.String as String
import Data.Tuple.Nested ((/\))
import Debug.Trace (spy)
import Effect (Effect)
import Effect.Unsafe (unsafePerformEffect)
import JSS (jss, jssClasses)
import Justifill (justifill)
import Milkis.Impl (FetchImpl)
@ -16,13 +19,15 @@ import React.Basic (JSX, ReactComponent)
import React.Basic.DOM (unsafeCreateDOMComponent)
import React.Basic.DOM as R
import React.Basic.Helpers (jsx)
import React.Basic.Hooks (ReactChildren, component, componentWithChildren, element, reactChildrenToArray, useState)
import React.Basic.Hooks (ReactChildren, component, componentWithChildren, element, reactChildrenToArray, useEffect, useState)
import React.Basic.Hooks as React
import Unsafe.Coerce (unsafeCoerce)
import Yoga.Box.Component as Box
import Yoga.CompileEditor.Component (mkCompileEditor)
import Yoga.Compiler.Api (apiCompiler)
import Yoga.FillInTheGaps.Component as FillInTheGaps
import Yoga.Header.Component (mkHeader)
import Yoga.Helpers ((?||))
import Yoga.InlineCode.Component as InlineCode
import Yoga.Theme (fromTheme)
import Yoga.Theme.CSSBaseline (mkCssBaseline)
@ -78,9 +83,12 @@ mkLayout fetchImpl = do
]
}
mkSecret :: Effect ( ReactComponent { kids ∷ Array JSX , visible ∷ Boolean })
mkSecret :: Effect ( ReactComponent { kids ∷ Array JSX , visible ∷ Boolean, register :: Effect Unit })
mkSecret = do
component "Secret" \{ kids, visible } -> React.do
component "Secret" \{ kids, visible, register } -> React.do
useEffect visible do
unless visible register
pure mempty
pure
$ R.div
{ style: R.css { visibility: if visible then "visible" else "hidden" }
@ -124,16 +132,20 @@ mkMdxProviderComponent fetchImpl = do
}
componentWithChildren "MDXProviderComponent" \{ children, siteInfo } -> React.do
classes <- useStyles {}
sections <- useState []
visibleThroughKey /\ updateVisible <- useState ""
let
baseline child = element cssBaseline { kids: child }
kids = reactChildrenToArray children
visibleKids = spy "visibleKids" $ fromMaybe kids do
i <- kids # A.findIndex (\x -> (unsafeCoerce x).key == visibleThroughKey)
pure $ A.take (i+1) kids
siteInfoJSX =
R.div
{ children:
[ jsx header { className: "" } [ R.text siteInfo.site.siteMetadata.title ]
, element sidebar { links: siteInfo.site.siteMetadata.menuLinks }
, jsx box {} (reactChildrenToArray children)
, jsx box {} visibleKids
]
}
@ -152,8 +164,8 @@ mkMdxProviderComponent fetchImpl = do
R.div
{ children: [ element p { text: props.children } ]
}
, thematicBreak:
\props -> R.div_ [ R.text "HOUI" ]
-- , thematicBreak:
-- \props -> jsx secret { register: updateSections (Map.insert ) }
, inlineCode:
\props -> do
R.span { className: classes.code, children: props.children }
@ -194,6 +206,13 @@ mkMdxProviderComponent fetchImpl = do
}
false, _ -> element (unsafeCreateDOMComponent "pre") props
}
useEffect (A.length kids) do
let
firstGaps = kids # A.find (\x -> (unsafeCoerce x).props.mdxType == "pre" && ((unsafeCoerce x).props.children.props.className == "language-puregaps")) <#> (\x -> (unsafeCoerce x).key)
lastKey = kids # A.last # unsafeCoerce # _.key
updateVisible (const (firstGaps ?|| lastKey))
pure mempty
pure
$ baseline
[ element mdxProvider

View File

@ -6,7 +6,7 @@ import SEO from '../components/seo'
# Optional
```puregaps
--result Apero
--result Hello World
module Main where
import Grimoire
@ -19,4 +19,16 @@ incantation = cast $
---
```puregaps
--result Hello World
module Main where
import Grimoire
incantation :: Effect Unit
--start here
incantation = cast $
"{-Hello World-}"
--end here
```
## Hans Hölzel

View File

@ -1,83 +1,24 @@
module Yoga.FillInTheGaps.Component where
import Prelude
import Data.Array (foldMap, foldr, intercalate)
import Data.Array as A
import Data.Either (Either(..))
import Data.Foldable (foldl)
import Data.Function.Uncurried (mkFn2)
import Data.Lens ((%~))
import Data.Lens.Index (ix)
import Data.Maybe (Maybe(..), fromMaybe', isJust, isNothing)
import Data.Monoid (guard)
import Data.String (Pattern(..), split)
import Data.String as S
import Data.String.Regex (Regex)
import Data.String.Regex as Regex
import Data.String.Regex.Flags as RegexFlags
import Data.String.Regex.Unsafe (unsafeRegex)
import Data.Tuple.Nested ((/\))
import Debug.Trace (spy)
import Effect (Effect)
import Effect.Aff (launchAff_)
import Effect.Aff (Milliseconds(..), delay)
import Effect.Class (liftEffect)
import Justifill (justifill)
import Partial.Unsafe (unsafeCrashWith)
import React.Basic (JSX, ReactComponent, element, fragment)
import React.Basic.DOM (css)
import React.Basic (JSX, ReactComponent, element)
import React.Basic.DOM as R
import React.Basic.Events (handler_)
import React.Basic.Helpers (jsx)
import React.Basic.Hooks (component, useState)
import React.Basic.Hooks as React
import React.Basic.Hooks.Spring (useTransition)
import React.Basic.Hooks.Aff (useAff)
import React.Basic.SyntaxHighlighter.Component (HighlighterTheme, syntaxHighlighterImpl)
import Shared.Models.Body (CompileResult, RunResult)
import Yoga.Button.Component (ButtonType(..), mkButton)
import Yoga.ClickAway.Component as ClickAway
import Yoga.CloseIcon.Component as CloseIcon
import Yoga.Cluster.Component as Cluster
import Yoga.Compiler.Types (Compiler)
import Yoga.FillInTheGaps.Logic (Segment(..), updateSegments)
import Yoga.Helpers ((?||))
import Yoga.InlineCode.Component as InlineCode
import Yoga.Modal.Component as Modal
import Yoga.Theme.Styles (useTheme)
import Yoga.Theme.Syntax (mkHighlighterTheme)
data Segment
= ExpectedResult String
| Start
| End
| Filler String
| Hole Int String
derive instance eqSegment ∷ Eq Segment
getResult ∷ Segment -> Maybe String
getResult = case _ of
ExpectedResult r -> Just r
_ -> Nothing
findResult ∷ Array Segment -> String
findResult = fromMaybe' (\_ -> unsafeCrashWith "Even teachers make mistakes") <<< A.findMap getResult
toCode ∷ Array (Array Segment) -> String
toCode lines = intercalate "\n" mapped
where
mapped = lines <#> (intercalate "" <<< map segmentToCode)
segmentToCode = case _ of
Filler s -> s
Hole _ s -> s
_ -> ""
complete ∷ Array (Array Segment) -> Boolean
complete arr = foldl f true (join arr)
where
f acc seg =
acc
&& case seg of
Hole _ s -> s /= ""
_ -> true
visibleRange ∷ Array (Array Segment) -> { end ∷ Int, start ∷ Int }
visibleRange arr = { start, end }
where
@ -91,7 +32,7 @@ renderSegments highlighterTheme ic update arrs = R.div_ (A.mapWithIndex renderLi
renderLine i l = R.div_ (A.mapWithIndex (renderSegment i) l)
renderSegment i j s =
if between start end i then case s of
Filler s' -> element syntaxHighlighterImpl { style: highlighterTheme, language: "purescript", children: spy "ch" s' }
Filler s' -> element syntaxHighlighterImpl { style: highlighterTheme, language: "purescript", children: s' }
Hole width _ ->
element ic
$ justifill
@ -102,150 +43,13 @@ renderSegments highlighterTheme ic update arrs = R.div_ (A.mapWithIndex renderLi
else
mempty
updateSegments ∷ Int -> Int -> String -> Array (Array Segment) -> Array (Array Segment)
updateSegments i j v = (ix i <<< ix j) %~ f
where
f = case _ of
Hole h _ -> Hole h v
_ -> unsafeCrashWith "Updated a non-hole"
holeRegex ∷ Regex
holeRegex = unsafeRegex "({-.*?-})" RegexFlags.global
resultRegex ∷ Regex
resultRegex = unsafeRegex "--result" RegexFlags.global
startRegex ∷ Regex
startRegex = unsafeRegex "--start here" RegexFlags.global
endRegex ∷ Regex
endRegex = unsafeRegex "--end here" RegexFlags.global
rawSegments ∷ String -> Array String
rawSegments = Regex.split holeRegex
toSegment ∷ String -> Segment
toSegment = case _ of
x
| Regex.test holeRegex x -> Hole (S.length x - 4) ""
x
| S.indexOf (Pattern "--result ") x == Just 0 -> ExpectedResult ((S.stripPrefix (Pattern "--result ") x) ?|| "")
x
| Regex.test startRegex x -> Start
x
| Regex.test endRegex x -> End
other -> Filler other
-- Glues together Filler segments
-- [[Filler "line1"], [Filler "line2"]] --> [[Filler "line1\nline2"]]
-- [[Filler "line1", Filler "line2"]] --> [[Filler "line1\nline2"]]
-- [[Filler "line1", Filler "line2"], [Filler "line3"]] --> [[Filler "line1\nline2\line3"]]
smooshFillers ∷ Array (Array Segment) -> Array (Array Segment)
smooshFillers = foldl smooshOuter []
where
smooshInner segments segment = case segment, A.unsnoc segments of
Filler f, Just { init, last: Filler prev } -> A.snoc init (Filler (prev <> "\n" <> f))
_, _ -> A.snoc segments segment
smooshOuter acc segs = case foldl smooshInner [] segs, A.unsnoc acc of
[ Filler f ], Just { init, last: [ Filler prev ] } -> A.snoc init [ Filler (prev <> "\n" <> f) ]
_, _ -> A.snoc acc segs
type Ctx r
= (Compiler r)
makeComponent ∷ { | Ctx () } -> Effect (ReactComponent { code ∷ String })
makeComponent { compileAndRun } = do
makeComponent ∷ Effect (ReactComponent { initialSegments ∷ Array (Array Segment), update ∷ Array (Array Segment) -> Effect Unit })
makeComponent = do
ic <- InlineCode.makeComponent
modal <- Modal.makeComponent
closeIcon <- CloseIcon.makeComponent
cluster <- Cluster.makeComponent
clickAway <- ClickAway.makeComponent
button <- mkButton
component "FillInTheGaps" \{ code } -> React.do
let
lines = split (Pattern "\n") code
initialSegments = smooshFillers (lines <#> \line -> rawSegments line <#> toSegment)
component "FillInTheGaps" \{ initialSegments, update } -> React.do
segments /\ modifySegments <- useState initialSegments
result /\ modifyResult <- useState Nothing
cssTheme <- useTheme
let
highlighterTheme = mkHighlighterTheme cssTheme
modalTransitions <-
useTransition [ result ] (Just show)
$ css
{ from: { opacity: 0.0, transform: "translate3d(-50%, -50%, 0) scale3d(0.3, 0.3, 1.0)" }
, enter: { opacity: 1.0, transform: "translate3d(-50%, -50%, 0) scale3d(1.0, 1.0, 1.0)" }
, leave: { opacity: 0.0, transform: "translate3d(-50%, -50%, 0) scale3d(0.3, 0.3, 1.0)" }
, config:
mkFn2 \_ state -> case state of
"leave" -> { mass: 1.0, tension: 140, friction: 15 }
_ -> { mass: 1.0, tension: 170, friction: 20 }
}
clickAwayTransitions <-
useTransition [ result ] (Just show)
$ css
{ from: { opacity: 0.0 }
, enter: { opacity: 1.0 }
, leave: { opacity: 0.0 }
, config:
mkFn2 \_ state -> case state of
"leave" -> { mass: 1.0, tension: 140, friction: 15 }
_ -> { mass: 1.0, tension: 170, friction: 20 }
}
let
expectedResult = findResult (join segments)
onClick =
launchAff_ do
do
res <- compileAndRun { code: toCode segments }
modifyResult (const (Just $ res)) # liftEffect
pure
$ fragment
$ [ renderSegments highlighterTheme ic (modifySegments) segments
, jsx cluster {}
[ R.div_
[ jsx button
{ onClick: handler_ onClick
, buttonType:
if complete segments && isNothing result then
HighlightedButton
else
DisabledButton
}
[ R.text "Incantate" ]
]
]
, fragment
$ clickAwayTransitions
>>= \{ item, key, props } ->
guard (isJust item)
$ join item
# foldMap \_ ->
[ element clickAway (justifill { allowEscape: true, style: props, onClick: modifyResult (const Nothing) }) ]
, fragment
$ modalTransitions
>>= \{ item, key, props } ->
guard (isJust item)
$ join item
# foldMap \r ->
[ jsx modal
{ title:
case r of
(Right { stdout })
| S.stripSuffix (S.Pattern "\n") stdout == Just expectedResult -> "Hooray!"
(Left l) -> "Does not compile"
(Right _) -> "Not " <> (findResult $ join segments)
, icon: element closeIcon { onClick: modifyResult (const Nothing), style: Nothing }
, style: props
}
[ R.pre_ [ R.text $ compileResultToString result ] ]
]
]
compileResultToString ∷ Maybe (Either CompileResult RunResult) -> String
compileResultToString = case _ of
Nothing -> ""
Just (Left cr) -> cr.result <#> _.message # intercalate "\n"
Just (Right r) -> r.stdout
useAff segments do
delay (200.0 # Milliseconds)
update segments # liftEffect
highlighterTheme <- useTheme <#> mkHighlighterTheme
pure $ renderSegments highlighterTheme ic (modifySegments) segments

View File

@ -0,0 +1,118 @@
module Yoga.FillInTheGaps.Logic where
import Prelude
import Data.Array (intercalate)
import Data.Array as A
import Data.Either (Either(..))
import Data.Foldable (foldl)
import Data.Lens ((%~))
import Data.Lens.Index (ix)
import Data.Maybe (Maybe(..), fromMaybe')
import Data.String (Pattern(..), split)
import Data.String as S
import Data.String.Regex (Regex)
import Data.String.Regex as Regex
import Data.String.Regex.Flags as RegexFlags
import Data.String.Regex.Unsafe (unsafeRegex)
import Data.Tuple.Nested ((/\))
import Debug.Trace (spy)
import Effect (Effect)
import Justifill (justifill)
import Partial.Unsafe (unsafeCrashWith)
import React.Basic (JSX, ReactComponent, element)
import React.Basic.DOM as R
import React.Basic.Hooks (component, useState)
import React.Basic.Hooks as React
import React.Basic.SyntaxHighlighter.Component (HighlighterTheme, syntaxHighlighterImpl)
import Shared.Models.Body (CompileResult, RunResult)
import Yoga.Helpers ((?||))
import Yoga.InlineCode.Component as InlineCode
import Yoga.Theme.Styles (useTheme)
import Yoga.Theme.Syntax (mkHighlighterTheme)
complete ∷ Array (Array Segment) -> Boolean
complete arr = foldl f true (join arr)
where
f acc seg =
acc
&& case seg of
Hole _ s -> s /= ""
_ -> true
data Segment
= ExpectedResult String
| Start
| End
| Filler String
| Hole Int String
derive instance eqSegment ∷ Eq Segment
getResult ∷ Segment -> Maybe String
getResult = case _ of
ExpectedResult r -> Just r
_ -> Nothing
findResult ∷ Array Segment -> String
findResult = fromMaybe' (\_ -> unsafeCrashWith "Even teachers make mistakes") <<< A.findMap getResult
toCode ∷ Array (Array Segment) -> String
toCode lines = intercalate "\n" mapped
where
mapped = lines <#> (intercalate "" <<< map segmentToCode)
segmentToCode = case _ of
Filler s -> s
Hole _ s -> s
_ -> ""
holeRegex ∷ Regex
holeRegex = unsafeRegex "({-.*?-})" RegexFlags.global
resultRegex ∷ Regex
resultRegex = unsafeRegex "--result" RegexFlags.global
startRegex ∷ Regex
startRegex = unsafeRegex "--start here" RegexFlags.global
endRegex ∷ Regex
endRegex = unsafeRegex "--end here" RegexFlags.global
rawSegments ∷ String -> Array String
rawSegments = Regex.split holeRegex
toSegment ∷ String -> Segment
toSegment = case _ of
x
| Regex.test holeRegex x -> Hole (S.length x - 4) ""
x
| S.indexOf (Pattern "--result ") x == Just 0 -> ExpectedResult ((S.stripPrefix (Pattern "--result ") x) ?|| "")
x
| Regex.test startRegex x -> Start
x
| Regex.test endRegex x -> End
other -> Filler other
updateSegments ∷ Int -> Int -> String -> Array (Array Segment) -> Array (Array Segment)
updateSegments i j v = (ix i <<< ix j) %~ f
where
f = case _ of
Hole h _ -> Hole h v
_ -> unsafeCrashWith "Updated a non-hole"
-- Glues together Filler segments
-- [[Filler "line1"], [Filler "line2"]] --> [[Filler "line1\nline2"]]
-- [[Filler "line1", Filler "line2"]] --> [[Filler "line1\nline2"]]
-- [[Filler "line1", Filler "line2"], [Filler "line3"]] --> [[Filler "line1\nline2\line3"]]
smooshFillers ∷ Array (Array Segment) -> Array (Array Segment)
smooshFillers = foldl smooshOuter []
where
smooshInner segments segment = case segment, A.unsnoc segments of
Filler f, Just { init, last: Filler prev } -> A.snoc init (Filler (prev <> "\n" <> f))
_, _ -> A.snoc segments segment
smooshOuter acc segs = case foldl smooshInner [] segs, A.unsnoc acc of
[ Filler f ], Just { init, last: [ Filler prev ] } -> A.snoc init [ Filler (prev <> "\n" <> f) ]
_, _ -> A.snoc acc segs
parseSegments code = segments
where
lines = split (Pattern "\n") code
segments = smooshFillers (lines <#> \line -> rawSegments line <#> toSegment)

View File

@ -1,30 +1,19 @@
module Yoga.FillInTheGaps.Stories where
import Prelude hiding (add)
import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Data.String as String
import Effect (Effect)
import Storybook.Decorator.FullScreen (fullScreenDecorator)
import Storybook.React (Storybook, add, addDecorator, storiesOf)
import Yoga.FillInTheGaps.Component as FillInTheGaps
ctx ∷ { | FillInTheGaps.Ctx () }
ctx =
{ compileAndRun
}
where
compileAndRun = case _ of
{ code }
| code == correctCode -> pure (Right { code: Nothing, stdout: "Hello World\n", stderr: "" })
other -> pure (Right { code: Nothing, stdout: "Not hello world", stderr: "" })
import Yoga.FillInTheGaps.Logic (parseSegments)
stories ∷ Effect Storybook
stories = do
storiesOf "FillInTheGaps" do
addDecorator fullScreenDecorator
add "The FillInTheGaps" (FillInTheGaps.makeComponent ctx)
[ { code: codeWithHoles }
add "The FillInTheGaps" (FillInTheGaps.makeComponent)
[ { initialSegments: parseSegments codeWithHoles, update: pure (pure unit) }
]
codeWithHoles =
@ -33,9 +22,9 @@ codeWithHoles =
module Main where
import Grimoire
main :: Effect Unit
incantation :: Effect Unit
--start here
main = cast
incantation = cast
"{-Hello World-}"
--end here
"""