Add a slides example project.
53
examples/slides/Data/Author.elm
Normal file
@ -0,0 +1,53 @@
|
||||
module Data.Author exposing (Author, all, decoder, dillon, view)
|
||||
|
||||
import Cloudinary
|
||||
import Element exposing (Element)
|
||||
import Html.Attributes as Attr
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
import List.Extra
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
|
||||
|
||||
type alias Author =
|
||||
{ name : String
|
||||
, avatar : ImagePath
|
||||
, bio : String
|
||||
}
|
||||
|
||||
|
||||
all : List Author
|
||||
all =
|
||||
[ dillon
|
||||
]
|
||||
|
||||
|
||||
dillon : Author
|
||||
dillon =
|
||||
{ name = "Dillon Kearns"
|
||||
, avatar = Cloudinary.url "v1602899672/elm-radio/dillon-profile_n2lqst.jpg" Nothing 140
|
||||
, bio = "Elm developer and educator. Founder of Incremental Elm Consulting."
|
||||
}
|
||||
|
||||
|
||||
decoder : Decoder Author
|
||||
decoder =
|
||||
Decode.string
|
||||
|> Decode.andThen
|
||||
(\lookupName ->
|
||||
case List.Extra.find (\currentAuthor -> currentAuthor.name == lookupName) all of
|
||||
Just author ->
|
||||
Decode.succeed author
|
||||
|
||||
Nothing ->
|
||||
Decode.fail ("Couldn't find author with name " ++ lookupName ++ ". Options are " ++ String.join ", " (List.map .name all))
|
||||
)
|
||||
|
||||
|
||||
view : List (Element.Attribute msg) -> Author -> Element msg
|
||||
view attributes author =
|
||||
Element.image
|
||||
(Element.width (Element.px 70)
|
||||
:: Element.htmlAttribute (Attr.class "avatar")
|
||||
:: attributes
|
||||
)
|
||||
{ src = ImagePath.toString author.avatar, description = author.name }
|
124
examples/slides/Template/Blog.elm
Normal file
@ -0,0 +1,124 @@
|
||||
module Template.Blog exposing (Model, Msg, template)
|
||||
|
||||
import Article
|
||||
import Document exposing (Document)
|
||||
import Element
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Index
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.PagePath exposing (PagePath)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Shared
|
||||
import SiteOld
|
||||
import Template exposing (StaticPayload, TemplateWithState)
|
||||
|
||||
|
||||
type Msg
|
||||
= Msg
|
||||
|
||||
|
||||
template : TemplateWithState {} StaticData Model Msg
|
||||
template =
|
||||
Template.withStaticData
|
||||
{ head = head
|
||||
, staticData = \_ -> staticData
|
||||
, staticRoutes = StaticHttp.succeed []
|
||||
}
|
||||
|> Template.buildWithLocalState
|
||||
{ view = view
|
||||
, init = init
|
||||
, update = update
|
||||
|
||||
--\_ _ _ model -> ( model, Cmd.none )
|
||||
, subscriptions = \_ _ _ -> Sub.none
|
||||
}
|
||||
|
||||
|
||||
staticData : StaticHttp.Request StaticData
|
||||
staticData =
|
||||
--StaticFile.glob "content/blog/*.md"
|
||||
Article.allMetadata
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
List ( PagePath, Article.ArticleMetadata )
|
||||
|
||||
|
||||
init : {} -> ( Model, Cmd Msg )
|
||||
init _ =
|
||||
( Model, Cmd.none )
|
||||
|
||||
|
||||
update :
|
||||
Shared.Model
|
||||
-> {}
|
||||
-> Msg
|
||||
-> Model
|
||||
-> ( Model, Cmd Msg )
|
||||
update sharedModel metadata msg model =
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
|
||||
|
||||
view :
|
||||
Model
|
||||
-> Shared.Model
|
||||
-> StaticPayload StaticData {}
|
||||
-> Document Msg
|
||||
view thing model staticPayload =
|
||||
{ title = "elm-pages blog"
|
||||
, body =
|
||||
[ Element.column [ Element.width Element.fill ]
|
||||
[ Element.column [ Element.padding 20, Element.centerX ]
|
||||
[ --Element.text
|
||||
-- (staticPayload.static
|
||||
-- |> String.join ", "
|
||||
-- )
|
||||
Index.view staticPayload.static
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
head : StaticPayload StaticData {} -> List Head.Tag
|
||||
head staticPayload =
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = ImagePath.build [ "images", "icon-png.png" ]
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = SiteOld.tagline
|
||||
, locale = Nothing
|
||||
, title = "elm-pages blog"
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
|
||||
|
||||
--fileRequest : StaticHttp.Request DataFromFile
|
||||
--fileRequest =
|
||||
-- StaticFile.request
|
||||
-- "content/blog/extensible-markdown-parsing-in-elm.md"
|
||||
-- (OptimizedDecoder.map2 DataFromFile
|
||||
-- (StaticFile.body
|
||||
-- |> OptimizedDecoder.andThen
|
||||
-- (\rawBody ->
|
||||
-- case rawBody |> MarkdownRenderer.view |> Result.map Tuple.second of
|
||||
-- Ok renderedBody ->
|
||||
-- OptimizedDecoder.succeed renderedBody
|
||||
--
|
||||
-- Err error ->
|
||||
-- OptimizedDecoder.fail error
|
||||
-- )
|
||||
-- )
|
||||
-- (StaticFile.frontmatter frontmatterDecoder)
|
||||
-- )
|
283
examples/slides/Template/Blog/Slug_.elm
Normal file
@ -0,0 +1,283 @@
|
||||
module Template.Blog.Slug_ exposing (Model, Msg, articlesRequest, routes, template, toRssItem)
|
||||
|
||||
import Article
|
||||
import Cloudinary
|
||||
import Data.Author as Author exposing (Author)
|
||||
import Date exposing (Date)
|
||||
import Document exposing (Document)
|
||||
import Element exposing (Element)
|
||||
import Element.Font as Font
|
||||
import Element.Region
|
||||
import Glob
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import MarkdownRenderer
|
||||
import OptimizedDecoder
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.StaticFile as StaticFile
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Palette
|
||||
import Rss
|
||||
import Shared
|
||||
import SiteOld
|
||||
import StructuredData
|
||||
import Template exposing (StaticPayload, Template, TemplateWithState)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
type alias RouteParams =
|
||||
{ slug : String }
|
||||
|
||||
|
||||
routes : StaticHttp.Request (List RouteParams)
|
||||
routes =
|
||||
Article.blogPostsGlob
|
||||
|> StaticHttp.map
|
||||
(List.map
|
||||
(\globData ->
|
||||
{ slug = globData.slug }
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
type alias BlogPost =
|
||||
{ title : String
|
||||
, description : String
|
||||
, published : Date
|
||||
, author : Author
|
||||
, image : ImagePath
|
||||
, draft : Bool
|
||||
}
|
||||
|
||||
|
||||
template : Template RouteParams DataFromFile
|
||||
template =
|
||||
Template.withStaticData
|
||||
{ staticData = staticData
|
||||
, head = head
|
||||
, staticRoutes = routes
|
||||
|
||||
--, route = route
|
||||
}
|
||||
|> Template.buildNoState { view = view }
|
||||
|
||||
|
||||
view :
|
||||
StaticPayload DataFromFile RouteParams
|
||||
-> Document msg
|
||||
view { static } =
|
||||
{ title = static.frontmatter.title
|
||||
, body =
|
||||
let
|
||||
author =
|
||||
Author.dillon
|
||||
in
|
||||
[ Element.column [ Element.width Element.fill ]
|
||||
[ Element.column
|
||||
[ Element.padding 30
|
||||
, Element.spacing 40
|
||||
, Element.Region.mainContent
|
||||
, Element.width (Element.fill |> Element.maximum 800)
|
||||
, Element.centerX
|
||||
]
|
||||
(Element.column [ Element.spacing 10 ]
|
||||
[ Element.row [ Element.spacing 10 ]
|
||||
[ Author.view [] author
|
||||
, Element.column [ Element.spacing 10, Element.width Element.fill ]
|
||||
[ Element.paragraph [ Font.bold, Font.size 24 ]
|
||||
[ Element.text author.name
|
||||
]
|
||||
, Element.paragraph [ Font.size 16 ]
|
||||
[ Element.text author.bio
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
:: (publishedDateView static.frontmatter |> Element.el [ Font.size 16, Font.color (Element.rgba255 0 0 0 0.6) ])
|
||||
:: Palette.blogHeading static.frontmatter.title
|
||||
:: articleImageView static.frontmatter.image
|
||||
:: static.body
|
||||
|> List.map (Element.map never)
|
||||
)
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
head :
|
||||
StaticPayload DataFromFile RouteParams
|
||||
-> List Head.Tag
|
||||
head { path, static } =
|
||||
let
|
||||
metadata =
|
||||
static.frontmatter
|
||||
in
|
||||
Head.structuredData
|
||||
(StructuredData.article
|
||||
{ title = metadata.title
|
||||
, description = metadata.description
|
||||
, author = StructuredData.person { name = Author.dillon.name }
|
||||
, publisher = StructuredData.person { name = Author.dillon.name }
|
||||
, url = SiteOld.canonicalUrl ++ "/" ++ PagePath.toString path
|
||||
, imageUrl = SiteOld.canonicalUrl ++ "/" ++ ImagePath.toString metadata.image
|
||||
, datePublished = Date.toIsoString metadata.published
|
||||
, mainEntityOfPage =
|
||||
StructuredData.softwareSourceCode
|
||||
{ codeRepositoryUrl = "https://github.com/dillonkearns/elm-pages"
|
||||
, description = "A statically typed site generator for Elm."
|
||||
, author = "Dillon Kearns"
|
||||
, programmingLanguage = StructuredData.elmLang
|
||||
}
|
||||
}
|
||||
)
|
||||
:: (Seo.summaryLarge
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = metadata.image
|
||||
, alt = metadata.description
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = metadata.description
|
||||
, locale = Nothing
|
||||
, title = metadata.title
|
||||
}
|
||||
|> Seo.article
|
||||
{ tags = []
|
||||
, section = Nothing
|
||||
, publishedTime = Just (Date.toIsoString metadata.published)
|
||||
, modifiedTime = Nothing
|
||||
, expirationTime = Nothing
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
publishedDateView : { a | published : Date } -> Element msg
|
||||
publishedDateView metadata =
|
||||
Element.text
|
||||
(metadata.published
|
||||
|> Date.format "MMMM ddd, yyyy"
|
||||
)
|
||||
|
||||
|
||||
articleImageView : ImagePath -> Element msg
|
||||
articleImageView articleImage =
|
||||
Element.image [ Element.width Element.fill ]
|
||||
{ src = ImagePath.toString articleImage
|
||||
, description = "Article cover photo"
|
||||
}
|
||||
|
||||
|
||||
type alias DataFromFile =
|
||||
{ body : List (Element Msg)
|
||||
, frontmatter : ArticleMetadata
|
||||
}
|
||||
|
||||
|
||||
staticData : RouteParams -> StaticHttp.Request DataFromFile
|
||||
staticData route =
|
||||
StaticFile.request
|
||||
("content/blog/" ++ route.slug ++ ".md")
|
||||
(OptimizedDecoder.map2 DataFromFile
|
||||
(StaticFile.body
|
||||
|> OptimizedDecoder.andThen
|
||||
(\rawBody ->
|
||||
case rawBody |> MarkdownRenderer.view |> Result.map Tuple.second of
|
||||
Ok renderedBody ->
|
||||
OptimizedDecoder.succeed renderedBody
|
||||
|
||||
Err error ->
|
||||
OptimizedDecoder.fail error
|
||||
)
|
||||
)
|
||||
(StaticFile.frontmatter frontmatterDecoder)
|
||||
)
|
||||
|
||||
|
||||
type alias ArticleMetadata =
|
||||
{ title : String
|
||||
, description : String
|
||||
, published : Date
|
||||
, image : ImagePath
|
||||
, draft : Bool
|
||||
}
|
||||
|
||||
|
||||
frontmatterDecoder : OptimizedDecoder.Decoder ArticleMetadata
|
||||
frontmatterDecoder =
|
||||
OptimizedDecoder.map5 ArticleMetadata
|
||||
(OptimizedDecoder.field "title" OptimizedDecoder.string)
|
||||
(OptimizedDecoder.field "description" OptimizedDecoder.string)
|
||||
(OptimizedDecoder.field "published"
|
||||
(OptimizedDecoder.string
|
||||
|> OptimizedDecoder.andThen
|
||||
(\isoString ->
|
||||
case Date.fromIsoString isoString of
|
||||
Ok date ->
|
||||
OptimizedDecoder.succeed date
|
||||
|
||||
Err error ->
|
||||
OptimizedDecoder.fail error
|
||||
)
|
||||
)
|
||||
)
|
||||
(OptimizedDecoder.field "image" imageDecoder)
|
||||
(OptimizedDecoder.field "draft" OptimizedDecoder.bool
|
||||
|> OptimizedDecoder.maybe
|
||||
|> OptimizedDecoder.map (Maybe.withDefault False)
|
||||
)
|
||||
|
||||
|
||||
imageDecoder : OptimizedDecoder.Decoder ImagePath
|
||||
imageDecoder =
|
||||
OptimizedDecoder.string
|
||||
|> OptimizedDecoder.map (\cloudinaryAsset -> Cloudinary.url cloudinaryAsset Nothing 800)
|
||||
|
||||
|
||||
toRssItem :
|
||||
ArticleMetadata
|
||||
-> Maybe Rss.Item
|
||||
toRssItem article =
|
||||
if article.draft then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just
|
||||
{ title = article.title
|
||||
, description = article.description
|
||||
, url = "TODO" --PagePath.toString page.path
|
||||
, categories = []
|
||||
, author = Author.dillon.name
|
||||
, pubDate = Rss.Date article.published
|
||||
, content = Nothing
|
||||
}
|
||||
|
||||
|
||||
articlesRequest : StaticHttp.Request (List ArticleMetadata)
|
||||
articlesRequest =
|
||||
Glob.succeed identity
|
||||
|> Glob.keep Glob.fullFilePath
|
||||
|> Glob.drop (Glob.literal "content/blog/")
|
||||
|> Glob.drop Glob.wildcard
|
||||
|> Glob.drop (Glob.literal ".md")
|
||||
|> Glob.toStaticHttp
|
||||
|> StaticHttp.andThen
|
||||
(\articleFilePaths ->
|
||||
articleFilePaths
|
||||
|> List.filter (\filePath -> filePath |> String.contains "index" |> not)
|
||||
|> List.map
|
||||
(\articleFilePath ->
|
||||
StaticFile.request articleFilePath
|
||||
(StaticFile.frontmatter frontmatterDecoder)
|
||||
)
|
||||
|> StaticHttp.combine
|
||||
)
|
144
examples/slides/Template/Documentation.elm
Normal file
@ -0,0 +1,144 @@
|
||||
module Template.Documentation exposing (Model, Msg, template)
|
||||
|
||||
import DocSidebar
|
||||
import Document exposing (Document)
|
||||
import Element exposing (Element)
|
||||
import Element.Events
|
||||
import Element.Font as Font
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Json.Decode as Decode
|
||||
import MarkdownRenderer
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Palette
|
||||
import Shared
|
||||
import SiteOld
|
||||
import Template exposing (StaticPayload, TemplateWithState)
|
||||
|
||||
|
||||
type alias Documentation =
|
||||
{ title : String }
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
()
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
|
||||
|
||||
type Msg
|
||||
= Increment
|
||||
|
||||
|
||||
template : TemplateWithState {} StaticData Model Msg
|
||||
template =
|
||||
Template.noStaticData
|
||||
{ head = head
|
||||
, staticRoutes = StaticHttp.succeed []
|
||||
}
|
||||
|> Template.buildWithSharedState
|
||||
{ view = view
|
||||
, init = init
|
||||
, update = update
|
||||
, subscriptions = \_ _ _ _ -> Sub.none
|
||||
}
|
||||
|
||||
|
||||
init : {} -> ( Model, Cmd Msg )
|
||||
init _ =
|
||||
( {}, Cmd.none )
|
||||
|
||||
|
||||
update : {} -> Msg -> Model -> Shared.Model -> ( Model, Cmd Msg, Maybe Shared.SharedMsg )
|
||||
update _ msg model sharedModel =
|
||||
case msg of
|
||||
Increment ->
|
||||
( model, Cmd.none, Just Shared.IncrementFromChild )
|
||||
|
||||
|
||||
decoder : Decode.Decoder Documentation
|
||||
decoder =
|
||||
Decode.map Documentation
|
||||
(Decode.field "title" Decode.string)
|
||||
|
||||
|
||||
head : StaticPayload StaticData {} -> List Head.Tag
|
||||
head staticPayload =
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = ImagePath.build [ "images", "icon-png.png" ]
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = SiteOld.tagline
|
||||
, locale = Nothing
|
||||
, title = "TODO title" -- staticPayload.metadata.title -- TODO
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
|
||||
view :
|
||||
Model
|
||||
-> Shared.Model
|
||||
-> StaticPayload StaticData {}
|
||||
-> Document Msg
|
||||
view model sharedModel staticPayload =
|
||||
{ title = "TODO title" -- staticPayload.metadata.title -- TODO
|
||||
, body =
|
||||
[ [ Element.row []
|
||||
[ --counterView sharedModel,
|
||||
DocSidebar.view
|
||||
staticPayload.path
|
||||
-- allMetadata -- TODO
|
||||
|> Element.el [ Element.width (Element.fillPortion 2), Element.alignTop, Element.height Element.fill ]
|
||||
, Element.column [ Element.width (Element.fillPortion 8), Element.padding 35, Element.spacing 15 ]
|
||||
[ Palette.heading 1
|
||||
[ Element.text "TODO title" -- Element.text staticPayload.metadata.title -- TODO
|
||||
]
|
||||
, Element.column [ Element.spacing 20 ]
|
||||
[--tocView staticPayload.path (Tuple.first rendered) -- TODO use StaticHttp to render view
|
||||
--Element.column
|
||||
-- [ Element.padding 50
|
||||
-- , Element.spacing 30
|
||||
-- , Element.Region.mainContent
|
||||
-- ]
|
||||
-- (Tuple.second rendered |> List.map (Element.map never))
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|> Element.textColumn
|
||||
[ Element.width Element.fill
|
||||
, Element.height Element.fill
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
counterView : Shared.Model -> Element Msg
|
||||
counterView sharedModel =
|
||||
Element.el [ Element.Events.onClick Increment ] (Element.text <| "Docs count: " ++ String.fromInt sharedModel.counter)
|
||||
|
||||
|
||||
tocView : PagePath -> MarkdownRenderer.TableOfContents -> Element msg
|
||||
tocView path toc =
|
||||
Element.column [ Element.alignTop, Element.spacing 20 ]
|
||||
[ Element.el [ Font.bold, Font.size 22 ] (Element.text "Table of Contents")
|
||||
, Element.column [ Element.spacing 10 ]
|
||||
(toc
|
||||
|> List.map
|
||||
(\heading ->
|
||||
Element.link [ Font.color (Element.rgb255 100 100 100) ]
|
||||
{ url = PagePath.toString path ++ "#" ++ heading.anchorId
|
||||
, label = Element.text heading.name
|
||||
}
|
||||
)
|
||||
)
|
||||
]
|
68
examples/slides/Template/Hello/Name_.elm
Normal file
@ -0,0 +1,68 @@
|
||||
module Template.Hello.Name_ exposing (Model, Msg, template)
|
||||
|
||||
import Document exposing (Document)
|
||||
import Element
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Shared
|
||||
import SiteOld
|
||||
import Template exposing (StaticPayload, Template)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
type alias Route =
|
||||
{ name : String
|
||||
}
|
||||
|
||||
|
||||
template : Template Route ()
|
||||
template =
|
||||
Template.noStaticData
|
||||
{ head = head
|
||||
, staticRoutes = StaticHttp.succeed [ { name = "world" } ]
|
||||
}
|
||||
|> Template.buildNoState { view = view }
|
||||
|
||||
|
||||
head :
|
||||
StaticPayload () Route
|
||||
-> List Head.Tag
|
||||
head static =
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = ImagePath.build [ "images", "icon-png.png" ]
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = SiteOld.tagline
|
||||
, locale = Nothing
|
||||
, title = "TODO title" -- metadata.title -- TODO
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
()
|
||||
|
||||
|
||||
view :
|
||||
StaticPayload StaticData Route
|
||||
-> Document msg
|
||||
view static =
|
||||
{ title = "TODO title"
|
||||
, body =
|
||||
[ Element.text <| "👋 " ++ static.routeParams.name
|
||||
]
|
||||
}
|
98
examples/slides/Template/Index.elm
Normal file
@ -0,0 +1,98 @@
|
||||
module Template.Index exposing (Model, Msg, template)
|
||||
|
||||
import Document exposing (Document)
|
||||
import Element
|
||||
import Element.Region
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import MarkdownRenderer
|
||||
import OptimizedDecoder
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.StaticFile as StaticFile
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Shared
|
||||
import SiteOld
|
||||
import Template exposing (StaticPayload, Template)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
type alias Route =
|
||||
{}
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
List (Element.Element Msg)
|
||||
|
||||
|
||||
template : Template Route StaticData
|
||||
template =
|
||||
Template.withStaticData
|
||||
{ head = head
|
||||
, staticRoutes = StaticHttp.succeed []
|
||||
, staticData = staticData
|
||||
}
|
||||
|> Template.buildNoState { view = view }
|
||||
|
||||
|
||||
head :
|
||||
StaticPayload StaticData Route
|
||||
-> List Head.Tag
|
||||
head static =
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = ImagePath.build [ "images", "icon-png.png" ]
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = SiteOld.tagline
|
||||
, locale = Nothing
|
||||
, title = "TODO title" -- metadata.title -- TODO
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
|
||||
view :
|
||||
StaticPayload StaticData Route
|
||||
-> Document Msg
|
||||
view static =
|
||||
{ title = "elm-pages - a statically typed site generator" -- metadata.title -- TODO
|
||||
, body =
|
||||
[ [ Element.column
|
||||
[ Element.padding 50
|
||||
, Element.spacing 60
|
||||
, Element.Region.mainContent
|
||||
]
|
||||
static.static
|
||||
]
|
||||
|> Element.textColumn
|
||||
[ Element.width Element.fill
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
staticData : Route -> StaticHttp.Request (List (Element.Element msg))
|
||||
staticData route =
|
||||
StaticFile.request
|
||||
"content/index.md"
|
||||
(StaticFile.body
|
||||
|> OptimizedDecoder.andThen
|
||||
(\rawBody ->
|
||||
case rawBody |> MarkdownRenderer.view |> Result.map Tuple.second of
|
||||
Ok renderedBody ->
|
||||
OptimizedDecoder.succeed renderedBody
|
||||
|
||||
Err error ->
|
||||
OptimizedDecoder.fail error
|
||||
)
|
||||
)
|
84
examples/slides/Template/Showcase.elm
Normal file
@ -0,0 +1,84 @@
|
||||
module Template.Showcase exposing (Model, Msg, template)
|
||||
|
||||
import Document exposing (Document)
|
||||
import Element exposing (Element)
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Shared
|
||||
import Showcase
|
||||
import Template exposing (StaticPayload, TemplateWithState)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
template : TemplateWithState {} StaticData () Msg
|
||||
template =
|
||||
Template.withStaticData
|
||||
{ head = head
|
||||
, staticRoutes = StaticHttp.succeed []
|
||||
, staticData = \_ -> staticData
|
||||
}
|
||||
|> Template.buildNoState { view = view }
|
||||
|
||||
|
||||
staticData : StaticHttp.Request StaticData
|
||||
staticData =
|
||||
Showcase.staticRequest
|
||||
|
||||
|
||||
|
||||
--(StaticHttp.get
|
||||
-- (Secrets.succeed "file://elm.json")
|
||||
-- OptimizedDecoder.string
|
||||
--)
|
||||
|
||||
|
||||
type alias DataFromFile =
|
||||
{ body : List (Element Msg), title : String }
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
List Showcase.Entry
|
||||
|
||||
|
||||
view :
|
||||
StaticPayload StaticData {}
|
||||
-> Document Msg
|
||||
view static =
|
||||
{ title = "elm-pages blog"
|
||||
, body =
|
||||
let
|
||||
showcaseEntries =
|
||||
static.static
|
||||
in
|
||||
[ Element.column [ Element.width Element.fill ]
|
||||
[ Element.column [ Element.padding 20, Element.centerX ] [ Showcase.view showcaseEntries ]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
head : StaticPayload StaticData {} -> List Head.Tag
|
||||
head staticPayload =
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = ImagePath.build [ "images", "icon-png.png" ]
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = "See some neat sites built using elm-pages! (Or submit yours!)"
|
||||
, locale = Nothing
|
||||
, title = "elm-pages sites showcase"
|
||||
}
|
||||
|> Seo.website
|
138
examples/slides/Template/Slide/Number_.elm
Normal file
@ -0,0 +1,138 @@
|
||||
module Template.Slide.Number_ exposing (Model, Msg, template)
|
||||
|
||||
import Document exposing (Document)
|
||||
import Element exposing (Element)
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Markdown.Block
|
||||
import Markdown.Parser
|
||||
import Markdown.Renderer
|
||||
import MarkdownRenderer
|
||||
import OptimizedDecoder
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.StaticFile as StaticFile
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Shared
|
||||
import Template exposing (StaticPayload, Template, TemplateWithState)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
type alias RouteParams =
|
||||
{ number : String }
|
||||
|
||||
|
||||
template : Template RouteParams StaticData
|
||||
template =
|
||||
Template.withStaticData
|
||||
{ head = head
|
||||
, staticRoutes = StaticHttp.succeed []
|
||||
, staticData = staticData
|
||||
}
|
||||
|> Template.buildNoState { view = view }
|
||||
|
||||
|
||||
staticData : RouteParams -> StaticHttp.Request StaticData
|
||||
staticData route =
|
||||
StaticFile.request
|
||||
"content/slides.md"
|
||||
(StaticFile.body
|
||||
|> OptimizedDecoder.andThen
|
||||
(\rawBody ->
|
||||
case rawBody |> Markdown.Parser.parse of
|
||||
Ok okBlocks ->
|
||||
case
|
||||
okBlocks
|
||||
|> markdownIndexedByHeading (route.number |> String.toInt |> Maybe.withDefault 1)
|
||||
|> Markdown.Renderer.render MarkdownRenderer.renderer
|
||||
of
|
||||
Ok renderedBody ->
|
||||
OptimizedDecoder.succeed renderedBody
|
||||
|
||||
Err error ->
|
||||
OptimizedDecoder.fail error
|
||||
|
||||
Err _ ->
|
||||
OptimizedDecoder.fail ""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
markdownIndexedByHeading :
|
||||
Int
|
||||
-> List Markdown.Block.Block
|
||||
-> List Markdown.Block.Block
|
||||
markdownIndexedByHeading index markdownBlocks =
|
||||
Markdown.Block.foldl
|
||||
(\block ( currentIndex, markdownToKeep ) ->
|
||||
case block of
|
||||
Markdown.Block.Heading Markdown.Block.H2 _ ->
|
||||
let
|
||||
newIndex =
|
||||
currentIndex + 1
|
||||
in
|
||||
--_ ->
|
||||
if newIndex == index then
|
||||
( newIndex, block :: markdownToKeep )
|
||||
|
||||
else
|
||||
( newIndex, markdownToKeep )
|
||||
|
||||
_ ->
|
||||
if currentIndex == index then
|
||||
( currentIndex, block :: markdownToKeep )
|
||||
|
||||
else
|
||||
( currentIndex, markdownToKeep )
|
||||
)
|
||||
( 0, [] )
|
||||
markdownBlocks
|
||||
|> Tuple.second
|
||||
|> List.reverse
|
||||
|
||||
|
||||
head :
|
||||
StaticPayload StaticData RouteParams
|
||||
-> List Head.Tag
|
||||
head static =
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = ImagePath.build [ "TODO" ]
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = "TODO"
|
||||
, locale = Nothing
|
||||
, title = "TODO title" -- metadata.title -- TODO
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
List (Element Msg)
|
||||
|
||||
|
||||
view :
|
||||
StaticPayload StaticData RouteParams
|
||||
-> Document Msg
|
||||
view static =
|
||||
{ title = "TODO title"
|
||||
, body =
|
||||
[ Element.column
|
||||
[ Element.padding 40
|
||||
]
|
||||
[ Element.text static.routeParams.number
|
||||
, Element.column []
|
||||
static.static
|
||||
]
|
||||
]
|
||||
}
|
72
examples/slides/Template/Time.elm
Normal file
@ -0,0 +1,72 @@
|
||||
module Template.Time exposing (Model, Msg, template)
|
||||
|
||||
import Document exposing (Document)
|
||||
import Element exposing (Element)
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Shared
|
||||
import Template exposing (StaticPayload, Template, TemplateWithState)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
String
|
||||
|
||||
|
||||
template : Template {} StaticData
|
||||
template =
|
||||
Template.withStaticData
|
||||
{ head = head
|
||||
, staticRoutes = StaticHttp.succeed []
|
||||
, staticData = staticData
|
||||
}
|
||||
|> Template.buildNoState { view = view }
|
||||
|
||||
|
||||
staticData routeParams =
|
||||
StaticHttp.succeed "TIME RESPONSE"
|
||||
|
||||
|
||||
|
||||
--StaticHttp.get (Secrets.succeed "http://worldtimeapi.org/api/timezone/America/Los_Angeles")
|
||||
-- (OptimizedDecoder.field "datetime" OptimizedDecoder.string)
|
||||
|
||||
|
||||
head :
|
||||
StaticPayload StaticData {}
|
||||
-> List Head.Tag
|
||||
head static =
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = ImagePath.build [ "images", "icon-png.png" ]
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = "TODO"
|
||||
, locale = Nothing
|
||||
, title = "TODO title" -- metadata.title -- TODO
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
|
||||
view :
|
||||
StaticPayload StaticData {}
|
||||
-> Document msg
|
||||
view static =
|
||||
{ title = "TODO title"
|
||||
, body =
|
||||
[ Element.text static.static
|
||||
]
|
||||
}
|
188
examples/slides/blog/extensible-markdown-parsing-in-elm.md
Normal file
@ -0,0 +1,188 @@
|
||||
---
|
||||
{
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Extensible Markdown Parsing in Pure Elm",
|
||||
"description": "Introducing a new parser that extends your palette with no additional syntax",
|
||||
"image": "v1603304397/elm-pages/article-covers/extensible-markdown-parsing_x9oolz.jpg",
|
||||
"published": "2019-10-08",
|
||||
}
|
||||
---
|
||||
|
||||
I'm excited to share a new approach to markdown parsing for the Elm ecosystem: [`dillonkearns/elm-markdown`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/)!
|
||||
|
||||
As a matter of fact, the blog post you're reading right now is being rendered with it.
|
||||
|
||||
## Why does Elm need another markdown parser?
|
||||
|
||||
I built this tool so that I could:
|
||||
|
||||
- Render markdown blocks using my preferred UI library (`elm-ui`, in my case, but you could use `elm-css` or anything else!)
|
||||
- Extend what can be expressed beyond the standard markdown blocks like headings, lists, etc.
|
||||
- Inject data from my Elm model into my markdown
|
||||
|
||||
And yet, I wanted to do all of this with the benefits that come from using standard Markdown:
|
||||
|
||||
- Familiar syntax
|
||||
- Great editor tooling (I write my blog posts in Ulysses using Markdown, and I have prettier set up to auto format markdown when I'm tweaking markdown directly in my code projects)
|
||||
- Previews render in Github
|
||||
- Easy for others to contribute (for example, to the [`elm-pages` docs](https://elm-pages.com/docs))
|
||||
|
||||
## Core Features
|
||||
|
||||
So how do you get the best of both worlds? There are three key features that give `dillonkearns/elm-markdown` rich extensibility without actually adding to the Markdown syntax:
|
||||
|
||||
- ⚙️ **Map HTML to custom Elm rendering functions** (for extensible markdown!)
|
||||
- 🎨 **Use [custom renderers](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#Renderer)** (for custom rendering with your preferred styles and UI library)
|
||||
- 🌳 **Give users access to the parsed Markdown Blocks before rendering** (for inspecting, transforming, or extracting data from the parsed Markdown before passing it to your Markdown Renderer)
|
||||
|
||||
Let's explore these three key features in more depth.
|
||||
|
||||
## ⚙️ Map HTML to custom Elm rendering functions
|
||||
|
||||
I didn't want to add additional features that weren't already a part of Markdown syntax. Since HTML is already valid Markdown, it seemed ideal to just use declarative HTML tags to express these custom view elements. `dillonkearns/elm-markdown` leverages that to give you a declarative Elm syntax to explicitly say what kind of HTML is accepted (think JSON Decoders) and, given that accepted HTML, how to render it.
|
||||
|
||||
## Markdown Within HTML Tags
|
||||
|
||||
What makes this especially useful is that we can render any Markdown content within our HTML tags. So you could have a Markdown file that looks like this.
|
||||
|
||||
```markdown
|
||||
## Markdown Within HTML (Within Markdown)
|
||||
|
||||
You can now:
|
||||
|
||||
- Render HTML within your Markdown
|
||||
- Render Markdown within that HTML!
|
||||
|
||||
<bio
|
||||
name="Dillon Kearns"
|
||||
photo="https://avatars2.githubusercontent.com/u/1384166"
|
||||
twitter="dillontkearns"
|
||||
github="dillonkearns"
|
||||
|
||||
>
|
||||
|
||||
Dillon really likes building things with Elm!
|
||||
|
||||
Here are some links:
|
||||
|
||||
- [Articles](https://incrementalelm.com/articles)
|
||||
|
||||
</bio>
|
||||
```
|
||||
|
||||
And here's the output:
|
||||
|
||||
<ellie-output id="7PLvgQ2kSzja1" />
|
||||
|
||||
This is a nice way to abstract the presentation logic for team members' bios on an `about-us` page. We want richer presentation logic than plain markdown provides (for example, showing icons with the right dimensions, and displaying them in a row not column view, etc.) Also, since we're using Elm, we get pretty spoiled by explicit and precise error messages. So we'd like to get an error message if we don't provide a required attribute!
|
||||
|
||||
Here's the relevant code for handling the `<bio>` HTML tag in our Markdown:
|
||||
|
||||
```elm
|
||||
Markdown.Html.oneOf
|
||||
[ Markdown.Html.tag "bio"
|
||||
(\name photoUrl twitter github dribbble renderedChildren ->
|
||||
bioView renderedChildren name photoUrl twitter github dribbble
|
||||
)
|
||||
|> Markdown.Html.withAttribute "name"
|
||||
|> Markdown.Html.withAttribute "photo"
|
||||
|> Markdown.Html.withOptionalAttribute "twitter"
|
||||
|> Markdown.Html.withOptionalAttribute "github"
|
||||
|> Markdown.Html.withOptionalAttribute "dribbble"
|
||||
]
|
||||
```
|
||||
|
||||
If we forget to pass in the required `photo` attribute, we'll get an error message like this:
|
||||
|
||||
```
|
||||
Problem with the given value:
|
||||
|
||||
<bio
|
||||
name="Dillon Kearns"
|
||||
twitter="dillontkearns"
|
||||
github="dillonkearns"
|
||||
>
|
||||
|
||||
Expecting attribute "photo".
|
||||
```
|
||||
|
||||
### Avoiding low-level HTML in markdown
|
||||
|
||||
If you're familiar with [MDX](https://mdxjs.com) (it's Markdown syntax, but extended with some extra syntax from JSX, including like JS `import`s and JSX HTML tags). Guillermo Rauch, the creator of MDX even talks about the benefits that a more declarative approach, like the one `dillonkearns/elm-markdown` takes, could have over the current MDX approach of using low-level `import` statements and JSX syntax [in this talk (around 20:36 - 22:30)](https://www.youtube.com/watch?v=8oFJPVOT7FU&feature=youtu.be&t=1236).
|
||||
|
||||
Even with this declarative approach to explicitly allowing the HTML tags you want, it's possible to get very low-level and just create mappings to standard HTML tags. I like to treat the HTML tags within these markdown documents like Web Components rather than raw HTML. That means using it as a very high-level way of expressing your custom views. With standard Github-flavored markdown, you'll often see people injecting `<div>` tags with styles, or `<img>` tags, etc. I consider this too low-level to be injecting into Markdown in most cases. The Markdown document should be more declarative, concerned only with _what_ to render, not _how_ to render it.
|
||||
|
||||
## 🎨 Use custom renderers
|
||||
|
||||
Many Markdown libraries just give you the rendered HTML directly. With `dillonkearns/elm-markdown`, one of the main goals was to give you full control over presentation at the initial render (rather than needing to add CSS rules to apply to your rendered output). I personally like to use `elm-ui` whenever I can, so I wanted to use that directly not just for my navbar, but to style my rendered markdown blocks.
|
||||
|
||||
Beyond just rendering directly to your preferred UI library, custom Renderers also open up a number of new potential uses. You can render your Markdown into `elm-ui` `Element`s, but you could also render it to any other Elm type. That could be data, or even functions. Why would you render a function? Well, that would allow you to inject dynamic data from your Elm model!
|
||||
|
||||
Some other use cases that custom Renderers enable:
|
||||
|
||||
- Regular old `Html` (using the [`defaultHtmlRenderer`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#defaultHtmlRenderer))
|
||||
- Render into [`elm-ui`](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) `Element`s
|
||||
- Render into ANSI color codes for rich formatting in terminal output
|
||||
- Render into plain text with all formatting stripped out (for search functionality)
|
||||
|
||||
### Performing validations in Renderers
|
||||
|
||||
Another goal with `dillonkearns/elm-markdown` is to allow early and precise feedback. One of my favorite uses of Custom Renderers is to catch dead links (or images). `elm-pages` will stop the production build when the Renderer fails. [Here's the relevant code](https://github.com/dillonkearns/elm-pages/blob/c76e96af497406fb9acf294acebbcb0c0e391197/examples/docs/src/MarkdownRenderer.elm#L90-L93) from elm-pages.com
|
||||
|
||||
```elm
|
||||
renderer : Markdown.Parser.Renderer (Element msg)
|
||||
renderer =
|
||||
{
|
||||
link =
|
||||
\link body ->
|
||||
Pages.isValidRoute link.destination
|
||||
|> Result.map
|
||||
, -- rest of the Renderer definition
|
||||
}
|
||||
```
|
||||
|
||||
## 🌳 Give users access to the parsed Markdown Blocks before rendering
|
||||
|
||||
Exposing the AST allows for a number of powerful use cases as well. And it does so without requiring you to dig into the internals. You just get access to a nice Elm custom type and you can do what you want with it before passing it on to your Custom Renderer.
|
||||
|
||||
Here are some use cases that this feature enables:
|
||||
|
||||
- Extract metadata before rendering, like building a table of contents data structure with proper links ([here's an Ellie demo of that!](https://ellie-app.com/7LDzS6r48n8a1))
|
||||
- Run a validation and turn it into an `Err`, for example, if there are multiple level 1 headings (having multiple `h1`s on a page causes accessibility problems)
|
||||
- Transform the blocks by applying formatting rules, for example use a title casing function on all headings
|
||||
- Transform the AST before rendering it, for example dropping each heading down one level (H1s become H2s, etc.)
|
||||
|
||||
## The future of `dillonkearns/elm-markdown`
|
||||
|
||||
I've been really enjoying using this in production for several weeks. But it certainly isn't fully handling all cases in Github-flavored markdown.
|
||||
|
||||
I'm running all 1400 end-to-end test cases from the Marked.js test suite (which is what `elm-explorations/markdown` runs under the hood). And that test suite includes running through every example in the [Github-flavored markdown spec](https://github.github.com/gfm/). You can see nicely formatted markdown with all of the current failures [here](https://github.com/dillonkearns/elm-markdown/tree/master/test-results/failing/GFM). It includes all failures from the Marked.js test suite, organized by feature area. I'm working through handling more of these cases to make it more widely useful, but feel free to use it now with that caveat in mind.
|
||||
|
||||
Pull requests are very welcome, I would love community contributions on this project! If you're interested in contributing, check out [the contributing guide in the Github repo](https://github.com/dillonkearns/elm-markdown/blob/master/CONTRIBUTING.md).
|
||||
|
||||
### Fault-Tolerance Versus Helpful Errors
|
||||
|
||||
That said, the goal is not to get to 100% compliance with the Github-Flavored Markdown Spec. Markdown has a goal of being Fault-Tolerant, meaning it will always try to "do the best it can" rather than giving an error message when something unexpected happens. That means there's no such thing as "invalid markdown." But there is most certainly **"markup that probably doesn't do what you expected."** For example
|
||||
|
||||
```
|
||||
[My link](/home oh wait I forgot to close this link tag...
|
||||
```
|
||||
|
||||
⚠️ This is technically **valid** Markdown!
|
||||
|
||||
It "does the best it can" with the input and renders to a raw string rather than rendering a link. So this is an example that is squarely in the category of markup that **"probably doesn't do what you expected."**
|
||||
|
||||
The goal of `dillonkearns/elm-markdown` is not fault-tolerance. It prioritizes **helpful error messages** over fault-tolerance. Sound familiar? There is a very similar difference in philosophy between JavaScript and Elm.
|
||||
|
||||
So the rule of thumb for `dillonkearns/elm-markdown` is:
|
||||
|
||||
- Follow the Github-Flavored Markdown Spec whenever it doesn't cover up feedback about something that "probably doesn't do what you expected"
|
||||
- Otherwise, break with the Github-Flavored Markdown Spec and instead give a helpful error message
|
||||
|
||||
You can follow along with the [current GFM Spec Compliance here](https://github.com/dillonkearns/elm-markdown#current-github-flavored-markdown-compliance).
|
||||
|
||||
Thanks for reading! If you give this library a try, let me know what you think. I'd love to hear from you!
|
||||
|
||||
You can keep the conversation going on the #elm-pages channel on [the Elm Slack](http://elmlang.herokuapp.com/), or on this Twitter thread 👇
|
||||
|
||||
<Oembed url="https://twitter.com/dillontkearns/status/1181588809349091328" />
|
189
examples/slides/blog/generate-files-hook.md
Normal file
@ -0,0 +1,189 @@
|
||||
---
|
||||
{
|
||||
"draft": true,
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Generating Files with Pure Elm",
|
||||
"description": "Introducing a new parser that extends your palette with no additional syntax",
|
||||
"image": "v1603304397/elm-pages/article-covers/generating-files_blzn2d.jpg",
|
||||
"published": "2020-01-28",
|
||||
}
|
||||
---
|
||||
|
||||
I'm excited to share a new approach to markdown parsing for the Elm ecosystem: [`dillonkearns/elm-markdown`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/)!
|
||||
|
||||
As a matter of fact, the blog post you're reading right now is being rendered with it.
|
||||
|
||||
## Why does Elm need another markdown parser?
|
||||
|
||||
I built this tool so that I could:
|
||||
|
||||
- Render markdown blocks using my preferred UI library (`elm-ui`, in my case, but you could use `elm-css` or anything else!)
|
||||
- Extend what can be expressed beyond the standard markdown blocks like headings, lists, etc.
|
||||
- Inject data from my Elm model into my markdown
|
||||
|
||||
And yet, I wanted to do all of this with the benefits that come from using standard Markdown:
|
||||
|
||||
- Familiar syntax
|
||||
- Great editor tooling (I write my blog posts in Ulysses using Markdown, and I have prettier set up to auto format markdown when I'm tweaking markdown directly in my code projects)
|
||||
- Previews render in Github
|
||||
- Easy for others to contribute (for example, to the [`elm-pages` docs](https://elm-pages.com/docs))
|
||||
|
||||
## Core Features
|
||||
|
||||
So how do you get the best of both worlds? There are three key features that give `dillonkearns/elm-markdown` rich extensibility without actually adding to the Markdown syntax:
|
||||
|
||||
- ⚙️ **Map HTML to custom Elm rendering functions** (for extensible markdown!)
|
||||
- 🎨 **Use [custom renderers](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#Renderer)** (for custom rendering with your preferred styles and UI library)
|
||||
- 🌳 **Give users access to the parsed Markdown Blocks before rendering** (for inspecting, transforming, or extracting data from the parsed Markdown before passing it to your Markdown Renderer)
|
||||
|
||||
Let's explore these three key features in more depth.
|
||||
|
||||
## ⚙️ Map HTML to custom Elm rendering functions
|
||||
|
||||
I didn't want to add additional features that weren't already a part of Markdown syntax. Since HTML is already valid Markdown, it seemed ideal to just use declarative HTML tags to express these custom view elements. `dillonkearns/elm-markdown` leverages that to give you a declarative Elm syntax to explicitly say what kind of HTML is accepted (think JSON Decoders) and, given that accepted HTML, how to render it.
|
||||
|
||||
## Markdown Within HTML Tags
|
||||
|
||||
What makes this especially useful is that we can render any Markdown content within our HTML tags. So you could have a Markdown file that looks like this.
|
||||
|
||||
```markdown
|
||||
## Markdown Within HTML (Within Markdown)
|
||||
|
||||
You can now:
|
||||
|
||||
- Render HTML within your Markdown
|
||||
- Render Markdown within that HTML!
|
||||
|
||||
<bio
|
||||
name="Dillon Kearns"
|
||||
photo="https://avatars2.githubusercontent.com/u/1384166"
|
||||
twitter="dillontkearns"
|
||||
github="dillonkearns"
|
||||
|
||||
>
|
||||
|
||||
Dillon really likes building things with Elm!
|
||||
|
||||
Here are some links:
|
||||
|
||||
- [Articles](https://incrementalelm.com/articles)
|
||||
|
||||
</bio>
|
||||
```
|
||||
|
||||
And here's the output:
|
||||
|
||||
<ellie-output id="7PLvgQ2kSzja1" />
|
||||
|
||||
This is a nice way to abstract the presentation logic for team members' bios on an `about-us` page. We want richer presentation logic than plain markdown provides (for example, showing icons with the right dimensions, and displaying them in a row not column view, etc.) Also, since we're using Elm, we get pretty spoiled by explicit and precise error messages. So we'd like to get an error message if we don't provide a required attribute!
|
||||
|
||||
Here's the relevant code for handling the `<bio>` HTML tag in our Markdown:
|
||||
|
||||
```elm
|
||||
Markdown.Html.oneOf
|
||||
[ Markdown.Html.tag "bio"
|
||||
(\name photoUrl twitter github dribbble renderedChildren ->
|
||||
bioView renderedChildren name photoUrl twitter github dribbble
|
||||
)
|
||||
|> Markdown.Html.withAttribute "name"
|
||||
|> Markdown.Html.withAttribute "photo"
|
||||
|> Markdown.Html.withOptionalAttribute "twitter"
|
||||
|> Markdown.Html.withOptionalAttribute "github"
|
||||
|> Markdown.Html.withOptionalAttribute "dribbble"
|
||||
]
|
||||
```
|
||||
|
||||
If we forget to pass in the required `photo` attribute, we'll get an error message like this:
|
||||
|
||||
```
|
||||
Problem with the given value:
|
||||
|
||||
<bio
|
||||
name="Dillon Kearns"
|
||||
twitter="dillontkearns"
|
||||
github="dillonkearns"
|
||||
>
|
||||
|
||||
Expecting attribute "photo".
|
||||
```
|
||||
|
||||
### Avoiding low-level HTML in markdown
|
||||
|
||||
If you're familiar with [MDX](https://mdxjs.com) (it's Markdown syntax, but extended with some extra syntax from JSX, including like JS `import`s and JSX HTML tags). Guillermo Rauch, the creator of MDX even talks about the benefits that a more declarative approach, like the one `dillonkearns/elm-markdown` takes, could have over the current MDX approach of using low-level `import` statements and JSX syntax [in this talk (around 20:36 - 22:30)](https://www.youtube.com/watch?v=8oFJPVOT7FU&feature=youtu.be&t=1236).
|
||||
|
||||
Even with this declarative approach to explicitly allowing the HTML tags you want, it's possible to get very low-level and just create mappings to standard HTML tags. I like to treat the HTML tags within these markdown documents like Web Components rather than raw HTML. That means using it as a very high-level way of expressing your custom views. With standard Github-flavored markdown, you'll often see people injecting `<div>` tags with styles, or `<img>` tags, etc. I consider this too low-level to be injecting into Markdown in most cases. The Markdown document should be more declarative, concerned only with _what_ to render, not _how_ to render it.
|
||||
|
||||
## 🎨 Use custom renderers
|
||||
|
||||
Many Markdown libraries just give you the rendered HTML directly. With `dillonkearns/elm-markdown`, one of the main goals was to give you full control over presentation at the initial render (rather than needing to add CSS rules to apply to your rendered output). I personally like to use `elm-ui` whenever I can, so I wanted to use that directly not just for my navbar, but to style my rendered markdown blocks.
|
||||
|
||||
Beyond just rendering directly to your preferred UI library, custom Renderers also open up a number of new potential uses. You can render your Markdown into `elm-ui` `Element`s, but you could also render it to any other Elm type. That could be data, or even functions. Why would you render a function? Well, that would allow you to inject dynamic data from your Elm model!
|
||||
|
||||
Some other use cases that custom Renderers enable:
|
||||
|
||||
- Regular old `Html` (using the [`defaultHtmlRenderer`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#defaultHtmlRenderer))
|
||||
- Render into [`elm-ui`](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) `Element`s
|
||||
- Render into ANSI color codes for rich formatting in terminal output
|
||||
- Render into plain text with all formatting stripped out (for search functionality)
|
||||
|
||||
### Performing validations in Renderers
|
||||
|
||||
Another goal with `dillonkearns/elm-markdown` is to allow early and precise feedback. One of my favorite uses of Custom Renderers is to catch dead links (or images). `elm-pages` will stop the production build when the Renderer fails. [Here's the relevant code](https://github.com/dillonkearns/elm-pages/blob/c76e96af497406fb9acf294acebbcb0c0e391197/examples/docs/src/MarkdownRenderer.elm#L90-L93) from elm-pages.com
|
||||
|
||||
```elm
|
||||
renderer : Markdown.Parser.Renderer (Element msg)
|
||||
renderer =
|
||||
{
|
||||
link =
|
||||
\link body ->
|
||||
Pages.isValidRoute link.destination
|
||||
|> Result.map
|
||||
, -- rest of the Renderer definition
|
||||
}
|
||||
```
|
||||
|
||||
## 🌳 Give users access to the parsed Markdown Blocks before rendering
|
||||
|
||||
Exposing the AST allows for a number of powerful use cases as well. And it does so without requiring you to dig into the internals. You just get access to a nice Elm custom type and you can do what you want with it before passing it on to your Custom Renderer.
|
||||
|
||||
Here are some use cases that this feature enables:
|
||||
|
||||
- Extract metadata before rendering, like building a table of contents data structure with proper links ([here's an Ellie demo of that!](https://ellie-app.com/7LDzS6r48n8a1))
|
||||
- Run a validation and turn it into an `Err`, for example, if there are multiple level 1 headings (having multiple `h1`s on a page causes accessibility problems)
|
||||
- Transform the blocks by applying formatting rules, for example use a title casing function on all headings
|
||||
- Transform the AST before rendering it, for example dropping each heading down one level (H1s become H2s, etc.)
|
||||
|
||||
## The future of `dillonkearns/elm-markdown`
|
||||
|
||||
I've been really enjoying using this in production for several weeks. But it certainly isn't fully handling all cases in Github-flavored markdown.
|
||||
|
||||
I'm running all 1400 end-to-end test cases from the Marked.js test suite (which is what `elm-explorations/markdown` runs under the hood). And that test suite includes running through every example in the [Github-flavored markdown spec](https://github.github.com/gfm/). You can see nicely formatted markdown with all of the current failures [here](https://github.com/dillonkearns/elm-markdown/tree/master/test-results/failing/GFM). It includes all failures from the Marked.js test suite, organized by feature area. I'm working through handling more of these cases to make it more widely useful, but feel free to use it now with that caveat in mind.
|
||||
|
||||
Pull requests are very welcome, I would love community contributions on this project! If you're interested in contributing, check out [the contributing guide in the Github repo](https://github.com/dillonkearns/elm-markdown/blob/master/CONTRIBUTING.md).
|
||||
|
||||
### Fault-Tolerance Versus Helpful Errors
|
||||
|
||||
That said, the goal is not to get to 100% compliance with the Github-Flavored Markdown Spec. Markdown has a goal of being Fault-Tolerant, meaning it will always try to "do the best it can" rather than giving an error message when something unexpected happens. That means there's no such thing as "invalid markdown." But there is most certainly **"markup that probably doesn't do what you expected."** For example
|
||||
|
||||
```
|
||||
[My link](/home oh wait I forgot to close this link tag...
|
||||
```
|
||||
|
||||
⚠️ This is technically **valid** Markdown!
|
||||
|
||||
It "does the best it can" with the input and renders to a raw string rather than rendering a link. So this is an example that is squarely in the category of markup that **"probably doesn't do what you expected."**
|
||||
|
||||
The goal of `dillonkearns/elm-markdown` is not fault-tolerance. It prioritizes **helpful error messages** over fault-tolerance. Sound familiar? There is a very similar difference in philosophy between JavaScript and Elm.
|
||||
|
||||
So the rule of thumb for `dillonkearns/elm-markdown` is:
|
||||
|
||||
- Follow the Github-Flavored Markdown Spec whenever it doesn't cover up feedback about something that "probably doesn't do what you expected"
|
||||
- Otherwise, break with the Github-Flavored Markdown Spec and instead give a helpful error message
|
||||
|
||||
You can follow along with the [current GFM Spec Compliance here](https://github.com/dillonkearns/elm-markdown#current-github-flavored-markdown-compliance).
|
||||
|
||||
Thanks for reading! If you give this library a try, let me know what you think. I'd love to hear from you!
|
||||
|
||||
You can keep the conversation going on the #elm-pages channel on [the Elm Slack](http://elmlang.herokuapp.com/), or on this Twitter thread 👇
|
||||
|
||||
<Oembed url="https://twitter.com/dillontkearns/status/1181588809349091328" />
|
164
examples/slides/blog/introducing-elm-pages.md
Normal file
@ -0,0 +1,164 @@
|
||||
---
|
||||
{
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Introducing elm-pages 🚀 - a type-centric static site generator",
|
||||
"description": "Elm is the perfect fit for a static site generator. Learn about some of the features and philosophy behind elm-pages.",
|
||||
"image": "v1603304397/elm-pages/article-covers/introducing-elm-pages_ceksg2.jpg",
|
||||
"published": "2019-09-24",
|
||||
}
|
||||
---
|
||||
|
||||
After a round of closed beta testing (thank you to [Brian](https://twitter.com/brianhicks) and the [`elm-conf 2019`](https://2019.elm-conf.com/) organizing team!), I'm excited to share a new static site generator for Elm!
|
||||
|
||||
[Matthew Griffith](https://twitter.com/mech_elephant) and I have had a lot of design discussions and sending code snippets back-and-forth to get to the current design. A big thank you to Matthew for the great discussions and, as always, his ability to look at the bigger picture and question basic assumptions to come up with awesome innovations!
|
||||
|
||||
## What is `elm-pages` exactly?
|
||||
|
||||
Well, this site you're looking at _right now_ is built with `elm-pages`! For example, the raw content for this post is from [`content/blog/introducing-elm-pages.md`](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/content/blog/introducing-elm-pages.md).
|
||||
|
||||
`elm-pages` takes your static content and turns it into a modern, performant, single-page app. You can do anything you can with a regular Elm site, and yet the framework does a lot for you to optimize site performance and minimize tedious work.
|
||||
|
||||
I see a lot of "roll your own" Elm static sites out there these days. When you roll your own Elm static site, you often:
|
||||
|
||||
- Manage Strings for each page's content (rather than just having a file for each page)
|
||||
- Wire up the routing for each page manually (or with a hand-made script)
|
||||
- Add `<meta>` tags for SEO and to make Twitter/Slack/etc. link shares display the right image and title (or just skip it because it's a pain)
|
||||
|
||||
I hope that `elm-pages` will make people's lives easier (and their load times faster). But `elm-pages` is for more than just building your blog or portfolio site. There's a movement now called JAMstack (JavaScript, APIs, and Markup) that is solving a broader set of problems with static sites. JAMstack apps do this by pulling in data from external sources, and using modern frontend frameworks to render the content (which then rehydrate into interactive apps). The goal is to move as much work as possible away from the user's browser and into a build step before pushing static files to your CDN host (but without sacrificing functionality). More and more sites are seeing that optimizing performance improves conversion rates and user engagement, and it can also make apps simpler to maintain.
|
||||
|
||||
This is just the first release of `elm-pages`, but I've built a prototype for pulling in external data and am refining the design in preparation for the next release. Once that ships, the use cases `elm-pages` can handle will expand to things like ecommerce sites, job boards, and sites with content written by non-technical content editors. You can find a very informative FAQ and resources page about these ideas at [jamstack.org](https://jamstack.org/) (plus a more in-depth definition of the term JAMstack).
|
||||
|
||||
## Comparing `elm-pages` and `elmstatic`
|
||||
|
||||
`elm-pages` and [`elmstatic`](https://korban.net/elm/elmstatic/) have a lot of differences. At the core, they have two different goals. `elmstatic` generates HTML for you that doesn't include an Elm runtime. It uses Elm as a templating engine to do page layouts. It also makes some assumptions about the structure of your page content, separating `posts` and `pages` and automatically generating post indexes based on the top-level directories within the `posts` folder. It's heavily inspired by traditional static site generators like Jekyll.
|
||||
|
||||
`elm-pages` hydrates into a single-page app that includes a full Elm runtime, meaning that you can have whatever client-side interactivity you want. It supports similar use cases to static site generators like [Gatsby](http://gatsbyjs.org). `elm-pages` makes a lot of optimizations by splitting and lazy-loading pages, optimizing image assets, and using service workers for repeat visits. It pre-renders HTML for fast first renders, but because it ships with JavaScript code it is also able to do some performance optimizations to make page changes faster (and without page flashes). So keep in mind that shipping without JavaScript doesn't necessarily mean your site performance suffers! You may have good reasons to want a static site with no JavaScript, but open up a Lighthouse audit and try it out for yourself rather than speculating about performance!
|
||||
|
||||
Either framework might be the right fit depending on your goals. I hope this helps illuminate the differences!
|
||||
|
||||
## How does `elm-pages` work?
|
||||
|
||||
The flow is something like this:
|
||||
|
||||
- Put your static content in your `content` folder (it could be Markdown, `elm-markup`, or something else entirely)
|
||||
- Register Elm functions that define what to do with the [frontmatter](https://jekyllrb.com/docs/front-matter/) (that YAML data at the top of your markup files) and the body of each type of file you want to handle
|
||||
- Define your app's configuration in pure Elm (just like a regular Elm `Browser.application` but with a few extra functions for SEO and site configuration)
|
||||
- Run `elm-pages build` and ship your static files (JS, HTML, etc.) to Netlify, Github Pages, or your CDN of choice!
|
||||
|
||||
The result is a blazing fast static site that is optimized both for the first load experience, and also uses some caching strategies to improve site performance for repeat visitors. You can look in your dev tools or run a Lighthouse audit on this page to see some of the performance optimizations `elm-pages` does for you!
|
||||
|
||||
The way you set up an `elm-pages` app will look familiar if you have some experience with wiring up standard Elm boilerplate:
|
||||
|
||||
```elm
|
||||
main : Pages.Platform.Program Model Msg Metadata (List (Element Msg))
|
||||
main =
|
||||
Pages.application
|
||||
{ init = init
|
||||
, view = view
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
, documents = [ markdownHandler ]
|
||||
, head = head
|
||||
, manifest = manifest
|
||||
, canonicalSiteUrl = "https://elm-pages.com"
|
||||
}
|
||||
```
|
||||
|
||||
You can take a look at [the `Main.elm` file for this site](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/src/Main.elm#L52) to get a better sense of the bigger picture. I'll do a more in-depth explanation of this setup in a future post. The short version is that
|
||||
|
||||
`init`, `update`, and `subscriptions` are as you would expect (but maybe a bit simpler since `elm-pages` manages things like the URL for you).
|
||||
|
||||
`documents` are where you define how to handle the frontmatter and body of the files in your `content` folder. And the `view` function gives you the result from your frontmatter and body, as well as your `Model`.
|
||||
|
||||
`head` is just a function that passes you the metadata for a given page and lets you define tags to put in the `<head>` (mostly for SEO).
|
||||
|
||||
`manifest` lets you configure some settings that allow your app to be installed for offline use.
|
||||
|
||||
And the end result is that `elm-pages` gets everything it needs about your site in order to optimize it and turn it into a modern, performant site that will get a great Lighthouse audit score! The goal is to make following best practices for a modern, performant static site one of the following:
|
||||
|
||||
- Built-in
|
||||
- Enforced by the Elm compiler
|
||||
- Or at the very least the path of least resistence
|
||||
|
||||
## What makes Elm awesome for building static sites
|
||||
|
||||
JAMstack frameworks, like [Gatsby](http://gatsbyjs.org), can make powerful optimizations because they are dealing with strong constraints (specifically, content that is known at build time). Elm is the perfect tool for the JAMstack because it can leverage those constraints and turn them into compiler guarantees. Not only can we do more with static guarantees using Elm, but we can get additional guarantees using Elm's type-system and managed side-effects. It's a virtuous cycle that enables a lot of innovation.
|
||||
|
||||
## Why use `elm-pages`?
|
||||
|
||||
Let's take a look at a few of the features that make `elm-pages` worthwhile for the users (both the end users, and the team using it to build their site).
|
||||
|
||||
### Performance
|
||||
|
||||
- Pre-rendered pages for blazing fast first renders and improved SEO
|
||||
- Your content is loaded as a single-page app behind the scenes, giving you smooth page changes
|
||||
- Split individual page content and lazy load each page
|
||||
- Prefetch page content on link hover so page changes are almost instant
|
||||
- Image assets are optimized
|
||||
- App skeleton is cached with a service worker (with zero configuration) so it's available offline
|
||||
|
||||
One of the early beta sites that used `elm-pages` instantly shaved off over a megabyte for the images on a single page! Optimizations like that need to be built-in and automatic otherwise some things inevitably slip through the cracks.
|
||||
|
||||
### Type-safety and simplicity
|
||||
|
||||
- The type system guarantees that you use valid images and routes in the right places
|
||||
- You can even set up a validation to give build errors if there are any broken links or images in your markdown
|
||||
- You can set up validations to define your own custom rules for your domain! (Maximum title length, tag name from a set to avoid multiple tags with different wording, etc.)
|
||||
|
||||
## Progressive Web Apps
|
||||
|
||||
[Lighthouse recommends having a Web Manifest file](https://developers.google.com/web/tools/lighthouse/audits/manifest-exists) for your app to allow users to install the app to your home screen and have an appropriate icon, app name, etc.
|
||||
Elm pages gives you a type-safe way to define a web manifest for your app:
|
||||
|
||||
```elm
|
||||
manifest : Manifest.Config PagesNew.PathKey
|
||||
manifest =
|
||||
{ backgroundColor = Just Color.white
|
||||
, categories = [ Pages.Manifest.Category.education ]
|
||||
, displayMode = Manifest.Standalone
|
||||
, orientation = Manifest.Portrait
|
||||
, description = "elm-pages - A statically typed site generator."
|
||||
, iarcRatingId = Nothing
|
||||
, name = "elm-pages docs"
|
||||
, themeColor = Just Color.white
|
||||
, startUrl = pages.index
|
||||
, shortName = Just "elm-pages"
|
||||
, sourceIcon = images.icon
|
||||
}
|
||||
```
|
||||
|
||||
Lighthouse will also ding you [if you don't have the appropriately sized icons and favicon images](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-192px-icon). `elm-pages` guarantees that you will follow these best practices (and gives you the confidence that you haven't made any mistakes). It will automatically generate the recommended set of icons and favicons for you, based on a single source image. And, of course, you get a compile-time guarantee that you are using an image that exists! For example, here's what happens if we try to access an image as `logo` when the actual file is called `icon`.
|
||||
|
||||
```haskell
|
||||
sourceIcon = images.logo
|
||||
```
|
||||
|
||||
We then get this elm compiler error:
|
||||
![Missing image compiler error](/images/compiler-error.png)
|
||||
|
||||
## `elm-pages` is just Elm!
|
||||
|
||||
`elm-pages` hydrates into a full-fledged Elm app (the pre-rendered pages are just for faster loads and better SEO). So you can do whatever you need to using Elm and the typed data that `elm-pages` provides you with. In a future post, I'll explain some of the ways that `elm-pages` leverages the Elm type system for a better developer experience. There's a lot to explore here, this really just scratches the surface!
|
||||
|
||||
## SEO
|
||||
|
||||
One of the main motivations for building `elm-pages` was to make SEO easier and less error-prone. Have you ever seen a link shared on Twitter or elsewhere online that just renders like a plain link? No image, no title, no description. As a user, I'm a little afraid to click those links because I don't have any clues about where it will take me. As a user posting those links, it's very anticlimactic to share the blog post that I lovingly wrote only to see a boring link there in my tweet sharing it with the world.
|
||||
|
||||
I'll also be digging into the topic of SEO in a future post, showing how `elm-pages` makes SEO dead simple. For now, you can take a look at [the built-in `elm-pages` SEO module](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/Head-Seo) or take a look at [how this site uses the SEO module](https://github.com/dillonkearns/elm-pages/blob/8448bb60b680fb171319988fb716cb21e0345826/examples/docs/src/Main.elm#L294-L400).
|
||||
|
||||
## Next steps
|
||||
|
||||
There are so many possibilities when you pair Elm with static content! I'm excited to explore this area further with the help of the community. Here are some features that are on my radar.
|
||||
|
||||
- Allow users to pass a set of HTTP requests to fetch during the build step (for making CMS or API data available statically in the build)
|
||||
- An API to programmatically add pages from metadata (rather than just from files in the `content` folder)
|
||||
- Allow users to configure the caching strategy for service workers (through pure Elm config of course)
|
||||
- More SEO features (possibly an API for adding structured data, i.e. JSON-LD, for more interactive and engaging search results)
|
||||
|
||||
And of course, responding to your feedback! Please don't hesitate to share your thoughts, on everything from the documentation to the developer experience. I'd love to hear from you!
|
||||
|
||||
## Getting started with `elm-pages`
|
||||
|
||||
If you'd like to try out `elm-pages` for yourself, or look at some code, the best place to start is the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter). See the site live at [elm-pages-starter.netlify.com](https://elm-pages-starter.netlify.com). Let me know your thoughts on Slack, I'd love to hear from you! Or continue the conversation on Twitter!
|
||||
|
||||
<Oembed url="https://twitter.com/dillontkearns/status/1176556756249432065" />
|
205
examples/slides/blog/static-http.md
Normal file
@ -0,0 +1,205 @@
|
||||
---
|
||||
{
|
||||
"author": "Dillon Kearns",
|
||||
"title": "A is for API - Introducing Static HTTP Requests",
|
||||
"description": "The new StaticHttp API lets you fetch data when your site is built. That lets you remove loading spinners, and even access environment variables.",
|
||||
"image": "v1603304397/elm-pages/article-covers/static-http_ohefua.jpg",
|
||||
"published": "2019-12-10",
|
||||
}
|
||||
---
|
||||
|
||||
I'm excited to announce a new feature that brings `elm-pages` solidly into the JAMstack: Static HTTP requests. JAMstack stands for JavaScript, APIs, and Markup. And Static HTTP is all about pulling API data into your `elm-pages` site.
|
||||
|
||||
If you’ve tried `elm-pages`, you may be thinking, "elm-pages hydrates into a full Elm app... so couldn’t you already make HTTP requests to fetch API data, like you would in any Elm app?" Very astute observation! You absolutely could.
|
||||
|
||||
So what's new? It all comes down to these key points:
|
||||
|
||||
- Less boilerplate
|
||||
- Improved reliability
|
||||
- Better performance
|
||||
|
||||
Let's dive into these points in more detail.
|
||||
|
||||
## Less boilerplate
|
||||
|
||||
Let's break down how you perform HTTP requests in vanilla Elm, and compare that to how you perform a Static HTTP request with `elm-pages`.
|
||||
|
||||
### Anatomy of HTTP Requests in Vanilla Elm
|
||||
|
||||
- Cmd for an HTTP request on init (or update)
|
||||
- You receive a `Msg` in `update` with the payload
|
||||
- Store the data in `Model`
|
||||
- Tell Elm how to handle `Http.Error`s (including JSON decoding failures)
|
||||
|
||||
### Anatomy of Static HTTP Requests in `elm-pages`
|
||||
|
||||
- `view` function specifies some `StaticHttp` data, and a function to turn that data into your `view` and `head` tags for that page
|
||||
|
||||
That's actually all of the boilerplate for `StaticHttp` requests!
|
||||
|
||||
There is a lifecycle, because things can still fail. But the entire Static HTTP lifecycle happens _before your users have even requested a page_. The requests are performed at build-time, and that means less boilerplate for you to maintain in your Elm code!
|
||||
|
||||
### Let's see some code!
|
||||
|
||||
Here's a code snippet for making a StaticHttp request. This code makes an HTTP request to the Github API to grab the current number of stars for the `elm-pages` repo.
|
||||
|
||||
```elm
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Pages
|
||||
import Head
|
||||
import Secrets
|
||||
import Json.Decode.Exploration as Decode
|
||||
|
||||
|
||||
view :
|
||||
{ path : PagePath Pages.PathKey
|
||||
, frontmatter : Metadata
|
||||
}
|
||||
->
|
||||
StaticHttp.Request
|
||||
{ view : Model ->
|
||||
View -> { title : String, body : Html Msg }
|
||||
, head : List (Head.Tag Pages.PathKey)
|
||||
}
|
||||
view page =
|
||||
(StaticHttp.get
|
||||
(Secrets.succeed
|
||||
"https://api.github.com/repos/dillonkearns/elm-pages")
|
||||
(Decode.field "stargazers_count" Decode.int)
|
||||
)
|
||||
|> StaticHttp.map
|
||||
(\starCount ->
|
||||
{ view =
|
||||
\model renderedMarkdown ->
|
||||
{ title = "Landing Page"
|
||||
, body =
|
||||
[ header starCount
|
||||
, pageView model renderedMarkdown
|
||||
]
|
||||
}
|
||||
, head = head starCount
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
head : Int -> List (Head.Tag Pages.PathKey)
|
||||
head starCount =
|
||||
Seo.summaryLarge
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages - "
|
||||
++ String.fromInt starCount
|
||||
++ " GitHub Stars"
|
||||
, image =
|
||||
{ url = images.iconPng
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = siteTagline
|
||||
, locale = Nothing
|
||||
, title = "External Data Example"
|
||||
}
|
||||
|> Seo.website
|
||||
```
|
||||
|
||||
The data is baked into our built code, which means that the star count will only update when we trigger a new build. This is a common JAMstack technique. Many sites will trigger builds periodically to refresh data. Or better yet, use a webhook to trigger new builds whenever new data is available (for example, if you add a new blog post or a new page using a service like Contentful).
|
||||
|
||||
Notice that this app's `Msg`, `Model`, and `update` function are not involved in the process at all! It's also worth noting that we are passing that data into our `head` function, which allows us to use it in our `<meta>` tags for the page.
|
||||
|
||||
The `StaticHttp` functions are very similar to Elm libraries
|
||||
you've likely used already, such as `elm/json` or `elm/random`.
|
||||
If you don't depend on any StaticHttp data, you use `StaticHttp.succeed`,
|
||||
similar to how you might use `Json.Decode.succeed`, `Random.constant`,
|
||||
etc.
|
||||
|
||||
```elm
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
|
||||
|
||||
StaticHttp.succeed
|
||||
{ view =
|
||||
\model renderedMarkdown ->
|
||||
{ title = "Landing Page"
|
||||
, body =
|
||||
[ header
|
||||
, pageView model renderedMarkdown
|
||||
]
|
||||
}
|
||||
, head = head
|
||||
}
|
||||
```
|
||||
|
||||
This is actually the same as our previous example that had a `StaticHttp.request`, except that it doesn't make a request or have the
|
||||
stargazer count data.
|
||||
|
||||
### Secure Secrets
|
||||
|
||||
A common pattern is to use environment variables in your local environment or your CI environment in order to securely manage
|
||||
auth tokens and other secure data. `elm-pages` provides an API for accessing this data directly from your environment variables.
|
||||
You don't need to wire through any flags or ports, simply use the [`Pages.Secrets` module (see the docs for more info)](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/Pages-Secrets). It will take care of masking the secret data for you
|
||||
so that it won't be accessible in the bundled assets (it's just used to perform the requests during the build step, and then
|
||||
it's masked in the production assets).
|
||||
|
||||
### The Static HTTP Lifecycle
|
||||
|
||||
If you have a bad auth token in your URL, or your JSON decoder fails, then that code will never run for your `elm-pages` site. Instead, you'll get a friendly `elm-pages` build-time error telling you exactly what the problem was and where it occurred (as you're familiar with in Elm).
|
||||
|
||||
![StaticHttp build error](/images/static-http-error.png)
|
||||
|
||||
These error messages are inspired by Elm's famously helpful errors. They're designed to point you in the right direction, and provide as much context as possible.
|
||||
|
||||
Which brings us to our next key point...
|
||||
|
||||
## Improved reliability
|
||||
|
||||
Static HTTP requests are performed at build-time. Which means that if you have a problem with one of your Static HTTP requests, _your users will never see it_. Even if a JSON decoder fails, `elm-pages` will report back the decode failure and wait until its fixed before it allows you to create your production build.
|
||||
|
||||
Your API might go down, but your Static HTTP requests will always be up (assuming your site is up). The responses from your Static requests are baked into the static files for your `elm-pages` build. If there is an API outage, you of course won't be able to rebuild your site with fresh data from that API. But you can be confident that, though your build may break, your site will always have a working set of Static HTTP data.
|
||||
|
||||
Compare this to an HTTP request in a vanilla Elm app. Elm can guarantee that you've handled all error cases. But you still need to handle the case where you have a bad HTTP response, or a JSON decoder fails. That's the best that Elm can do because it can't guarantee anything about the data you'll receive at runtime. But `elm-pages` _can_ make guarantees about the data you'll receive! Because it introduces a new concept of data that you get a snapshot of during your build step. `elm-pages` guarantees that this frozen moment of time has no problems before the build succeeds, so we can make even stronger guarantees than we can with plain Elm.
|
||||
|
||||
## Better performance
|
||||
|
||||
The StaticHttp API also comes with some significant performance boosts. StaticHttp data is just a static JSON file for each page in your `elm-pages` site. That means that:
|
||||
|
||||
- No waiting on database queries to fetch API data
|
||||
- Your site, including API responses, is just static files so it can be served through a blazing-fast CDN (which serves files from the nearest server in the user's region)
|
||||
- Scaling is cheap and doesn't require an Ops team
|
||||
- `elm-pages` intelligently prefetches the Static HTTP data for a page when you're likely to navigate to that page, so page loads are instant and there's no spinner waiting to load that initial data
|
||||
- `elm-pages` optimizes your `StaticHttp` JSON data, stripping out everything but what you use in your JSON decoder
|
||||
|
||||
### JSON Optimization
|
||||
|
||||
The JSON optimization is made possible by a JSON parsing library created by Ilias Van Peer. Here's the pull request where he introduced the JSON optimization functionality: [github.com/zwilias/json-decode-exploration/pull/9](https://github.com/zwilias/json-decode-exploration/pull/9).
|
||||
|
||||
Let's take our Github API request as an example. Our Github API request from our previous code snippet ([https://api.github.com/repos/dillonkearns/elm-pages](https://api.github.com/repos/dillonkearns/elm-pages)) has a payload of 5.6KB (2.4KB gzipped). That size of the optimized JSON drops down to about 3% of that.
|
||||
|
||||
You can inspect the network tab on this page and you'll see something like this:
|
||||
|
||||
![StaticHttp content request](/images/static-http-content-requests.png)
|
||||
|
||||
If you click on Github API link above and compare it, you'll see that it's quite a bit smaller! It just has the one field that we grabbed in our JSON decoder.
|
||||
|
||||
This is quite nice for privacy and security purposes as well because any personally identifying information that might be included in an API response you consume won't show up in your production bundle (unless you were to explicitly include it in a JSON decoder).
|
||||
|
||||
### Comparing StaticHttp to other JAMstack data source strategies
|
||||
|
||||
You may be familiar with frameworks like Gatsby or Gridsome which also allow you to build data from external sources into your static site. Those frameworks, however, use a completely different approach, [using a GraphQL layer to store data from those data sources](https://www.gatsbyjs.org/docs/page-query/), and then looking that data up in GraphQL queries from within your static pages.
|
||||
|
||||
This approach makes sense for those frameworks. But since `elm-pages` is built on top of a language that already has an excellent type system, I wanted to remove that additional layer of abstraction and provide a simpler way to consume static data. The fact that Elm functions are all deterministic (i.e. given the same inputs they will always have the same outputs) opens up exciting new approaches to these problems as well. One of Gatsby's stated reasons for encouraging the use of their GraphQL layer is that it allows you to have your data all in one place. But the `elm-pages` StaticHttp API gives you similar benefits, using familiar Elm techniques like `map`, `andThen`, etc to massage your data into the desired format.
|
||||
|
||||
## Future plans
|
||||
|
||||
I'm looking forward to exploring more possibilities for using static data in `elm-pages`. Some things I plan to explore are:
|
||||
|
||||
- Programatically creating pages using the Static HTTP API
|
||||
- Configurable image optimization (including producing multiple dimensions for `srcset`s) using a similar API
|
||||
- Optimizing the page metadata that is included for each page (i.e. code splitting) by explicitly specifying what metadata the page depends on using an API similar to StaticHttp
|
||||
|
||||
## Getting started with StaticHttp
|
||||
|
||||
You can [take a look at this an end-to-end example app that uses the new `StaticHttp` library](https://github.com/dillonkearns/elm-pages/blob/master/examples/external-data/src/Main.elm) to get started.
|
||||
|
||||
Or just use the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter) and start building something cool! Let me know your thoughts on Slack, I'd love to hear from you! Or continue the conversation on Twitter!
|
||||
|
||||
<Oembed url="https://twitter.com/dillontkearns/status/1214238507163471872" />
|
169
examples/slides/blog/types-over-conventions.md
Normal file
@ -0,0 +1,169 @@
|
||||
---
|
||||
{
|
||||
"author": "Dillon Kearns",
|
||||
"title": "Types Over Conventions",
|
||||
"description": "How elm-pages approaches configuration, using type-safe Elm.",
|
||||
"image": "v1603304397/elm-pages/article-covers/introducing-elm-pages_ceksg2.jpg",
|
||||
"draft": true,
|
||||
"published": "2019-09-21",
|
||||
}
|
||||
---
|
||||
|
||||
Rails started a movement of simplifying project setup with [a philosophy of "Convention Over Configuration"](https://rubyonrails.org/doctrine/#convention-over-configuration). This made for a very easy experience bootstrapping a new web server. The downside is that you have a lot of implicit rules that can be hard to follow.
|
||||
|
||||
`elm-pages` takes a different approach. Rather than implicit conventions, or verbose configuration, `elm-pages` is centered around letting you explicitly configure your project using Elm's type system. This makes it a lot easier to configure because the Elm compiler will give you feedback on what the valid options are. And it also gives you the ability to define your own defaults and conventions explicitly, giving you the confidence getting started that Rails gives you, but the explicitness and helpful compiler support we're accustomed to in Elm.
|
||||
|
||||
**Note:** `elm-pages` currently relies on a few basic conventions such as the name of the `content` folder which has your markup. Convention over configuration isn't evil. It just has a set of tradeoffs, like any other design. `elm-pages` shares the Elm philosophy's idea that ["There are worse things than being explicit"](https://twitter.com/czaplic/status/928359289135046656). In other words, implicit rules that are hard to trace is more likely to cause maintainability issues than a little extra typing to explicitly lay out some core rules. (As long as that extra typing is nice, type-safe Elm code!)
|
||||
|
||||
Consider how `elm-pages` handles choosing a template for your pages. Many static site generators use [a special framework-provided frontmatter directive](https://jekyllrb.com/docs/front-matter/#predefined-global-variables) that determines which layout to use. And a special file naming convention will be used as the fallback for the default layout if you don't specify a layout in the frontmatter.
|
||||
|
||||
With `elm-pages`, there are no magic frontmatter directives. The way you define and handle your metadata is completely up to you. `elm-pages` simply hands you the metadata types you define and allows you to choose how to handle them with the Elm compiler there to support you.
|
||||
|
||||
## Let's see the code!
|
||||
|
||||
If we wanted to define a particular layout for blog posts, and a different layout for podcast episodes, then it's as simple as defining a JSON decoder for the data in our frontmatter.
|
||||
|
||||
So here's the frontmatter for a blog post:
|
||||
|
||||
```markdown
|
||||
---
|
||||
author: dillon
|
||||
title: Types Over Conventions
|
||||
published: 2019-09-21
|
||||
---
|
||||
```
|
||||
|
||||
And here's the frontmatter for a regular page:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: About elm-pages
|
||||
---
|
||||
```
|
||||
|
||||
As far as `elm-pages` is concerned, this is just data. We define the rules for what to do with those different data types in our code.
|
||||
|
||||
```elm
|
||||
import Author
|
||||
import Json.Decode
|
||||
|
||||
type Metadata
|
||||
= Page { title : String }
|
||||
| BlogPost { author : String, title : String }
|
||||
|
||||
|
||||
document =
|
||||
Pages.Document.parser
|
||||
{ extension = "md"
|
||||
, metadata =
|
||||
Json.Decode.oneOf
|
||||
[
|
||||
Json.Decode.map
|
||||
(\title -> Page { title = title })
|
||||
(Json.Decode.field "title" Json.Decode.string)
|
||||
, Json.Decode.map2
|
||||
(\author title ->
|
||||
BlogPost { author = author, title = title }
|
||||
)
|
||||
(Json.Decode.field "author" Author.decoder)
|
||||
(Json.Decode.field "title" Json.Decode.string)
|
||||
]
|
||||
, body = markdownView
|
||||
}
|
||||
|
||||
markdownView : String -> Result String (List (Html Msg))
|
||||
markdownView markdownBody =
|
||||
MarkdownRenderer.view markdownBody
|
||||
```
|
||||
|
||||
Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like.
|
||||
|
||||
Now, in our `elm-pages` app, our `view` function will get the markdown that we rendered for a given page along with the corresponding `Metadata`. It's completely in our hands what we want to do with that data.
|
||||
|
||||
Rails started a movement of simplifying project setup with [a philosophy of "Convention Over Configuration"](https://rubyonrails.org/doctrine/#convention-over-configuration). This made for a very easy experience bootstrapping a new web server. The downside is that you have a lot of implicit rules that can be hard to follow.
|
||||
|
||||
`elm-pages` gives you the best of both worlds. Rather than implicit conventions, or verbose configuration, `elm-pages` is centered around letting you explicitly configure your project using Elm's type system. This makes it a lot easier to configure because the Elm compiler will give you feedback on what the valid options are. And it also gives you the ability to define your own defaults and conventions explicitly, giving you the simplicity of the Rails philosophy, but the explicitness and helpful compiler support we're accustomed to in Elm.
|
||||
|
||||
**Note:** `elm-pages` currently relies on a few basic conventions such as the name of the `content` folder which has your markup. Convention over configuration isn't evil. It just has a set of tradeoffs, like any other design. `elm-pages` shares the Elm philosophy's idea that ["There are worse things than being explicit"](https://twitter.com/czaplic/status/928359289135046656). In other words, implicit rules that are hard to trace is more likely to cause maintainability issues than a little extra typing to explicitly lay out some core rules. As long as that extra typing is nice, type-safe Elm code!
|
||||
|
||||
<Oembed url="https://twitter.com/czaplic/status/928359289135046656" />
|
||||
|
||||
Consider how `elm-pages` handles choosing a template for your pages. Many static site generators use [a special framework-provided frontmatter directive](https://jekyllrb.com/docs/front-matter/#predefined-global-variables) that determines which layout to use. And a special file naming convention will be used as the fallback for the default layout if you don't specify a layout in the frontmatter.
|
||||
|
||||
With `elm-pages`, there are no magic frontmatter directives. The way you define and handle your metadata is completely up to you. `elm-pages` simply hands you the metadata types you define and allows you to choose how to handle them with the Elm compiler there to support you.
|
||||
|
||||
## Let's see the code!
|
||||
|
||||
If we wanted to define a particular layout for blog posts, and a different layout for regular pages, then it's as simple as defining a JSON decoder for the data in our frontmatter.
|
||||
|
||||
So here's the frontmatter for a blog post:
|
||||
|
||||
```markdown
|
||||
---
|
||||
author: dillon
|
||||
title: Types Over Conventions
|
||||
published: 2019-09-21
|
||||
---
|
||||
```
|
||||
|
||||
And here's the frontmatter for a regular page:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: About elm-pages
|
||||
---
|
||||
```
|
||||
|
||||
As far as `elm-pages` is concerned, this is just data. We define the rules for what to do with those different data types in our code.
|
||||
|
||||
Here's how we set up a parser to handle the frontmatter and body of our `.md` files in our `content` folder.
|
||||
The raw frontmatter can be a variety of formats, including YAML, TOML, and JSON. But in our Elm code,
|
||||
we turn that data into the data representing our app's metadata using a `Json.Decoder`.
|
||||
|
||||
```elm
|
||||
import Author
|
||||
-- Author is our custom module that looks
|
||||
-- up author data from their first name
|
||||
import Json.Decode
|
||||
|
||||
type Metadata
|
||||
= Page { title : String }
|
||||
| BlogPost { author : String, title : String }
|
||||
|
||||
document =
|
||||
Pages.Document.parser
|
||||
{ extension = "md"
|
||||
, metadata =
|
||||
Json.Decode.oneOf
|
||||
[ Json.Decode.map
|
||||
(\title ->
|
||||
Page { title = title }
|
||||
)
|
||||
(Json.Decode.field "title" Json.Decode.string)
|
||||
, Json.Decode.map2
|
||||
(\author title ->
|
||||
BlogPost { author = author, title = title }
|
||||
)
|
||||
(Json.Decode.field "author" Author.decoder)
|
||||
(Json.Decode.field "title" Json.Decode.string)
|
||||
]
|
||||
, body = markdownView
|
||||
}
|
||||
|
||||
|
||||
markdownView : String -> Result String (List (Html Msg))
|
||||
markdownView markdownBody =
|
||||
MarkdownRenderer.view markdownBody
|
||||
```
|
||||
|
||||
Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like.
|
||||
|
||||
Now, in our `elm-pages` app, our `view` function will get the markdown that we rendered for a given page along with the corresponding `Metadata`. It's completely in our hands what we want to do with that data.
|
||||
|
||||
## Takeaways
|
||||
|
||||
So which is better, configuration through types or configuration by convention?
|
||||
|
||||
They both have their benefits. If you're like me, then you enjoy being able to figure out what your Elm code is doing by just following the types. And I hope you'll agree that `elm-pages` gives you that experience for wiring up your content and your parsers.
|
||||
|
||||
And when you need to do something more advanced, you've got all the typed data right there and you're empowered to solve the problem using Elm!
|
31150
examples/slides/browser-elm.js
Normal file
7
examples/slides/docs/core-concepts.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Core Concepts
|
||||
type: doc
|
||||
---
|
||||
## StaticHttp
|
||||
|
||||
Gives you a way to pull in data during the build step. This data changes every time you run a build. You won't see a loading spinner or error with this data in your built production site. You might get a build error that you can fix.
|
150
examples/slides/docs/directory-structure.md
Normal file
@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Directory Structure
|
||||
type: doc
|
||||
---
|
||||
|
||||
## Philosophy
|
||||
|
||||
As a general rule, `elm-pages` strives to be unopinionated about how you organize
|
||||
your files (both code and content).
|
||||
|
||||
```shell
|
||||
.
|
||||
├── content/
|
||||
├── elm.json
|
||||
├── images/
|
||||
├── static/
|
||||
├── index.js
|
||||
├── package.json
|
||||
└── src/
|
||||
└── Template/
|
||||
├── Bio.elm # user-defined template modules
|
||||
└── Catalog.elm
|
||||
└── Main.elm
|
||||
```
|
||||
|
||||
## `content` folder
|
||||
|
||||
Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like.
|
||||
|
||||
```elm
|
||||
helloDocument : Pages.Document.DocumentParser Metadata (List (Html Msg))
|
||||
helloDocument =
|
||||
Pages.Document.parser
|
||||
{ extension = "txt"
|
||||
, metadata =
|
||||
-- pages will use the layout for Docs if they have
|
||||
-- `type: doc` in their markdown frontmatter
|
||||
Json.Decode.map2
|
||||
(\title maybeType ->
|
||||
case maybeType of
|
||||
Just "doc" ->
|
||||
Metadata.Doc { title = title }
|
||||
|
||||
_ ->
|
||||
Metadata.Page { title = title }
|
||||
)
|
||||
(Json.Decode.field "title" Json.Decode.string)
|
||||
(Json.Decode.field "type" Json.Decode.string
|
||||
|> Json.Decode.maybe
|
||||
)
|
||||
, body = MarkdownRenderer.view
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```elm
|
||||
markdownDocument : Pages.Document.DocumentParser Metadata (List (Element Msg))
|
||||
markdownDocument =
|
||||
Pages.Document.parser
|
||||
{ extension = "md"
|
||||
, metadata =
|
||||
Json.Decode.map2
|
||||
(\title maybeType ->
|
||||
case maybeType of
|
||||
Just "doc" ->
|
||||
Metadata.Doc { title = title }
|
||||
|
||||
_ ->
|
||||
Metadata.Page { title = title }
|
||||
)
|
||||
(Json.Decode.field "title" Json.Decode.string)
|
||||
(Json.Decode.field "type" Json.Decode.string
|
||||
|> Json.Decode.maybe
|
||||
)
|
||||
, body = MarkdownRenderer.view
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
### Templates
|
||||
`src/Template/*.elm`
|
||||
|
||||
A template represents a type of page. For example, a BlogPost template could live in `src/Template/BlogPost.elm`. Any files in your `content/` folder with frontmatter that you decode into type `TemplateType.BlogPost` will be rendered using your `BlogPost` template.
|
||||
|
||||
Think of each template as having its own mini `elm-pages architecture` lifecycle.
|
||||
|
||||
Imagine you have a site called thegreatcomposers.com that lists the greatest works of Classical composers.
|
||||
|
||||
Let's say you have a file called `content/catalog/sibelius.md` with these contents:
|
||||
|
||||
```markdown
|
||||
---
|
||||
template: catalog
|
||||
composer: Sibelius
|
||||
---
|
||||
## Symphony 2, Op. 47
|
||||
### Notable Recordings
|
||||
Bernstein Vienna Philharmonic
|
||||
```
|
||||
|
||||
You have a metadata decoder like this:
|
||||
|
||||
```elm
|
||||
module Metadata exposing (Metadata, decoder)
|
||||
|
||||
type Metadata = Catalog Composer | Bio Composer
|
||||
|
||||
type Composer = Sibelius | Mozart
|
||||
|
||||
decoder =
|
||||
Decode.string
|
||||
|> Decode.field "template"
|
||||
|> Decode.andThen (\template ->
|
||||
case template of
|
||||
"catalog" -> Decode.map Catalog decodeComposer
|
||||
"bio" -> Decode.map Bio decodeComposer
|
||||
)
|
||||
```
|
||||
|
||||
Now say you navigate to `/catalog/sibelius`. Let's look at the `elm-pages architecture` lifecycle steps that kick in.
|
||||
|
||||
### Build
|
||||
|
||||
* `staticData` - When you build your site (using `elm-pages build` for prod or `elm-pages develop` in dev mode), the `staticData` will be fetched for this page. Your `staticData` request has access to the page's `Metadata`. So if you wanted to request `api.composers.com/portrait-images/<composer-name>` to get the list of images for each composer's catalog page, you could. Behind the scenes, `elm-pages` will make sure this data is loaded for you in the browser so you have access to this data, even though the API is only hit during the initial build and then stored as a JSON asset for your site.
|
||||
#### Page Load
|
||||
* `init` - the page for Sibelius' catalog has its own state. Let's display a Carousel that shows photos of the composer. `init` is called when you navigate to this page. If you navigate to another composer's catalog page, like Mozart, it will call the same `init` function to get a fresh Model for the new page, passing in the metadata for the Mozart page (from the frontmatter in `content/catalog/mozart`.
|
||||
* `view` given the page's state, metadata, and StaticHttp data, you can render the catalog for Sibelius.
|
||||
#### Page Interaction
|
||||
* `update` - if you click the Carousel, the page's state gets updated.
|
||||
|
||||
|
||||
### Shared
|
||||
`src/Shared.elm`
|
||||
* `staticData` (loaded per-app, not per-page)
|
||||
* `View` - the data type that pages render to in your app
|
||||
* `view` - the top-level view function for your app
|
||||
|
||||
### Build
|
||||
`src/Build.elm`
|
||||
* `staticData` (build-only)
|
||||
* `manifest`
|
||||
* `generateFiles`
|
||||
|
||||
|
||||
### Global Metadata
|
||||
`src/TemplateType.elm`
|
||||
|
||||
This module must define a variant for each template module.
|
19
examples/slides/docs/index.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Quick Start
|
||||
type: doc
|
||||
---
|
||||
|
||||
## Installing
|
||||
|
||||
The easiest way to get set up is to use the starter template. Just go to the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter) and click "Use this template" to fork the repo.
|
||||
|
||||
Or clone down the repo:
|
||||
|
||||
```
|
||||
git clone git@github.com:dillonkearns/elm-pages-starter.git
|
||||
cd elm-pages-starter
|
||||
npm install
|
||||
npm start # starts a local dev server using `elm-pages develop`
|
||||
```
|
||||
|
||||
From there, start editing the posts in the `content` folder. You can change the types of content in `src/Metadata.elm`, or render your content using a different renderer (the template uses `elm-explorations/markdown`) by changing [the configuring the document handlers](https://github.com/dillonkearns/elm-pages-starter/blob/2c2241c177cf8e0144af4a8afec0115f93169ac5/src/Main.elm#L70-L80).
|
8
examples/slides/elm-tooling.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"tools": {
|
||||
"elm": "0.19.1",
|
||||
"elm-format": "0.8.4",
|
||||
"elm-json": "0.2.10",
|
||||
"elm-test-rs": "1.0.0"
|
||||
}
|
||||
}
|
64
examples/slides/elm.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src",
|
||||
"../../src",
|
||||
"gen",
|
||||
"../../plugins"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"ThinkAlexandria/elm-html-in-elm": "1.0.1",
|
||||
"avh4/elm-color": "1.0.0",
|
||||
"billstclair/elm-xml-eeue56": "1.0.1",
|
||||
"danyx23/elm-mimetype": "4.0.1",
|
||||
"dillonkearns/elm-markdown": "4.0.2",
|
||||
"dillonkearns/elm-oembed": "1.0.0",
|
||||
"dillonkearns/elm-rss": "1.0.1",
|
||||
"dillonkearns/elm-sitemap": "1.0.1",
|
||||
"dmy/elm-imf-date-time": "1.0.1",
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/http": "2.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/parser": "1.1.0",
|
||||
"elm/svg": "1.0.1",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2",
|
||||
"elm-community/dict-extra": "2.4.0",
|
||||
"elm-community/list-extra": "8.3.0",
|
||||
"elm-community/result-extra": "2.4.0",
|
||||
"elm-community/string-extra": "4.0.1",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"justinmimbs/date": "3.2.0",
|
||||
"lukewestby/elm-string-interpolate": "1.0.4",
|
||||
"mdgriffith/elm-ui": "1.1.8",
|
||||
"miniBill/elm-codec": "1.2.0",
|
||||
"noahzgordon/elm-color-extra": "1.0.2",
|
||||
"pablohirafuji/elm-syntax-highlight": "3.3.0",
|
||||
"rtfeldman/elm-hex": "1.0.0",
|
||||
"tripokey/elm-fuzzy": "5.2.1",
|
||||
"zwilias/json-decode-exploration": "6.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/bytes": "1.0.8",
|
||||
"elm/file": "1.0.5",
|
||||
"elm/random": "1.0.0",
|
||||
"elm/regex": "1.0.0",
|
||||
"fredcy/elm-parseint": "2.0.1",
|
||||
"justinmimbs/time-extra": "1.1.0",
|
||||
"lazamar/dict-parser": "1.0.2",
|
||||
"mgold/elm-nonempty-list": "4.1.0",
|
||||
"ryannhg/date-format": "2.3.0"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {
|
||||
"elm-explorations/test": "1.2.2"
|
||||
},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
8
examples/slides/gen/Pages.elm
Normal file
@ -0,0 +1,8 @@
|
||||
module Pages exposing (builtAt)
|
||||
|
||||
import Time
|
||||
|
||||
|
||||
builtAt : Time.Posix
|
||||
builtAt =
|
||||
Time.millisToPosix 1618410729151
|
29
examples/slides/gen/Route.elm
Normal file
@ -0,0 +1,29 @@
|
||||
module Route exposing (..)
|
||||
|
||||
import Url
|
||||
import Url.Parser as Parser exposing ((</>), Parser)
|
||||
|
||||
|
||||
type Route
|
||||
= Slide__Number_ { number : String }
|
||||
|
||||
|
||||
urlToRoute : Url.Url -> Maybe Route
|
||||
urlToRoute url =
|
||||
Parser.parse (Parser.oneOf routes) url
|
||||
|
||||
|
||||
routes : List (Parser (Route -> a) a)
|
||||
routes =
|
||||
[ Parser.map (\number -> Slide__Number_ { number = number }) (Parser.s "slide" </> Parser.string)
|
||||
|
||||
]
|
||||
|
||||
|
||||
routeToPath : Maybe Route -> List String
|
||||
routeToPath maybeRoute =
|
||||
case maybeRoute of
|
||||
Nothing ->
|
||||
[]
|
||||
Just (Slide__Number_ params) ->
|
||||
[ "slide", params.number ]
|
206
examples/slides/gen/Template.elm
Normal file
@ -0,0 +1,206 @@
|
||||
module Template exposing
|
||||
( Builder(..)
|
||||
, StaticPayload
|
||||
, withStaticData, noStaticData
|
||||
, Template, buildNoState
|
||||
, TemplateWithState, buildWithLocalState, buildWithSharedState
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
## Building a Template
|
||||
|
||||
@docs Builder
|
||||
|
||||
|
||||
## Static Data
|
||||
|
||||
Every template will have access to a `StaticPayload`.
|
||||
|
||||
@docs StaticPayload
|
||||
|
||||
Since this data is _static_, you have access to it before the user has loaded the page, including at build time.
|
||||
An example of dynamic data would be keyboard input from the user, query params, or any other data that comes from the app running in the browser.
|
||||
|
||||
But before the user even requests the page, we have the following data:
|
||||
|
||||
- `path` - these paths are static. In other words, we know every single path when we build an elm-pages site.
|
||||
- `metadata` - we have a decoded Elm value for the page's metadata.
|
||||
- `sharedStatic` - we can access any shared data between pages. For example, you may have fetched the name of a blog ("Jane's Blog") from the API for a Content Management System (CMS).
|
||||
- `static` - this is the static data for this specific page. If you use `noStaticData`, then this will be `()`, meaning there is no page-specific static data.
|
||||
|
||||
@docs withStaticData, noStaticData
|
||||
|
||||
|
||||
## Stateless Templates
|
||||
|
||||
@docs Template, buildNoState
|
||||
|
||||
|
||||
## Stateful Templates
|
||||
|
||||
@docs TemplateWithState, buildWithLocalState, buildWithSharedState
|
||||
|
||||
-}
|
||||
|
||||
import Document exposing (Document)
|
||||
import Head
|
||||
import Pages.PagePath exposing (PagePath)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Shared
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias TemplateWithState routeParams templateStaticData templateModel templateMsg =
|
||||
{ staticData : routeParams -> StaticHttp.Request templateStaticData
|
||||
, staticRoutes : StaticHttp.Request (List routeParams)
|
||||
, view :
|
||||
templateModel
|
||||
-> Shared.Model
|
||||
-> StaticPayload templateStaticData routeParams
|
||||
-> Document templateMsg
|
||||
, head :
|
||||
StaticPayload templateStaticData routeParams
|
||||
-> List Head.Tag
|
||||
, init : routeParams -> ( templateModel, Cmd templateMsg )
|
||||
, update : routeParams -> templateMsg -> templateModel -> Shared.Model -> ( templateModel, Cmd templateMsg, Maybe Shared.SharedMsg )
|
||||
, subscriptions : routeParams -> PagePath -> templateModel -> Shared.Model -> Sub templateMsg
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Template routeParams staticData =
|
||||
TemplateWithState routeParams staticData () Never
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias StaticPayload staticData routeParams =
|
||||
{ static : staticData -- local
|
||||
, sharedStatic : Shared.StaticData -- share
|
||||
, routeParams : routeParams
|
||||
, path : PagePath
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
type Builder routeParams templateStaticData
|
||||
= WithStaticData
|
||||
{ staticData : routeParams -> StaticHttp.Request templateStaticData
|
||||
, staticRoutes : StaticHttp.Request (List routeParams)
|
||||
, head :
|
||||
StaticPayload templateStaticData routeParams
|
||||
-> List Head.Tag
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
buildNoState :
|
||||
{ view :
|
||||
StaticPayload templateStaticData routeParams
|
||||
-> Document Never
|
||||
}
|
||||
-> Builder routeParams templateStaticData
|
||||
-> TemplateWithState routeParams templateStaticData () Never
|
||||
buildNoState { view } builderState =
|
||||
case builderState of
|
||||
WithStaticData record ->
|
||||
{ view = \() _ -> view
|
||||
, head = record.head
|
||||
, staticData = record.staticData
|
||||
, staticRoutes = record.staticRoutes
|
||||
, init = \_ -> ( (), Cmd.none )
|
||||
, update = \_ _ _ _ -> ( (), Cmd.none, Nothing )
|
||||
, subscriptions = \_ _ _ _ -> Sub.none
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
buildWithLocalState :
|
||||
{ view :
|
||||
templateModel
|
||||
-> Shared.Model
|
||||
-> StaticPayload templateStaticData routeParams
|
||||
-> Document templateMsg
|
||||
, init : routeParams -> ( templateModel, Cmd templateMsg )
|
||||
, update : Shared.Model -> routeParams -> templateMsg -> templateModel -> ( templateModel, Cmd templateMsg )
|
||||
, subscriptions : routeParams -> PagePath -> templateModel -> Sub templateMsg
|
||||
}
|
||||
-> Builder routeParams templateStaticData
|
||||
-> TemplateWithState routeParams templateStaticData templateModel templateMsg
|
||||
buildWithLocalState config builderState =
|
||||
case builderState of
|
||||
WithStaticData record ->
|
||||
{ view =
|
||||
\model sharedModel staticPayload ->
|
||||
config.view model sharedModel staticPayload
|
||||
, head = record.head
|
||||
, staticData = record.staticData
|
||||
, staticRoutes = record.staticRoutes
|
||||
, init = config.init
|
||||
, update =
|
||||
\metadata msg templateModel sharedModel ->
|
||||
let
|
||||
( updatedModel, cmd ) =
|
||||
config.update sharedModel metadata msg templateModel
|
||||
in
|
||||
( updatedModel, cmd, Nothing )
|
||||
, subscriptions =
|
||||
\routeParams path templateModel sharedModel ->
|
||||
config.subscriptions routeParams path templateModel
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
buildWithSharedState :
|
||||
{ view :
|
||||
templateModel
|
||||
-> Shared.Model
|
||||
-> StaticPayload templateStaticData routeParams
|
||||
-> Document templateMsg
|
||||
, init : routeParams -> ( templateModel, Cmd templateMsg )
|
||||
, update : routeParams -> templateMsg -> templateModel -> Shared.Model -> ( templateModel, Cmd templateMsg, Maybe Shared.SharedMsg )
|
||||
, subscriptions : routeParams -> PagePath -> templateModel -> Shared.Model -> Sub templateMsg
|
||||
}
|
||||
-> Builder routeParams templateStaticData
|
||||
-> TemplateWithState routeParams templateStaticData templateModel templateMsg
|
||||
buildWithSharedState config builderState =
|
||||
case builderState of
|
||||
WithStaticData record ->
|
||||
{ view = config.view
|
||||
, head = record.head
|
||||
, staticData = record.staticData
|
||||
, staticRoutes = record.staticRoutes
|
||||
, init = config.init
|
||||
, update = config.update
|
||||
, subscriptions = config.subscriptions
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
withStaticData :
|
||||
{ staticData : routeParams -> StaticHttp.Request templateStaticData
|
||||
, staticRoutes : StaticHttp.Request (List routeParams)
|
||||
, head : StaticPayload templateStaticData routeParams -> List Head.Tag
|
||||
}
|
||||
-> Builder routeParams templateStaticData
|
||||
withStaticData { staticData, head, staticRoutes } =
|
||||
WithStaticData
|
||||
{ staticData = staticData
|
||||
, staticRoutes = staticRoutes
|
||||
, head = head
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
noStaticData :
|
||||
{ head : StaticPayload () routeParams -> List Head.Tag
|
||||
, staticRoutes : StaticHttp.Request (List routeParams)
|
||||
}
|
||||
-> Builder routeParams ()
|
||||
noStaticData { head, staticRoutes } =
|
||||
WithStaticData
|
||||
{ staticData = \_ -> StaticHttp.succeed ()
|
||||
, staticRoutes = staticRoutes
|
||||
, head = head
|
||||
}
|
337
examples/slides/gen/TemplateModulesBeta.elm
Normal file
@ -0,0 +1,337 @@
|
||||
port module TemplateModulesBeta exposing (..)
|
||||
|
||||
import Browser
|
||||
import Route exposing (Route)
|
||||
import Document
|
||||
import Json.Decode
|
||||
import Json.Encode
|
||||
import Pages.Internal.Platform
|
||||
import Pages.Internal.Platform.ToJsPayload
|
||||
import Pages.Manifest as Manifest
|
||||
import Shared
|
||||
import Site
|
||||
import Head
|
||||
import Html exposing (Html)
|
||||
import Pages.PagePath exposing (PagePath)
|
||||
import Url
|
||||
import Url.Parser as Parser exposing ((</>), Parser)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
|
||||
import Template.Slide.Number_
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ global : Shared.Model
|
||||
, page : TemplateModel
|
||||
, current :
|
||||
Maybe
|
||||
{ path :
|
||||
{ path : PagePath
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
, metadata : Maybe Route
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type TemplateModel
|
||||
= ModelSlide__Number_ Template.Slide.Number_.Model
|
||||
|
||||
| NotFound
|
||||
|
||||
|
||||
|
||||
|
||||
type Msg
|
||||
= MsgGlobal Shared.Msg
|
||||
| OnPageChange
|
||||
{ path : PagePath
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
, metadata : Maybe Route
|
||||
}
|
||||
| MsgSlide__Number_ Template.Slide.Number_.Msg
|
||||
|
||||
|
||||
|
||||
view :
|
||||
{ path : PagePath
|
||||
, frontmatter : Maybe Route
|
||||
}
|
||||
->
|
||||
StaticHttp.Request
|
||||
{ view : Model -> { title : String, body : Html Msg }
|
||||
, head : List Head.Tag
|
||||
}
|
||||
view page =
|
||||
case page.frontmatter of
|
||||
Nothing ->
|
||||
StaticHttp.fail <| "Page not found: " ++ Pages.PagePath.toString page.path
|
||||
Just (Route.Slide__Number_ s) ->
|
||||
StaticHttp.map2
|
||||
(\data globalData ->
|
||||
{ view =
|
||||
\model ->
|
||||
case model.page of
|
||||
ModelSlide__Number_ subModel ->
|
||||
Template.Slide.Number_.template.view
|
||||
subModel
|
||||
model.global
|
||||
{ static = data
|
||||
, sharedStatic = globalData
|
||||
, routeParams = s
|
||||
, path = page.path
|
||||
}
|
||||
|> (\{ title, body } ->
|
||||
Shared.template.view
|
||||
globalData
|
||||
page
|
||||
model.global
|
||||
MsgGlobal
|
||||
({ title = title, body = body }
|
||||
|> Document.map MsgSlide__Number_
|
||||
)
|
||||
)
|
||||
|
||||
_ ->
|
||||
{ title = "Model mismatch", body = Html.text <| "Model mismatch" }
|
||||
, head = Template.Slide.Number_.template.head
|
||||
{ static = data
|
||||
, sharedStatic = globalData
|
||||
, routeParams = s
|
||||
, path = page.path
|
||||
}
|
||||
}
|
||||
)
|
||||
(Template.Slide.Number_.template.staticData s)
|
||||
(Shared.template.staticData)
|
||||
|
||||
|
||||
|
||||
init :
|
||||
Maybe Shared.Model
|
||||
->
|
||||
Maybe
|
||||
{ path :
|
||||
{ path : PagePath
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
, metadata : Maybe Route
|
||||
}
|
||||
-> ( Model, Cmd Msg )
|
||||
init currentGlobalModel maybePagePath =
|
||||
let
|
||||
( sharedModel, globalCmd ) =
|
||||
currentGlobalModel |> Maybe.map (\m -> ( m, Cmd.none )) |> Maybe.withDefault (Shared.template.init maybePagePath)
|
||||
|
||||
( templateModel, templateCmd ) =
|
||||
case maybePagePath |> Maybe.andThen .metadata of
|
||||
Nothing ->
|
||||
( NotFound, Cmd.none )
|
||||
|
||||
Just (Route.Slide__Number_ routeParams) ->
|
||||
Template.Slide.Number_.template.init routeParams
|
||||
|> Tuple.mapBoth ModelSlide__Number_ (Cmd.map MsgSlide__Number_)
|
||||
|
||||
|
||||
in
|
||||
( { global = sharedModel
|
||||
, page = templateModel
|
||||
, current = maybePagePath
|
||||
}
|
||||
, Cmd.batch
|
||||
[ templateCmd
|
||||
, globalCmd |> Cmd.map MsgGlobal
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
MsgGlobal msg_ ->
|
||||
let
|
||||
( sharedModel, globalCmd ) =
|
||||
Shared.template.update msg_ model.global
|
||||
in
|
||||
( { model | global = sharedModel }
|
||||
, globalCmd |> Cmd.map MsgGlobal
|
||||
)
|
||||
|
||||
OnPageChange record ->
|
||||
(init (Just model.global) <|
|
||||
Just
|
||||
{ path =
|
||||
{ path = record.path
|
||||
, query = record.query
|
||||
, fragment = record.fragment
|
||||
}
|
||||
, metadata = record.metadata
|
||||
}
|
||||
)
|
||||
|> (\( updatedModel, cmd ) ->
|
||||
case Shared.template.onPageChange of
|
||||
Nothing ->
|
||||
( updatedModel, cmd )
|
||||
|
||||
Just thingy ->
|
||||
let
|
||||
( updatedGlobalModel, globalCmd ) =
|
||||
Shared.template.update
|
||||
(thingy
|
||||
{ path = record.path
|
||||
, query = record.query
|
||||
, fragment = record.fragment
|
||||
}
|
||||
)
|
||||
model.global
|
||||
in
|
||||
( { updatedModel
|
||||
| global = updatedGlobalModel
|
||||
}
|
||||
, Cmd.batch [ cmd, Cmd.map MsgGlobal globalCmd ]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
MsgSlide__Number_ msg_ ->
|
||||
let
|
||||
( updatedPageModel, pageCmd, ( newGlobalModel, newGlobalCmd ) ) =
|
||||
case ( model.page, model.current |> Maybe.andThen .metadata ) of
|
||||
( ModelSlide__Number_ pageModel, Just (Route.Slide__Number_ routeParams) ) ->
|
||||
Template.Slide.Number_.template.update
|
||||
routeParams
|
||||
msg_
|
||||
pageModel
|
||||
model.global
|
||||
|> mapBoth ModelSlide__Number_ (Cmd.map MsgSlide__Number_)
|
||||
|> (\( a, b, c ) ->
|
||||
case c of
|
||||
Just sharedMsg ->
|
||||
( a, b, Shared.template.update (Shared.SharedMsg sharedMsg) model.global )
|
||||
|
||||
Nothing ->
|
||||
( a, b, ( model.global, Cmd.none ) )
|
||||
)
|
||||
|
||||
_ ->
|
||||
( model.page, Cmd.none, ( model.global, Cmd.none ) )
|
||||
in
|
||||
( { model | page = updatedPageModel, global = newGlobalModel }
|
||||
, Cmd.batch [ pageCmd, newGlobalCmd |> Cmd.map MsgGlobal ]
|
||||
)
|
||||
|
||||
|
||||
|
||||
type alias SiteConfig =
|
||||
{ canonicalUrl : String
|
||||
, manifest : Manifest.Config
|
||||
}
|
||||
|
||||
templateSubscriptions : Route -> PagePath -> Model -> Sub Msg
|
||||
templateSubscriptions route path model =
|
||||
case ( model.page, route ) of
|
||||
|
||||
( ModelSlide__Number_ templateModel, Route.Slide__Number_ routeParams ) ->
|
||||
Template.Slide.Number_.template.subscriptions
|
||||
routeParams
|
||||
path
|
||||
templateModel
|
||||
model.global
|
||||
|> Sub.map MsgSlide__Number_
|
||||
|
||||
|
||||
|
||||
_ ->
|
||||
Sub.none
|
||||
|
||||
|
||||
main : Pages.Internal.Platform.Program Model Msg (Maybe Route)
|
||||
main =
|
||||
Pages.Internal.Platform.application
|
||||
{ init = init Nothing
|
||||
, urlToRoute = Route.urlToRoute
|
||||
, routeToPath = Route.routeToPath
|
||||
, site = Site.config
|
||||
, getStaticRoutes = getStaticRoutes
|
||||
, view = view
|
||||
, update = update
|
||||
, subscriptions =
|
||||
\path model ->
|
||||
Sub.batch
|
||||
[ Shared.template.subscriptions path model.global |> Sub.map MsgGlobal
|
||||
-- , templateSubscriptions (Route.Blog {}) path model
|
||||
]
|
||||
, onPageChange = Just OnPageChange
|
||||
, canonicalSiteUrl = "TODO"
|
||||
, toJsPort = toJsPort
|
||||
, fromJsPort = fromJsPort identity
|
||||
, generateFiles =
|
||||
getStaticRoutes
|
||||
|> StaticHttp.andThen
|
||||
(\resolvedStaticRoutes ->
|
||||
StaticHttp.map2 (::)
|
||||
(manifestGenerator
|
||||
resolvedStaticRoutes
|
||||
)
|
||||
(Site.config
|
||||
resolvedStaticRoutes
|
||||
|> .generateFiles
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
getStaticRoutes =
|
||||
StaticHttp.combine
|
||||
[ StaticHttp.succeed
|
||||
[
|
||||
]
|
||||
, Template.Slide.Number_.template.staticRoutes |> StaticHttp.map (List.map Route.Slide__Number_)
|
||||
]
|
||||
|> StaticHttp.map List.concat
|
||||
|> StaticHttp.map (List.map Just)
|
||||
|
||||
|
||||
manifestGenerator : List ( Maybe Route ) -> StaticHttp.Request (Result anyError { path : List String, content : String })
|
||||
manifestGenerator resolvedRoutes =
|
||||
Site.config resolvedRoutes
|
||||
|> .staticData
|
||||
|> StaticHttp.map
|
||||
(\data ->
|
||||
(Site.config resolvedRoutes |> .manifest) data
|
||||
|> manifestToFile ((Site.config resolvedRoutes |> .canonicalUrl) data)
|
||||
)
|
||||
|
||||
|
||||
manifestToFile : String -> Manifest.Config -> Result anyError { path : List String, content : String }
|
||||
manifestToFile resolvedCanonicalUrl manifestConfig =
|
||||
manifestConfig
|
||||
|> Manifest.toJson resolvedCanonicalUrl
|
||||
|> (\manifestJsonValue ->
|
||||
Ok
|
||||
{ path = [ "manifest.json" ]
|
||||
, content = Json.Encode.encode 0 manifestJsonValue
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
port toJsPort : Json.Encode.Value -> Cmd msg
|
||||
|
||||
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
|
||||
|
||||
|
||||
mapDocument : Browser.Document Never -> Browser.Document mapped
|
||||
mapDocument document =
|
||||
{ title = document.title
|
||||
, body = document.body |> List.map (Html.map never)
|
||||
}
|
||||
|
||||
|
||||
mapBoth fnA fnB ( a, b, c ) =
|
||||
( fnA a, fnB b, c )
|
68
examples/slides/index.md
Normal file
@ -0,0 +1,68 @@
|
||||
<Banner>A **statically typed** site generator</Banner>
|
||||
|
||||
<Boxes>
|
||||
<Box>
|
||||
### Pure Elm Configuration
|
||||
|
||||
Layouts, styles, even a full-fledged elm application.
|
||||
|
||||
### 📄 Type-Safe Content
|
||||
|
||||
Configuration, errors for broken links
|
||||
</Box>
|
||||
<Box>
|
||||
|
||||
### 🚀 `elm-pages build`
|
||||
|
||||
No `elm make` or `webpack` setup needed! Just one simple command.
|
||||
|
||||
</Box>
|
||||
<Box>
|
||||
|
||||
### 📦 Optimized Elm Progressive Web App
|
||||
|
||||
Layouts, styles, even a full-fledged elm application.
|
||||
|
||||
### Deploy anywhere
|
||||
|
||||
Ship to Netlify, Github Pages, or any host that will serve up static files!
|
||||
</Box>
|
||||
</Boxes>
|
||||
|
||||
<Values>
|
||||
<Value>
|
||||
# No magic, just types
|
||||
|
||||
The magic is in how the pieces snap together. The basic platform provided is simple, letting you compose exactly what you need with types to support you.
|
||||
</Value>
|
||||
|
||||
<Value>
|
||||
# Extensible through pure elm
|
||||
|
||||
Behavior is shared through packages exposing simple helper functions to help you build up your data.
|
||||
</Value>
|
||||
|
||||
<Value>
|
||||
# If it compiles, it works
|
||||
|
||||
`elm-pages` just makes more of the things you do in your static site feel like elm. Did you misspell the name of an image asset or a link to a blog post? `elm-pages` will give you a friendly error message and some helpful suggestions.
|
||||
</Value>
|
||||
|
||||
<Value>
|
||||
# An extended elm platform
|
||||
|
||||
`elm-pages` is just elm, but with a broader set of primitives for declaring meta tags to improve SEO, or generate RSS feeds and other files based on your static content.
|
||||
</Value>
|
||||
|
||||
<Value>
|
||||
# Blazing fast
|
||||
|
||||
All you have to do is create your content and choose how to present it. Optimized images, pre-rendered pages, and a snappy lightweight single-page app all come for free.
|
||||
</Value>
|
||||
|
||||
<Value>
|
||||
# Simple
|
||||
|
||||
`elm-pages` gives you the smallest set of core concepts possible, and a type system to make sure all the pieces fit together just right. The rest is up to you. Rather than remember a lot of magic and special cases, you can just rely on your elm types to build what you need with a set of simple primitives.
|
||||
</Value>
|
||||
</Values>
|
537
examples/slides/package-lock.json
generated
Normal file
@ -0,0 +1,537 @@
|
||||
{
|
||||
"name": "elm-pages-example",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "elm-pages-example",
|
||||
"version": "1.0.0",
|
||||
"license": "BSD-3",
|
||||
"devDependencies": {
|
||||
"elm-oembed": "0.0.6",
|
||||
"elm-pages": "file:../..",
|
||||
"elm-tooling": "^1.3.0",
|
||||
"http-server": "^0.11.1"
|
||||
}
|
||||
},
|
||||
"../..": {
|
||||
"version": "1.5.5",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"connect": "^3.7.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"elm-hot": "^1.1.6",
|
||||
"elm-optimize-level-2": "^0.1.5",
|
||||
"globby": "^11.0.3",
|
||||
"gray-matter": "^4.0.2",
|
||||
"kleur": "^3.0.3",
|
||||
"micromatch": "^4.0.2",
|
||||
"serve-static": "^1.14.1",
|
||||
"terser": "^5.6.1",
|
||||
"xhr2": "^0.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"elm-pages": "generator/src/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
"@types/node": "^12.7.7",
|
||||
"elm-review": "^2.5.0",
|
||||
"elm-test": "^0.19.1-revision6",
|
||||
"elm-tooling": "^1.3.0",
|
||||
"jest": "^26.0.1",
|
||||
"typescript": "^4.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/colors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
||||
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
"integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ecstatic": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz",
|
||||
"integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"he": "^1.1.1",
|
||||
"mime": "^1.6.0",
|
||||
"minimist": "^1.1.0",
|
||||
"url-join": "^2.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"ecstatic": "lib/ecstatic.js"
|
||||
}
|
||||
},
|
||||
"node_modules/elm-oembed": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/elm-oembed/-/elm-oembed-0.0.6.tgz",
|
||||
"integrity": "sha512-EbDvhEgs5FmUF7T45HzcynsGnm/hqFslYU9z81UkjiE74/czedrQr8RXfMjSqnZ+Oloo7pi7ClO6xHHHyxcGKA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/elm-pages": {
|
||||
"resolved": "../..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/elm-tooling": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/elm-tooling/-/elm-tooling-1.3.0.tgz",
|
||||
"integrity": "sha512-OLRg8D7QCbzMmi8QTaebIIsH2qaIj4NiltuJ8vZj4JFHOIVa3utyD5tlfySBGZNfDG7+vZ2K8iDk82TalFaAbw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"elm-tooling": "index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz",
|
||||
"integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-server": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz",
|
||||
"integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"colors": "1.0.3",
|
||||
"corser": "~2.0.0",
|
||||
"ecstatic": "^3.0.0",
|
||||
"http-proxy": "^1.8.1",
|
||||
"opener": "~1.4.0",
|
||||
"optimist": "0.6.x",
|
||||
"portfinder": "^1.0.13",
|
||||
"union": "~0.4.3"
|
||||
},
|
||||
"bin": {
|
||||
"hs": "bin/http-server",
|
||||
"http-server": "bin/http-server"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
|
||||
"integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"opener": "opener.js"
|
||||
}
|
||||
},
|
||||
"node_modules/optimist": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
|
||||
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "~0.0.1",
|
||||
"wordwrap": "~0.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/optimist/node_modules/minimist": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
|
||||
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
|
||||
"integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"async": "^2.6.2",
|
||||
"debug": "^3.1.1",
|
||||
"mkdirp": "^0.5.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
|
||||
"integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/union": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
|
||||
"integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"qs": "~2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-join": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
|
||||
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
|
||||
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
||||
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
|
||||
"dev": true
|
||||
},
|
||||
"corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
"integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ecstatic": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz",
|
||||
"integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"he": "^1.1.1",
|
||||
"mime": "^1.6.0",
|
||||
"minimist": "^1.1.0",
|
||||
"url-join": "^2.0.5"
|
||||
}
|
||||
},
|
||||
"elm-oembed": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/elm-oembed/-/elm-oembed-0.0.6.tgz",
|
||||
"integrity": "sha512-EbDvhEgs5FmUF7T45HzcynsGnm/hqFslYU9z81UkjiE74/czedrQr8RXfMjSqnZ+Oloo7pi7ClO6xHHHyxcGKA==",
|
||||
"dev": true
|
||||
},
|
||||
"elm-pages": {
|
||||
"version": "file:../..",
|
||||
"requires": {
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
"@types/node": "^12.7.7",
|
||||
"connect": "^3.7.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"elm-hot": "^1.1.6",
|
||||
"elm-optimize-level-2": "^0.1.5",
|
||||
"elm-review": "^2.5.0",
|
||||
"elm-test": "^0.19.1-revision6",
|
||||
"elm-tooling": "^1.3.0",
|
||||
"globby": "^11.0.3",
|
||||
"gray-matter": "^4.0.2",
|
||||
"jest": "^26.0.1",
|
||||
"kleur": "^3.0.3",
|
||||
"micromatch": "^4.0.2",
|
||||
"serve-static": "^1.14.1",
|
||||
"terser": "^5.6.1",
|
||||
"typescript": "^4.2.3",
|
||||
"xhr2": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"elm-tooling": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/elm-tooling/-/elm-tooling-1.3.0.tgz",
|
||||
"integrity": "sha512-OLRg8D7QCbzMmi8QTaebIIsH2qaIj4NiltuJ8vZj4JFHOIVa3utyD5tlfySBGZNfDG7+vZ2K8iDk82TalFaAbw==",
|
||||
"dev": true
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"dev": true
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz",
|
||||
"integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==",
|
||||
"dev": true
|
||||
},
|
||||
"he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"http-server": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz",
|
||||
"integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"colors": "1.0.3",
|
||||
"corser": "~2.0.0",
|
||||
"ecstatic": "^3.0.0",
|
||||
"http-proxy": "^1.8.1",
|
||||
"opener": "~1.4.0",
|
||||
"optimist": "0.6.x",
|
||||
"portfinder": "^1.0.13",
|
||||
"union": "~0.4.3"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"opener": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
|
||||
"integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=",
|
||||
"dev": true
|
||||
},
|
||||
"optimist": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
|
||||
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "~0.0.1",
|
||||
"wordwrap": "~0.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
|
||||
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
|
||||
"integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async": "^2.6.2",
|
||||
"debug": "^3.1.1",
|
||||
"mkdirp": "^0.5.5"
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
|
||||
"integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=",
|
||||
"dev": true
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"union": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
|
||||
"integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"qs": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"url-join": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
|
||||
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
|
||||
"dev": true
|
||||
},
|
||||
"wordwrap": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
|
||||
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
18
examples/slides/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "elm-pages-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Example site built with elm-pages.",
|
||||
"scripts": {
|
||||
"start": "elm-pages develop --port 1234",
|
||||
"serve": "npm run build && http-server ./dist -a localhost -p 3000 -c-1",
|
||||
"build": "node ../../generator/src/cli.js"
|
||||
},
|
||||
"author": "Dillon Kearns",
|
||||
"license": "BSD-3",
|
||||
"devDependencies": {
|
||||
"elm-oembed": "0.0.6",
|
||||
"elm-pages": "file:../..",
|
||||
"elm-tooling": "^1.3.0",
|
||||
"http-server": "^0.11.1"
|
||||
}
|
||||
}
|
7
examples/slides/slides.md
Normal file
@ -0,0 +1,7 @@
|
||||
## Slide 1
|
||||
|
||||
Here's some content
|
||||
|
||||
## Another slide
|
||||
|
||||
Here's another body
|
137
examples/slides/src/Article.elm
Normal file
@ -0,0 +1,137 @@
|
||||
module Article exposing (..)
|
||||
|
||||
import Cloudinary
|
||||
import Date exposing (Date)
|
||||
import Element exposing (Element)
|
||||
import Glob
|
||||
import OptimizedDecoder
|
||||
import Pages.ImagePath exposing (ImagePath)
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.StaticFile as StaticFile
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
|
||||
|
||||
type alias BlogPost =
|
||||
{ filePath : String
|
||||
, slug : String
|
||||
}
|
||||
|
||||
|
||||
blogPostsGlob : StaticHttp.Request (List { filePath : String, slug : String })
|
||||
blogPostsGlob =
|
||||
Glob.succeed BlogPost
|
||||
|> Glob.keep Glob.fullFilePath
|
||||
|> Glob.drop (Glob.literal "content/blog/")
|
||||
|> Glob.keep Glob.wildcard
|
||||
|> Glob.drop (Glob.literal ".md")
|
||||
|> Glob.toStaticHttp
|
||||
|
||||
|
||||
allMetadata : StaticHttp.Request (List ( PagePath, ArticleMetadata ))
|
||||
allMetadata =
|
||||
--StaticFile.glob "content/blog/*.md"
|
||||
blogPostsGlob
|
||||
|> StaticHttp.map
|
||||
(\paths ->
|
||||
paths
|
||||
|> List.map
|
||||
(\{ filePath, slug } ->
|
||||
StaticHttp.map2 Tuple.pair
|
||||
(StaticHttp.succeed <| "blog/" ++ slug)
|
||||
(StaticFile.request filePath (StaticFile.frontmatter frontmatterDecoder))
|
||||
)
|
||||
)
|
||||
|> StaticHttp.resolve
|
||||
|> StaticHttp.map
|
||||
(\articles ->
|
||||
articles
|
||||
|> List.filterMap
|
||||
(\( path, metadata ) ->
|
||||
if metadata.draft then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just
|
||||
( path |> PagePath.external
|
||||
, metadata
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
type alias DataFromFile msg =
|
||||
{ body : List (Element msg)
|
||||
, metadata : ArticleMetadata
|
||||
}
|
||||
|
||||
|
||||
|
||||
--fileRequest : String -> StaticHttp.Request (DataFromFile msg)
|
||||
|
||||
|
||||
fileRequest : String -> StaticHttp.Request ArticleMetadata
|
||||
fileRequest filePath =
|
||||
StaticFile.request
|
||||
--"content/blog/extensible-markdown-parsing-in-elm.md"
|
||||
filePath
|
||||
--(OptimizedDecoder.map2 DataFromFile
|
||||
-- (StaticFile.body
|
||||
-- |> OptimizedDecoder.andThen
|
||||
-- (\rawBody ->
|
||||
-- case
|
||||
-- rawBody
|
||||
-- |> MarkdownRenderer.view
|
||||
-- |> Result.map Tuple.second
|
||||
-- of
|
||||
-- Ok renderedBody ->
|
||||
-- OptimizedDecoder.succeed renderedBody
|
||||
--
|
||||
-- Err error ->
|
||||
-- OptimizedDecoder.fail error
|
||||
-- )
|
||||
-- )
|
||||
(StaticFile.frontmatter frontmatterDecoder)
|
||||
|
||||
|
||||
|
||||
--)
|
||||
|
||||
|
||||
type alias ArticleMetadata =
|
||||
{ title : String
|
||||
, description : String
|
||||
, published : Date
|
||||
, image : ImagePath
|
||||
, draft : Bool
|
||||
}
|
||||
|
||||
|
||||
frontmatterDecoder : OptimizedDecoder.Decoder ArticleMetadata
|
||||
frontmatterDecoder =
|
||||
OptimizedDecoder.map5 ArticleMetadata
|
||||
(OptimizedDecoder.field "title" OptimizedDecoder.string)
|
||||
(OptimizedDecoder.field "description" OptimizedDecoder.string)
|
||||
(OptimizedDecoder.field "published"
|
||||
(OptimizedDecoder.string
|
||||
|> OptimizedDecoder.andThen
|
||||
(\isoString ->
|
||||
case Date.fromIsoString isoString of
|
||||
Ok date ->
|
||||
OptimizedDecoder.succeed date
|
||||
|
||||
Err error ->
|
||||
OptimizedDecoder.fail error
|
||||
)
|
||||
)
|
||||
)
|
||||
(OptimizedDecoder.field "image" imageDecoder)
|
||||
(OptimizedDecoder.field "draft" OptimizedDecoder.bool
|
||||
|> OptimizedDecoder.maybe
|
||||
|> OptimizedDecoder.map (Maybe.withDefault False)
|
||||
)
|
||||
|
||||
|
||||
imageDecoder : OptimizedDecoder.Decoder ImagePath
|
||||
imageDecoder =
|
||||
OptimizedDecoder.string
|
||||
|> OptimizedDecoder.map (\cloudinaryAsset -> Cloudinary.url cloudinaryAsset Nothing 800)
|
53
examples/slides/src/Data/Author.elm
Normal file
@ -0,0 +1,53 @@
|
||||
module Data.Author exposing (Author, all, decoder, dillon, view)
|
||||
|
||||
import Cloudinary
|
||||
import Element exposing (Element)
|
||||
import Html.Attributes as Attr
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
import List.Extra
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
|
||||
|
||||
type alias Author =
|
||||
{ name : String
|
||||
, avatar : ImagePath
|
||||
, bio : String
|
||||
}
|
||||
|
||||
|
||||
all : List Author
|
||||
all =
|
||||
[ dillon
|
||||
]
|
||||
|
||||
|
||||
dillon : Author
|
||||
dillon =
|
||||
{ name = "Dillon Kearns"
|
||||
, avatar = Cloudinary.url "v1602899672/elm-radio/dillon-profile_n2lqst.jpg" Nothing 140
|
||||
, bio = "Elm developer and educator. Founder of Incremental Elm Consulting."
|
||||
}
|
||||
|
||||
|
||||
decoder : Decoder Author
|
||||
decoder =
|
||||
Decode.string
|
||||
|> Decode.andThen
|
||||
(\lookupName ->
|
||||
case List.Extra.find (\currentAuthor -> currentAuthor.name == lookupName) all of
|
||||
Just author ->
|
||||
Decode.succeed author
|
||||
|
||||
Nothing ->
|
||||
Decode.fail ("Couldn't find author with name " ++ lookupName ++ ". Options are " ++ String.join ", " (List.map .name all))
|
||||
)
|
||||
|
||||
|
||||
view : List (Element.Attribute msg) -> Author -> Element msg
|
||||
view attributes author =
|
||||
Element.image
|
||||
(Element.width (Element.px 70)
|
||||
:: Element.htmlAttribute (Attr.class "avatar")
|
||||
:: attributes
|
||||
)
|
||||
{ src = ImagePath.toString author.avatar, description = author.name }
|
71
examples/slides/src/DocSidebar.elm
Normal file
@ -0,0 +1,71 @@
|
||||
module DocSidebar exposing (view)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Element.Border as Border
|
||||
import Element.Font
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Palette
|
||||
|
||||
|
||||
view :
|
||||
PagePath
|
||||
-> Element msg
|
||||
view currentPage =
|
||||
Element.column
|
||||
[ Element.spacing 10
|
||||
, Border.widthEach { bottom = 0, left = 0, right = 1, top = 0 }
|
||||
, Border.color (Element.rgba255 40 80 40 0.4)
|
||||
, Element.padding 12
|
||||
, Element.height Element.fill
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
|
||||
--(posts
|
||||
-- |> List.filterMap
|
||||
-- (\( path, metadata ) ->
|
||||
-- case metadata of
|
||||
-- TemplateType.Documentation meta ->
|
||||
-- Just ( currentPage == path, path, meta )
|
||||
--
|
||||
-- _ ->
|
||||
-- Nothing
|
||||
-- )
|
||||
-- |> List.map postSummary
|
||||
--)
|
||||
|
||||
|
||||
postSummary :
|
||||
( Bool, PagePath, { title : String } )
|
||||
-> Element msg
|
||||
postSummary ( isCurrentPage, postPath, post ) =
|
||||
[ Element.text post.title ]
|
||||
|> Element.paragraph
|
||||
([ Element.Font.size 18
|
||||
, Element.Font.family [ Element.Font.typeface "Roboto" ]
|
||||
, Element.Font.semiBold
|
||||
, Element.padding 16
|
||||
]
|
||||
++ (if isCurrentPage then
|
||||
[ Element.Font.underline
|
||||
, Element.Font.color Palette.color.primary
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
)
|
||||
)
|
||||
|> linkToPost postPath
|
||||
|
||||
|
||||
linkToPost : PagePath -> Element msg -> Element msg
|
||||
linkToPost postPath content =
|
||||
Element.link [ Element.width Element.fill ]
|
||||
{ url = PagePath.toString postPath, label = content }
|
||||
|
||||
|
||||
docUrl : List String -> String
|
||||
docUrl postPath =
|
||||
"/"
|
||||
++ String.join "/" postPath
|
16
examples/slides/src/Document.elm
Normal file
@ -0,0 +1,16 @@
|
||||
module Document exposing (Document, map)
|
||||
|
||||
import Element exposing (Element)
|
||||
|
||||
|
||||
type alias Document msg =
|
||||
{ title : String
|
||||
, body : List (Element msg)
|
||||
}
|
||||
|
||||
|
||||
map : (msg1 -> msg2) -> Document msg1 -> Document msg2
|
||||
map fn doc =
|
||||
{ title = doc.title
|
||||
, body = List.map (Element.map fn) doc.body
|
||||
}
|
91
examples/slides/src/DocumentSvg.elm
Normal file
@ -0,0 +1,91 @@
|
||||
module DocumentSvg exposing (view)
|
||||
|
||||
import Color
|
||||
import Element exposing (Element)
|
||||
import Svg exposing (..)
|
||||
import Svg.Attributes exposing (..)
|
||||
|
||||
|
||||
strokeColor =
|
||||
-- "url(#grad1)"
|
||||
"black"
|
||||
|
||||
|
||||
pageTextColor =
|
||||
"black"
|
||||
|
||||
|
||||
fillColor =
|
||||
"url(#grad1)"
|
||||
|
||||
|
||||
|
||||
-- "none"
|
||||
|
||||
|
||||
fillGradient =
|
||||
gradient
|
||||
(Color.rgb255 5 117 230)
|
||||
(Color.rgb255 0 242 96)
|
||||
|
||||
|
||||
|
||||
-- (Color.rgb255 252 0 255)
|
||||
-- (Color.rgb255 0 219 222)
|
||||
-- (Color.rgb255 255 93 194)
|
||||
-- (Color.rgb255 255 150 250)
|
||||
|
||||
|
||||
gradient color1 color2 =
|
||||
linearGradient [ id "grad1", x1 "0%", y1 "0%", x2 "100%", y2 "0%" ]
|
||||
[ stop
|
||||
[ offset "10%"
|
||||
, Svg.Attributes.style ("stop-color:" ++ Color.toCssString color1 ++ ";stop-opacity:1")
|
||||
]
|
||||
[]
|
||||
, stop [ offset "100%", Svg.Attributes.style ("stop-color:" ++ Color.toCssString color2 ++ ";stop-opacity:1") ] []
|
||||
]
|
||||
|
||||
|
||||
view : Element msg
|
||||
view =
|
||||
svg
|
||||
[ version "1.1"
|
||||
, viewBox "251.0485 144.52063 56.114286 74.5"
|
||||
, width "56.114286"
|
||||
, height "74.5"
|
||||
, Svg.Attributes.width "30px"
|
||||
]
|
||||
[ defs []
|
||||
[ fillGradient ]
|
||||
, metadata [] []
|
||||
, g
|
||||
[ id "Canvas_11"
|
||||
, stroke "none"
|
||||
, fill fillColor
|
||||
, strokeOpacity "1"
|
||||
, fillOpacity "1"
|
||||
, strokeDasharray "none"
|
||||
]
|
||||
[ g [ id "Canvas_11: Layer 1" ]
|
||||
[ g [ id "Group_38" ]
|
||||
[ g [ id "Graphic_32" ]
|
||||
[ Svg.path
|
||||
[ d "M 252.5485 146.02063 L 252.5485 217.52063 L 305.66277 217.52063 L 305.66277 161.68254 L 290.00087 146.02063 Z"
|
||||
, stroke strokeColor
|
||||
, strokeLinecap "round"
|
||||
, strokeLinejoin "round"
|
||||
, strokeWidth "3"
|
||||
]
|
||||
[]
|
||||
]
|
||||
, g [ id "Line_34" ] [ line [ x1 "266.07286", y1 "182.8279", x2 "290.75465", y2 "183.00997", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_35" ] [ line [ x1 "266.07286", y1 "191.84156", x2 "290.75465", y2 "192.02363", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_36" ] [ line [ x1 "266.07286", y1 "200.85522", x2 "290.75465", y2 "201.0373", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_37" ] [ line [ x1 "266.07286", y1 "164.80058", x2 "278.3874", y2 "164.94049", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|> Element.html
|
||||
|> Element.el []
|
54
examples/slides/src/Dotted.elm
Normal file
@ -0,0 +1,54 @@
|
||||
module Dotted exposing (lines)
|
||||
|
||||
import Element
|
||||
import Svg
|
||||
import Svg.Attributes as Attr
|
||||
|
||||
|
||||
|
||||
{-
|
||||
.css-m2heu9 {
|
||||
stroke: #8a4baf;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 0.5 10;
|
||||
-webkit-animation: animation-yweh2o 400ms linear infinite;
|
||||
animation: animation-yweh2o 400ms linear infinite;
|
||||
}
|
||||
-}
|
||||
{-
|
||||
<svg width="20" height="30" viewBox="0 0 20 30" class="css-p2euw5">
|
||||
<path d="M10 40 L10 -10" class="css-m2heu9"></path>
|
||||
</svg>
|
||||
-}
|
||||
|
||||
|
||||
lines =
|
||||
Svg.svg
|
||||
[ Attr.width "20"
|
||||
, Attr.height "30"
|
||||
, Attr.viewBox "0 0 20 30"
|
||||
]
|
||||
[ Svg.path
|
||||
[ Attr.stroke "#2a75ff"
|
||||
, Attr.strokeWidth "4"
|
||||
, Attr.strokeLinecap "round"
|
||||
, Attr.strokeDasharray "0.5 10"
|
||||
, Attr.d "M10 40 L10 -10"
|
||||
, Attr.class "dotted-line"
|
||||
]
|
||||
[]
|
||||
]
|
||||
|> Element.html
|
||||
|> Element.el
|
||||
[ Element.centerX
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- rgb(0, 36, 71)
|
||||
-- #002447
|
||||
{-
|
||||
|
||||
.css-m2heu9{stroke:#8a4baf;stroke-width:3;stroke-linecap:round;stroke-dasharray:0.5 10;-webkit-animation:animation-yweh2o 400ms linear infinite;animation:animation-yweh2o 400ms linear infinite;}@-webkit-keyframes animation-yweh2o{to{stroke-dashoffset:10;}}@keyframes animation-yweh2o{to{stroke-dashoffset:10;}}
|
||||
-}
|
20
examples/slides/src/Ellie.elm
Normal file
@ -0,0 +1,20 @@
|
||||
module Ellie exposing (outputTab)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Html
|
||||
import Html.Attributes as Attr
|
||||
|
||||
|
||||
outputTab : String -> Element msg
|
||||
outputTab ellieId =
|
||||
Html.iframe
|
||||
[ Attr.src <| "https://ellie-app.com/embed/" ++ ellieId ++ "?panel=output"
|
||||
, Attr.style "width" "100%"
|
||||
, Attr.style "height" "400px"
|
||||
, Attr.style "border" "0"
|
||||
, Attr.style "overflow" "hidden"
|
||||
, Attr.attribute "sandbox" "allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
|
||||
]
|
||||
[]
|
||||
|> Element.html
|
||||
|> Element.el [ Element.width Element.fill ]
|
76
examples/slides/src/Feed.elm
Normal file
@ -0,0 +1,76 @@
|
||||
module Feed exposing (fileToGenerate)
|
||||
|
||||
import Metadata exposing (Metadata(..))
|
||||
import Pages
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Rss
|
||||
|
||||
|
||||
fileToGenerate :
|
||||
{ siteTagline : String
|
||||
, siteUrl : String
|
||||
}
|
||||
->
|
||||
List
|
||||
{ path : PagePath
|
||||
, frontmatter : Metadata
|
||||
, body : String
|
||||
}
|
||||
->
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
fileToGenerate config siteMetadata =
|
||||
{ path = [ "blog", "feed.xml" ]
|
||||
, content = generate config siteMetadata
|
||||
}
|
||||
|
||||
|
||||
generate :
|
||||
{ siteTagline : String
|
||||
, siteUrl : String
|
||||
}
|
||||
->
|
||||
List
|
||||
{ path : PagePath
|
||||
, frontmatter : Metadata
|
||||
, body : String
|
||||
}
|
||||
-> String
|
||||
generate { siteTagline, siteUrl } siteMetadata =
|
||||
Rss.generate
|
||||
{ title = "elm-pages Blog"
|
||||
, description = siteTagline
|
||||
, url = "https://elm-pages.com/blog"
|
||||
, lastBuildTime = Pages.builtAt
|
||||
, generator = Just "elm-pages"
|
||||
, items = siteMetadata |> List.filterMap metadataToRssItem
|
||||
, siteUrl = siteUrl
|
||||
}
|
||||
|
||||
|
||||
metadataToRssItem :
|
||||
{ path : PagePath
|
||||
, frontmatter : Metadata
|
||||
, body : String
|
||||
}
|
||||
-> Maybe Rss.Item
|
||||
metadataToRssItem page =
|
||||
case page.frontmatter of
|
||||
Article article ->
|
||||
if article.draft then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just
|
||||
{ title = article.title
|
||||
, description = article.description
|
||||
, url = PagePath.toString page.path
|
||||
, categories = []
|
||||
, author = article.author.name
|
||||
, pubDate = Rss.Date article.published
|
||||
, content = Nothing
|
||||
}
|
||||
|
||||
_ ->
|
||||
Nothing
|
18
examples/slides/src/FontAwesome.elm
Normal file
@ -0,0 +1,18 @@
|
||||
module FontAwesome exposing (icon, styledIcon)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Html
|
||||
import Html.Attributes
|
||||
|
||||
|
||||
styledIcon : String -> List (Element.Attribute msg) -> Element msg
|
||||
styledIcon classString styles =
|
||||
Html.i [ Html.Attributes.class classString ] []
|
||||
|> Element.html
|
||||
|> Element.el styles
|
||||
|
||||
|
||||
icon : String -> Element msg
|
||||
icon classString =
|
||||
Html.i [ Html.Attributes.class classString ] []
|
||||
|> Element.html
|
106
examples/slides/src/Index.elm
Normal file
@ -0,0 +1,106 @@
|
||||
module Index exposing (view)
|
||||
|
||||
import Article
|
||||
import Data.Author
|
||||
import Date
|
||||
import Element exposing (Element)
|
||||
import Element.Border
|
||||
import Element.Font
|
||||
import Pages
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
|
||||
|
||||
view :
|
||||
List ( PagePath, Article.ArticleMetadata )
|
||||
-> Element msg
|
||||
view posts =
|
||||
Element.column [ Element.spacing 20 ]
|
||||
(posts
|
||||
|> List.sortBy
|
||||
(\( _, metadata ) -> -(metadata.published |> Date.toRataDie))
|
||||
|> List.map postSummary
|
||||
)
|
||||
|
||||
|
||||
postSummary :
|
||||
( PagePath, Article.ArticleMetadata )
|
||||
-> Element msg
|
||||
postSummary ( path, post ) =
|
||||
articleIndex post |> linkToPost path
|
||||
|
||||
|
||||
|
||||
-- postPath
|
||||
|
||||
|
||||
linkToPost : PagePath -> Element msg -> Element msg
|
||||
linkToPost postPath content =
|
||||
Element.link [ Element.width Element.fill ]
|
||||
{ url = PagePath.toString postPath, label = content }
|
||||
|
||||
|
||||
title : String -> Element msg
|
||||
title text =
|
||||
[ Element.text text ]
|
||||
|> Element.paragraph
|
||||
[ Element.Font.size 36
|
||||
, Element.Font.center
|
||||
, Element.Font.family [ Element.Font.typeface "Montserrat" ]
|
||||
, Element.Font.semiBold
|
||||
, Element.padding 16
|
||||
]
|
||||
|
||||
|
||||
articleIndex : Article.ArticleMetadata -> Element msg
|
||||
articleIndex metadata =
|
||||
Element.el
|
||||
[ Element.centerX
|
||||
, Element.width (Element.maximum 600 Element.fill)
|
||||
, Element.padding 40
|
||||
, Element.spacing 10
|
||||
, Element.Border.width 1
|
||||
, Element.Border.color (Element.rgba255 0 0 0 0.1)
|
||||
, Element.mouseOver
|
||||
[ Element.Border.color (Element.rgba255 0 0 0 1)
|
||||
]
|
||||
]
|
||||
(postPreview metadata)
|
||||
|
||||
|
||||
grey =
|
||||
Element.Font.color (Element.rgba255 0 0 0 0.5)
|
||||
|
||||
|
||||
postPreview : Article.ArticleMetadata -> Element msg
|
||||
postPreview post =
|
||||
let
|
||||
author =
|
||||
Data.Author.dillon
|
||||
in
|
||||
Element.textColumn
|
||||
[ Element.centerX
|
||||
, Element.width Element.fill
|
||||
, Element.spacing 30
|
||||
, Element.Font.size 18
|
||||
]
|
||||
[ title post.title
|
||||
, Element.image [ Element.width Element.fill ] { src = post.image |> ImagePath.toString, description = "Blog post cover photo" }
|
||||
, Element.row
|
||||
[ Element.spacing 10
|
||||
, Element.centerX
|
||||
, grey
|
||||
]
|
||||
[ Data.Author.view [ Element.width (Element.px 40) ] author
|
||||
, Element.text author.name
|
||||
, Element.text "•"
|
||||
, Element.text (post.published |> Date.format "MMMM ddd, yyyy")
|
||||
]
|
||||
, post.description
|
||||
|> Element.text
|
||||
|> List.singleton
|
||||
|> Element.paragraph
|
||||
[ Element.Font.size 22
|
||||
, Element.Font.center
|
||||
]
|
||||
]
|
313
examples/slides/src/MarkdownRenderer.elm
Normal file
@ -0,0 +1,313 @@
|
||||
module MarkdownRenderer exposing (TableOfContents, renderer, view)
|
||||
|
||||
import Dotted
|
||||
import Element exposing (Element)
|
||||
import Element.Background
|
||||
import Element.Border
|
||||
import Element.Font as Font
|
||||
import Element.Input
|
||||
import Element.Region
|
||||
import Ellie
|
||||
import Html
|
||||
import Html.Attributes
|
||||
import Markdown.Block as Block exposing (Block, Inline, ListItem(..), Task(..))
|
||||
import Markdown.Html
|
||||
import Markdown.Parser
|
||||
import Markdown.Renderer
|
||||
import Oembed
|
||||
import Palette
|
||||
import SyntaxHighlight
|
||||
|
||||
|
||||
buildToc : List Block -> TableOfContents
|
||||
buildToc blocks =
|
||||
let
|
||||
headings =
|
||||
gatherHeadings blocks
|
||||
in
|
||||
headings
|
||||
|> List.map Tuple.second
|
||||
|> List.map
|
||||
(\styledList ->
|
||||
{ anchorId = styledToString styledList |> rawTextToId
|
||||
, name = styledToString styledList
|
||||
, level = 1
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
type alias TableOfContents =
|
||||
List { anchorId : String, name : String, level : Int }
|
||||
|
||||
|
||||
view : String -> Result String ( TableOfContents, List (Element msg) )
|
||||
view markdown =
|
||||
case
|
||||
markdown
|
||||
|> Markdown.Parser.parse
|
||||
of
|
||||
Ok okAst ->
|
||||
case Markdown.Renderer.render renderer okAst of
|
||||
Ok rendered ->
|
||||
Ok ( buildToc okAst, rendered )
|
||||
|
||||
Err errors ->
|
||||
Err errors
|
||||
|
||||
Err error ->
|
||||
Err (error |> List.map Markdown.Parser.deadEndToString |> String.join "\n")
|
||||
|
||||
|
||||
renderer : Markdown.Renderer.Renderer (Element msg)
|
||||
renderer =
|
||||
{ heading = heading
|
||||
, paragraph =
|
||||
Element.paragraph
|
||||
[ Element.spacing 15 ]
|
||||
, thematicBreak = Element.none
|
||||
, text = \value -> Element.paragraph [] [ Element.text value ]
|
||||
, strong = \content -> Element.paragraph [ Font.bold ] content
|
||||
, emphasis = \content -> Element.paragraph [ Font.italic ] content
|
||||
, codeSpan = code
|
||||
, link =
|
||||
\{ destination } body ->
|
||||
Element.newTabLink []
|
||||
{ url = destination
|
||||
, label =
|
||||
Element.paragraph
|
||||
[ Font.color (Element.rgb255 0 0 255)
|
||||
, Element.htmlAttribute (Html.Attributes.style "overflow-wrap" "break-word")
|
||||
, Element.htmlAttribute (Html.Attributes.style "word-break" "break-word")
|
||||
]
|
||||
body
|
||||
}
|
||||
, hardLineBreak = Html.br [] [] |> Element.html
|
||||
, image =
|
||||
\image ->
|
||||
case image.title of
|
||||
Just _ ->
|
||||
Element.image [ Element.width Element.fill ] { src = image.src, description = image.alt }
|
||||
|
||||
Nothing ->
|
||||
Element.image [ Element.width Element.fill ] { src = image.src, description = image.alt }
|
||||
, blockQuote =
|
||||
\children ->
|
||||
Element.column
|
||||
[ Element.Border.widthEach { top = 0, right = 0, bottom = 0, left = 10 }
|
||||
, Element.padding 10
|
||||
, Element.Border.color (Element.rgb255 145 145 145)
|
||||
, Element.Background.color (Element.rgb255 245 245 245)
|
||||
]
|
||||
children
|
||||
, unorderedList =
|
||||
\items ->
|
||||
Element.column [ Element.spacing 15 ]
|
||||
(items
|
||||
|> List.map
|
||||
(\(ListItem task children) ->
|
||||
Element.paragraph [ Element.spacing 5 ]
|
||||
[ Element.row
|
||||
[ Element.alignTop ]
|
||||
((case task of
|
||||
IncompleteTask ->
|
||||
Element.Input.defaultCheckbox False
|
||||
|
||||
CompletedTask ->
|
||||
Element.Input.defaultCheckbox True
|
||||
|
||||
NoTask ->
|
||||
Element.text "•"
|
||||
)
|
||||
:: Element.text " "
|
||||
:: children
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
, orderedList =
|
||||
\startingIndex items ->
|
||||
Element.column [ Element.spacing 15 ]
|
||||
(items
|
||||
|> List.indexedMap
|
||||
(\index itemBlocks ->
|
||||
Element.row [ Element.spacing 5 ]
|
||||
[ Element.row [ Element.alignTop ]
|
||||
(Element.text (String.fromInt (index + startingIndex) ++ " ") :: itemBlocks)
|
||||
]
|
||||
)
|
||||
)
|
||||
, codeBlock = codeBlock
|
||||
, table = Element.column []
|
||||
, tableHeader = Element.column []
|
||||
, tableBody = Element.column []
|
||||
, tableRow = Element.row []
|
||||
, tableHeaderCell =
|
||||
\_ children ->
|
||||
Element.paragraph [] children
|
||||
, tableCell = Element.paragraph []
|
||||
, html =
|
||||
Markdown.Html.oneOf
|
||||
[ Markdown.Html.tag "banner"
|
||||
(\children ->
|
||||
Element.paragraph
|
||||
[ Font.center
|
||||
, Font.size 47
|
||||
, Font.family [ Font.typeface "Montserrat" ]
|
||||
, Font.color Palette.color.primary
|
||||
]
|
||||
children
|
||||
)
|
||||
, Markdown.Html.tag "boxes"
|
||||
(\children ->
|
||||
children
|
||||
|> List.indexedMap
|
||||
(\index aBox ->
|
||||
let
|
||||
isLast =
|
||||
index == (List.length children - 1)
|
||||
in
|
||||
[ Just aBox
|
||||
, if isLast then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just Dotted.lines
|
||||
]
|
||||
|> List.filterMap identity
|
||||
)
|
||||
|> List.concat
|
||||
|> List.reverse
|
||||
|> Element.column [ Element.centerX ]
|
||||
)
|
||||
, Markdown.Html.tag "box"
|
||||
(\children ->
|
||||
Element.textColumn
|
||||
[ Element.centerX
|
||||
, Font.center
|
||||
, Element.padding 30
|
||||
, Element.Border.shadow { offset = ( 2, 2 ), size = 3, blur = 3, color = Element.rgba255 40 80 80 0.1 }
|
||||
, Element.spacing 15
|
||||
]
|
||||
children
|
||||
)
|
||||
, Markdown.Html.tag "values"
|
||||
(\children ->
|
||||
Element.row
|
||||
[ Element.spacing 30
|
||||
, Element.htmlAttribute (Html.Attributes.style "flex-wrap" "wrap")
|
||||
]
|
||||
children
|
||||
)
|
||||
, Markdown.Html.tag "value"
|
||||
(\children ->
|
||||
Element.column
|
||||
[ Element.width Element.fill
|
||||
, Element.padding 20
|
||||
, Element.spacing 20
|
||||
, Element.height Element.fill
|
||||
, Element.centerX
|
||||
]
|
||||
children
|
||||
)
|
||||
, Markdown.Html.tag "oembed"
|
||||
(\url _ ->
|
||||
Oembed.view [] Nothing url
|
||||
|> Maybe.map Element.html
|
||||
|> Maybe.withDefault Element.none
|
||||
|> Element.el [ Element.centerX ]
|
||||
)
|
||||
|> Markdown.Html.withAttribute "url"
|
||||
, Markdown.Html.tag "ellie-output"
|
||||
(\ellieId _ ->
|
||||
-- Oembed.view [] Nothing url
|
||||
-- |> Maybe.map Element.html
|
||||
-- |> Maybe.withDefault Element.none
|
||||
-- |> Element.el [ Element.centerX ]
|
||||
Ellie.outputTab ellieId
|
||||
)
|
||||
|> Markdown.Html.withAttribute "id"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
styledToString : List Inline -> String
|
||||
styledToString inlines =
|
||||
--List.map .string list
|
||||
--|> String.join "-"
|
||||
-- TODO do I need to hyphenate?
|
||||
inlines
|
||||
|> Block.extractInlineText
|
||||
|
||||
|
||||
gatherHeadings : List Block -> List ( Block.HeadingLevel, List Inline )
|
||||
gatherHeadings blocks =
|
||||
List.filterMap
|
||||
(\block ->
|
||||
case block of
|
||||
Block.Heading level content ->
|
||||
Just ( level, content )
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
blocks
|
||||
|
||||
|
||||
rawTextToId : String -> String
|
||||
rawTextToId rawText =
|
||||
rawText
|
||||
|> String.split " "
|
||||
|> String.join "-"
|
||||
|> String.toLower
|
||||
|
||||
|
||||
heading : { level : Block.HeadingLevel, rawText : String, children : List (Element msg) } -> Element msg
|
||||
heading { level, rawText, children } =
|
||||
Element.paragraph
|
||||
[ Font.size
|
||||
(case level of
|
||||
Block.H1 ->
|
||||
36
|
||||
|
||||
Block.H2 ->
|
||||
24
|
||||
|
||||
_ ->
|
||||
20
|
||||
)
|
||||
, Font.bold
|
||||
, Font.family [ Font.typeface "Montserrat" ]
|
||||
, Element.Region.heading (Block.headingLevelToInt level)
|
||||
, Element.htmlAttribute
|
||||
(Html.Attributes.attribute "name" (rawTextToId rawText))
|
||||
, Element.htmlAttribute
|
||||
(Html.Attributes.id (rawTextToId rawText))
|
||||
]
|
||||
children
|
||||
|
||||
|
||||
code : String -> Element msg
|
||||
code snippet =
|
||||
Element.el
|
||||
[ Element.Background.color
|
||||
(Element.rgba255 50 50 50 0.07)
|
||||
, Element.Border.rounded 2
|
||||
, Element.paddingXY 5 3
|
||||
, Font.family [ Font.typeface "Roboto Mono", Font.monospace ]
|
||||
]
|
||||
(Element.text snippet)
|
||||
|
||||
|
||||
codeBlock : { body : String, language : Maybe String } -> Element msg
|
||||
codeBlock details =
|
||||
Element.paragraph [] [ Element.text details.body ]
|
||||
|
||||
|
||||
|
||||
-- TODO turn this back on - it's off for now to get more accurate performance benchmarks
|
||||
--SyntaxHighlight.elm details.body
|
||||
-- |> Result.map (SyntaxHighlight.toBlockHtml (Just 1))
|
||||
-- |> Result.withDefault
|
||||
-- (Html.pre [] [ Html.code [] [ Html.text details.body ] ])
|
||||
-- |> Element.html
|
||||
-- |> Element.el [ Element.width Element.fill ]
|
29
examples/slides/src/MySitemap.elm
Normal file
@ -0,0 +1,29 @@
|
||||
module MySitemap exposing (install)
|
||||
|
||||
import Head
|
||||
import Pages.Platform exposing (Builder)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Sitemap
|
||||
|
||||
|
||||
install :
|
||||
{ siteUrl : String
|
||||
}
|
||||
-> (List item -> List { path : String, lastMod : Maybe String })
|
||||
-> StaticHttp.Request (List item)
|
||||
-> Builder pathKey userModel userMsg route
|
||||
-> Builder pathKey userModel userMsg route
|
||||
install config toSitemapEntry request builder =
|
||||
builder
|
||||
|> Pages.Platform.withGlobalHeadTags [ Head.sitemapLink "/sitemap.xml" ]
|
||||
|> Pages.Platform.withFileGenerator
|
||||
(request
|
||||
|> StaticHttp.map
|
||||
(\items ->
|
||||
[ Ok
|
||||
{ path = [ "sitemap.xml" ]
|
||||
, content = Sitemap.build config (toSitemapEntry items)
|
||||
}
|
||||
]
|
||||
)
|
||||
)
|
44
examples/slides/src/Palette.elm
Normal file
@ -0,0 +1,44 @@
|
||||
module Palette exposing (blogHeading, color, heading)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Element.Font as Font
|
||||
import Element.Region
|
||||
|
||||
|
||||
color =
|
||||
{ primary = Element.rgb255 0 6 255
|
||||
, secondary = Element.rgb255 0 242 96
|
||||
}
|
||||
|
||||
|
||||
heading : Int -> List (Element msg) -> Element msg
|
||||
heading level content =
|
||||
Element.paragraph
|
||||
([ Font.bold
|
||||
, Font.family [ Font.typeface "Montserrat" ]
|
||||
, Element.Region.heading level
|
||||
]
|
||||
++ (case level of
|
||||
1 ->
|
||||
[ Font.size 36 ]
|
||||
|
||||
2 ->
|
||||
[ Font.size 24 ]
|
||||
|
||||
_ ->
|
||||
[ Font.size 20 ]
|
||||
)
|
||||
)
|
||||
content
|
||||
|
||||
|
||||
blogHeading : String -> Element msg
|
||||
blogHeading title =
|
||||
Element.paragraph
|
||||
[ Font.bold
|
||||
, Font.family [ Font.typeface "Montserrat" ]
|
||||
, Element.Region.heading 1
|
||||
, Font.size 36
|
||||
, Font.center
|
||||
]
|
||||
[ Element.text title ]
|
53
examples/slides/src/RssPlugin.elm
Normal file
@ -0,0 +1,53 @@
|
||||
module RssPlugin exposing (generate)
|
||||
|
||||
import Head
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.Platform exposing (Builder)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Rss
|
||||
import Time
|
||||
|
||||
|
||||
generate :
|
||||
{ siteTagline : String
|
||||
, siteUrl : String
|
||||
, title : String
|
||||
, builtAt : Time.Posix
|
||||
, indexPage : PagePath
|
||||
}
|
||||
-> (item -> Maybe Rss.Item)
|
||||
-> StaticHttp.Request (List item)
|
||||
-> Builder pathKey userModel userMsg route
|
||||
-> Builder pathKey userModel userMsg route
|
||||
generate options metadataToRssItem itemsRequest builder =
|
||||
let
|
||||
feedFilePath =
|
||||
(options.indexPage
|
||||
|> PagePath.toPath
|
||||
)
|
||||
++ [ "feed.xml" ]
|
||||
in
|
||||
builder
|
||||
|> Pages.Platform.withFileGenerator
|
||||
(itemsRequest
|
||||
|> StaticHttp.map
|
||||
(\items ->
|
||||
{ path = feedFilePath
|
||||
, content =
|
||||
Rss.generate
|
||||
{ title = options.title
|
||||
, description = options.siteTagline
|
||||
|
||||
-- TODO make sure you don't add an extra "/"
|
||||
, url = options.siteUrl ++ "/" ++ PagePath.toString options.indexPage
|
||||
, lastBuildTime = options.builtAt
|
||||
, generator = Just "elm-pages"
|
||||
, items = items |> List.filterMap metadataToRssItem
|
||||
, siteUrl = options.siteUrl
|
||||
}
|
||||
}
|
||||
|> Ok
|
||||
|> List.singleton
|
||||
)
|
||||
)
|
||||
|> Pages.Platform.withGlobalHeadTags [ Head.rssLink (feedFilePath |> String.join "/") ]
|
48
examples/slides/src/ServerRequest.elm
Normal file
@ -0,0 +1,48 @@
|
||||
module ServerRequest exposing (ServerRequest, expectHeader, init, optionalHeader, staticData, toStaticHttp)
|
||||
|
||||
import Internal.OptimizedDecoder exposing (OptimizedDecoder)
|
||||
import OptimizedDecoder
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Secrets
|
||||
|
||||
|
||||
type ServerRequest decodesTo
|
||||
= ServerRequest (OptimizedDecoder.Decoder decodesTo)
|
||||
|
||||
|
||||
init : constructor -> ServerRequest constructor
|
||||
init constructor =
|
||||
ServerRequest (OptimizedDecoder.succeed constructor)
|
||||
|
||||
|
||||
staticData : StaticHttp.Request String
|
||||
staticData =
|
||||
StaticHttp.get (Secrets.succeed "$$elm-pages$$headers")
|
||||
(OptimizedDecoder.field "headers"
|
||||
(OptimizedDecoder.field "accept-language" OptimizedDecoder.string)
|
||||
)
|
||||
|
||||
|
||||
toStaticHttp : ServerRequest decodesTo -> StaticHttp.Request decodesTo
|
||||
toStaticHttp (ServerRequest decoder) =
|
||||
StaticHttp.get (Secrets.succeed "$$elm-pages$$headers") decoder
|
||||
|
||||
|
||||
expectHeader : String -> ServerRequest (String -> value) -> ServerRequest value
|
||||
expectHeader headerName (ServerRequest decoder) =
|
||||
decoder
|
||||
|> OptimizedDecoder.andMap
|
||||
(OptimizedDecoder.field headerName OptimizedDecoder.string
|
||||
|> OptimizedDecoder.field "headers"
|
||||
)
|
||||
|> ServerRequest
|
||||
|
||||
|
||||
optionalHeader : String -> ServerRequest (Maybe String -> value) -> ServerRequest value
|
||||
optionalHeader headerName (ServerRequest decoder) =
|
||||
decoder
|
||||
|> OptimizedDecoder.andMap
|
||||
(OptimizedDecoder.optionalField headerName OptimizedDecoder.string
|
||||
|> OptimizedDecoder.field "headers"
|
||||
)
|
||||
|> ServerRequest
|
296
examples/slides/src/Shared.elm
Normal file
@ -0,0 +1,296 @@
|
||||
module Shared exposing (Model, Msg(..), SharedMsg(..), StaticData, template)
|
||||
|
||||
import Document exposing (Document)
|
||||
import DocumentSvg
|
||||
import Element exposing (Element)
|
||||
import Element.Background
|
||||
import Element.Border
|
||||
import Element.Events
|
||||
import Element.Font as Font
|
||||
import Element.Region
|
||||
import FontAwesome
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import OptimizedDecoder as D
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Palette
|
||||
import Secrets
|
||||
import SharedTemplate exposing (SharedTemplate)
|
||||
|
||||
|
||||
template : SharedTemplate Msg Model StaticData msg
|
||||
template =
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, staticData = staticData
|
||||
, subscriptions = subscriptions
|
||||
, onPageChange = Just OnPageChange
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= OnPageChange
|
||||
{ path : PagePath
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
| ToggleMobileMenu
|
||||
| Increment
|
||||
| SharedMsg SharedMsg
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
Int
|
||||
|
||||
|
||||
type SharedMsg
|
||||
= IncrementFromChild
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ showMobileMenu : Bool
|
||||
, counter : Int
|
||||
}
|
||||
|
||||
|
||||
init :
|
||||
Maybe
|
||||
{ path :
|
||||
{ path : PagePath
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
, metadata : route
|
||||
}
|
||||
-> ( Model, Cmd Msg )
|
||||
init maybePagePath =
|
||||
( { showMobileMenu = False
|
||||
, counter = 0
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
OnPageChange _ ->
|
||||
( { model | showMobileMenu = False }, Cmd.none )
|
||||
|
||||
ToggleMobileMenu ->
|
||||
( { model | showMobileMenu = not model.showMobileMenu }, Cmd.none )
|
||||
|
||||
Increment ->
|
||||
( { model | counter = model.counter + 1 }, Cmd.none )
|
||||
|
||||
SharedMsg globalMsg ->
|
||||
case globalMsg of
|
||||
IncrementFromChild ->
|
||||
( { model | counter = model.counter + 1 }, Cmd.none )
|
||||
|
||||
|
||||
subscriptions : PagePath -> Model -> Sub Msg
|
||||
subscriptions _ _ =
|
||||
Sub.none
|
||||
|
||||
|
||||
staticData : StaticHttp.Request StaticData
|
||||
staticData =
|
||||
StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
|
||||
(D.field "stargazers_count" D.int)
|
||||
|
||||
|
||||
view :
|
||||
StaticData
|
||||
->
|
||||
{ path : PagePath
|
||||
, frontmatter : route
|
||||
}
|
||||
-> Model
|
||||
-> (Msg -> msg)
|
||||
-> Document msg
|
||||
-> { body : Html msg, title : String }
|
||||
view stars page model toMsg pageView =
|
||||
{ body =
|
||||
(if model.showMobileMenu then
|
||||
Element.column
|
||||
[ Element.width Element.fill
|
||||
, Element.padding 20
|
||||
]
|
||||
[ Element.row [ Element.width Element.fill, Element.spaceEvenly ]
|
||||
[ logoLinkMobile |> Element.map toMsg
|
||||
, FontAwesome.styledIcon "fas fa-bars" [ Element.Events.onClick ToggleMobileMenu ]
|
||||
|> Element.map toMsg
|
||||
]
|
||||
, Element.column [ Element.centerX, Element.spacing 20 ]
|
||||
(navbarLinks stars page.path)
|
||||
]
|
||||
|
||||
else
|
||||
Element.column [ Element.width Element.fill ]
|
||||
(List.concat
|
||||
[ [ header stars page.path |> Element.map toMsg
|
||||
|
||||
--, incrementView model |> Element.map toMsg
|
||||
]
|
||||
, pageView.body
|
||||
]
|
||||
)
|
||||
)
|
||||
|> Element.layout
|
||||
[ Element.width Element.fill
|
||||
, Font.size 20
|
||||
, Font.family [ Font.typeface "Roboto" ]
|
||||
, Font.color (Element.rgba255 0 0 0 0.8)
|
||||
]
|
||||
, title = pageView.title
|
||||
}
|
||||
|
||||
|
||||
logoLinkMobile =
|
||||
Element.link []
|
||||
{ url = "/"
|
||||
, label =
|
||||
Element.row
|
||||
[ Font.size 30
|
||||
, Element.spacing 16
|
||||
, Element.htmlAttribute (Attr.class "navbar-title")
|
||||
]
|
||||
[ Element.text "elm-pages"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
navbarLinks stars currentPath =
|
||||
[ elmDocsLink
|
||||
, githubRepoLink stars
|
||||
, highlightableLink currentPath [ "docs" ] "Docs"
|
||||
, highlightableLink currentPath [ "showcase" ] "Showcase"
|
||||
, highlightableLink currentPath [ "blog" ] "Blog"
|
||||
]
|
||||
|
||||
|
||||
header : Int -> PagePath -> Element Msg
|
||||
header stars currentPath =
|
||||
Element.column [ Element.width Element.fill ]
|
||||
[ responsiveHeader
|
||||
, Element.column
|
||||
[ Element.width Element.fill
|
||||
, Element.htmlAttribute (Attr.class "responsive-desktop")
|
||||
]
|
||||
[ Element.el
|
||||
[ Element.height (Element.px 4)
|
||||
, Element.width Element.fill
|
||||
, Element.Background.gradient
|
||||
{ angle = 0.2
|
||||
, steps =
|
||||
[ Element.rgb255 0 242 96
|
||||
, Element.rgb255 5 117 230
|
||||
]
|
||||
}
|
||||
]
|
||||
Element.none
|
||||
, Element.row
|
||||
[ Element.paddingXY 25 4
|
||||
, Element.spaceEvenly
|
||||
, Element.width Element.fill
|
||||
, Element.Region.navigation
|
||||
, Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 }
|
||||
, Element.Border.color (Element.rgba255 40 80 40 0.4)
|
||||
]
|
||||
[ logoLink
|
||||
, Element.row [ Element.spacing 15 ] (navbarLinks stars currentPath)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
logoLink =
|
||||
Element.link []
|
||||
{ url = "/"
|
||||
, label =
|
||||
Element.row
|
||||
[ Font.size 30
|
||||
, Element.spacing 16
|
||||
, Element.htmlAttribute (Attr.class "navbar-title")
|
||||
]
|
||||
[ DocumentSvg.view
|
||||
, Element.text "elm-pages"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
responsiveHeader =
|
||||
Element.row
|
||||
[ Element.width Element.fill
|
||||
, Element.spaceEvenly
|
||||
, Element.htmlAttribute (Attr.class "responsive-mobile")
|
||||
, Element.width Element.fill
|
||||
, Element.padding 20
|
||||
]
|
||||
[ logoLinkMobile
|
||||
, FontAwesome.icon "fas fa-bars" |> Element.el [ Element.alignRight, Element.Events.onClick ToggleMobileMenu ]
|
||||
]
|
||||
|
||||
|
||||
githubRepoLink : Int -> Element msg
|
||||
githubRepoLink starCount =
|
||||
Element.newTabLink []
|
||||
{ url = "https://github.com/dillonkearns/elm-pages"
|
||||
, label =
|
||||
Element.row [ Element.spacing 5 ]
|
||||
[ Element.image
|
||||
[ Element.width (Element.px 22)
|
||||
, Font.color Palette.color.primary
|
||||
]
|
||||
{ src = "/images/github.svg", description = "Github repo" }
|
||||
, Element.text <| String.fromInt starCount
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
elmDocsLink : Element msg
|
||||
elmDocsLink =
|
||||
Element.newTabLink []
|
||||
{ url = "https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/"
|
||||
, label =
|
||||
Element.image
|
||||
[ Element.width (Element.px 22)
|
||||
, Font.color Palette.color.primary
|
||||
]
|
||||
{ src = "/images/elm-logo.svg", description = "Elm Package Docs" }
|
||||
}
|
||||
|
||||
|
||||
highlightableLink :
|
||||
PagePath
|
||||
-> List String
|
||||
-> String
|
||||
-> Element msg
|
||||
highlightableLink currentPath linkDirectory displayName =
|
||||
let
|
||||
isHighlighted =
|
||||
(currentPath |> PagePath.toPath)
|
||||
== linkDirectory
|
||||
|| (currentPath
|
||||
|> PagePath.toPath
|
||||
|> List.reverse
|
||||
|> List.drop 1
|
||||
|> List.reverse
|
||||
)
|
||||
== linkDirectory
|
||||
in
|
||||
Element.link
|
||||
(if isHighlighted then
|
||||
[ Font.underline
|
||||
, Font.color Palette.color.primary
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
)
|
||||
{ url = linkDirectory |> String.join "/"
|
||||
, label = Element.text displayName
|
||||
}
|
42
examples/slides/src/SharedTemplate.elm
Normal file
@ -0,0 +1,42 @@
|
||||
module SharedTemplate exposing (..)
|
||||
|
||||
import Document exposing (Document)
|
||||
import Html exposing (Html)
|
||||
import Pages.PagePath exposing (PagePath)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Route exposing (Route)
|
||||
|
||||
|
||||
type alias SharedTemplate sharedMsg sharedModel sharedStaticData mappedMsg =
|
||||
{ init :
|
||||
Maybe
|
||||
{ path :
|
||||
{ path : PagePath
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
, metadata : Maybe Route
|
||||
}
|
||||
-> ( sharedModel, Cmd sharedMsg )
|
||||
, update : sharedMsg -> sharedModel -> ( sharedModel, Cmd sharedMsg )
|
||||
, view :
|
||||
sharedStaticData
|
||||
->
|
||||
{ path : PagePath
|
||||
, frontmatter : Maybe Route
|
||||
}
|
||||
-> sharedModel
|
||||
-> (sharedMsg -> mappedMsg)
|
||||
-> Document mappedMsg
|
||||
-> { body : Html mappedMsg, title : String }
|
||||
, staticData : StaticHttp.Request sharedStaticData
|
||||
, subscriptions : PagePath -> sharedModel -> Sub sharedMsg
|
||||
, onPageChange :
|
||||
Maybe
|
||||
({ path : PagePath
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
-> sharedMsg
|
||||
)
|
||||
}
|
160
examples/slides/src/Showcase.elm
Normal file
@ -0,0 +1,160 @@
|
||||
module Showcase exposing (..)
|
||||
|
||||
import Element
|
||||
import Element.Border
|
||||
import Element.Font
|
||||
import FontAwesome
|
||||
import OptimizedDecoder as Decode
|
||||
import Pages.Secrets as Secrets
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Palette
|
||||
|
||||
|
||||
view : List Entry -> Element.Element msg
|
||||
view entries =
|
||||
Element.column
|
||||
[ Element.spacing 30
|
||||
]
|
||||
(submitShowcaseItemButton
|
||||
:: List.map entryView entries
|
||||
)
|
||||
|
||||
|
||||
submitShowcaseItemButton =
|
||||
Element.newTabLink
|
||||
[ Element.Font.color Palette.color.primary
|
||||
, Element.Font.underline
|
||||
]
|
||||
{ url = "https://airtable.com/shrPSenIW2EQqJ083"
|
||||
, label = Element.text "Submit your site to the showcase"
|
||||
}
|
||||
|
||||
|
||||
entryView : Entry -> Element.Element msg
|
||||
entryView entry =
|
||||
Element.column
|
||||
[ Element.spacing 15
|
||||
, Element.Border.shadow { offset = ( 2, 2 ), size = 3, blur = 3, color = Element.rgba255 40 80 80 0.1 }
|
||||
, Element.padding 40
|
||||
, Element.width (Element.maximum 700 Element.fill)
|
||||
]
|
||||
[ Element.newTabLink [ Element.Font.size 14, Element.Font.color Palette.color.primary ]
|
||||
{ url = entry.liveUrl
|
||||
, label =
|
||||
Element.image [ Element.width Element.fill ]
|
||||
{ src = "https://image.thum.io/get/width/800/crop/800/" ++ entry.screenshotUrl
|
||||
, description = "Site Screenshot"
|
||||
}
|
||||
}
|
||||
, Element.text entry.displayName |> Element.el [ Element.Font.extraBold ]
|
||||
, Element.newTabLink [ Element.Font.size 14, Element.Font.color Palette.color.primary ]
|
||||
{ url = entry.liveUrl
|
||||
, label = Element.text entry.liveUrl
|
||||
}
|
||||
, Element.paragraph [ Element.Font.size 14 ]
|
||||
[ Element.text "By "
|
||||
, Element.newTabLink [ Element.Font.color Palette.color.primary ]
|
||||
{ url = entry.authorUrl
|
||||
, label = Element.text entry.authorName
|
||||
}
|
||||
]
|
||||
, Element.row [ Element.width Element.fill ]
|
||||
[ categoriesView entry.categories
|
||||
, Element.row [ Element.alignRight ]
|
||||
[ case entry.repoUrl of
|
||||
Just repoUrl ->
|
||||
Element.newTabLink []
|
||||
{ url = repoUrl
|
||||
, label = FontAwesome.icon "fas fa-code-branch"
|
||||
}
|
||||
|
||||
Nothing ->
|
||||
Element.none
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
categoriesView : List String -> Element.Element msg
|
||||
categoriesView categories =
|
||||
categories
|
||||
|> List.map
|
||||
(\category ->
|
||||
Element.text category
|
||||
)
|
||||
|> Element.wrappedRow
|
||||
[ Element.spacing 7
|
||||
, Element.Font.size 14
|
||||
, Element.Font.color (Element.rgba255 0 0 0 0.6)
|
||||
, Element.width (Element.fillPortion 8)
|
||||
]
|
||||
|
||||
|
||||
type alias Entry =
|
||||
{ screenshotUrl : String
|
||||
, displayName : String
|
||||
, liveUrl : String
|
||||
, authorName : String
|
||||
, authorUrl : String
|
||||
, categories : List String
|
||||
, repoUrl : Maybe String
|
||||
}
|
||||
|
||||
|
||||
decoder : Decode.Decoder (List Entry)
|
||||
decoder =
|
||||
Decode.field "records" <|
|
||||
Decode.list entryDecoder
|
||||
|
||||
|
||||
entryDecoder : Decode.Decoder Entry
|
||||
entryDecoder =
|
||||
Decode.field "fields" <|
|
||||
Decode.map7 Entry
|
||||
(Decode.field "Screenshot URL" Decode.string)
|
||||
(Decode.field "Site Display Name" Decode.string)
|
||||
(Decode.field "Live URL" Decode.string)
|
||||
(Decode.field "Author" Decode.string)
|
||||
(Decode.field "Author URL" Decode.string)
|
||||
(Decode.field "Categories" (Decode.list Decode.string))
|
||||
(Decode.maybe (Decode.field "Repository URL" Decode.string))
|
||||
|
||||
|
||||
staticRequest : StaticHttp.Request (List Entry)
|
||||
staticRequest =
|
||||
StaticHttp.request
|
||||
(Secrets.succeed
|
||||
(\airtableToken ->
|
||||
{ url = "https://api.airtable.com/v0/appDykQzbkQJAidjt/elm-pages%20showcase?maxRecords=100&view=Grid%202"
|
||||
, method = "GET"
|
||||
, headers = [ ( "Authorization", "Bearer " ++ airtableToken ), ( "view", "viwayJBsr63qRd7q3" ) ]
|
||||
, body = StaticHttp.emptyBody
|
||||
}
|
||||
)
|
||||
|> Secrets.with "AIRTABLE_TOKEN"
|
||||
)
|
||||
decoder
|
||||
|
||||
|
||||
allCategroies : List String
|
||||
allCategroies =
|
||||
[ "Documentation"
|
||||
, "eCommerce"
|
||||
, "Conference"
|
||||
, "Consulting"
|
||||
, "Education"
|
||||
, "Entertainment"
|
||||
, "Event"
|
||||
, "Food"
|
||||
, "Freelance"
|
||||
, "Gallery"
|
||||
, "Landing Page"
|
||||
, "Music"
|
||||
, "Nonprofit"
|
||||
, "Podcast"
|
||||
, "Portfolio"
|
||||
, "Programming"
|
||||
, "Sports"
|
||||
, "Travel"
|
||||
, "Blog"
|
||||
]
|
143
examples/slides/src/Site.elm
Normal file
@ -0,0 +1,143 @@
|
||||
module Site exposing (config)
|
||||
|
||||
import Cloudinary
|
||||
import Color
|
||||
import Head
|
||||
import Json.Encode
|
||||
import MimeType
|
||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||
import Pages.Manifest as Manifest
|
||||
import Pages.Manifest.Category
|
||||
import Pages.PagePath as PagePath
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Route exposing (Route)
|
||||
import SiteConfig exposing (SiteConfig)
|
||||
import Sitemap
|
||||
|
||||
|
||||
config : SiteConfig StaticData
|
||||
config =
|
||||
\routes ->
|
||||
{ staticData = staticData
|
||||
, canonicalUrl = \_ -> canonicalUrl
|
||||
, manifest = manifest
|
||||
, head = head
|
||||
, generateFiles = generateFiles routes
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- TODO wire this in as part of the config
|
||||
|
||||
|
||||
generateFiles :
|
||||
List (Maybe Route)
|
||||
->
|
||||
StaticHttp.Request
|
||||
(List
|
||||
(Result
|
||||
String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
)
|
||||
)
|
||||
generateFiles allRoutes =
|
||||
StaticHttp.succeed
|
||||
[ siteMap allRoutes |> Ok
|
||||
]
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
{ siteName : String
|
||||
}
|
||||
|
||||
|
||||
staticData : StaticHttp.Request StaticData
|
||||
staticData =
|
||||
StaticHttp.map StaticData
|
||||
--(StaticFile.request "site-name.txt" StaticFile.body)
|
||||
(StaticHttp.succeed "site-name")
|
||||
|
||||
|
||||
head : StaticData -> List Head.Tag
|
||||
head static =
|
||||
[ Head.icon [ ( 32, 32 ) ] MimeType.Png (cloudinaryIcon MimeType.Png 32)
|
||||
, Head.icon [ ( 16, 16 ) ] MimeType.Png (cloudinaryIcon MimeType.Png 16)
|
||||
, Head.appleTouchIcon (Just 180) (cloudinaryIcon MimeType.Png 180)
|
||||
, Head.appleTouchIcon (Just 192) (cloudinaryIcon MimeType.Png 192)
|
||||
, Head.sitemapLink "/sitemap.xml"
|
||||
]
|
||||
|
||||
|
||||
canonicalUrl : String
|
||||
canonicalUrl =
|
||||
"https://elm-pages.com"
|
||||
|
||||
|
||||
manifest : StaticData -> Manifest.Config
|
||||
manifest static =
|
||||
{ backgroundColor = Just Color.white
|
||||
, categories = [ Pages.Manifest.Category.education ]
|
||||
, displayMode = Manifest.Standalone
|
||||
, orientation = Manifest.Portrait
|
||||
, description = "elm-pages - " ++ tagline
|
||||
, iarcRatingId = Nothing
|
||||
, name = static.siteName
|
||||
, themeColor = Just Color.white
|
||||
, startUrl = PagePath.build []
|
||||
, shortName = Just "elm-pages"
|
||||
, sourceIcon = ImagePath.build [ "images", "icon-png.png" ]
|
||||
, icons =
|
||||
[ icon webp 192
|
||||
, icon webp 512
|
||||
, icon MimeType.Png 192
|
||||
, icon MimeType.Png 512
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
tagline : String
|
||||
tagline =
|
||||
"A statically typed site generator"
|
||||
|
||||
|
||||
webp : MimeType.MimeImage
|
||||
webp =
|
||||
MimeType.OtherImage "webp"
|
||||
|
||||
|
||||
icon :
|
||||
MimeType.MimeImage
|
||||
-> Int
|
||||
-> Manifest.Icon
|
||||
icon format width =
|
||||
{ src = cloudinaryIcon format width
|
||||
, sizes = [ ( width, width ) ]
|
||||
, mimeType = format |> Just
|
||||
, purposes = [ Manifest.IconPurposeAny, Manifest.IconPurposeMaskable ]
|
||||
}
|
||||
|
||||
|
||||
cloudinaryIcon :
|
||||
MimeType.MimeImage
|
||||
-> Int
|
||||
-> ImagePath
|
||||
cloudinaryIcon mimeType width =
|
||||
Cloudinary.urlSquare "v1603234028/elm-pages/elm-pages-icon" (Just mimeType) width
|
||||
|
||||
|
||||
siteMap :
|
||||
List (Maybe Route)
|
||||
-> { path : List String, content : String }
|
||||
siteMap allRoutes =
|
||||
allRoutes
|
||||
|> List.filterMap identity
|
||||
|> List.map
|
||||
(\route ->
|
||||
{ path = Route.routeToPath (Just route) |> String.join "/"
|
||||
, lastMod = Nothing
|
||||
}
|
||||
)
|
||||
|> Sitemap.build { siteUrl = "https://elm-pages.com" }
|
||||
|> (\sitemapXmlString -> { path = [ "sitemap.xml" ], content = sitemapXmlString })
|
8
examples/slides/src/SiteConfig.elm
Normal file
@ -0,0 +1,8 @@
|
||||
module SiteConfig exposing (SiteConfig)
|
||||
|
||||
import Pages.SiteConfig
|
||||
import Route exposing (Route)
|
||||
|
||||
|
||||
type alias SiteConfig staticData =
|
||||
Pages.SiteConfig.SiteConfig (Maybe Route) staticData
|
11
examples/slides/src/SiteOld.elm
Normal file
@ -0,0 +1,11 @@
|
||||
module SiteOld exposing (canonicalUrl, tagline)
|
||||
|
||||
|
||||
canonicalUrl : String
|
||||
canonicalUrl =
|
||||
"https://elm-pages.com"
|
||||
|
||||
|
||||
tagline : String
|
||||
tagline =
|
||||
"A statically typed site generator - elm-pages"
|
138
examples/slides/src/Template/Slide/Number_.elm
Normal file
@ -0,0 +1,138 @@
|
||||
module Template.Slide.Number_ exposing (Model, Msg, template)
|
||||
|
||||
import Document exposing (Document)
|
||||
import Element exposing (Element)
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Markdown.Block
|
||||
import Markdown.Parser
|
||||
import Markdown.Renderer
|
||||
import MarkdownRenderer
|
||||
import OptimizedDecoder
|
||||
import Pages.ImagePath as ImagePath
|
||||
import Pages.StaticFile as StaticFile
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Shared
|
||||
import Template exposing (StaticPayload, Template, TemplateWithState)
|
||||
|
||||
|
||||
type alias Model =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
Never
|
||||
|
||||
|
||||
type alias RouteParams =
|
||||
{ number : String }
|
||||
|
||||
|
||||
template : Template RouteParams StaticData
|
||||
template =
|
||||
Template.withStaticData
|
||||
{ head = head
|
||||
, staticRoutes = StaticHttp.succeed [ { number = "1" } ]
|
||||
, staticData = staticData
|
||||
}
|
||||
|> Template.buildNoState { view = view }
|
||||
|
||||
|
||||
staticData : RouteParams -> StaticHttp.Request StaticData
|
||||
staticData route =
|
||||
StaticFile.request
|
||||
"slides.md"
|
||||
(StaticFile.body
|
||||
|> OptimizedDecoder.andThen
|
||||
(\rawBody ->
|
||||
case rawBody |> Markdown.Parser.parse of
|
||||
Ok okBlocks ->
|
||||
case
|
||||
okBlocks
|
||||
|> markdownIndexedByHeading (route.number |> String.toInt |> Maybe.withDefault 1)
|
||||
|> Markdown.Renderer.render MarkdownRenderer.renderer
|
||||
of
|
||||
Ok renderedBody ->
|
||||
OptimizedDecoder.succeed renderedBody
|
||||
|
||||
Err error ->
|
||||
OptimizedDecoder.fail error
|
||||
|
||||
Err _ ->
|
||||
OptimizedDecoder.fail ""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
markdownIndexedByHeading :
|
||||
Int
|
||||
-> List Markdown.Block.Block
|
||||
-> List Markdown.Block.Block
|
||||
markdownIndexedByHeading index markdownBlocks =
|
||||
Markdown.Block.foldl
|
||||
(\block ( currentIndex, markdownToKeep ) ->
|
||||
case block of
|
||||
Markdown.Block.Heading Markdown.Block.H2 _ ->
|
||||
let
|
||||
newIndex =
|
||||
currentIndex + 1
|
||||
in
|
||||
--_ ->
|
||||
if newIndex == index then
|
||||
( newIndex, block :: markdownToKeep )
|
||||
|
||||
else
|
||||
( newIndex, markdownToKeep )
|
||||
|
||||
_ ->
|
||||
if currentIndex == index then
|
||||
( currentIndex, block :: markdownToKeep )
|
||||
|
||||
else
|
||||
( currentIndex, markdownToKeep )
|
||||
)
|
||||
( 0, [] )
|
||||
markdownBlocks
|
||||
|> Tuple.second
|
||||
|> List.reverse
|
||||
|
||||
|
||||
head :
|
||||
StaticPayload StaticData RouteParams
|
||||
-> List Head.Tag
|
||||
head static =
|
||||
Seo.summary
|
||||
{ canonicalUrlOverride = Nothing
|
||||
, siteName = "elm-pages"
|
||||
, image =
|
||||
{ url = ImagePath.build [ "TODO" ]
|
||||
, alt = "elm-pages logo"
|
||||
, dimensions = Nothing
|
||||
, mimeType = Nothing
|
||||
}
|
||||
, description = "TODO"
|
||||
, locale = Nothing
|
||||
, title = "TODO title" -- metadata.title -- TODO
|
||||
}
|
||||
|> Seo.website
|
||||
|
||||
|
||||
type alias StaticData =
|
||||
List (Element Msg)
|
||||
|
||||
|
||||
view :
|
||||
StaticPayload StaticData RouteParams
|
||||
-> Document Msg
|
||||
view static =
|
||||
{ title = "TODO title"
|
||||
, body =
|
||||
[ Element.column
|
||||
[ Element.padding 40
|
||||
]
|
||||
[ Element.text static.routeParams.number
|
||||
, Element.column []
|
||||
static.static
|
||||
]
|
||||
]
|
||||
}
|
52
examples/slides/src/TemplateHardcoded.elm
Normal file
@ -0,0 +1,52 @@
|
||||
module TemplateHardcoded exposing (..)
|
||||
|
||||
import Head
|
||||
import Pages
|
||||
import Pages.PagePath exposing (PagePath)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import TemplateType
|
||||
|
||||
|
||||
template :
|
||||
{ staticData :
|
||||
List ( PagePath, TemplateType.Metadata )
|
||||
-> StaticHttp.Request staticData
|
||||
, view :
|
||||
List ( PagePath, TemplateType.Metadata )
|
||||
-> staticData
|
||||
-> model
|
||||
-> metadata
|
||||
-> renderedTemplate
|
||||
-> view
|
||||
, head :
|
||||
staticData
|
||||
-> PagePath
|
||||
-> metadata
|
||||
-> List (Head.Tag Pages.PathKey)
|
||||
, init : metadata -> ( model, Cmd templateMsg )
|
||||
, update : metadata -> templateMsg -> model -> ( model, Cmd templateMsg )
|
||||
}
|
||||
-> Template metadata renderedTemplate staticData model view templateMsg
|
||||
template config =
|
||||
config
|
||||
|
||||
|
||||
type alias Template metadata renderedTemplate staticData model view templateMsg =
|
||||
{ staticData :
|
||||
List ( PagePath, TemplateType.Metadata )
|
||||
-> StaticHttp.Request staticData
|
||||
, view :
|
||||
List ( PagePath, TemplateType.Metadata )
|
||||
-> staticData
|
||||
-> model
|
||||
-> metadata
|
||||
-> renderedTemplate
|
||||
-> view
|
||||
, head :
|
||||
staticData
|
||||
-> PagePath
|
||||
-> metadata
|
||||
-> List (Head.Tag Pages.PathKey)
|
||||
, init : metadata -> ( model, Cmd templateMsg )
|
||||
, update : metadata -> templateMsg -> model -> ( model, Cmd templateMsg )
|
||||
}
|
46
examples/slides/static/admin/config.yml
Normal file
@ -0,0 +1,46 @@
|
||||
backend:
|
||||
name: git-gateway
|
||||
|
||||
media_folder: "examples/docs/images" # Folder where user uploaded files should go
|
||||
public_folder: "examples/docs/images"
|
||||
publish_mode: "editorial_workflow" # see https://www.netlifycms.org/docs/open-authoring/
|
||||
|
||||
collections: # A list of collections the CMS should be able to edit
|
||||
- name: "post" # Used in routes, ie.: /admin/collections/:slug/edit
|
||||
label: "Post" # Used in the UI, ie.: "New Post"
|
||||
folder: "examples/docs/content/blog" # The path to the folder where the documents are stored
|
||||
filter: {field: "type", value: "blog"}
|
||||
create: true # Allow users to create new documents in this collection
|
||||
fields: # The fields each document in this collection have
|
||||
- { label: "Title", name: "title", widget: "string" }
|
||||
- { label: "Publish Date", name: "published", widget: "date" }
|
||||
- { label: "Intro Blurb", name: "description", widget: "text" }
|
||||
- { label: "Image", name: "image", widget: "image", required: true }
|
||||
- label: "Author"
|
||||
name: "author"
|
||||
widget: "select"
|
||||
options: ["Dillon Kearns"]
|
||||
default: "Dillon Kearns"
|
||||
- { label: "Body", name: "body", widget: "markdown" }
|
||||
- {
|
||||
label: "Type",
|
||||
name: "type",
|
||||
widget: "hidden",
|
||||
default: "blog",
|
||||
required: false,
|
||||
}
|
||||
- name: "docs" # Used in routes, ie.: /admin/collections/:slug/edit
|
||||
label: "Docs" # Used in the UI, ie.: "New Post"
|
||||
folder: "examples/docs/content/docs" # The path to the folder where the documents are stored
|
||||
filter: {field: "type", value: "doc"}
|
||||
create: true # Allow users to create new documents in this collection
|
||||
fields: # The fields each document in this collection have
|
||||
- { label: "Title", name: "title", widget: "string" }
|
||||
- { label: "Body", name: "body", widget: "markdown" }
|
||||
- {
|
||||
label: "Type",
|
||||
name: "type",
|
||||
widget: "hidden",
|
||||
default: "doc",
|
||||
required: false,
|
||||
}
|
13
examples/slides/static/admin/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Content Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://identity-js.netlify.com/v1/netlify-identity-widget.js"></script>
|
||||
<!-- Include the script that builds the page and powers Netlify CMS -->
|
||||
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
|
||||
</body>
|
||||
</html>
|
149
examples/slides/static/elm-oembed.js
Normal file
@ -0,0 +1,149 @@
|
||||
export function setup() {
|
||||
customElements.define(
|
||||
"oembed-element",
|
||||
class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
let shadow = this.attachShadow({ mode: "closed" });
|
||||
const urlAttr = this.getAttribute("url");
|
||||
if (urlAttr) {
|
||||
renderOembed(shadow, urlAttr, {
|
||||
maxwidth: this.getAttribute("maxwidth"),
|
||||
maxheight: this.getAttribute("maxheight"),
|
||||
});
|
||||
} else {
|
||||
const discoverUrl = this.getAttribute("discover-url");
|
||||
if (discoverUrl) {
|
||||
getDiscoverUrl(discoverUrl, function (discoveredUrl) {
|
||||
if (discoveredUrl) {
|
||||
renderOembed(shadow, discoveredUrl, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ShadowRoot} shadow
|
||||
* @param {string} urlToEmbed
|
||||
* @param {{maxwidth: string?; maxheight: string?}?} options
|
||||
*/
|
||||
function renderOembed(shadow, urlToEmbed, options) {
|
||||
let apiUrlBuilder = new URL(
|
||||
`https://cors-anywhere.herokuapp.com/${urlToEmbed}`
|
||||
);
|
||||
if (options && options.maxwidth) {
|
||||
apiUrlBuilder.searchParams.set("maxwidth", options.maxwidth);
|
||||
}
|
||||
if (options && options.maxheight) {
|
||||
apiUrlBuilder.searchParams.set("maxheight", options.maxheight);
|
||||
}
|
||||
const apiUrl = apiUrlBuilder.toString();
|
||||
httpGetAsync(apiUrl, (rawResponse) => {
|
||||
const response = JSON.parse(rawResponse);
|
||||
|
||||
switch (response.type) {
|
||||
case "rich":
|
||||
tryRenderingHtml(shadow, response);
|
||||
break;
|
||||
case "video":
|
||||
tryRenderingHtml(shadow, response);
|
||||
break;
|
||||
case "photo":
|
||||
let img = document.createElement("img");
|
||||
img.setAttribute("src", response.url);
|
||||
if (options) {
|
||||
img.setAttribute(
|
||||
"style",
|
||||
`max-width: ${options.maxwidth}px; max-height: ${options.maxheight}px;`
|
||||
);
|
||||
}
|
||||
shadow.appendChild(img);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
height: ?number;
|
||||
width: ?number;
|
||||
html: any;
|
||||
}} response
|
||||
* @param {ShadowRoot} shadow
|
||||
*/
|
||||
function tryRenderingHtml(shadow, response) {
|
||||
if (response && typeof response.html) {
|
||||
let iframe = createIframe(response);
|
||||
shadow.appendChild(iframe);
|
||||
setTimeout(() => {
|
||||
let refetchedIframe = shadow.querySelector("iframe");
|
||||
if (refetchedIframe && !response.height) {
|
||||
refetchedIframe.setAttribute(
|
||||
"height",
|
||||
// @ts-ignore
|
||||
(iframe.contentWindow.document.body.scrollHeight + 10).toString()
|
||||
);
|
||||
}
|
||||
if (refetchedIframe && !response.width) {
|
||||
refetchedIframe.setAttribute(
|
||||
"width",
|
||||
// @ts-ignore
|
||||
(iframe.contentWindow.document.body.scrollWidth + 10).toString()
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ height: number?; width: number?; html: string; }} response
|
||||
* @returns {HTMLIFrameElement}
|
||||
*/
|
||||
function createIframe(response) {
|
||||
let iframe = document.createElement("iframe");
|
||||
iframe.setAttribute("border", "0");
|
||||
iframe.setAttribute("frameborder", "0");
|
||||
iframe.setAttribute("height", ((response.height || 500) + 20).toString());
|
||||
iframe.setAttribute("width", ((response.width || 500) + 20).toString());
|
||||
iframe.setAttribute("style", "max-width: 100%;");
|
||||
iframe.srcdoc = response.html;
|
||||
return iframe;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {{ (discoveredUrl: string?): void;}} callback
|
||||
*/
|
||||
function getDiscoverUrl(url, callback) {
|
||||
let apiUrl = new URL(
|
||||
`https://cors-anywhere.herokuapp.com/${url}`
|
||||
).toString();
|
||||
httpGetAsync(apiUrl, function (response) {
|
||||
let dom = document.createElement("html");
|
||||
dom.innerHTML = response;
|
||||
/** @type {HTMLLinkElement | null} */ const oembedTag = dom.querySelector(
|
||||
'link[type="application/json+oembed"]'
|
||||
);
|
||||
callback(oembedTag && oembedTag.href);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} theUrl
|
||||
* @param {{ (rawResponse: string): void }} callback
|
||||
*/
|
||||
function httpGetAsync(theUrl, callback) {
|
||||
var xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.onreadystatechange = function () {
|
||||
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
|
||||
callback(xmlHttp.responseText);
|
||||
};
|
||||
xmlHttp.open("GET", theUrl, true); // true for asynchronous
|
||||
xmlHttp.send(null);
|
||||
}
|
||||
}
|
553
examples/slides/static/hmr.js
Normal file
@ -0,0 +1,553 @@
|
||||
console.log("Loaded HMR");
|
||||
var eventSource = null;
|
||||
|
||||
/** @type {Promise<() => void>} */
|
||||
let updateAppContentJson = new Promise((resolve, reject) => resolve(() => {}));
|
||||
|
||||
function connect(sendContentJsonPort) {
|
||||
// Listen for the server to tell us that an HMR update is available
|
||||
eventSource = new EventSource("stream");
|
||||
eventSource.onmessage = async function (evt) {
|
||||
showCompiling("");
|
||||
if (evt.data === "content.json") {
|
||||
const elmJsRequest = elmJsFetch();
|
||||
const fetchContentJson = fetchContentJsonForCurrentPage();
|
||||
updateAppContentJson = updateContentJsonWith(
|
||||
fetchContentJson,
|
||||
sendContentJsonPort
|
||||
);
|
||||
|
||||
try {
|
||||
await fetchContentJson;
|
||||
const elmJsResponse = await elmJsRequest;
|
||||
thenApplyHmr(elmJsResponse);
|
||||
} catch (errorJson) {
|
||||
console.log({ errorJson });
|
||||
showError({
|
||||
type: "compile-errors",
|
||||
errors: errorJson,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
elmJsFetch().then(thenApplyHmr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} fetchContentJsonPromise
|
||||
* @param {*} sendContentJsonPort
|
||||
* @returns {Promise<() => void>}
|
||||
*/
|
||||
async function updateContentJsonWith(
|
||||
fetchContentJsonPromise,
|
||||
sendContentJsonPort
|
||||
) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const newContentJson = await fetchContentJsonPromise;
|
||||
hideError();
|
||||
|
||||
resolve(() => {
|
||||
sendContentJsonPort(newContentJson);
|
||||
hideCompiling("fast");
|
||||
});
|
||||
} catch (errorJson) {
|
||||
showError({
|
||||
type: "compile-errors",
|
||||
errors: errorJson,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchContentJsonForCurrentPage() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let currentPath = window.location.pathname.replace(/(\w)$/, "$1/");
|
||||
|
||||
const contentJsonForPage = await fetch(
|
||||
`${window.location.origin}${currentPath}content.json`
|
||||
);
|
||||
if (contentJsonForPage.ok) {
|
||||
resolve(await contentJsonForPage.json());
|
||||
} else {
|
||||
try {
|
||||
reject(await contentJsonForPage.json());
|
||||
} catch (error) {
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expose the Webpack HMR API
|
||||
|
||||
// var myDisposeCallback = null;
|
||||
var myDisposeCallback = function () {
|
||||
console.log("dispose...");
|
||||
};
|
||||
|
||||
// simulate the HMR api exposed by webpack
|
||||
var module = {
|
||||
hot: {
|
||||
accept: async function () {
|
||||
const sendInUpdatedContentJson = await updateAppContentJson;
|
||||
sendInUpdatedContentJson();
|
||||
},
|
||||
|
||||
dispose: function (callback) {
|
||||
myDisposeCallback = callback;
|
||||
},
|
||||
|
||||
data: null,
|
||||
|
||||
apply: function () {
|
||||
var newData = {};
|
||||
myDisposeCallback(newData);
|
||||
module.hot.data = newData;
|
||||
},
|
||||
|
||||
verbose: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Thanks to the elm-live maintainers and contributors for this code for rendering errors as an HTML overlay
|
||||
// https://github.com/wking-io/elm-live/blob/e317b4914c471addea7243c47f28dcebe27a5d36/lib/src/websocket.js
|
||||
|
||||
const pipe = (...fns) => (x) => fns.reduce((y, f) => f(y), x);
|
||||
|
||||
function elmJsFetch() {
|
||||
var elmJsRequest = new Request("/elm.js");
|
||||
elmJsRequest.cache = "no-cache";
|
||||
return fetch(elmJsRequest);
|
||||
}
|
||||
|
||||
async function waitFor(millis) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, millis);
|
||||
});
|
||||
}
|
||||
|
||||
async function thenApplyHmr(response) {
|
||||
if (response.ok) {
|
||||
response.text().then(function (value) {
|
||||
module.hot.apply();
|
||||
delete Elm;
|
||||
eval(value);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const errorJson = await response.json();
|
||||
console.error("JSON", errorJson);
|
||||
showError(errorJson);
|
||||
} catch (jsonParsingError) {
|
||||
console.log("Couldn't parse error", jsonParsingError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function colorConverter(color) {
|
||||
return {
|
||||
black: "#000000",
|
||||
red: "#F77F00",
|
||||
green: "#33ff00",
|
||||
yellow: "#ffff00",
|
||||
blue: "#99B1BC",
|
||||
magenta: "#cc00ff",
|
||||
cyan: "#00ffff",
|
||||
white: "#d0d0d0",
|
||||
BLACK: "#808080",
|
||||
RED: "#ff0000",
|
||||
GREEN: "#33ff00",
|
||||
YELLOW: "#ffff00",
|
||||
BLUE: "#0066ff",
|
||||
MAGENTA: "#cc00ff",
|
||||
CYAN: "#00ffff",
|
||||
WHITE: "#ffffff",
|
||||
}[color];
|
||||
}
|
||||
|
||||
const addNewLine = (str) => str + "\n";
|
||||
const styleColor = (str = "WHITE") => `color: ${colorConverter(str)};`;
|
||||
const styleUnderline = `text-decoration: underline;`;
|
||||
const styleBold = `text-decoration: bold;`;
|
||||
const parseStyle = ({ underline, color, bold }) =>
|
||||
`${underline ? styleUnderline : ""}${color ? styleColor(color) : ""}${
|
||||
bold ? styleBold : ""
|
||||
}`;
|
||||
|
||||
function capitalizeFirstLetter(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
function consoleSanitize(str) {
|
||||
return str.replace(/<(http[^>]*)>/, "$1");
|
||||
}
|
||||
|
||||
function htmlSanitize(str, type) {
|
||||
var temp = document.createElement("div");
|
||||
temp.textContent = str;
|
||||
return temp.innerHTML.replace(
|
||||
/<(http[^>]*)>/,
|
||||
"<<a style='color: inherit' target='_blank' href='$1'>$1</a>>"
|
||||
);
|
||||
}
|
||||
|
||||
const parseHeader = (title, path) =>
|
||||
`-- ${title.replace("-", " ")} --------------- ${path}`;
|
||||
|
||||
/*
|
||||
|-------------------------------------------------------------------------------
|
||||
| Console Logging
|
||||
|-------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const wrapConsole = (str) => `%c${str}`;
|
||||
const consoleHeader = pipe(parseHeader, wrapConsole, addNewLine, addNewLine);
|
||||
|
||||
const parseMsg = pipe(consoleSanitize, wrapConsole);
|
||||
const consoleMsg = ({ error, style }, msg) => ({
|
||||
error: error.concat(parseMsg(typeof msg === "string" ? msg : msg.string)),
|
||||
style: style.concat(
|
||||
parseStyle(typeof msg === "string" ? { color: "black" } : msg)
|
||||
),
|
||||
});
|
||||
|
||||
const joinMessage = ({ error, style }) => [error.join("")].concat(style);
|
||||
|
||||
const parseConsoleErrors = (path) => ({ title, message }) =>
|
||||
joinMessage(
|
||||
message.reduce(consoleMsg, {
|
||||
error: [consoleHeader(title, path)],
|
||||
style: [styleColor("blue")],
|
||||
})
|
||||
);
|
||||
|
||||
const restoreColorConsole = ({ errors }) =>
|
||||
errors.reduce(
|
||||
(acc, { problems, path }) =>
|
||||
acc.concat(problems.map(parseConsoleErrors(path))),
|
||||
[]
|
||||
);
|
||||
|
||||
/*
|
||||
|-------------------------------------------------------------------------------
|
||||
| Html Logging
|
||||
|-------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const htmlHeader = (title, path) =>
|
||||
`<span style="${parseStyle({ color: "blue" })}">${parseHeader(
|
||||
title,
|
||||
path
|
||||
)}</span>\n\n`;
|
||||
|
||||
const htmlMsg = (acc, msg) =>
|
||||
`${acc}<span style="${parseStyle(
|
||||
typeof msg === "string" ? { color: "WHITE" } : msg
|
||||
)}">${htmlSanitize(typeof msg === "string" ? msg : msg.string)}</span>`;
|
||||
|
||||
const parseHtmlErrors = (path) => ({ title, message }) =>
|
||||
message.reduce(htmlMsg, htmlHeader(title, path));
|
||||
|
||||
const restoreColorHtml = ({ errors }) =>
|
||||
errors.reduce(
|
||||
(acc, { problems, path }) =>
|
||||
acc.concat(problems.map(parseHtmlErrors(path))),
|
||||
[]
|
||||
);
|
||||
|
||||
/*
|
||||
|-------------------------------------------------------------------------------
|
||||
| TODO: Refactor Below
|
||||
|-------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
var speed = 400;
|
||||
var delay = 20;
|
||||
|
||||
function showError(error) {
|
||||
restoreColorConsole(error).forEach((error) => {
|
||||
console.log.apply(this, error);
|
||||
});
|
||||
hideCompiling("fast");
|
||||
setTimeout(function () {
|
||||
showError_(restoreColorHtml(error));
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function showError_(error) {
|
||||
var nodeContainer = document.getElementById("elm-live:elmErrorContainer");
|
||||
|
||||
if (!nodeContainer) {
|
||||
nodeContainer = document.createElement("div");
|
||||
nodeContainer.id = "elm-live:elmErrorContainer";
|
||||
document.body.appendChild(nodeContainer);
|
||||
}
|
||||
|
||||
nodeContainer.innerHTML = `
|
||||
<div
|
||||
id="elm-live:elmErrorBackground"
|
||||
style="
|
||||
z-index: 100;
|
||||
perspective: 500px;
|
||||
transition: opacity 400ms;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(13,31,45,0.2);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div
|
||||
onclick="elmLive.hideError()"
|
||||
style="
|
||||
background-color: rgba(0,0,0,0);
|
||||
position: fixed;
|
||||
top:0;
|
||||
left:0;
|
||||
bottom:0;
|
||||
right:0
|
||||
"
|
||||
></div>
|
||||
<pre
|
||||
id="elm-live:elmError"
|
||||
style="
|
||||
white-space: pre-wrap;
|
||||
transform: rotateX(0deg);
|
||||
transition: transform 400ms;
|
||||
transform-style: preserve-3d;
|
||||
font-size: 16px;
|
||||
overflow: scroll;
|
||||
background-color: rgba(13, 31, 45, 0.9);
|
||||
color: #ddd;
|
||||
width: calc(100% - 150px);
|
||||
height: calc(100% - 150px);
|
||||
margin: 0;
|
||||
padding: 30px;
|
||||
font-family: 'Fira Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
"
|
||||
>${error}</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(function () {
|
||||
document.getElementById("elm-live:elmErrorBackground").style.opacity = 1;
|
||||
document.getElementById("elm-live:elmError").style.transform =
|
||||
"rotateX(0deg)";
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function hideError(velocity) {
|
||||
var node = document.getElementById("elm-live:elmErrorContainer");
|
||||
if (node) {
|
||||
if (velocity === "fast") {
|
||||
document.getElementById("elm-live:elmErrorContainer").remove();
|
||||
} else {
|
||||
document.getElementById("elm-live:elmErrorBackground").style.opacity = 0;
|
||||
document.getElementById("elm-live:elmError").style.transform =
|
||||
"rotateX(90deg)";
|
||||
setTimeout(function () {
|
||||
document.getElementById("elm-live:elmErrorContainer").remove();
|
||||
}, speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showCompiling(message) {
|
||||
hideError("fast");
|
||||
setTimeout(function () {
|
||||
showCompiling_(message);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function showCompiling_(message) {
|
||||
var nodeContainer = document.getElementById("__elm-pages-loading");
|
||||
|
||||
if (!nodeContainer) {
|
||||
nodeContainer = document.createElement("div");
|
||||
nodeContainer.id = "__elm-pages-loading";
|
||||
nodeContainer.class = "lds-default";
|
||||
nodeContainer.style = `
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 110px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: white;
|
||||
display: block;
|
||||
box-shadow: rgba(0, 0, 0, 0.25) 0px 8px 15px 0px,
|
||||
rgba(0, 0, 0, 0.12) 0px 2px 10px 0px;
|
||||
`;
|
||||
document.body.appendChild(nodeContainer);
|
||||
}
|
||||
|
||||
nodeContainer.innerHTML = `
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear 0s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 37px;
|
||||
left: 66px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.1s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 22px;
|
||||
left: 62px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.2s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 11px;
|
||||
left: 52px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.3s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 7px;
|
||||
left: 37px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.4s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 11px;
|
||||
left: 22px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.5s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 22px;
|
||||
left: 11px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.6s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 37px;
|
||||
left: 7px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.7s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 52px;
|
||||
left: 11px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.8s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 62px;
|
||||
left: 22px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -0.9s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 66px;
|
||||
left: 37px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -1s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 62px;
|
||||
left: 52px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
style="
|
||||
animation: 1.2s linear -1.1s infinite normal none running lds-default;
|
||||
background: rgb(0, 0, 0);
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
top: 52px;
|
||||
left: 62px;
|
||||
"
|
||||
></div>
|
||||
`;
|
||||
setTimeout(function () {
|
||||
document.getElementById("__elm-pages-loading").style.opacity = 1;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function hideCompiling(velocity) {
|
||||
const node = document.getElementById("__elm-pages-loading");
|
||||
if (node) {
|
||||
if (velocity === "fast") {
|
||||
node.remove();
|
||||
} else {
|
||||
document.getElementById("__elm-pages-loading").style.opacity = 0;
|
||||
setTimeout(function () {
|
||||
node.remove();
|
||||
}, speed);
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 149 KiB |
After Width: | Height: | Size: 437 KiB |
After Width: | Height: | Size: 307 KiB |
BIN
examples/slides/static/images/article-covers/static-http.jpg
Normal file
After Width: | Height: | Size: 928 KiB |
BIN
examples/slides/static/images/author/dillon.jpg
Normal file
After Width: | Height: | Size: 2.4 MiB |
BIN
examples/slides/static/images/compiler-error.png
Normal file
After Width: | Height: | Size: 154 KiB |
39
examples/slides/static/images/elm-logo.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 323.141 322.95" enable-background="new 0 0 323.141 322.95" xml:space="preserve">
|
||||
<g>
|
||||
<polygon
|
||||
fill="#F0AD00"
|
||||
points="161.649,152.782 231.514,82.916 91.783,82.916"/>
|
||||
|
||||
<polygon
|
||||
fill="#7FD13B"
|
||||
points="8.867,0 79.241,70.375 232.213,70.375 161.838,0"/>
|
||||
|
||||
<rect
|
||||
fill="#7FD13B"
|
||||
x="192.99"
|
||||
y="107.392"
|
||||
transform="matrix(0.7071 0.7071 -0.7071 0.7071 186.4727 -127.2386)"
|
||||
width="107.676"
|
||||
height="108.167"/>
|
||||
|
||||
<polygon
|
||||
fill="#60B5CC"
|
||||
points="323.298,143.724 323.298,0 179.573,0"/>
|
||||
|
||||
<polygon
|
||||
fill="#5A6378"
|
||||
points="152.781,161.649 0,8.868 0,314.432"/>
|
||||
|
||||
<polygon
|
||||
fill="#F0AD00"
|
||||
points="255.522,246.655 323.298,314.432 323.298,178.879"/>
|
||||
|
||||
<polygon
|
||||
fill="#60B5CC"
|
||||
points="161.649,170.517 8.869,323.298 314.43,323.298"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
examples/slides/static/images/github.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub icon</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
After Width: | Height: | Size: 827 B |
BIN
examples/slides/static/images/icon-png.png
Normal file
After Width: | Height: | Size: 976 B |
2
examples/slides/static/images/icon.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<svg version="1.1" viewBox="251.0485 144.52063 56.114286 74.5" width="50px" height="74.5"><defs><linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="10%" style="stop-color:rgba(1.96%,45.88%,90.2%,1);stop-opacity:1"></stop><stop offset="100%" style="stop-color:rgba(0%,94.9%,37.65%,1);stop-opacity:1"></stop></linearGradient></defs><metadata></metadata><g id="Canvas_11" stroke="none" fill="url(#grad1)" stroke-opacity="1" fill-opacity="1" stroke-dasharray="none"><g id="Canvas_11: Layer 1"><g id="Group_38"><g id="Graphic_32"><path d="M 252.5485 146.02063 L 252.5485 217.52063 L 305.66277 217.52063 L 305.66277 161.68254 L 290.00087 146.02063 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"></path></g><g id="Line_34"><line x1="266.07286" y1="182.8279" x2="290.75465" y2="183.00997" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_35"><line x1="266.07286" y1="191.84156" x2="290.75465" y2="192.02363" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_36"><line x1="266.07286" y1="200.85522" x2="290.75465" y2="201.0373" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_37"><line x1="266.07286" y1="164.80058" x2="278.3874" y2="164.94049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
examples/slides/static/images/mountains.jpg
Normal file
After Width: | Height: | Size: 293 KiB |
BIN
examples/slides/static/images/static-http-content-requests.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
examples/slides/static/images/static-http-error.png
Normal file
After Width: | Height: | Size: 20 KiB |
8
examples/slides/static/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import * as elmOembed from "/elm-oembed.js";
|
||||
// import "./lib/native-shim.js";
|
||||
|
||||
export default function (elmLoaded) {
|
||||
document.addEventListener("DOMContentLoaded", function (event) {
|
||||
elmOembed.setup();
|
||||
});
|
||||
}
|
41
examples/slides/static/style.css
Normal file
@ -0,0 +1,41 @@
|
||||
@import "/syntax.css";
|
||||
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto|Roboto+Mono&display=swap");
|
||||
@import url("https://use.fontawesome.com/releases/v5.9.0/css/all.css");
|
||||
|
||||
.dotted-line {
|
||||
-webkit-animation: animation-yweh2o 400ms linear infinite;
|
||||
animation: animation-yweh2o 400ms linear infinite;
|
||||
}
|
||||
@-webkit-keyframes animation-yweh2o {
|
||||
to {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
}
|
||||
@keyframes animation-yweh2o {
|
||||
to {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
@media all and (max-width: 600px) {
|
||||
.navbar-title {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.navbar-title svg {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.responsive-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 600px) {
|
||||
.responsive-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
43
examples/slides/static/syntax.css
Normal file
@ -0,0 +1,43 @@
|
||||
pre.elmsh {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
code.elmsh {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Roboto Mono' !important;
|
||||
font-size: 20px !important;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.elmsh-line:before {
|
||||
/* content: attr(data-elmsh-lc); */
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
width: 40px;
|
||||
padding: 0 20px 0 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.elmsh {
|
||||
color: #f8f8f2;
|
||||
background: #000;
|
||||
}
|
||||
.elmsh-hl {background: #343434;}
|
||||
.elmsh-add {background: #003800;}
|
||||
.elmsh-del {background: #380000;}
|
||||
.elmsh-comm {color: #75715e;}
|
||||
.elmsh1 {color: #ae81ff;}
|
||||
.elmsh2 {color: #e6db74;}
|
||||
.elmsh3 {color: #66d9ef;}
|
||||
.elmsh4 {color: #f92672;}
|
||||
.elmsh5 {color: #a6e22e;}
|
||||
.elmsh6 {color: #ae81ff;}
|
||||
.elmsh7 {color: #fd971f;}
|
||||
|