Commits: 10

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

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

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.

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

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

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

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

Merge branch 'content' into navigation

Add anchor icon to Fana's bio.

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.

Fix home link in content navigation bar

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
=              }