Week 04 of 2019

Development log of Elm Tree Workshop

21 items
  1. Remove tree from the examples model, messages and subscriptions.
  2. Expose Rule and defaults from Examples.Tree module
  3. Make the default tree more beautiful.
  4. WIP tree block for our markup
  5. Replace Code block with Editor in day 5
  6. Create a navigation bar
  7. Make navigation bar fixed
  8. Make progress bar respond to scroll position.
  9. Hide navigation bar when printing.
  10. Show navigation bar in content only. WIP to show different navigation bar in home.
  11. Add support for Fold annotation to Editor
  12. Make navigation bar respond to viewport width changes.
  13. Split the code from Editor module into Examples.Editor and Mark.Custom
  14. Fix the fold over highlight in the Editor example
  15. Disable CI when not on master branch
  16. Merge branch 'content' into about-fana
  17. Merge branch 'content' into navigation
  18. Add anchor icon to Fana's bio.
  19. Fix home link in content navigation bar
  20. Merge branch 'content' into tree-example
  21. Fix the tree example markup

Remove tree from the examples model, messages and subscriptions.

On by Fana

The tree is static now.

index e4d98a5..94e0a3f 100644
--- a/src/Examples.elm
+++ b/src/Examples.elm
@@ -21,7 +21,6 @@ type alias Model =
=    , nestedTransformations : Examples.NestedTransformations.Model
=    , cartesianCoordinates : Examples.CartesianCoordinates.Model
=    , polarCoordinates : Examples.PolarCoordinates.Model
-    , tree : Maybe Examples.Tree.Model
=    , viewBox : Examples.ViewBox.Model
=    }
=
@@ -32,8 +31,6 @@ type Msg
=    | NestedTransformationsMsg Examples.NestedTransformations.Msg
=    | CartesianCoordinatesMsg Examples.CartesianCoordinates.Msg
=    | PolarCoordinatesMsg Examples.PolarCoordinates.Msg
-    | ToggleTree (Maybe Examples.Tree.Model)
-    | TreeMsg Examples.Tree.Msg
=    | ViewBoxMsg Examples.ViewBox.Msg
=
=
@@ -44,7 +41,6 @@ init =
=      , nestedTransformations = Examples.NestedTransformations.init
=      , cartesianCoordinates = Examples.CartesianCoordinates.init
=      , polarCoordinates = Examples.PolarCoordinates.init
-      , tree = Nothing
=      , viewBox = Examples.ViewBox.init
=      }
=    , Cmd.none
@@ -91,32 +87,10 @@ update msg model =
=            , Cmd.none
=            )
=
-        ToggleTree tree ->
-            ( { model | tree = tree }, Cmd.none )
-
-        TreeMsg m ->
-            case model.tree of
-                Just tree ->
-                    Examples.Tree.update m tree
-                        |> (\( newTree, treeCmd ) ->
-                                ( { model | tree = Just newTree }
-                                , Cmd.map TreeMsg treeCmd
-                                )
-                           )
-
-                Nothing ->
-                    ( model, Cmd.none )
-
=        ViewBoxMsg m ->
=            ( { model | viewBox = Examples.ViewBox.update m model.viewBox }, Cmd.none )
=
=
=subscriptions : Model -> Sub Msg
=subscriptions model =
-    case model.tree of
-        Nothing ->
-            Sub.none
-
-        Just tree ->
-            Examples.Tree.subscriptions tree
-                |> Sub.map TreeMsg
+    Sub.none

Expose Rule and defaults from Examples.Tree module

On by Fana

index 26d559e..88234ae 100644
--- a/src/Examples/Tree.elm
+++ b/src/Examples/Tree.elm
@@ -1,7 +1,9 @@
=module Examples.Tree exposing
=    ( Axiom
=    , Config
+    , Rule
=    , Segment
+    , defaults
=    , main
=    , ui
=    )

Make the default tree more beautiful.

On by Fana

index 88234ae..8fee380 100644
--- a/src/Examples/Tree.elm
+++ b/src/Examples/Tree.elm
@@ -17,45 +17,27 @@ import Svg.Attributes
=defaults : Config
=defaults =
=    { axiom =
-        { color = "brown"
+        { color = "purple"
=        , rotation = -90
-        , age = 8
+        , age = 7
=        }
=    , rules =
-        [ ( "brown"
-          , [ { color = "green"
-              , rotation = 45
-              }
-            , { color = "green"
-              , rotation = 20
-              }
-            , { color = "green"
-              , rotation = -20
-              }
+        [ ( "purple"
+          , [ { color = "green", rotation = 0.0 }
+            , { color = "green", rotation = 30 }
+            , { color = "green", rotation = -30 }
=            ]
=          )
=        , ( "green"
-          , [ { color = "purple"
-              , rotation = 0
-              }
-            , { color = "purple"
-              , rotation = -90
-              }
-            , { color = "purple"
-              , rotation = -20
-              }
+          , [ { color = "brown", rotation = 0.0 }
+            , { color = "brown", rotation = 30 }
+            , { color = "brown", rotation = -30 }
=            ]
=          )
-        , ( "purple"
-          , [ { color = "orange"
-              , rotation = 0
-              }
-            , { color = "orange"
-              , rotation = 60
-              }
-            , { color = "brown"
-              , rotation = -20
-              }
+        , ( "brown"
+          , [ { color = "green", rotation = 0.0 }
+            , { color = "green", rotation = 30 }
+            , { color = "green", rotation = -30 }
=            ]
=          )
=        ]

WIP tree block for our markup

On by Fana

We believe that our code is correct but there is a bug in the mdgriffith/elm-markup package.

index 03acab1..17c4bf3 100644
--- a/content/day-4.txt
+++ b/content/day-4.txt
@@ -18,8 +18,63 @@
=| Header
=    The Problem
=
-| Monospace
-    TODO: Static tree example
+| Note
+    FIXME: The tree example is broken.
+
+| Window
+    | Tree
+        | Axiom
+            color = green
+            rotation = 0
+            age = 9
+
+        | Rule
+            | Parent
+                purple
+
+            | Child
+                color = green
+                rotation = 0.0
+
+            | Child
+                color = green
+                rotation = 30
+
+            | Child
+                color = green
+                rotation = -30
+
+        | Rule
+            | Parent
+                green
+
+            | Child
+                color = brown
+                rotation = 0.0
+
+            | Child
+                color = brown
+                rotation = 30
+
+            | Child
+                color = brown
+                rotation = -30
+
+        | Rule
+
+            | Parent
+                brown
+
+            | Child
+                color = green
+                rotation = 0.0
+            | Child
+                color = green
+                rotation = 30
+            | Child
+                color = green
+                rotation = -30
+
=
=The tree is built from segments: a line and a dot. In this respect it is similar to the connected dots we created yesterday. If we group the dot and a line, we will have the building block for our tree. We can do it with {Code|Svg.g} function ({Code|g} is an abbreviation of "group"). Just like {Code|Svg.svg}, it takes list of attributes and list of children. We can use it like that:
=
index a0c8d8f..2b7617c 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -685,20 +685,52 @@ document =
=        tree : Mark.Block (Examples.Model -> Element Msg)
=        tree =
=            let
-                render model =
-                    model.tree
-                        |> Maybe.map Examples.Tree.ui
-                        |> Maybe.withDefault Element.none
-                        |> Element.el
-                            [ Element.height (Element.px 600)
-                            , Element.width Element.fill
-                            ]
-                        |> BrowserWindow.window []
-                        |> Element.map Examples.TreeMsg
-                        |> Element.map ExamplesMsg
+                render : Examples.Tree.Config -> Examples.Model -> Element Msg
+                render config model =
+                    Examples.Tree.ui config
+
+                axiom : Mark.Block Examples.Tree.Axiom
+                axiom =
+                    Mark.record3 "Axiom"
+                        Examples.Tree.Axiom
+                        (Mark.field "color" Mark.string)
+                        (Mark.field "rotation" Mark.float)
+                        (Mark.field "age" Mark.float)
+
+                rules : Mark.Block (List Examples.Tree.Rule)
+                rules =
+                    Mark.manyOf [ rule ]
+
+                rule =
+                    Mark.startWith Tuple.pair
+                        parent
+                        (Mark.manyOf [ child ])
+
+                -- FIXME: Parser breaks with empty list of dead ends
+                -- Mark.stub "Rule" ( "green", [ Examples.Tree.Segment "green" 30 ] )
+                -- Mark.manyOf [ child ]
+                --     |> Mark.map (Tuple.pair "green")
+                parent =
+                    Mark.block "Parent" identity Mark.string
+
+                -- Mark.stub "Parent" "green"
+                child =
+                    Mark.record2 "Child"
+                        Examples.Tree.Segment
+                        (Mark.field "color" Mark.string)
+                        (Mark.field "rotation" Mark.float)
=            in
-            Mark.stub "Tree" render
+            Mark.block "Tree"
+                render
+                (Mark.startWith Examples.Tree.Config
+                    axiom
+                    rules
+                )
=
+        -- Mark.Block Examples.Tree.Config
+        -- result = Examples.Tree.Config
+        -- start = Axiom
+        -- rest =  List Rule
=        viewBox : Mark.Block (Examples.Model -> Element Msg)
=        viewBox =
=            let

Replace Code block with Editor in day 5

On by Tadeusz Łazurski

index ce02674..c073ebc 100644
--- a/content/day-5.txt
+++ b/content/day-5.txt
@@ -92,117 +92,136 @@ State is sometimes called model. There are also commands, but we will not use th
=
=First let's change the value of {Code|main} and create the {Code|view} function, like this:
=
-| Code
-    module Main exposing (main)
-
-    import Browser
-    import Dict
-    import Element
-    import Svg exposing (Svg)
-    import Svg.Attributes
-
-
-    main =
-        Browser.element
-            { init = init
-            , view = view
-            , update = update
-            , subscriptions = subscriptions
-            }
-
-
-    view age =
-        [ segment (age / 5000) { color = "brown", rotation = -90 } ]
-            |> Svg.svg
-                [ Svg.Attributes.height "100%"
-                , Svg.Attributes.width "100%"
-                , Svg.Attributes.style "background: none"
-                , Svg.Attributes.viewBox "-500 -500 1000 1000"
-                ]
-            |> Element.html
-            |> Element.layout
-                [ Element.width Element.fill
-                , Element.height Element.fill
-                ]
+| Editor
+    | Code
+        module Main exposing (main)
+
+        import Browser
+        import Dict
+        import Element
+        import Svg exposing (Svg)
+        import Svg.Attributes
+
+
+        main =
+            Browser.element
+                { init = init
+                , view = view
+                , update = update
+                , subscriptions = subscriptions
+                }
+
+
+        view age =
+            [ segment (age / 5000) { color = "brown", rotation = -90 } ]
+                |> Svg.svg
+                    [ Svg.Attributes.height "100%"
+                    , Svg.Attributes.width "100%"
+                    , Svg.Attributes.style "background: none"
+                    , Svg.Attributes.viewBox "-500 -500 1000 1000"
+                    ]
+                |> Element.html
+                |> Element.layout
+                    [ Element.width Element.fill
+                    , Element.height Element.fill
+                    ]
=
=
-    rules =
-        Dict.empty
-            |> Dict.insert "brown"
-                [ { color = "brown", rotation = 0 }
-                , { color = "green", rotation = 20 }
-                , { color = "green", rotation = -30 }
-                ]
-            |> Dict.insert "green"
-                [ { color = "red", rotation = -45 }
-                , { color = "red", rotation = -5 }
-                , { color = "red", rotation = 50 }
-                ]
+        rules =
+            Dict.empty
+                |> Dict.insert "brown"
+                    [ { color = "brown", rotation = 0 }
+                    , { color = "green", rotation = 20 }
+                    , { color = "green", rotation = -30 }
+                    ]
+                |> Dict.insert "green"
+                    [ { color = "red", rotation = -45 }
+                    , { color = "red", rotation = -5 }
+                    , { color = "red", rotation = 50 }
+                    ]
=
=
-    segment age { color, rotation } =
-        if age <= 0 then
-            Svg.g [] []
-
-        else
-            Svg.g []
-                [ rules
-                    |> Dict.get color
-                    |> Maybe.withDefault []
-                    |> List.map (segment (age - 1))
-                    |> Svg.g
-                        [ Svg.Attributes.transform
-                            (String.concat
-                                [ "rotate("
-                                , String.fromFloat rotation
-                                , ") translate("
-                                , String.fromFloat (age * 10)
-                                , ")"
-                                ]
-                            )
-                        ]
-                , dot age color rotation
-                , line age color rotation
-                ]
+        segment age { color, rotation } =
+            if age <= 0 then
+                Svg.g [] []
+
+            else
+                Svg.g []
+                    [ rules
+                        |> Dict.get color
+                        |> Maybe.withDefault []
+                        |> List.map (segment (age - 1))
+                        |> Svg.g
+                            [ Svg.Attributes.transform
+                                (String.concat
+                                    [ "rotate("
+                                    , String.fromFloat rotation
+                                    , ") translate("
+                                    , String.fromFloat (age * 10)
+                                    , ")"
+                                    ]
+                                )
+                            ]
+                    , dot age color rotation
+                    , line age color rotation
+                    ]
=
=
-    dot age color rotation =
-        Svg.circle
-            [ Svg.Attributes.r (String.fromFloat age)
-            , Svg.Attributes.cx "0"
-            , Svg.Attributes.cy "0"
-            , Svg.Attributes.fill color
-            , Svg.Attributes.transform
-                (String.concat
-                    [ "rotate("
-                    , String.fromFloat rotation
-                    , ") translate("
-                    , String.fromFloat (age * 10)
-                    , ")"
-                    ]
-                )
-            ]
-            []
-
-
-    line age color rotation =
-        Svg.line
-            [ Svg.Attributes.strokeWidth "1"
-            , Svg.Attributes.x1 "0"
-            , Svg.Attributes.y1 "0"
-            , Svg.Attributes.x1 (String.fromFloat (age * 10))
-            , Svg.Attributes.y1 "0"
-            , Svg.Attributes.stroke color
-            , Svg.Attributes.strokeWidth (String.fromFloat age)
-            , Svg.Attributes.transform
-                (String.concat
-                    [ "rotate("
-                    , String.fromFloat rotation
-                    , ")"
-                    ]
-                )
-            ]
-            []
+        dot age color rotation =
+            Svg.circle
+                [ Svg.Attributes.r (String.fromFloat age)
+                , Svg.Attributes.cx "0"
+                , Svg.Attributes.cy "0"
+                , Svg.Attributes.fill color
+                , Svg.Attributes.transform
+                    (String.concat
+                        [ "rotate("
+                        , String.fromFloat rotation
+                        , ") translate("
+                        , String.fromFloat (age * 10)
+                        , ")"
+                        ]
+                    )
+                ]
+                []
+
+
+        line age color rotation =
+            Svg.line
+                [ Svg.Attributes.strokeWidth "1"
+                , Svg.Attributes.x1 "0"
+                , Svg.Attributes.y1 "0"
+                , Svg.Attributes.x1 (String.fromFloat (age * 10))
+                , Svg.Attributes.y1 "0"
+                , Svg.Attributes.stroke color
+                , Svg.Attributes.strokeWidth (String.fromFloat age)
+                , Svg.Attributes.transform
+                    (String.concat
+                        [ "rotate("
+                        , String.fromFloat rotation
+                        , ")"
+                        ]
+                    )
+                ]
+                []
+
+    | Highlight
+        from = 11
+        to = 16
+        offset = 4
+        width = 35
+
+    | Highlight
+        from = 19
+        to = 19
+        offset = 0
+        width = 10
+
+    | Highlight
+        from = 20
+        to = 20
+        offset = 15
+        width = 10
=
=Notice that the {Code|view} takes an argument called {Code|age} and passes it to the first (brown) segment. The value of {Code|age} is our state. In some programs the type of state can be very complex, but in our program it's simply a {Code|Float} number. It will indicate how much time in milliseconds have passed from the start of the program. Because the time is counted in milliseconds, to get correct results we need to divide it by 5000 (so the tree will grows one generation of segments per five second).
=
@@ -217,9 +236,16 @@ First value is more important. This is the initial age of the tree, right after
=
=That's our init:
=
-| Code
-    init () =
-        ( 0, Cmd.none )
+| Editor
+    | Code
+        init () =
+            ( 0, Cmd.none )
+
+    | Highlight
+        from = 0
+        to = 0
+        offset = 0
+        width = 10
=
=Time for the {Code|update} function. It will get two arguments: an incoming message and the current state. Incoming message will always be a duration of time since previous frame, so let's simply call it {Code|duration}. The state is the current age of the tree, so again we can call it {Code|age}. In return we must produce a tuple with:
=
@@ -229,11 +255,18 @@ Time for the {Code|update} function. It will get two arguments: an incoming mess
=
=It looks like this:
=
-| Code
-    update duration age =
-        ( age + duration
-        , Cmd.none
-        )
+| Editor
+    | Code
+        update duration age =
+            ( age + duration
+            , Cmd.none
+            )
+
+    | Highlight
+        from = 0
+        to = 0
+        offset = 0
+        width = 10
=
=Finally let's subscribe to the events marking the passage of time. We can do it using {Code|Browser.Events.onAnimationFrameDelta}. Here is how it works. Whenever the browser is ready for the next frame it will send us a message containing the number of milliseconds that have passed since previous frame.
=
@@ -244,141 +277,156 @@ To use it we first need to import the {Code|Browser.Events} module. The {Code|Br
=
=The whole {Code|subscriptions} looks like that:
=
-| Code
-    subscriptions age =
-        Browser.Events.onAnimationFrameDelta identity
+| Editor
+    | Code
+        subscriptions age =
+            Browser.Events.onAnimationFrameDelta identity
+
+    | Highlight
+        from = 0
+        to = 0
+        offset = 0
+        width = 10
=
=And the complete code like this:
=
-| Code
-    module Main exposing (main)
+| Editor
+    | Code
+        module Main exposing (main)
=
-    import Browser
-    import Browser.Events
-    import Dict
-    import Element
-    import Svg exposing (Svg)
-    import Svg.Attributes
+        import Browser
+        import Browser.Events
+        import Dict
+        import Element
+        import Svg exposing (Svg)
+        import Svg.Attributes
=
=
-    main =
-        Browser.element
-            { init = init
-            , view = view
-            , update = update
-            , subscriptions = subscriptions
-            }
+        main =
+            Browser.element
+                { init = init
+                , view = view
+                , update = update
+                , subscriptions = subscriptions
+                }
=
=
-    init () =
-        ( 0, Cmd.none )
+        init () =
+            ( 0, Cmd.none )
=
=
-    view age =
-        [ segment (age / 5000) { color = "brown", rotation = -90 } ]
-            |> Svg.svg
-                [ Svg.Attributes.height "100%"
-                , Svg.Attributes.width "100%"
-                , Svg.Attributes.style "background: none"
-                , Svg.Attributes.viewBox "-500 -500 1000 1000"
-                ]
-            |> Element.html
-            |> Element.layout
-                [ Element.width Element.fill
-                , Element.height Element.fill
-                ]
+        view age =
+            [ segment (age / 5000) { color = "brown", rotation = -90 } ]
+                |> Svg.svg
+                    [ Svg.Attributes.height "100%"
+                    , Svg.Attributes.width "100%"
+                    , Svg.Attributes.style "background: none"
+                    , Svg.Attributes.viewBox "-500 -500 1000 1000"
+                    ]
+                |> Element.html
+                |> Element.layout
+                    [ Element.width Element.fill
+                    , Element.height Element.fill
+                    ]
=
=
-    update duration age =
-        ( duration
-            |> Debug.log "Duration"
-            |> (+) age
-            |> Debug.log "Age"
-        , Cmd.none
-        )
+        update duration age =
+            ( duration
+                |> Debug.log "Duration"
+                |> (+) age
+                |> Debug.log "Age"
+            , Cmd.none
+            )
=
=
-    subscriptions age =
-        Browser.Events.onAnimationFrameDelta identity
+        subscriptions age =
+            Browser.Events.onAnimationFrameDelta identity
=
=
-    rules =
-        Dict.empty
-            |> Dict.insert "brown"
-                [ { color = "brown", rotation = 0 }
-                , { color = "green", rotation = 20 }
-                , { color = "green", rotation = -30 }
-                ]
-            |> Dict.insert "green"
-                [ { color = "red", rotation = -45 }
-                , { color = "red", rotation = -5 }
-                , { color = "red", rotation = 50 }
-                ]
+        rules =
+            Dict.empty
+                |> Dict.insert "brown"
+                    [ { color = "brown", rotation = 0 }
+                    , { color = "green", rotation = 20 }
+                    , { color = "green", rotation = -30 }
+                    ]
+                |> Dict.insert "green"
+                    [ { color = "red", rotation = -45 }
+                    , { color = "red", rotation = -5 }
+                    , { color = "red", rotation = 50 }
+                    ]
+
+
+        segment age { color, rotation } =
+            if age <= 0 then
+                Svg.g [] []
+
+            else
+                Svg.g []
+                    [ rules
+                        |> Dict.get color
+                        |> Maybe.withDefault []
+                        |> List.map (segment (age - 1))
+                        |> Svg.g
+                            [ Svg.Attributes.transform
+                                (String.concat
+                                    [ "rotate("
+                                    , String.fromFloat rotation
+                                    , ") translate("
+                                    , String.fromFloat (age * 10)
+                                    , ")"
+                                    ]
+                                )
+                            ]
+                    , dot age color rotation
+                    , line age color rotation
+                    ]
=
=
-    segment age { color, rotation } =
-        if age <= 0 then
-            Svg.g [] []
-
-        else
-            Svg.g []
-                [ rules
-                    |> Dict.get color
-                    |> Maybe.withDefault []
-                    |> List.map (segment (age - 1))
-                    |> Svg.g
-                        [ Svg.Attributes.transform
-                            (String.concat
-                                [ "rotate("
-                                , String.fromFloat rotation
-                                , ") translate("
-                                , String.fromFloat (age * 10)
-                                , ")"
-                                ]
-                            )
+        dot age color rotation =
+            Svg.circle
+                [ Svg.Attributes.r (String.fromFloat age)
+                , Svg.Attributes.cx "0"
+                , Svg.Attributes.cy "0"
+                , Svg.Attributes.fill color
+                , Svg.Attributes.transform
+                    (String.concat
+                        [ "rotate("
+                        , String.fromFloat rotation
+                        , ") translate("
+                        , String.fromFloat (age * 10)
+                        , ")"
=                        ]
-                , dot age color rotation
-                , line age color rotation
+                    )
=                ]
+                []
+
+
+        line age color rotation =
+            Svg.line
+                [ Svg.Attributes.strokeWidth "1"
+                , Svg.Attributes.x1 "0"
+                , Svg.Attributes.y1 "0"
+                , Svg.Attributes.x1 (String.fromFloat (age * 10))
+                , Svg.Attributes.y1 "0"
+                , Svg.Attributes.stroke color
+                , Svg.Attributes.strokeWidth (String.fromFloat age)
+                , Svg.Attributes.transform
+                    (String.concat
+                        [ "rotate("
+                        , String.fromFloat rotation
+                        , ")"
+                        ]
+                    )
+                ]
+                []
=
+    | Highlight
+        from = 0
+        to = 0
+        offset = 0
+        width = 10
=
-    dot age color rotation =
-        Svg.circle
-            [ Svg.Attributes.r (String.fromFloat age)
-            , Svg.Attributes.cx "0"
-            , Svg.Attributes.cy "0"
-            , Svg.Attributes.fill color
-            , Svg.Attributes.transform
-                (String.concat
-                    [ "rotate("
-                    , String.fromFloat rotation
-                    , ") translate("
-                    , String.fromFloat (age * 10)
-                    , ")"
-                    ]
-                )
-            ]
-            []
-
-
-    line age color rotation =
-        Svg.line
-            [ Svg.Attributes.strokeWidth "1"
-            , Svg.Attributes.x1 "0"
-            , Svg.Attributes.y1 "0"
-            , Svg.Attributes.x1 (String.fromFloat (age * 10))
-            , Svg.Attributes.y1 "0"
-            , Svg.Attributes.stroke color
-            , Svg.Attributes.strokeWidth (String.fromFloat age)
-            , Svg.Attributes.transform
-                (String.concat
-                    [ "rotate("
-                    , String.fromFloat rotation
-                    , ")"
-                    ]
-                )
-            ]
-            []
=
=| Emphasize
=    That's it!

Create a navigation bar

On by Fana

index a0c8d8f..5d2609b 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -37,6 +37,7 @@ import FeatherIcons exposing (icons)
=import Html exposing (Html)
=import Html.Attributes
=import Http
+import List.Extra as List
=import Mark
=import Mark.Custom
=import Parser
@@ -66,7 +67,7 @@ type alias Flags =
=
=
=type alias Model =
-    { url : Url
+    { location : Url
=    , key : Navigation.Key
=    , content : Content
=    , examples : Examples.Model
@@ -85,7 +86,7 @@ type Content
=    = Loading
=    | FetchError Http.Error
=    | ParseError (List DeadEnd)
-    | Loaded (Examples.Model -> View)
+    | Loaded (Model -> View)
=
=
=type alias DeadEnd =
@@ -99,7 +100,7 @@ type alias View =
=
=
=type alias Document =
-    Mark.Document (Examples.Model -> View)
+    Mark.Document (Model -> View)
=
=
=init : Flags -> Url -> Navigation.Key -> ( Model, Cmd Msg )
@@ -108,7 +109,7 @@ init flags url key =
=        ( examplesModel, examplesCmd ) =
=            Examples.init
=    in
-    ( { url = url
+    ( { location = url
=      , key = key
=      , content = Loading
=      , examples = examplesModel
@@ -155,7 +156,7 @@ view model =
=                    deadEndsView deadEnds
=
=                Loaded documentView ->
-                    documentView model.examples
+                    documentView model
=
=        loadingView : View
=        loadingView =
@@ -329,7 +330,7 @@ update msg model =
=
=        UrlChanged url ->
=            ( { model
-                | url = url
+                | location = url
=                , content = Loading
=              }
=            , Cmd.batch
@@ -393,23 +394,145 @@ loadContent route =
=            Cmd.none
=
=
+contentNavigationBar : Url -> Element Msg
+contentNavigationBar location =
+    let
+        links : List { url : String, label : String }
+        links =
+            [ { url = "http://localhost:8001"
+              , label = "Home"
+              }
+            , { url = "/preparation.html"
+              , label = "Get ready"
+              }
+            , { url = "/day-1.html"
+              , label = "Day 1"
+              }
+            , { url = "/day-2.html"
+              , label = "Day 2"
+              }
+            , { url = "/day-3.html"
+              , label = "Day 3"
+              }
+            , { url = "/day-4.html"
+              , label = "Day 4"
+              }
+            , { url = "/day-5.html"
+              , label = "Day 5"
+              }
+            ]
+
+        linkElement : { url : String, label : String } -> Element Msg
+        linkElement link =
+            Element.link
+                [ Element.width Element.fill
+                , Element.padding 20
+                ]
+                { url = link.url
+                , label = Element.el [ Element.centerX ] (Element.text link.label)
+                }
+
+        linksRow =
+            links
+                |> List.map linkElement
+                |> Element.row
+                    [ Element.width Element.fill
+                    , Element.spaceEvenly
+                    , Font.bold
+                    ]
+
+        progressBar : Int -> Int -> Float -> Element Msg
+        progressBar total done progress =
+            Element.row [ Element.width Element.fill ]
+                [ Element.el
+                    [ Element.width (Element.fillPortion done)
+                    , Element.height (Element.px 4)
+                    , Background.color (Element.rgb 0 0.6 0)
+                    ]
+                    Element.none
+                , Element.row
+                    [ Element.width (Element.fillPortion 1)
+                    , Element.height (Element.px 4)
+                    ]
+                    [ Element.el
+                        [ Element.width (Element.fillPortion (round (progress * 1000)))
+                        , Element.height (Element.px 4)
+                        , Background.color (Element.rgb 0 0.6 0)
+                        ]
+                        Element.none
+                    , Element.el
+                        [ Element.width (Element.fillPortion (1000 - round (progress * 1000)))
+                        , Element.height (Element.px 4)
+                        ]
+                        Element.none
+                    ]
+                , Element.el
+                    [ Element.width (Element.fillPortion (total - done - 1))
+                    , Element.height (Element.px 4)
+                    ]
+                    Element.none
+                ]
+
+        route =
+            Routes.parse location
+
+        past : Int
+        past =
+            links
+                |> List.map .url
+                |> List.map (\newPath -> { location | path = newPath })
+                |> List.map Routes.parse
+                |> List.elemIndex route
+                |> Maybe.withDefault 0
+    in
+    Element.column [ Element.width Element.fill ]
+        [ linksRow
+        , progressBar
+            (List.length links)
+            -- index of current route
+            past
+            -- scroll position (0 - 1)
+            0.5
+        ]
+
+
=document : Document
=document =
=    let
-        content :
+        page :
=            { title : String
=            , children : List (Examples.Model -> Element Msg)
=            }
-            -> Examples.Model
+            -> Model
=            -> View
-        content { title, children } model =
-            children
-                |> List.map (\child -> child model)
-                |> Element.textColumn
-                    [ Element.centerX
-                    , Element.spacing 20
-                    , Element.paddingXY 0 80
-                    ]
+        page { title, children } model =
+            let
+                navigationBar =
+                    case Routes.parse model.location of
+                        Routes.Home ->
+                            -- TODO: Implement different navigation bar for home
+                            contentNavigationBar model.location
+
+                        Routes.Content _ ->
+                            contentNavigationBar model.location
+
+                        Routes.NotFound ->
+                            contentNavigationBar model.location
+
+                content =
+                    children
+                        |> List.map (\child -> child model.examples)
+                        |> Element.textColumn
+                            [ Element.centerX
+                            , Element.spacing 20
+                            , Element.paddingXY 0 80
+                            ]
+            in
+            [ navigationBar
+            , content
+            ]
+                |> Element.column
+                    [ Element.width Element.fill ]
=                |> View title
=
=        {- The document has to start with a Title block containing a String (i.e. single line of unforamtted text). This String will be used in two ways:
@@ -714,5 +837,5 @@ document =
=            Mark.stub "ViewBox" render
=    in
=    Mark.document
-        content
+        page
=        structure

Make navigation bar fixed

On by Fana

Move navigationBar from document to view.

Add background color to contentNavigationBar.

Co-Authored-By: Tadeusz Łazurski tadeusz@lazurski.pl

index 5d2609b..64223f3 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -158,6 +158,18 @@ view model =
=                Loaded documentView ->
=                    documentView model
=
+        navigationBar =
+            case Routes.parse model.location of
+                Routes.Home ->
+                    -- TODO: Implement different navigation bar for home
+                    contentNavigationBar model.location
+
+                Routes.Content _ ->
+                    contentNavigationBar model.location
+
+                Routes.NotFound ->
+                    contentNavigationBar model.location
+
=        loadingView : View
=        loadingView =
=            { title = "Loading Content"
@@ -297,13 +309,14 @@ view model =
=    in
=    { title = "Software Garden : " ++ content.title
=    , body =
-        [ Element.layout
-            [ -- Below is a hack that enables static site generation
-              Element.htmlAttribute (Html.Attributes.id "app-container")
-            , Element.htmlAttribute (Html.Attributes.lang "en")
-            , Font.family [ Font.typeface "Montserrat", Font.sansSerif ]
-            ]
-            content.body
+        [ content.body
+            |> Element.layout
+                [ -- Below is a hack that enables static site generation
+                  Element.htmlAttribute (Html.Attributes.id "app-container")
+                , Element.htmlAttribute (Html.Attributes.lang "en")
+                , Font.family [ Font.typeface "Montserrat", Font.sansSerif ]
+                , Element.inFront navigationBar
+                ]
=        ]
=    }
=
@@ -485,7 +498,10 @@ contentNavigationBar location =
=                |> List.elemIndex route
=                |> Maybe.withDefault 0
=    in
-    Element.column [ Element.width Element.fill ]
+    Element.column
+        [ Element.width Element.fill
+        , Background.color (Element.rgb 1 1 1)
+        ]
=        [ linksRow
=        , progressBar
=            (List.length links)
@@ -506,33 +522,13 @@ document =
=            -> Model
=            -> View
=        page { title, children } model =
-            let
-                navigationBar =
-                    case Routes.parse model.location of
-                        Routes.Home ->
-                            -- TODO: Implement different navigation bar for home
-                            contentNavigationBar model.location
-
-                        Routes.Content _ ->
-                            contentNavigationBar model.location
-
-                        Routes.NotFound ->
-                            contentNavigationBar model.location
-
-                content =
-                    children
-                        |> List.map (\child -> child model.examples)
-                        |> Element.textColumn
-                            [ Element.centerX
-                            , Element.spacing 20
-                            , Element.paddingXY 0 80
-                            ]
-            in
-            [ navigationBar
-            , content
-            ]
-                |> Element.column
-                    [ Element.width Element.fill ]
+            children
+                |> List.map (\child -> child model.examples)
+                |> Element.textColumn
+                    [ Element.centerX
+                    , Element.spacing 20
+                    , Element.paddingXY 0 80
+                    ]
=                |> View title
=
=        {- The document has to start with a Title block containing a String (i.e. single line of unforamtted text). This String will be used in two ways:

Make progress bar respond to scroll position.

On by Fana

Co-Authored-By: Tadeusz Łazurski tadeusz@lazurski.pl

index 64223f3..d7e3485 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -36,7 +36,9 @@ import Examples.ViewBox
=import FeatherIcons exposing (icons)
=import Html exposing (Html)
=import Html.Attributes
+import Html.Events
=import Http
+import Json.Decode exposing (Decoder)
=import List.Extra as List
=import Mark
=import Mark.Custom
@@ -71,6 +73,7 @@ type alias Model =
=    , key : Navigation.Key
=    , content : Content
=    , examples : Examples.Model
+    , scroll : Float
=    }
=
=
@@ -80,6 +83,7 @@ type Msg
=    | UrlChanged Url
=    | ContentFetched (Result Http.Error String)
=    | ExamplesMsg Examples.Msg
+    | Scroll Float
=
=
=type Content
@@ -113,6 +117,7 @@ init flags url key =
=      , key = key
=      , content = Loading
=      , examples = examplesModel
+      , scroll = 1
=      }
=    , Cmd.batch
=        [ url
@@ -162,13 +167,13 @@ view model =
=            case Routes.parse model.location of
=                Routes.Home ->
=                    -- TODO: Implement different navigation bar for home
-                    contentNavigationBar model.location
+                    contentNavigationBar model
=
=                Routes.Content _ ->
-                    contentNavigationBar model.location
+                    contentNavigationBar model
=
=                Routes.NotFound ->
-                    contentNavigationBar model.location
+                    contentNavigationBar model
=
=        loadingView : View
=        loadingView =
@@ -306,6 +311,19 @@ view model =
=                    ++ ":"
=                    ++ String.fromInt col
=                )
+
+        scrollDecoder : Decoder Msg
+        scrollDecoder =
+            Json.Decode.field "target"
+                (Json.Decode.map3
+                    (\top height clientHeight ->
+                        top / (height - clientHeight)
+                    )
+                    (Json.Decode.field "scrollTop" Json.Decode.float)
+                    (Json.Decode.field "scrollHeight" Json.Decode.float)
+                    (Json.Decode.field "clientHeight" Json.Decode.float)
+                )
+                |> Json.Decode.map Scroll
=    in
=    { title = "Software Garden : " ++ content.title
=    , body =
@@ -316,6 +334,9 @@ view model =
=                , Element.htmlAttribute (Html.Attributes.lang "en")
=                , Font.family [ Font.typeface "Montserrat", Font.sansSerif ]
=                , Element.inFront navigationBar
+                , Element.height Element.fill
+                , Element.scrollbars
+                , Element.htmlAttribute (Html.Events.on "scroll" scrollDecoder)
=                ]
=        ]
=    }
@@ -345,6 +366,7 @@ update msg model =
=            ( { model
=                | location = url
=                , content = Loading
+                , scroll = 0
=              }
=            , Cmd.batch
=                [ url
@@ -380,6 +402,11 @@ update msg model =
=            , Cmd.map ExamplesMsg examplesCmd
=            )
=
+        Scroll position ->
+            ( { model | scroll = position }
+            , Cmd.none
+            )
+
=
=subscriptions : Model -> Sub Msg
=subscriptions model =
@@ -407,8 +434,8 @@ loadContent route =
=            Cmd.none
=
=
-contentNavigationBar : Url -> Element Msg
-contentNavigationBar location =
+contentNavigationBar : Model -> Element Msg
+contentNavigationBar { location, scroll } =
=    let
=        links : List { url : String, label : String }
=        links =
@@ -501,6 +528,7 @@ contentNavigationBar location =
=    Element.column
=        [ Element.width Element.fill
=        , Background.color (Element.rgb 1 1 1)
+        , Element.htmlAttribute (Html.Attributes.id "navigation-bar")
=        ]
=        [ linksRow
=        , progressBar
@@ -508,7 +536,7 @@ contentNavigationBar location =
=            -- index of current route
=            past
=            -- scroll position (0 - 1)
-            0.5
+            scroll
=        ]
=
=

Hide navigation bar when printing.

On by Fana

Co-Authored-By: Tadeusz Łazurski tadeusz@lazurski.pl

index 1f43711..b2d458d 100644
--- a/container.html
+++ b/container.html
@@ -5,6 +5,17 @@
=    <title>Software Garden</title>
=    <style>
=      @import url('https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Source+Code+Pro:300,400,700');
+
+      @media print {
+        #app-container {
+          height: auto;
+        }
+
+        #navigation-bar {
+          display: none;
+        }
+
+      }
=    </style>
=  </head>
=  <body>

Show navigation bar in content only. WIP to show different navigation bar in home.

On by Fana

Co-Authored-By: Tadeusz Łazurski tadeusz@lazurski.pl

index d7e3485..9cc4789 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -166,8 +166,9 @@ view model =
=        navigationBar =
=            case Routes.parse model.location of
=                Routes.Home ->
-                    -- TODO: Implement different navigation bar for home
-                    contentNavigationBar model
+                    -- TODO: Implement fragment identifier navigation
+                    -- homeNavigationBar
+                    Element.none
=
=                Routes.Content _ ->
=                    contentNavigationBar model
@@ -434,6 +435,35 @@ loadContent route =
=            Cmd.none
=
=
+homeNavigationBar : Element Msg
+homeNavigationBar =
+    Element.row
+        [ Element.width Element.fill
+        , Element.padding 40
+        , Element.spacing 80
+        , Font.bold
+        , Background.color (Element.rgb 1 1 1)
+        ]
+        [ Element.link
+            [ Element.alignLeft
+            ]
+            { url = "/"
+            , label = Element.text "Home"
+            }
+        , Element.link
+            [ Element.alignRight
+            ]
+            { url = "#about-us"
+            , label = Element.text "About Us"
+            }
+        , Element.link
+            []
+            { url = "#contact"
+            , label = Element.text "Contact"
+            }
+        ]
+
+
=contentNavigationBar : Model -> Element Msg
=contentNavigationBar { location, scroll } =
=    let
@@ -466,7 +496,7 @@ contentNavigationBar { location, scroll } =
=        linkElement link =
=            Element.link
=                [ Element.width Element.fill
-                , Element.padding 20
+                , Element.paddingEach { top = 40, right = 5, bottom = 20, left = 5 }
=                ]
=                { url = link.url
=                , label = Element.el [ Element.centerX ] (Element.text link.label)

Add support for Fold annotation to Editor

On by Tadeusz Łazurski

index 2553eb0..6c4cc3c 100644
--- a/src/Editor.elm
+++ b/src/Editor.elm
@@ -1,5 +1,6 @@
-module Editor exposing (Config, Highlight, defaults, editor)
+module Editor exposing (Annotation(..), Config, Region, defaults, editor)
=
+import Basics.Extra exposing (uncurry)
=import Browser
=import Dict exposing (Dict)
=import Element exposing (Element)
@@ -7,9 +8,11 @@ import Element.Background as Background
=import Element.Border as Border
=import Element.Extra as Element
=import Element.Font as Font
+import Element.Input as Input
=import FeatherIcons exposing (icons)
=import Html exposing (Html)
=import List.Extra as List
+import Set exposing (Set)
=import Svg exposing (Svg)
=import Svg.Attributes
=
@@ -41,34 +44,49 @@ defaults =
=    }
=
=
-type alias Highlight =
-    { from : Int
-    , to : Int
-    , offset : Int
+type Annotation
+    = None -- A dummy annotation to satisfy manyOf block. See https://github.com/mdgriffith/elm-markup/issues/12
+    | Highlight Region
+    | Fold Int Int
+
+
+type alias Region =
+    { top : Int
+    , left : Int
=    , width : Int
+    , height : Int
=    }
=
=
-highlight : Highlight -> Element msg
-highlight { from, to, offset, width } =
+highlight : Region -> Element msg
+highlight { top, left, width, height } =
=    Element.row []
=        [ " "
-            |> String.repeat offset
+            |> String.repeat left
=            |> Element.text
=        , " "
=            |> String.repeat width
-            |> List.repeat (to - from + 1)
+            |> List.repeat height
=            |> List.map Element.text
-            |> List.map (Element.el [ Element.padding 10 ])
+            |> List.map (Element.el [ Element.paddingXY 0 10 ])
=            |> Element.column
-                [ Element.inFront
+                [ Element.behindContent
=                    (Element.el
=                        [ Element.width Element.fill
=                        , Element.height Element.fill
=                        , Border.color (Element.rgba 1 0 0 0.8)
-                        , Border.rounded 10
-                        , Border.width 2
-                        , Border.dashed
+                        , -- Hand-drawn style borders. See https://codepen.io/tmrDevelops/pen/VeRvKX/
+                          [ width, height ]
+                            |> List.map ((*) 2)
+                            |> List.map String.fromInt
+                            |> List.map (\num -> num ++ "px")
+                            |> List.repeat 2
+                            |> List.concat
+                            |> (\list -> [ list, List.reverse list ])
+                            |> List.map (String.join " ")
+                            |> String.join " / "
+                            |> Element.css "border-radius"
+                        , Border.width 3
=                        , Element.scale 1.1
=                        ]
=                        Element.none
@@ -84,91 +102,298 @@ highlight { from, to, offset, width } =
=        ]
=
=
-editor : Config -> List Highlight -> String -> Element msg
-editor { path, offset, colors } highlights contents =
-    let
-        highlighted : Dict Int Highlight
-        highlighted =
-            highlights
-                |> List.foldl extract Dict.empty
+type Fragment
+    = Folded Int
+    | Unfolded (List String)
=
-        extract item memo =
-            Dict.insert item.from item memo
-    in
-    Element.column
-        [ Border.width 3
-        , Border.rounded 5
-        , Border.color colors.window
-        , Element.css "page-break-inside" "avoid"
-        , Font.family
-            [ Font.typeface "Source Code Pro"
-            , Font.monospace
-            ]
-        , Element.css "page-break-inside" "avoid"
-        , Element.width Element.fill
-        ]
-        [ Element.row
-            [ Element.width Element.fill
-            , Background.color colors.window
-            , Font.color colors.secondary
-            ]
-            [ FeatherIcons.fileText
-                |> FeatherIcons.toHtml []
-                |> Element.html
-                |> Element.el
-                    [ Element.height (Element.px 35)
-                    , Element.padding 8
-                    ]
-            , Element.el
+
+editor : Config -> List Annotation -> String -> Element msg
+editor { path, offset, colors } annotations code =
+    let
+        topBar =
+            Element.row
=                [ Element.width Element.fill
-                , Font.size 16
-                , Font.family
-                    [ Font.typeface "Source Code Pro"
-                    , Font.monospace
+                , Background.color colors.window
+                , Font.color colors.secondary
+                ]
+                [ FeatherIcons.fileText
+                    |> FeatherIcons.toHtml []
+                    |> Element.html
+                    |> Element.el
+                        [ Element.height (Element.px 35)
+                        , Element.padding 8
+                        ]
+                , Element.el
+                    [ Element.width Element.fill
+                    , Font.size 16
+                    , Font.family
+                        [ Font.typeface "Source Code Pro"
+                        , Font.monospace
+                        ]
=                    ]
+                    (Element.text path)
=                ]
-                (Element.text "src/Main.elm")
-            ]
-        , contents
-            |> String.lines
-            |> List.indexedMap
-                (\n loc ->
-                    Element.row []
-                        [ Element.el
-                            [ Font.color colors.secondary
-                            , Font.extraLight
-                            , Element.width (Element.px 40)
-                            , Element.padding 10
-                            , Font.alignRight
-                            , Element.css "user-select" "none"
-                            , Element.css "-webkit-user-select" "none"
-                            , Element.css "-ms-user-select" "none"
-                            , Element.css "-webkit-touch-callout" "none"
-                            , Element.css "-o-user-select" "none"
-                            , Element.css "-moz-user-select" "none"
-                            ]
-                            ((n + 1)
-                                |> String.fromInt
+
+        contents : Element msg
+        contents =
+            code
+                |> String.lines
+                |> List.indexedMap Tuple.pair
+                |> extractFragments []
+                |> renderFragments 1 []
+                |> Element.column [ Element.width Element.fill ]
+
+        renderFragments :
+            Int
+            -> List (Element msg)
+            -> List Fragment
+            -> List (Element msg)
+        renderFragments start rendered fragments =
+            case fragments of
+                [] ->
+                    -- We are done
+                    List.reverse rendered
+
+                (Folded length) :: rest ->
+                    let
+                        lineNumbers =
+                            [ start, start + length ]
+                                |> List.map String.fromInt
+                                |> String.join " - "
=                                |> Element.text
-                            )
-                        , Element.el
-                            [ Element.width Element.fill
-                            , Element.padding 10
-                            , highlighted
-                                |> Dict.get (n + 1)
-                                |> Maybe.map highlight
-                                |> Maybe.withDefault Element.none
-                                |> Element.inFront
-                            ]
-                            (Element.text loc)
-                        ]
-                )
-            |> Element.column
-                [ Element.width Element.fill
-                , Font.size 16
-                , Element.scrollbarY
+                                |> Element.el
+                                    [ Font.color colors.secondary
+                                    , Font.size 14
+                                    ]
+
+                        button =
+                            Input.button
+                                [ Font.size 10
+                                , Element.paddingXY 10 5
+                                , Background.color colors.secondary
+                                , Font.color colors.background
+                                , Border.rounded 3
+                                , Font.variant Font.smallCaps
+                                ]
+                                { onPress = Nothing
+                                , label = Element.text "unfold"
+                                }
+
+                        shadow =
+                            Element.el
+                                [ Element.width Element.fill
+                                , Element.height Element.fill
+                                , Border.innerShadow
+                                    { offset = ( 0, 3 )
+                                    , size = -6
+                                    , blur = 12
+                                    , color = colors.window
+                                    }
+                                ]
+                                Element.none
+
+                        fold =
+                            [ lineNumbers, button ]
+                                |> Element.row
+                                    [ Element.spacing 20
+                                    , Element.padding 10
+                                    , Background.color colors.background
+                                    , Element.width Element.fill
+                                    ]
+                                |> Element.el
+                                    [ Element.paddingXY 5 10
+                                    , Element.width Element.fill
+                                    , Element.paddingEach
+                                        { top = 0
+                                        , right = 1
+                                        , bottom = 0
+                                        , left = 1
+                                        }
+                                    , Background.color colors.window
+                                    , Element.inFront shadow
+                                    ]
+                    in
+                    fold
+                        :: renderFragments (start + length) rendered rest
+
+                (Unfolded lines) :: rest ->
+                    let
+                        unfolded =
+                            lines
+                                |> List.indexedMap
+                                    (\n loc ->
+                                        Element.row []
+                                            [ Element.el
+                                                [ Font.color colors.secondary
+                                                , Font.extraLight
+                                                , Element.width (Element.px 40)
+                                                , Element.padding 10
+                                                , Font.alignRight
+                                                , Element.css "user-select" "none"
+                                                , Element.css "-webkit-user-select" "none"
+                                                , Element.css "-ms-user-select" "none"
+                                                , Element.css "-webkit-touch-callout" "none"
+                                                , Element.css "-o-user-select" "none"
+                                                , Element.css "-moz-user-select" "none"
+                                                ]
+                                                ((n + start)
+                                                    |> String.fromInt
+                                                    |> Element.text
+                                                )
+                                            , Element.el
+                                                [ Element.width Element.fill
+                                                , Element.padding 10
+                                                , highlighted
+                                                    |> Dict.get (n + start)
+                                                    |> Maybe.map highlight
+                                                    |> Maybe.withDefault Element.none
+                                                    |> Element.behindContent
+                                                ]
+                                                (Element.text loc)
+                                            ]
+                                    )
+                                |> Element.column
+                                    [ Element.width Element.fill
+                                    , Font.size 16
+                                    , Element.scrollbarY
+                                    ]
+                    in
+                    unfolded
+                        :: renderFragments (start + List.length lines) rendered rest
+
+        folded : Set Int
+        folded =
+            annotations
+                |> List.foldl folds []
+                |> List.map (uncurry List.range)
+                |> List.concat
+                |> Set.fromList
+
+        folds : Annotation -> List ( Int, Int ) -> List ( Int, Int )
+        folds item memo =
+            case item of
+                Fold start size ->
+                    ( start, start + size ) :: memo
+
+                _ ->
+                    memo
+
+        extractFragments :
+            List Fragment
+            -> List ( Int, String )
+            -> List Fragment
+        extractFragments accumulated lines =
+            case lines of
+                [] ->
+                    -- We have reached the end of the code
+                    case accumulated of
+                        [] ->
+                            -- It was empty. Add one empty line.
+                            [ Unfolded [ "" ] ]
+
+                        (Folded n) :: _ ->
+                            List.reverse accumulated
+
+                        (Unfolded linesReversed) :: previous ->
+                            List.reverse
+                                (Unfolded (List.reverse linesReversed)
+                                    :: previous
+                                )
+
+                ( n, line ) :: rest ->
+                    let
+                        isFolded =
+                            Set.member (n + 1) folded
+                    in
+                    case accumulated of
+                        [] ->
+                            -- It's the first fragment. Check if it's folded or not.
+                            if isFolded then
+                                extractFragments [ Folded 1 ] rest
+
+                            else
+                                extractFragments [ Unfolded [ line ] ] rest
+
+                        (Folded length) :: previous ->
+                            if isFolded then
+                                -- Continue the fold
+                                let
+                                    this =
+                                        Folded (length + 1)
+                                in
+                                extractFragments
+                                    (this :: previous)
+                                    rest
+
+                            else
+                                -- Fold is finished. Start a new unfolded fragment.
+                                let
+                                    this =
+                                        Folded length
+
+                                    next =
+                                        Unfolded [ line ]
+                                in
+                                extractFragments
+                                    (next :: this :: previous)
+                                    rest
+
+                        (Unfolded linesReversed) :: previous ->
+                            if isFolded then
+                                -- Unfolded fragment is finished. Reverse the lines and start a new fold.
+                                let
+                                    this =
+                                        Unfolded (List.reverse linesReversed)
+
+                                    next =
+                                        Folded 1
+                                in
+                                extractFragments
+                                    (next
+                                        :: this
+                                        :: previous
+                                    )
+                                    rest
+
+                            else
+                                -- Continue the unfolded fragment
+                                let
+                                    this =
+                                        Unfolded (line :: linesReversed)
+                                in
+                                extractFragments
+                                    (this :: previous)
+                                    rest
+
+        highlighted : Dict Int Region
+        highlighted =
+            annotations
+                |> List.foldl regions Dict.empty
+
+        regions : Annotation -> Dict Int Region -> Dict Int Region
+        regions item memo =
+            case item of
+                Highlight region ->
+                    Dict.insert region.top region memo
+
+                _ ->
+                    memo
+    in
+    [ topBar
+    , contents
+    ]
+        |> Element.column
+            [ Border.width 3
+            , Border.rounded 5
+            , Border.color colors.window
+            , Element.css "page-break-inside" "avoid"
+            , Font.family
+                [ Font.typeface "Source Code Pro"
+                , Font.monospace
=                ]
-        ]
+            , Element.css "page-break-inside" "avoid"
+            , Element.width Element.fill
+            ]
=
=
=main : Html msg
@@ -285,10 +510,11 @@ dot age color rotation =
=        []
="""
=        |> editor defaults
-            [ Highlight 10 16 0 38
-            , Highlight 3 3 0 15
-            , Highlight 19 19 0 10
-            , Highlight 20 20 15 10
+            [ Highlight { top = 10, left = 0, width = 38, height = 7 }
+            , Highlight { top = 3, left = 0, width = 15, height = 1 }
+            , Highlight { top = 19, left = 0, width = 10, height = 1 }
+            , Highlight { top = 20, left = 15, width = 10, height = 1 }
+            , Fold 35 11
=            ]
=        |> Element.layout
=            [ Element.width Element.fill
index a96ce20..7185019 100644
--- a/src/Mark/Custom.elm
+++ b/src/Mark/Custom.elm
@@ -85,8 +85,14 @@ monospace =
=editor : Mark.Block (a -> Element msg)
=editor =
=    let
-        render contents highlights model =
-            Editor.editor Editor.defaults highlights contents
+        render : List Editor.Annotation -> String -> model -> Element msg
+        render annotations_ contents model =
+            Editor.editor Editor.defaults annotations_ contents
+
+        annotations =
+            Mark.block "Annotations"
+                identity
+                (Mark.manyOf [ none, highlight, fold ])
=
=        code : Mark.Block String
=        code =
@@ -94,20 +100,32 @@ editor =
=                identity
=                Mark.multiline
=
-        highlight : Mark.Block Editor.Highlight
+        none : Mark.Block Editor.Annotation
+        none =
+            Mark.stub "None" Editor.None
+
+        highlight : Mark.Block Editor.Annotation
=        highlight =
=            Mark.record4 "Highlight"
-                Editor.Highlight
-                (Mark.field "from" Mark.int)
-                (Mark.field "to" Mark.int)
-                (Mark.field "offset" Mark.int)
+                Editor.Region
+                (Mark.field "top" Mark.int)
+                (Mark.field "left" Mark.int)
=                (Mark.field "width" Mark.int)
+                (Mark.field "height" Mark.int)
+                |> Mark.map Editor.Highlight
+
+        fold : Mark.Block Editor.Annotation
+        fold =
+            Mark.record2 "Fold"
+                Editor.Fold
+                (Mark.field "start" Mark.int)
+                (Mark.field "length" Mark.int)
=    in
=    Mark.block "Editor"
=        identity
=        (Mark.startWith render
+            annotations
=            code
-            (Mark.manyOf [ highlight ])
=        )
=
=

Make navigation bar respond to viewport width changes.

On by Fana

Use icons when viewport is < 600px.

Co-Authored-By: Tadeusz Łazurski tadeusz@lazurski.pl

index 9cc4789..87fee45 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -8,7 +8,8 @@ It fetches the Elm Markup file at /index.txt and renders it. There are a number
=
=import Basics.Extra exposing (curry)
=import Browser
-import Browser.Dom as Dom
+import Browser.Dom as Dom exposing (Viewport)
+import Browser.Events
=import Browser.Navigation as Navigation
=import BrowserWindow
=import Dict
@@ -74,6 +75,7 @@ type alias Model =
=    , content : Content
=    , examples : Examples.Model
=    , scroll : Float
+    , viewport : { width : Int, height : Int }
=    }
=
=
@@ -84,6 +86,7 @@ type Msg
=    | ContentFetched (Result Http.Error String)
=    | ExamplesMsg Examples.Msg
=    | Scroll Float
+    | ViewportMeasured { width : Int, height : Int }
=
=
=type Content
@@ -117,12 +120,22 @@ init flags url key =
=      , key = key
=      , content = Loading
=      , examples = examplesModel
-      , scroll = 1
+      , scroll = 0
+      , viewport =
+            { width = 800, height = 600 }
=      }
=    , Cmd.batch
=        [ url
=            |> Routes.parse
=            |> loadContent
+        , Dom.getViewport
+            |> Task.map
+                (\{ viewport } ->
+                    { width = round viewport.width
+                    , height = round viewport.height
+                    }
+                )
+            |> Task.perform ViewportMeasured
=        , Cmd.map ExamplesMsg examplesCmd
=        ]
=    )
@@ -408,12 +421,23 @@ update msg model =
=            , Cmd.none
=            )
=
+        ViewportMeasured viewport ->
+            ( { model | viewport = viewport }
+            , Cmd.none
+            )
+
=
=subscriptions : Model -> Sub Msg
=subscriptions model =
-    model.examples
-        |> Examples.subscriptions
-        |> Sub.map ExamplesMsg
+    Sub.batch
+        [ model.examples
+            |> Examples.subscriptions
+            |> Sub.map ExamplesMsg
+        , Browser.Events.onResize
+            (\width height ->
+                ViewportMeasured { width = width, height = height }
+            )
+        ]
=
=
=loadContent : Route -> Cmd Msg
@@ -465,41 +489,53 @@ homeNavigationBar =
=
=
=contentNavigationBar : Model -> Element Msg
-contentNavigationBar { location, scroll } =
+contentNavigationBar { location, scroll, viewport } =
=    let
-        links : List { url : String, label : String }
+        links : List { url : String, label : String, icon : Element msg }
=        links =
=            [ { url = "http://localhost:8001"
=              , label = "Home"
+              , icon = FeatherIcons.home |> FeatherIcons.toHtml [] |> Element.html
=              }
=            , { url = "/preparation.html"
=              , label = "Get ready"
+              , icon = FeatherIcons.bookOpen |> FeatherIcons.toHtml [] |> Element.html
=              }
=            , { url = "/day-1.html"
=              , label = "Day 1"
+              , icon = Element.text "1"
=              }
=            , { url = "/day-2.html"
=              , label = "Day 2"
+              , icon = Element.text "2"
=              }
=            , { url = "/day-3.html"
=              , label = "Day 3"
+              , icon = Element.text "3"
=              }
=            , { url = "/day-4.html"
=              , label = "Day 4"
+              , icon = Element.text "4"
=              }
=            , { url = "/day-5.html"
=              , label = "Day 5"
+              , icon = Element.text "5"
=              }
=            ]
=
-        linkElement : { url : String, label : String } -> Element Msg
+        linkElement : { url : String, label : String, icon : Element Msg } -> Element Msg
=        linkElement link =
=            Element.link
=                [ Element.width Element.fill
=                , Element.paddingEach { top = 40, right = 5, bottom = 20, left = 5 }
=                ]
=                { url = link.url
-                , label = Element.el [ Element.centerX ] (Element.text link.label)
+                , label =
+                    if viewport.width < 600 then
+                        Element.el [ Element.centerX ] link.icon
+
+                    else
+                        Element.el [ Element.centerX ] (Element.text link.label)
=                }
=
=        linksRow =

Split the code from Editor module into Examples.Editor and Mark.Custom

On by Tadeusz Łazurski

index 6c4cc3c..a25c922 100644
--- a/src/Editor.elm
+++ b/src/Editor.elm
@@ -1,7 +1,13 @@
-module Editor exposing (Annotation(..), Config, Region, defaults, editor)
+module Editor exposing
+    ( Annotations
+    , Colors
+    , Config
+    , Region
+    , defaults
+    , editor
+    )
=
=import Basics.Extra exposing (uncurry)
-import Browser
=import Dict exposing (Dict)
=import Element exposing (Element)
=import Element.Background as Background
@@ -10,46 +16,39 @@ import Element.Extra as Element
=import Element.Font as Font
=import Element.Input as Input
=import FeatherIcons exposing (icons)
-import Html exposing (Html)
=import List.Extra as List
=import Set exposing (Set)
-import Svg exposing (Svg)
=import Svg.Attributes
=
=
=type alias Config =
=    { path : String
-    , offset : Int
-    , colors :
-        { annotations : Element.Color
-        , background : Element.Color
-        , primary : Element.Color
-        , secondary : Element.Color
-        , window : Element.Color
-        }
+    , colors : Colors
+    }
+
+
+type alias Colors =
+    { annotations : Element.Color
+    , background : Element.Color
+    , primary : Element.Color
+    , secondary : Element.Color
+    , window : Element.Color
=    }
=
=
=defaults : Config
=defaults =
=    { path = "src/Main.elm"
-    , offset = 1
=    , colors =
=        { primary = Element.rgb 0 0 0
=        , secondary = Element.rgb 0.8 0.8 0.8
-        , annotations = Element.rgb 1 0.6 0.6
+        , annotations = Element.rgb 0.7 0 0
=        , background = Element.rgb 1 1 1
=        , window = Element.rgb 0.2 0.2 0.2
=        }
=    }
=
=
-type Annotation
-    = None -- A dummy annotation to satisfy manyOf block. See https://github.com/mdgriffith/elm-markup/issues/12
-    | Highlight Region
-    | Fold Int Int
-
-
=type alias Region =
=    { top : Int
=    , left : Int
@@ -58,57 +57,19 @@ type alias Region =
=    }
=
=
-highlight : Region -> Element msg
-highlight { top, left, width, height } =
-    Element.row []
-        [ " "
-            |> String.repeat left
-            |> Element.text
-        , " "
-            |> String.repeat width
-            |> List.repeat height
-            |> List.map Element.text
-            |> List.map (Element.el [ Element.paddingXY 0 10 ])
-            |> Element.column
-                [ Element.behindContent
-                    (Element.el
-                        [ Element.width Element.fill
-                        , Element.height Element.fill
-                        , Border.color (Element.rgba 1 0 0 0.8)
-                        , -- Hand-drawn style borders. See https://codepen.io/tmrDevelops/pen/VeRvKX/
-                          [ width, height ]
-                            |> List.map ((*) 2)
-                            |> List.map String.fromInt
-                            |> List.map (\num -> num ++ "px")
-                            |> List.repeat 2
-                            |> List.concat
-                            |> (\list -> [ list, List.reverse list ])
-                            |> List.map (String.join " ")
-                            |> String.join " / "
-                            |> Element.css "border-radius"
-                        , Border.width 3
-                        , Element.scale 1.1
-                        ]
-                        Element.none
-                    )
-                , Element.css "pointer-events" "none"
-                , Element.css "user-select" "none"
-                , Element.css "-webkit-user-select" "none"
-                , Element.css "-ms-user-select" "none"
-                , Element.css "-webkit-touch-callout" "none"
-                , Element.css "-o-user-select" "none"
-                , Element.css "-moz-user-select" "none"
-                ]
-        ]
-
-
=type Fragment
=    = Folded Int
=    | Unfolded (List String)
=
=
-editor : Config -> List Annotation -> String -> Element msg
-editor { path, offset, colors } annotations code =
+type alias Annotations =
+    { highlights : List Region
+    , folded : Set Int
+    }
+
+
+editor : Config -> Annotations -> String -> Element msg
+editor { path, colors } { highlights, folded } code =
=    let
=        topBar =
=            Element.row
@@ -139,9 +100,12 @@ editor { path, offset, colors } annotations code =
=            code
=                |> String.lines
=                |> List.indexedMap Tuple.pair
-                |> extractFragments []
+                |> extractFragments folded []
=                |> renderFragments 1 []
-                |> Element.column [ Element.width Element.fill ]
+                |> Element.column
+                    [ Element.width Element.fill
+                    , Element.scrollbarX
+                    ]
=
=        renderFragments :
=            Int
@@ -155,229 +119,17 @@ editor { path, offset, colors } annotations code =
=                    List.reverse rendered
=
=                (Folded length) :: rest ->
-                    let
-                        lineNumbers =
-                            [ start, start + length ]
-                                |> List.map String.fromInt
-                                |> String.join " - "
-                                |> Element.text
-                                |> Element.el
-                                    [ Font.color colors.secondary
-                                    , Font.size 14
-                                    ]
-
-                        button =
-                            Input.button
-                                [ Font.size 10
-                                , Element.paddingXY 10 5
-                                , Background.color colors.secondary
-                                , Font.color colors.background
-                                , Border.rounded 3
-                                , Font.variant Font.smallCaps
-                                ]
-                                { onPress = Nothing
-                                , label = Element.text "unfold"
-                                }
-
-                        shadow =
-                            Element.el
-                                [ Element.width Element.fill
-                                , Element.height Element.fill
-                                , Border.innerShadow
-                                    { offset = ( 0, 3 )
-                                    , size = -6
-                                    , blur = 12
-                                    , color = colors.window
-                                    }
-                                ]
-                                Element.none
-
-                        fold =
-                            [ lineNumbers, button ]
-                                |> Element.row
-                                    [ Element.spacing 20
-                                    , Element.padding 10
-                                    , Background.color colors.background
-                                    , Element.width Element.fill
-                                    ]
-                                |> Element.el
-                                    [ Element.paddingXY 5 10
-                                    , Element.width Element.fill
-                                    , Element.paddingEach
-                                        { top = 0
-                                        , right = 1
-                                        , bottom = 0
-                                        , left = 1
-                                        }
-                                    , Background.color colors.window
-                                    , Element.inFront shadow
-                                    ]
-                    in
-                    fold
+                    fold colors start (length + start - 1)
=                        :: renderFragments (start + length) rendered rest
=
=                (Unfolded lines) :: rest ->
-                    let
-                        unfolded =
-                            lines
-                                |> List.indexedMap
-                                    (\n loc ->
-                                        Element.row []
-                                            [ Element.el
-                                                [ Font.color colors.secondary
-                                                , Font.extraLight
-                                                , Element.width (Element.px 40)
-                                                , Element.padding 10
-                                                , Font.alignRight
-                                                , Element.css "user-select" "none"
-                                                , Element.css "-webkit-user-select" "none"
-                                                , Element.css "-ms-user-select" "none"
-                                                , Element.css "-webkit-touch-callout" "none"
-                                                , Element.css "-o-user-select" "none"
-                                                , Element.css "-moz-user-select" "none"
-                                                ]
-                                                ((n + start)
-                                                    |> String.fromInt
-                                                    |> Element.text
-                                                )
-                                            , Element.el
-                                                [ Element.width Element.fill
-                                                , Element.padding 10
-                                                , highlighted
-                                                    |> Dict.get (n + start)
-                                                    |> Maybe.map highlight
-                                                    |> Maybe.withDefault Element.none
-                                                    |> Element.behindContent
-                                                ]
-                                                (Element.text loc)
-                                            ]
-                                    )
-                                |> Element.column
-                                    [ Element.width Element.fill
-                                    , Font.size 16
-                                    , Element.scrollbarY
-                                    ]
-                    in
-                    unfolded
+                    unfolded colors highlighted start lines
=                        :: renderFragments (start + List.length lines) rendered rest
=
-        folded : Set Int
-        folded =
-            annotations
-                |> List.foldl folds []
-                |> List.map (uncurry List.range)
-                |> List.concat
-                |> Set.fromList
-
-        folds : Annotation -> List ( Int, Int ) -> List ( Int, Int )
-        folds item memo =
-            case item of
-                Fold start size ->
-                    ( start, start + size ) :: memo
-
-                _ ->
-                    memo
-
-        extractFragments :
-            List Fragment
-            -> List ( Int, String )
-            -> List Fragment
-        extractFragments accumulated lines =
-            case lines of
-                [] ->
-                    -- We have reached the end of the code
-                    case accumulated of
-                        [] ->
-                            -- It was empty. Add one empty line.
-                            [ Unfolded [ "" ] ]
-
-                        (Folded n) :: _ ->
-                            List.reverse accumulated
-
-                        (Unfolded linesReversed) :: previous ->
-                            List.reverse
-                                (Unfolded (List.reverse linesReversed)
-                                    :: previous
-                                )
-
-                ( n, line ) :: rest ->
-                    let
-                        isFolded =
-                            Set.member (n + 1) folded
-                    in
-                    case accumulated of
-                        [] ->
-                            -- It's the first fragment. Check if it's folded or not.
-                            if isFolded then
-                                extractFragments [ Folded 1 ] rest
-
-                            else
-                                extractFragments [ Unfolded [ line ] ] rest
-
-                        (Folded length) :: previous ->
-                            if isFolded then
-                                -- Continue the fold
-                                let
-                                    this =
-                                        Folded (length + 1)
-                                in
-                                extractFragments
-                                    (this :: previous)
-                                    rest
-
-                            else
-                                -- Fold is finished. Start a new unfolded fragment.
-                                let
-                                    this =
-                                        Folded length
-
-                                    next =
-                                        Unfolded [ line ]
-                                in
-                                extractFragments
-                                    (next :: this :: previous)
-                                    rest
-
-                        (Unfolded linesReversed) :: previous ->
-                            if isFolded then
-                                -- Unfolded fragment is finished. Reverse the lines and start a new fold.
-                                let
-                                    this =
-                                        Unfolded (List.reverse linesReversed)
-
-                                    next =
-                                        Folded 1
-                                in
-                                extractFragments
-                                    (next
-                                        :: this
-                                        :: previous
-                                    )
-                                    rest
-
-                            else
-                                -- Continue the unfolded fragment
-                                let
-                                    this =
-                                        Unfolded (line :: linesReversed)
-                                in
-                                extractFragments
-                                    (this :: previous)
-                                    rest
-
-        highlighted : Dict Int Region
=        highlighted =
-            annotations
-                |> List.foldl regions Dict.empty
-
-        regions : Annotation -> Dict Int Region -> Dict Int Region
-        regions item memo =
-            case item of
-                Highlight region ->
-                    Dict.insert region.top region memo
-
-                _ ->
-                    memo
+            highlights
+                |> List.zip (List.map .top highlights)
+                |> Dict.fromList
=    in
=    [ topBar
=    , contents
@@ -396,127 +148,240 @@ editor { path, offset, colors } annotations code =
=            ]
=
=
-main : Html msg
-main =
-    """module Main exposing (main)
-
-import Browser
-import Dict
-import Element
-import Svg exposing (Svg)
-import Svg.Attributes
-
-
-main =
-    Browser.element
-        { init = init
-        , view = view
-        , update = update
-        , subscriptions = subscriptions
-        }
+highlight : Element.Color -> Region -> Element msg
+highlight color { top, left, width, height } =
+    Element.row []
+        [ " "
+            |> String.repeat left
+            |> Element.text
+        , " "
+            |> String.repeat width
+            |> List.repeat height
+            |> List.map Element.text
+            |> List.map (Element.el [ Element.paddingXY 0 10 ])
+            |> Element.column
+                [ Element.behindContent
+                    (Element.el
+                        [ Element.width Element.fill
+                        , Element.height Element.fill
+                        , Border.color color
+                        , -- Hand-drawn style borders. See https://codepen.io/tmrDevelops/pen/VeRvKX/
+                          [ width, height ]
+                            |> List.map ((*) 2)
+                            |> List.map String.fromInt
+                            |> List.map (\num -> num ++ "px")
+                            |> List.repeat 2
+                            |> List.concat
+                            |> (\list -> [ list, List.reverse list ])
+                            |> List.map (String.join " ")
+                            |> String.join " / "
+                            |> Element.css "border-radius"
+                        , Border.width 5
+                        , Element.scale 1.1
+                        , Element.alpha 0.7
+                        ]
+                        Element.none
+                    )
+                , Element.css "pointer-events" "none"
+                , Element.css "user-select" "none"
+                , Element.css "-webkit-user-select" "none"
+                , Element.css "-ms-user-select" "none"
+                , Element.css "-webkit-touch-callout" "none"
+                , Element.css "-o-user-select" "none"
+                , Element.css "-moz-user-select" "none"
+                ]
+        ]
=
=
-view age =
-    [ segment (age / 5000) { color = "brown", rotation = -90 } ]
-        |> Svg.svg
-            [ Svg.Attributes.height "100%"
-            , Svg.Attributes.width "100%"
-            , Svg.Attributes.style "background: none"
-            , Svg.Attributes.viewBox "-500 -500 1000 1000"
-            ]
-        |> Element.html
-        |> Element.layout
-            [ Element.width Element.fill
-            , Element.height Element.fill
-            ]
+fold : Colors -> Int -> Int -> Element msg
+fold colors start end =
+    let
+        lineNumbers =
+            [ start, end ]
+                |> List.map String.fromInt
+                |> String.join " - "
+                |> Element.text
+                |> Element.el
+                    [ Font.color colors.secondary
+                    , Font.size 14
+                    ]
=
+        button =
+            Input.button
+                [ Font.size 10
+                , Element.paddingXY 10 5
+                , Background.color colors.secondary
+                , Font.color colors.background
+                , Border.rounded 3
+                , Font.variant Font.smallCaps
+                ]
+                { onPress = Nothing
+                , label = Element.text "unfold"
+                }
=
-rules =
-    Dict.empty
-        |> Dict.insert "brown"
-            [ { color = "brown", rotation = 0 }
-            , { color = "green", rotation = 20 }
-            , { color = "green", rotation = -30 }
+        shadow =
+            Element.el
+                [ Element.width Element.fill
+                , Element.height Element.fill
+                , Border.innerShadow
+                    { offset = ( 0, 3 )
+                    , size = -6
+                    , blur = 12
+                    , color = colors.window
+                    }
+                ]
+                Element.none
+    in
+    [ lineNumbers, button ]
+        |> Element.row
+            [ Element.spacing 20
+            , Element.padding 10
+            , Background.color colors.background
+            , Element.width Element.fill
=            ]
-        |> Dict.insert "green"
-            [ { color = "red", rotation = -45 }
-            , { color = "red", rotation = -5 }
-            , { color = "red", rotation = 50 }
+        |> Element.el
+            [ Element.paddingXY 5 10
+            , Element.width Element.fill
+            , Element.paddingEach
+                { top = 0
+                , right = 1
+                , bottom = 0
+                , left = 1
+                }
+            , Background.color colors.window
+            , Element.inFront shadow
=            ]
=
=
-segment age { color, rotation } =
-    if age <= 0 then
-        Svg.g [] []
-
-    else
-        Svg.g []
-            [ rules
-                |> Dict.get color
-                |> Maybe.withDefault []
-                |> List.map (segment (age - 1))
-                |> Svg.g
-                    [ Svg.Attributes.transform
-                        (String.concat
-                            [ "rotate("
-                            , String.fromFloat rotation
-                            , ") translate("
-                            , String.fromFloat (age * 10)
-                            , ")"
-                            ]
+unfolded : Colors -> Dict Int Region -> Int -> List String -> Element msg
+unfolded colors highlighted start lines =
+    lines
+        |> List.indexedMap
+            (\n loc ->
+                Element.row []
+                    [ Element.el
+                        [ Font.color colors.secondary
+                        , Font.extraLight
+                        , Element.width (Element.px 40)
+                        , Element.padding 10
+                        , Font.alignRight
+                        , Element.css "user-select" "none"
+                        , Element.css "-webkit-user-select" "none"
+                        , Element.css "-ms-user-select" "none"
+                        , Element.css "-webkit-touch-callout" "none"
+                        , Element.css "-o-user-select" "none"
+                        , Element.css "-moz-user-select" "none"
+                        ]
+                        ((n + start)
+                            |> String.fromInt
+                            |> Element.text
=                        )
+                    , Element.el
+                        [ Element.width Element.fill
+                        , Element.padding 10
+                        , highlighted
+                            |> Dict.get (n + start)
+                            |> Maybe.map (highlight colors.annotations)
+                            |> Maybe.withDefault Element.none
+                            |> Element.behindContent
+                        ]
+                        (Element.text loc)
=                    ]
-            , dot age color rotation
-            , line age color rotation
-            ]
-
-
-dot age color rotation =
-    Svg.circle
-        [ Svg.Attributes.r (String.fromFloat age)
-        , Svg.Attributes.cx "0"
-        , Svg.Attributes.cy "0"
-        , Svg.Attributes.fill color
-        , Svg.Attributes.transform
-            (String.concat
-                [ "rotate("
-                , String.fromFloat rotation
-                , ") translate("
-                , String.fromFloat (age * 10)
-                , ")"
-                ]
-            )
-        ]
-        []
-
-
-    line age color rotation =
-    Svg.line
-        [ Svg.Attributes.strokeWidth "1"
-        , Svg.Attributes.x1 "0"
-        , Svg.Attributes.y1 "0"
-        , Svg.Attributes.x1 (String.fromFloat (age * 10))
-        , Svg.Attributes.y1 "0"
-        , Svg.Attributes.stroke color
-        , Svg.Attributes.strokeWidth (String.fromFloat age)
-        , Svg.Attributes.transform
-            (String.concat
-                [ "rotate("
-                , String.fromFloat rotation
-                , ")"
-                ]
=            )
-        ]
-        []
-"""
-        |> editor defaults
-            [ Highlight { top = 10, left = 0, width = 38, height = 7 }
-            , Highlight { top = 3, left = 0, width = 15, height = 1 }
-            , Highlight { top = 19, left = 0, width = 10, height = 1 }
-            , Highlight { top = 20, left = 15, width = 10, height = 1 }
-            , Fold 35 11
-            ]
-        |> Element.layout
+        |> Element.column
=            [ Element.width Element.fill
-            , Element.height Element.fill
+            , Font.size 16
+            , Element.clipY
=            ]
+
+
+extractFragments :
+    Set Int
+    -> List Fragment
+    -> List ( Int, String )
+    -> List Fragment
+extractFragments folded accumulated lines =
+    case lines of
+        [] ->
+            -- We have reached the end of the code
+            case accumulated of
+                [] ->
+                    -- It was empty. Add one empty line.
+                    [ Unfolded [ "" ] ]
+
+                (Folded n) :: _ ->
+                    List.reverse accumulated
+
+                (Unfolded linesReversed) :: previous ->
+                    List.reverse
+                        (Unfolded (List.reverse linesReversed)
+                            :: previous
+                        )
+
+        ( n, line ) :: rest ->
+            let
+                isFolded =
+                    Set.member (n + 1) folded
+            in
+            case accumulated of
+                [] ->
+                    -- It's the first fragment. Check if it's folded or not.
+                    if isFolded then
+                        extractFragments folded
+                            [ Folded 1 ]
+                            rest
+
+                    else
+                        extractFragments folded
+                            [ Unfolded [ line ] ]
+                            rest
+
+                (Folded length) :: previous ->
+                    if isFolded then
+                        -- Continue the fold
+                        let
+                            this =
+                                Folded (length + 1)
+                        in
+                        extractFragments folded
+                            (this :: previous)
+                            rest
+
+                    else
+                        -- Fold is finished. Start a new unfolded fragment.
+                        let
+                            this =
+                                Folded length
+
+                            next =
+                                Unfolded [ line ]
+                        in
+                        extractFragments folded
+                            (next :: this :: previous)
+                            rest
+
+                (Unfolded linesReversed) :: previous ->
+                    if isFolded then
+                        -- Unfolded fragment is finished. Reverse the lines and start a new fold.
+                        let
+                            this =
+                                Unfolded (List.reverse linesReversed)
+
+                            next =
+                                Folded 1
+                        in
+                        extractFragments
+                            folded
+                            (next :: this :: previous)
+                            rest
+
+                    else
+                        -- Continue the unfolded fragment
+                        let
+                            this =
+                                Unfolded (line :: linesReversed)
+                        in
+                        extractFragments
+                            folded
+                            (this :: previous)
+                            rest
new file mode 100644
index 0000000..51fdac0
--- /dev/null
+++ b/src/Examples/Editor.elm
@@ -0,0 +1,64 @@
+module Examples.Editor exposing (main)
+
+import Editor exposing (editor)
+import Element exposing (Element)
+import Html exposing (Html)
+import Set exposing (Set)
+
+
+main : Html msg
+main =
+    """This is an example of how Editor module can be used.
+
+It supports highligs. Lines can be folded, like this
+
+This code is folded
+
+So is this.
+
+Highlights can span over multiple lines. Try:
+
+"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
+incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
+nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
+fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
+culpa qui officia deserunt mollit anim id est laborum."
+    |> editor Editor.defaults
+        { highlights =
+            [ { top = 10, left = 1, width = 40, height = 7 }
+            , { top = 3, left = 1, width = 15, height = 1 }
+            , { top = 19, left = 1, width = 10, height = 1 }
+            , { top = 20, left = 15, width = 12, height = 1 }
+            ]
+        , folded =
+            [ List.range 4 6
+            , List.range 74 91
+            ]
+                |> List.concat
+                |> Set.fromList
+        }
+    |> Element.layout
+        [ Element.width Element.fill
+        , Element.height Element.fill
+        ]
+
+
+"""
+        |> editor Editor.defaults
+            { highlights =
+                [ { top = 3, left = 13, width = 8, height = 1 }
+                , { top = 19, left = 15, width = 45, height = 4 }
+                ]
+            , folded =
+                [ List.range 5 7
+                , List.range 74 91
+                , List.range 20 21
+                ]
+                    |> List.concat
+                    |> Set.fromList
+            }
+        |> Element.layout
+            [ Element.width Element.fill
+            , Element.height Element.fill
+            ]
index 7185019..07cf136 100644
--- a/src/Mark/Custom.elm
+++ b/src/Mark/Custom.elm
@@ -29,6 +29,7 @@ import Html exposing (Html)
=import Html.Attributes
=import Mark
=import Mark.Default
+import Set
=
=
=title : Mark.Block String
@@ -82,42 +83,91 @@ monospace =
=        ]
=
=
+
+-- TODO: Separate to Mark.Editor ?
+
+
+type Annotation
+    = None
+    | Highlight Int Int Int Int
+    | Fold Int Int
+
+
=editor : Mark.Block (a -> Element msg)
=editor =
=    let
-        render : List Editor.Annotation -> String -> model -> Element msg
+        render : Editor.Annotations -> String -> model -> Element msg
=        render annotations_ contents model =
=            Editor.editor Editor.defaults annotations_ contents
=
=        annotations =
=            Mark.block "Annotations"
-                identity
+                (extractAnnotations { highlights = [], folded = Set.empty })
=                (Mark.manyOf [ none, highlight, fold ])
=
+        extractAnnotations : Editor.Annotations -> List Annotation -> Editor.Annotations
+        extractAnnotations ({ highlights, folded } as extracted) remaining =
+            case remaining of
+                [] ->
+                    { extracted
+                        | highlights = List.reverse highlights
+                    }
+
+                None :: rest ->
+                    extractAnnotations
+                        { highlights = highlights
+                        , folded = folded
+                        }
+                        rest
+
+                (Highlight top left width height) :: rest ->
+                    extractAnnotations
+                        { extracted
+                            | highlights =
+                                { top = top
+                                , left = left
+                                , width = width
+                                , height = height
+                                }
+                                    :: highlights
+                        }
+                        rest
+
+                (Fold start length) :: rest ->
+                    extractAnnotations
+                        { extracted
+                            | folded =
+                                (start + length)
+                                    |> List.range start
+                                    |> Set.fromList
+                                    |> Set.union folded
+                        }
+                        rest
+
=        code : Mark.Block String
=        code =
=            Mark.block "Code"
=                identity
=                Mark.multiline
=
-        none : Mark.Block Editor.Annotation
+        none : Mark.Block Annotation
=        none =
-            Mark.stub "None" Editor.None
+            -- A dummy annotation to satisfy manyOf block. See https://github.com/mdgriffith/elm-markup/issues/12
+            Mark.stub "None" None
=
-        highlight : Mark.Block Editor.Annotation
+        highlight : Mark.Block Annotation
=        highlight =
=            Mark.record4 "Highlight"
-                Editor.Region
+                Highlight
=                (Mark.field "top" Mark.int)
=                (Mark.field "left" Mark.int)
=                (Mark.field "width" Mark.int)
=                (Mark.field "height" Mark.int)
-                |> Mark.map Editor.Highlight
=
-        fold : Mark.Block Editor.Annotation
+        fold : Mark.Block Annotation
=        fold =
=            Mark.record2 "Fold"
-                Editor.Fold
+                Fold
=                (Mark.field "start" Mark.int)
=                (Mark.field "length" Mark.int)
=    in

Fix the fold over highlight in the Editor example

On by Tadeusz Łazurski

index 51fdac0..bbfb394 100644
--- a/src/Examples/Editor.elm
+++ b/src/Examples/Editor.elm
@@ -52,8 +52,7 @@ culpa qui officia deserunt mollit anim id est laborum."
=                ]
=            , folded =
=                [ List.range 5 7
-                , List.range 74 91
-                , List.range 20 21
+                , List.range 12 14
=                ]
=                    |> List.concat
=                    |> Set.fromList

Disable CI when not on master branch

On by Tadeusz Łazurski

index 8d347ad..4472935 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -13,5 +13,5 @@ pages:
=    paths:
=      - public
=
-  # only:
-  #   - master
+  only:
+    - master

Merge branch 'content' into about-fana

On by Tadeusz Łazurski

Merge branch 'content' into navigation

On by Tadeusz Łazurski

Add anchor icon to Fana's bio.

On by Tadeusz Łazurski

index c2939da..2547076 100644
--- a/content/index.txt
+++ b/content/index.txt
@@ -29,4 +29,4 @@ I love the creativity of the software development and hope to share that passion
=
=*Sam* is a co-author of the workshop. He is an Elm developer at {Link|itravel|url=https://www.itravel.de/} in Cologne, Germany.
=
-*Fana* is a junior software developer (an ex marine biologist) and coordinator of our project. She keeps us on the right track. Also she is taking care of our media presence.
+*Fana* is a junior software developer (an ex marine biologist {Icon|name=anchor}) and coordinator of our project. She keeps us on the right track. Also she is taking care of our media presence.

On by Tadeusz Łazurski

index 87fee45..471756e 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -493,7 +493,7 @@ contentNavigationBar { location, scroll, viewport } =
=    let
=        links : List { url : String, label : String, icon : Element msg }
=        links =
-            [ { url = "http://localhost:8001"
+            [ { url = "/"
=              , label = "Home"
=              , icon = FeatherIcons.home |> FeatherIcons.toHtml [] |> Element.html
=              }

Merge branch 'content' into tree-example

On by Tadeusz Łazurski

Fix the tree example markup

On by Tadeusz Łazurski

With a workaround for parser breaking on startWith + manyOf.

index 9ef5249..6110a50 100644
--- a/content/day-4.txt
+++ b/content/day-4.txt
@@ -18,62 +18,44 @@
=| Header
=    The Problem
=
-| Note
-    FIXME: The tree example is broken.
+We want to have a tree that looks like this:
=
=| Window
=    | Tree
=        | Axiom
=            color = green
-            rotation = 0
-            age = 9
-
-        | Rule
-            | Parent
-                purple
-
-            | Child
-                color = green
-                rotation = 0.0
-
-            | Child
-                color = green
-                rotation = 30
+            rotation = -90
+            age = 8
=
-            | Child
-                color = green
-                rotation = -30
=
=        | Rule
-            | Parent
-                green
+            parent = green
=
-            | Child
-                color = brown
-                rotation = 0.0
+            children =
+                | Child
+                    color = brown
+                    rotation = 0.0
=
-            | Child
-                color = brown
-                rotation = 30
+                | Child
+                    color = brown
+                    rotation = 30
=
-            | Child
-                color = brown
-                rotation = -30
+                | Child
+                    color = brown
+                    rotation = -30
=
=        | Rule
-
-            | Parent
-                brown
-
-            | Child
-                color = green
-                rotation = 0.0
-            | Child
-                color = green
-                rotation = 30
-            | Child
-                color = green
-                rotation = -30
+            parent = brown
+            children =
+                | Child
+                    color = green
+                    rotation = 0.0
+                | Child
+                    color = green
+                    rotation = 30
+                | Child
+                    color = green
+                    rotation = -30
=
=
=The tree is built from segments: a line and a dot. In this respect it is similar to the connected dots we created yesterday. If we group the dot and a line, we will have the building block for our tree. We can do it with {Code|Svg.g} function ({Code|g} is an abbreviation of "group"). Just like {Code|Svg.svg}, it takes list of attributes and list of children. We can use it like that:
index d73c74e..bf183c7 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -915,14 +915,11 @@ document =
=                    Mark.manyOf [ rule ]
=
=                rule =
-                    Mark.startWith Tuple.pair
-                        parent
-                        (Mark.manyOf [ child ])
-
-                -- FIXME: Parser breaks with empty list of dead ends
-                -- Mark.stub "Rule" ( "green", [ Examples.Tree.Segment "green" 30 ] )
-                -- Mark.manyOf [ child ]
-                --     |> Mark.map (Tuple.pair "green")
+                    Mark.record2 "Rule"
+                        Tuple.pair
+                        (Mark.field "parent" Mark.string)
+                        (Mark.field "children" (Mark.manyOf [ child ]))
+
=                parent =
=                    Mark.block "Parent" identity Mark.string
=