Merge branch 'master' into template-modules

# Conflicts:
#	.gitignore
#	examples/docs/src/Main.elm
This commit is contained in:
Dillon Kearns 2020-10-24 13:02:17 -07:00
commit 3442cf02a8
93 changed files with 16232 additions and 471 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [dillonkearns]

2
.gitignore vendored
View File

@ -4,4 +4,4 @@ dist/
.cache/
generator/src/Main.js
elm-pages-*.tgz
coverage
coverage

File diff suppressed because one or more lines are too long

View File

@ -21,7 +21,9 @@
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"ThinkAlexandria/elm-html-in-elm": "1.0.1 <= v < 2.0.0",
"avh4/elm-color": "1.0.0 <= v < 2.0.0",
"danyx23/elm-mimetype": "4.0.1 <= v < 5.0.0",
"elm/browser": "1.0.1 <= v < 2.0.0",
"elm/core": "1.0.2 <= v < 2.0.0",
"elm/html": "1.0.0 <= v < 2.0.0",

View 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();
});
}

View 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;
}
}

View File

@ -4,7 +4,7 @@
"author": "Dillon Kearns",
"title": "Extensible Markdown Parsing in Pure Elm",
"description": "Introducing a new parser that extends your palette with no additional syntax",
"image": "images/article-covers/extensible-markdown-parsing.jpg",
"image": "v1603304397/elm-pages/article-covers/extensible-markdown-parsing_x9oolz.jpg",
"published": "2019-10-08",
}
---

View File

@ -5,7 +5,7 @@
"author": "Dillon Kearns",
"title": "Generating Files with Pure Elm",
"description": "Introducing a new parser that extends your palette with no additional syntax",
"image": "images/article-covers/generating-files.jpg",
"image": "v1603304397/elm-pages/article-covers/generating-files_blzn2d.jpg",
"published": "2020-01-28",
}
---

View File

@ -4,7 +4,7 @@
"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": "images/article-covers/introducing-elm-pages.jpg",
"image": "v1603304397/elm-pages/article-covers/introducing-elm-pages_ceksg2.jpg",
"published": "2019-09-24",
}
---

View File

@ -4,7 +4,7 @@
"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": "images/article-covers/static-http.jpg",
"image": "v1603304397/elm-pages/article-covers/static-http_ohefua.jpg",
"published": "2019-12-10",
}
---

View File

@ -4,7 +4,7 @@
"author": "Dillon Kearns",
"title": "Types Over Conventions",
"description": "How elm-pages approaches configuration, using type-safe Elm.",
"image": "images/article-covers/introducing-elm-pages.jpg",
"image": "v1603304397/elm-pages/article-covers/introducing-elm-pages_ceksg2.jpg",
"draft": true,
"published": "2019-09-21",
}
@ -162,6 +162,7 @@ Each file in the `content` folder will result in a new route for your static sit
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.

View File

@ -4,13 +4,16 @@
"src",
"../../src",
"vendor/elm-ui",
"gen"
"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",
@ -58,4 +61,4 @@
},
"indirect": {}
}
}
}

View File

@ -5,7 +5,7 @@
"scripts": {
"start": "elm-pages develop --port 1234",
"serve": "npm run build && http-server ./dist -a localhost -p 3000 -c-1",
"build": "elm-pages build"
"build": "node ../../generator/src/cli.js"
},
"author": "Dillon Kearns",
"license": "BSD-3",
@ -18,4 +18,4 @@
"elm-pages": "file:../..",
"http-server": "^0.11.1"
}
}
}

View File

@ -1,5 +1,6 @@
module Data.Author exposing (Author, all, decoder, view)
import Cloudinary
import Element exposing (Element)
import Html.Attributes as Attr
import Json.Decode as Decode exposing (Decoder)
@ -18,7 +19,7 @@ type alias Author =
all : List Author
all =
[ { name = "Dillon Kearns"
, avatar = Pages.images.author.dillon
, avatar = Cloudinary.url "v1602899672/elm-radio/dillon-profile_n2lqst.jpg" Nothing 140
, bio = "Elm developer and educator. Founder of Incremental Elm Consulting."
}
]

View File

@ -1,10 +1,17 @@
module Main exposing (main)
import Cloudinary
import Color
import Data.Author
import Head
import MarkdownRenderer
import MetadataNew
import MimeType
import MySitemap
import Pages
import Pages exposing (images, pages)
import Pages.ImagePath exposing (ImagePath)
import Pages.Manifest as Manifest
import Pages.Manifest.Category
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.Platform exposing (Page)
import Rss
@ -15,7 +22,41 @@ import TemplateDemultiplexer
import TemplateType exposing (TemplateType)
main : Pages.Platform.Program TemplateDemultiplexer.Model TemplateDemultiplexer.Msg TemplateType Shared.RenderedBody
webp : MimeType.MimeImage
webp =
MimeType.OtherImage "webp"
icon :
MimeType.MimeImage
-> Int
-> Manifest.Icon pathKey
icon format width =
{ src = cloudinaryIcon format width
, sizes = [ ( width, width ) ]
, mimeType = format |> Just
, purposes = [ Manifest.IconPurposeAny, Manifest.IconPurposeMaskable ]
}
cloudinaryIcon :
MimeType.MimeImage
-> Int
-> ImagePath pathKey
cloudinaryIcon mimeType width =
Cloudinary.urlSquare "v1603234028/elm-pages/elm-pages-icon" (Just mimeType) width
socialIcon : ImagePath pathKey
socialIcon =
Cloudinary.urlSquare "v1603234028/elm-pages/elm-pages-icon" Nothing 250
--main : Pages.Platform.Program Model Msg Metadata View Pages.PathKey
main : Pages.Platform.Program TemplateDemultiplexer.Model TemplateDemultiplexer.Msg TemplateType Shared.RenderedBody Pages.PathKey
main =
TemplateDemultiplexer.mainTemplate
{ documents =
@ -35,6 +76,12 @@ main =
}
metadataToRssItem
|> MySitemap.install { siteUrl = Site.canonicalUrl } metadataToSitemapEntry
|> Pages.Platform.withGlobalHeadTags
[ 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)
]
|> Pages.Platform.toProgram

View File

@ -1,5 +1,6 @@
module Metadata exposing (ArticleMetadata, DocMetadata, Metadata(..), PageMetadata, decoder)
import Cloudinary
import Data.Author
import Date exposing (Date)
import Dict exposing (Dict)
@ -90,15 +91,7 @@ decoder =
imageDecoder : Decoder (ImagePath Pages.PathKey)
imageDecoder =
Decode.string
|> Decode.andThen
(\imageAssetPath ->
case findMatchingImage imageAssetPath of
Nothing ->
Decode.fail "Couldn't find image."
Just imagePath ->
Decode.succeed imagePath
)
|> Decode.map (\cloudinaryAsset -> Cloudinary.url cloudinaryAsset Nothing 800)
findMatchingImage : String -> Maybe (ImagePath Pages.PathKey)

View File

@ -1,7 +1,10 @@
module Site exposing (canonicalUrl, config, tagline)
import Cloudinary
import Color
import MimeType
import Pages exposing (images, pages)
import Pages.ImagePath exposing (ImagePath)
import Pages.Manifest as Manifest
import Pages.Manifest.Category
import Pages.PagePath exposing (PagePath)
@ -44,16 +47,52 @@ manifest =
, categories = [ Pages.Manifest.Category.education ]
, displayMode = Manifest.Standalone
, orientation = Manifest.Portrait
, description = tagline
, 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.iconPng
, icons =
[ icon webp 192
, icon webp 512
, icon MimeType.Png 192
, icon MimeType.Png 512
]
}
tagline : String
tagline =
"A statically typed site generator - elm-pages"
webp : MimeType.MimeImage
webp =
MimeType.OtherImage "webp"
icon :
MimeType.MimeImage
-> Int
-> Manifest.Icon pathKey
icon format width =
{ src = cloudinaryIcon format width
, sizes = [ ( width, width ) ]
, mimeType = format |> Just
, purposes = [ Manifest.IconPurposeAny, Manifest.IconPurposeMaskable ]
}
cloudinaryIcon :
MimeType.MimeImage
-> Int
-> ImagePath pathKey
cloudinaryIcon mimeType width =
Cloudinary.urlSquare "v1603234028/elm-pages/elm-pages-icon" (Just mimeType) width
socialIcon : ImagePath pathKey
socialIcon =
Cloudinary.urlSquare "v1603234028/elm-pages/elm-pages-icon" Nothing 250

149
examples/docs/static/elm-oembed.js vendored Normal file
View 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);
}
}

43
examples/docs/static/syntax.css vendored Normal file
View 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;}

View File

@ -9,6 +9,7 @@
"dependencies": {
"direct": {
"avh4/elm-color": "1.0.0",
"danyx23/elm-mimetype": "4.0.1",
"dillonkearns/elm-markdown": "1.1.3",
"dillonkearns/elm-oembed": "1.0.0",
"elm/browser": "1.0.1",
@ -50,4 +51,4 @@
},
"indirect": {}
}
}
}

2
examples/simple/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
gen/

View File

@ -0,0 +1,9 @@
export default function (elmLoaded) {
console.log("Hello outside of promise!");
elmLoaded.then((elmPagesApp) => {
console.log("Inside of promise");
elmPagesApp.ports.example.subscribe((message) => {
console.log("Elm port message: ", message);
});
});
}

View File

@ -0,0 +1,3 @@
h1 {
color: red;
}

View File

@ -0,0 +1,7 @@
---
title: Hello from another page.
type: page
repo: elm-markdown
---
This is another page

View File

@ -0,0 +1,8 @@
---
title: elm-pages - a statically typed site generator
type: page
repo: elm-pages
---
Hello!

64
examples/simple/elm.json Normal file
View File

@ -0,0 +1,64 @@
{
"type": "application",
"source-directories": [
"src",
"../../src",
"vendor/elm-ui",
"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.2.4",
"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",
"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": {}
}
}

View File

@ -0,0 +1,7 @@
#!/bin/bash
cd content
for i in {1..1000}
do
echo -e "---\ntitle: Post Number\ntype: page\n---\n\n## Page $i\n\nWelcome to page $i" > "page-$i.md"
done

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

View File

@ -0,0 +1,49 @@
/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
* This shim allows elements written in, or compiled to, ES5 to work on native
* implementations of Custom Elements v1. It sets new.target to the value of
* this.constructor so that the native HTMLElement constructor can access the
* current under-construction element's definition.
*/
(function() {
if (
// No Reflect, no classes, no need for shim because native custom elements
// require ES2015 classes or Reflect.
window.Reflect === undefined ||
window.customElements === undefined ||
// The webcomponentsjs custom elements polyfill doesn't require
// ES2015-compatible construction (`super()` or `Reflect.construct`).
window.customElements.polyfillWrapFlushCallback
) {
return;
}
const BuiltInHTMLElement = HTMLElement;
/**
* With jscompiler's RECOMMENDED_FLAGS the function name will be optimized away.
* However, if we declare the function as a property on an object literal, and
* use quotes for the property name, then closure will leave that much intact,
* which is enough for the JS VM to correctly set Function.prototype.name.
*/
const wrapperForTheName = {
HTMLElement: /** @this {!Object} */ function HTMLElement() {
return Reflect.construct(
BuiltInHTMLElement,
[],
/** @type {!Function} */ (this.constructor)
);
}
};
window.HTMLElement = wrapperForTheName["HTMLElement"];
HTMLElement.prototype = BuiltInHTMLElement.prototype;
HTMLElement.prototype.constructor = HTMLElement;
Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
})();

View File

@ -0,0 +1,21 @@
{
"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": "elm-pages build"
},
"author": "Dillon Kearns",
"license": "BSD-3",
"dependencies": {
"node-sass": "^4.12.0"
},
"devDependencies": {
"elm": "^0.19.1-3",
"elm-oembed": "0.0.6",
"elm-pages": "file:../..",
"http-server": "^0.11.1"
}
}

View File

@ -0,0 +1,48 @@
module Data.Author exposing (Author, all, decoder, view)
import Element exposing (Element)
import Html.Attributes as Attr
import Json.Decode as Decode exposing (Decoder)
import List.Extra
import Pages
import Pages.ImagePath as ImagePath exposing (ImagePath)
type alias Author =
{ name : String
, avatar : ImagePath Pages.PathKey
, bio : String
}
all : List Author
all =
[ { name = "Dillon Kearns"
, avatar = Pages.images.author.dillon
, 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 }

View File

@ -0,0 +1,70 @@
module DocSidebar exposing (view)
import Element exposing (Element)
import Element.Border as Border
import Element.Font
import Metadata exposing (Metadata)
import Pages
import Pages.PagePath as PagePath exposing (PagePath)
import Palette
view :
PagePath Pages.PathKey
-> List ( PagePath Pages.PathKey, Metadata )
-> Element msg
view currentPage posts =
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
Metadata.Doc meta ->
Just ( currentPage == path, path, meta )
_ ->
Nothing
)
|> List.map postSummary
)
postSummary :
( Bool, PagePath Pages.PathKey, { 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 Pages.PathKey -> 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

View 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 []

View 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;}}
-}

View 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 ]

View 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 Pages.PathKey
, 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 Pages.PathKey
, 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 Pages.PathKey
, 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

View 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

View File

@ -0,0 +1,116 @@
module Index exposing (view)
import Data.Author
import Date
import Element exposing (Element)
import Element.Border
import Element.Font
import Metadata exposing (Metadata)
import Pages
import Pages.ImagePath as ImagePath exposing (ImagePath)
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.Platform exposing (Page)
view :
List ( PagePath Pages.PathKey, Metadata )
-> Element msg
view posts =
Element.column [ Element.spacing 20 ]
(posts
|> List.filterMap
(\( path, metadata ) ->
case metadata of
Metadata.Article meta ->
if meta.draft then
Nothing
else
Just ( path, meta )
_ ->
Nothing
)
|> List.sortBy
(\( path, metadata ) ->
metadata.published
|> Date.toRataDie
)
|> List.reverse
|> List.map postSummary
)
postSummary :
( PagePath Pages.PathKey, Metadata.ArticleMetadata )
-> Element msg
postSummary ( postPath, post ) =
articleIndex post |> linkToPost postPath
linkToPost : PagePath Pages.PathKey -> 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 : Metadata.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 : Metadata.ArticleMetadata -> Element msg
postPreview post =
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) ] post.author
, Element.text post.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
]
]

View File

@ -0,0 +1,286 @@
port module Main exposing (main)
import Color
import Element exposing (Element)
import Element.Font as Font
import Head
import Head.Seo as Seo
import Html exposing (Html)
import Html.Attributes
import MarkdownRenderer
import Metadata exposing (Metadata)
import MimeType
import OptimizedDecoder as D
import Pages exposing (images, pages)
import Pages.ImagePath as ImagePath exposing (ImagePath)
import Pages.Manifest as Manifest
import Pages.Manifest.Category
import Pages.PagePath exposing (PagePath)
import Pages.Platform exposing (Page)
import Pages.StaticHttp as StaticHttp
import Secrets
import Time
port example : String -> Cmd msg
manifest : Manifest.Config Pages.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.iconPng
, icons =
[ icon webp 192
, icon webp 512
, icon MimeType.Png 192
, icon MimeType.Png 512
]
}
webp : MimeType.MimeImage
webp =
MimeType.OtherImage "webp"
icon :
MimeType.MimeImage
-> Int
-> Manifest.Icon pathKey
icon format width =
{ src = cloudinaryIcon format width
, sizes = [ ( width, width ) ]
, mimeType = format |> Just
, purposes = []
}
cloudinaryIcon :
MimeType.MimeImage
-> Int
-> ImagePath pathKey
cloudinaryIcon format width =
let
base =
"https://res.cloudinary.com/dillonkearns/image/upload"
asset =
"v1603234028/elm-pages/elm-pages-icon"
fetch_format =
case format of
MimeType.Png ->
"png"
MimeType.OtherImage "webp" ->
"webp"
_ ->
"auto"
transforms =
[ "c_pad"
, "w_" ++ String.fromInt width
, "h_" ++ String.fromInt width
, "q_auto"
, "f_" ++ fetch_format
]
|> String.join ","
in
ImagePath.external (base ++ "/" ++ transforms ++ "/" ++ asset)
type alias View =
( MarkdownRenderer.TableOfContents, List (Element Msg) )
main : Pages.Platform.Program Model Msg Metadata View Pages.PathKey
main =
Pages.Platform.init
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, documents =
[ { extension = "md"
, metadata = Metadata.decoder
, body = MarkdownRenderer.view
}
]
, onPageChange = Nothing
, manifest = manifest
, canonicalSiteUrl = canonicalSiteUrl
, internals = Pages.internals
}
|> Pages.Platform.withFileGenerator fileGenerator
|> Pages.Platform.withGlobalHeadTags
[ 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)
]
|> Pages.Platform.toProgram
fileGenerator :
List { path : PagePath Pages.PathKey, frontmatter : metadata, body : String }
->
StaticHttp.Request
(List
(Result String
{ path : List String
, content : String
}
)
)
fileGenerator siteMetadata =
StaticHttp.succeed
[ Ok { path = [ "hello.txt" ], content = "Hello there!" }
, Ok { path = [ "goodbye.txt" ], content = "Goodbye there!" }
]
type alias Model =
{ showMobileMenu : Bool
, counter : Int
}
init :
Maybe
{ path : PagePath Pages.PathKey
, query : Maybe String
, fragment : Maybe String
}
-> ( Model, Cmd Msg )
init maybePagePath =
( Model False 0, example "Whyyyyy hello there!" )
type Msg
= OnPageChange
{ path : PagePath Pages.PathKey
, query : Maybe String
, fragment : Maybe String
}
| ToggleMobileMenu
| Tick
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnPageChange page ->
( { model | showMobileMenu = False }, Cmd.none )
ToggleMobileMenu ->
( { model | showMobileMenu = not model.showMobileMenu }, Cmd.none )
Tick ->
( { model | counter = model.counter + 1 }, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions _ =
Time.every 1000 (\_ -> Tick)
view :
List ( PagePath Pages.PathKey, Metadata )
->
{ path : PagePath Pages.PathKey
, frontmatter : Metadata
}
->
StaticHttp.Request
{ view : Model -> View -> { title : String, body : Html Msg }
, head : List (Head.Tag Pages.PathKey)
}
view siteMetadata page =
case page.frontmatter of
Metadata.Page meta ->
StaticHttp.get
(Secrets.succeed <| "https://api.github.com/repos/dillonkearns/" ++ meta.repo)
(D.field "stargazers_count" D.int)
|> StaticHttp.map
(\stars ->
{ view =
\model _ ->
{ title = "Title"
, body =
Html.div []
[ Html.h1 [] [ Html.text meta.repo ]
, Html.div []
[ Html.text <| "GitHub Stars: " ++ String.fromInt stars ]
, Html.div []
[ Html.text <| "Counter: " ++ String.fromInt model.counter ]
, Html.div []
[ Html.a [ Html.Attributes.href "/" ] [ Html.text "elm-pages" ]
, Html.a [ Html.Attributes.href "/elm-markdown" ] [ Html.text "elm-markdown" ]
]
]
}
, head = head page.path page.frontmatter
}
)
{-| <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards>
<https://htmlhead.dev>
<https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names>
<https://ogp.me/>
-}
head : PagePath Pages.PathKey -> Metadata -> List (Head.Tag Pages.PathKey)
head currentPath metadata =
case metadata of
Metadata.Page meta ->
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = images.iconPng
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = siteTagline
, locale = Nothing
, title = meta.title
}
|> Seo.website
canonicalSiteUrl : String
canonicalSiteUrl =
"https://elm-pages.com"
siteTagline : String
siteTagline =
"A statically typed site generator - elm-pages"
tocView : MarkdownRenderer.TableOfContents -> Element msg
tocView 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 = "#" ++ heading.anchorId
, label = Element.text heading.name
}
)
)
]

View File

@ -0,0 +1,317 @@
module MarkdownRenderer exposing (TableOfContents, 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 exposing (Attribute, Html)
import Html.Attributes exposing (property)
import Json.Encode as Encode exposing (Value)
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.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 =
\{ title, 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 title ->
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 =
\maybeAlignment 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 children ->
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 children ->
-- 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 =
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 ]
editorValue : String -> Attribute msg
editorValue value =
value
|> String.trim
|> Encode.string
|> property "editorValue"

View File

@ -0,0 +1,51 @@
module Metadata exposing (Metadata(..), PageMetadata, decoder)
import Json.Decode as Decode exposing (Decoder)
import List.Extra
import Pages
import Pages.ImagePath as ImagePath exposing (ImagePath)
type Metadata
= Page PageMetadata
type alias PageMetadata =
{ title : String, repo : String }
decoder =
Decode.field "type" Decode.string
|> Decode.andThen
(\pageType ->
case pageType of
"page" ->
Decode.map2 PageMetadata
(Decode.field "title" Decode.string)
(Decode.field "repo" Decode.string)
|> Decode.map Page
_ ->
Decode.fail <| "Unexpected page \"type\" " ++ pageType
)
imageDecoder : Decoder (ImagePath Pages.PathKey)
imageDecoder =
Decode.string
|> Decode.andThen
(\imageAssetPath ->
case findMatchingImage imageAssetPath of
Nothing ->
Decode.fail "Couldn't find image."
Just imagePath ->
Decode.succeed imagePath
)
findMatchingImage : String -> Maybe (ImagePath Pages.PathKey)
findMatchingImage imageAssetPath =
List.Extra.find
(\image -> ImagePath.toString image == imageAssetPath)
Pages.allImages

View File

@ -0,0 +1,34 @@
module MySitemap exposing (install)
import Head
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.Platform exposing (Builder)
import Pages.StaticHttp as StaticHttp
import Sitemap
install :
{ siteUrl : String
}
->
(List
{ path : PagePath pathKey
, frontmatter : metadata
, body : String
}
-> List { path : String, lastMod : Maybe String }
)
-> Builder pathKey userModel userMsg metadata view
-> Builder pathKey userModel userMsg metadata view
install config toSitemapEntry builder =
builder
|> Pages.Platform.withGlobalHeadTags [ Head.sitemapLink "/sitemap.xml" ]
|> Pages.Platform.withFileGenerator
(\siteMetadata ->
StaticHttp.succeed
[ Ok
{ path = [ "sitemap.xml" ]
, content = Sitemap.build config (toSitemapEntry siteMetadata)
}
]
)

View 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 ]

View File

@ -0,0 +1,56 @@
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 pathKey
}
->
({ path : PagePath pathKey
, frontmatter : metadata
, body : String
}
-> Maybe Rss.Item
)
-> Builder pathKey userModel userMsg metadata view
-> Builder pathKey userModel userMsg metadata view
generate options metadataToRssItem builder =
let
feedFilePath =
(options.indexPage
|> PagePath.toPath
)
++ [ "feed.xml" ]
in
builder
|> Pages.Platform.withFileGenerator
(\siteMetadata ->
{ 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 = siteMetadata |> List.filterMap metadataToRssItem
, siteUrl = options.siteUrl
}
}
|> Ok
|> List.singleton
|> StaticHttp.succeed
)
|> Pages.Platform.withGlobalHeadTags [ Head.rssLink (feedFilePath |> String.join "/") ]

View File

@ -0,0 +1,161 @@
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
import Url.Builder
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"
]

View 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,
}

View 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>

View 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;}

1691
examples/simple/vendor/elm-ui/Element.elm vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,226 @@
module Element.Background exposing
( color, gradient
, image, uncropped, tiled, tiledX, tiledY
)
{-|
@docs color, gradient
# Images
@docs image, uncropped, tiled, tiledX, tiledY
**Note** if you want more control over a background image than is provided here, you should try just using a normal `Element.image` with something like `Element.behindContent`.
-}
import Element exposing (Attr, Attribute, Color)
import Internal.Flag as Flag
import Internal.Model as Internal
import VirtualDom
{-| -}
color : Color -> Attr decorative msg
color clr =
Internal.StyleClass Flag.bgColor (Internal.Colored ("bg-" ++ Internal.formatColorClass clr) "background-color" clr)
{-| Resize the image to fit the containing element while maintaining proportions and cropping the overflow.
-}
image : String -> Attribute msg
image src =
Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") center / cover no-repeat"))
{-| A centered background image that keeps its natural proportions, but scales to fit the space.
-}
uncropped : String -> Attribute msg
uncropped src =
Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") center / contain no-repeat"))
{-| Tile an image in the x and y axes.
-}
tiled : String -> Attribute msg
tiled src =
Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") repeat"))
{-| Tile an image in the x axis.
-}
tiledX : String -> Attribute msg
tiledX src =
Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") repeat-x"))
{-| Tile an image in the y axis.
-}
tiledY : String -> Attribute msg
tiledY src =
Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") repeat-y"))
type Direction
= ToUp
| ToDown
| ToRight
| ToTopRight
| ToBottomRight
| ToLeft
| ToTopLeft
| ToBottomLeft
| ToAngle Float
type Step
= ColorStep Color
| PercentStep Float Color
| PxStep Int Color
{-| -}
step : Color -> Step
step =
ColorStep
{-| -}
percent : Float -> Color -> Step
percent =
PercentStep
{-| -}
px : Int -> Color -> Step
px =
PxStep
{-| A linear gradient.
First you need to specify what direction the gradient is going by providing an angle in radians. `0` is up and `pi` is down.
The colors will be evenly spaced.
-}
gradient :
{ angle : Float
, steps : List Color
}
-> Attr decorative msg
gradient { angle, steps } =
case steps of
[] ->
Internal.NoAttribute
clr :: [] ->
Internal.StyleClass Flag.bgColor
(Internal.Colored ("bg-" ++ Internal.formatColorClass clr) "background-color" clr)
_ ->
Internal.StyleClass Flag.bgGradient <|
Internal.Single ("bg-grad-" ++ (String.join "-" <| Internal.floatClass angle :: List.map Internal.formatColorClass steps))
"background-image"
("linear-gradient(" ++ (String.join ", " <| (String.fromFloat angle ++ "rad") :: List.map Internal.formatColor steps) ++ ")")
-- {-| -}
-- gradientWith : { direction : Direction, steps : List Step } -> Attribute msg
-- gradientWith { direction, steps } =
-- StyleClass <|
-- Single ("bg-gradient-" ++ (String.join "-" <| renderDirectionClass direction :: List.map renderStepClass steps))
-- "background"
-- ("linear-gradient(" ++ (String.join ", " <| renderDirection direction :: List.map renderStep steps) ++ ")")
-- {-| -}
-- renderStep : Step -> String
-- renderStep step =
-- case step of
-- ColorStep color ->
-- formatColor color
-- PercentStep percent color ->
-- formatColor color ++ " " ++ toString percent ++ "%"
-- PxStep px color ->
-- formatColor color ++ " " ++ toString px ++ "px"
-- {-| -}
-- renderStepClass : Step -> String
-- renderStepClass step =
-- case step of
-- ColorStep color ->
-- formatColorClass color
-- PercentStep percent color ->
-- formatColorClass color ++ "-" ++ floatClass percent ++ "p"
-- PxStep px color ->
-- formatColorClass color ++ "-" ++ toString px ++ "px"
-- toUp : Direction
-- toUp =
-- ToUp
-- toDown : Direction
-- toDown =
-- ToDown
-- toRight : Direction
-- toRight =
-- ToRight
-- toTopRight : Direction
-- toTopRight =
-- ToTopRight
-- toBottomRight : Direction
-- toBottomRight =
-- ToBottomRight
-- toLeft : Direction
-- toLeft =
-- ToLeft
-- toTopLeft : Direction
-- toTopLeft =
-- ToTopLeft
-- toBottomLeft : Direction
-- toBottomLeft =
-- ToBottomLeft
-- angle : Float -> Direction
-- angle rad =
-- ToAngle rad
-- renderDirection : Direction -> String
-- renderDirection dir =
-- case dir of
-- ToUp ->
-- "to top"
-- ToDown ->
-- "to bottom"
-- ToRight ->
-- "to right"
-- ToTopRight ->
-- "to top right"
-- ToBottomRight ->
-- "to bottom right"
-- ToLeft ->
-- "to left"
-- ToTopLeft ->
-- "to top left"
-- ToBottomLeft ->
-- "to bottom left"
-- ToAngle angle ->
-- toString angle ++ "rad"
-- renderDirectionClass : Direction -> String
-- renderDirectionClass dir =
-- case dir of
-- ToUp ->
-- "to-top"
-- ToDown ->
-- "to-bottom"
-- ToRight ->
-- "to-right"
-- ToTopRight ->
-- "to-top-right"
-- ToBottomRight ->
-- "to-bottom-right"
-- ToLeft ->
-- "to-left"
-- ToTopLeft ->
-- "to-top-left"
-- ToBottomLeft ->
-- "to-bottom-left"
-- ToAngle angle ->
-- floatClass angle ++ "rad"

View File

@ -0,0 +1,281 @@
module Element.Border exposing
( color
, width, widthXY, widthEach
, solid, dashed, dotted
, rounded, roundEach
, glow, innerGlow, shadow, innerShadow
)
{-|
@docs color
## Border Widths
@docs width, widthXY, widthEach
## Border Styles
@docs solid, dashed, dotted
## Rounded Corners
@docs rounded, roundEach
## Shadows
@docs glow, innerGlow, shadow, innerShadow
-}
import Element exposing (Attr, Attribute, Color)
import Internal.Flag as Flag
import Internal.Model as Internal
import Internal.Style as Style exposing (classes)
{-| -}
color : Color -> Attr decorative msg
color clr =
Internal.StyleClass
Flag.borderColor
(Internal.Colored
("bc-" ++ Internal.formatColorClass clr)
"border-color"
clr
)
{-| -}
width : Int -> Attribute msg
width v =
Internal.StyleClass
Flag.borderWidth
(Internal.BorderWidth
("b-" ++ String.fromInt v)
v
v
v
v
)
{-| Set horizontal and vertical borders.
-}
widthXY : Int -> Int -> Attribute msg
widthXY x y =
Internal.StyleClass
Flag.borderWidth
(Internal.BorderWidth
("b-"
++ String.fromInt x
++ "-"
++ String.fromInt y
)
y
x
y
x
)
{-| -}
widthEach :
{ bottom : Int
, left : Int
, right : Int
, top : Int
}
-> Attribute msg
widthEach { bottom, top, left, right } =
if top == bottom && left == right then
if top == right then
width top
else
widthXY left top
else
Internal.StyleClass Flag.borderWidth
(Internal.BorderWidth
("b-"
++ String.fromInt top
++ "-"
++ String.fromInt right
++ "-"
++ String.fromInt bottom
++ "-"
++ String.fromInt left
)
top
right
bottom
left
)
-- {-| No Borders
-- -}
-- none : Attribute msg
-- none =
-- Class "border" "border-none"
{-| -}
solid : Attribute msg
solid =
Internal.Class Flag.borderStyle classes.borderSolid
{-| -}
dashed : Attribute msg
dashed =
Internal.Class Flag.borderStyle classes.borderDashed
{-| -}
dotted : Attribute msg
dotted =
Internal.Class Flag.borderStyle classes.borderDotted
{-| Round all corners.
-}
rounded : Int -> Attribute msg
rounded radius =
Internal.StyleClass
Flag.borderRound
(Internal.Single
("br-" ++ String.fromInt radius)
"border-radius"
(String.fromInt radius ++ "px")
)
{-| -}
roundEach :
{ topLeft : Int
, topRight : Int
, bottomLeft : Int
, bottomRight : Int
}
-> Attribute msg
roundEach { topLeft, topRight, bottomLeft, bottomRight } =
Internal.StyleClass Flag.borderRound
(Internal.Single
("br-"
++ String.fromInt topLeft
++ "-"
++ String.fromInt topRight
++ String.fromInt bottomLeft
++ "-"
++ String.fromInt bottomRight
)
"border-radius"
(String.fromInt topLeft
++ "px "
++ String.fromInt topRight
++ "px "
++ String.fromInt bottomRight
++ "px "
++ String.fromInt bottomLeft
++ "px"
)
)
{-| A simple glow by specifying the color and size.
-}
glow : Color -> Float -> Attr decorative msg
glow clr size =
shadow
{ offset = ( 0, 0 )
, size = size
, blur = size * 2
, color = clr
}
{-| -}
innerGlow : Color -> Float -> Attr decorative msg
innerGlow clr size =
innerShadow
{ offset = ( 0, 0 )
, size = size
, blur = size * 2
, color = clr
}
{-| -}
shadow :
{ offset : ( Float, Float )
, size : Float
, blur : Float
, color : Color
}
-> Attr decorative msg
shadow almostShade =
let
shade =
{ inset = False
, offset = almostShade.offset
, size = almostShade.size
, blur = almostShade.blur
, color = almostShade.color
}
in
Internal.StyleClass Flag.shadows <|
Internal.Single
(Internal.boxShadowClass shade)
"box-shadow"
(Internal.formatBoxShadow shade)
{-| -}
innerShadow :
{ offset : ( Float, Float )
, size : Float
, blur : Float
, color : Color
}
-> Attr decorative msg
innerShadow almostShade =
let
shade =
{ inset = True
, offset = almostShade.offset
, size = almostShade.size
, blur = almostShade.blur
, color = almostShade.color
}
in
Internal.StyleClass Flag.shadows <|
Internal.Single
(Internal.boxShadowClass shade)
"box-shadow"
(Internal.formatBoxShadow shade)
-- {-| -}
-- shadow :
-- { offset : ( Float, Float )
-- , blur : Float
-- , size : Float
-- , color : Color
-- }
-- -> Attr decorative msg
-- shadow shade =
-- Internal.BoxShadow
-- { inset = False
-- , offset = shade.offset
-- , size = shade.size
-- , blur = shade.blur
-- , color = shade.color
-- }

View File

@ -0,0 +1,265 @@
module Element.Events exposing
( onClick, onDoubleClick, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseMove
, onFocus, onLoseFocus
-- , onClickCoords
-- , onClickPageCoords
-- , onClickScreenCoords
-- , onMouseCoords
-- , onMousePageCoords
-- , onMouseScreenCoords
)
{-|
## Mouse Events
@docs onClick, onDoubleClick, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseMove
## Focus Events
@docs onFocus, onLoseFocus
-}
import Element exposing (Attribute)
import Html.Events
import Internal.Model as Internal
import Json.Decode as Json
import VirtualDom
-- MOUSE EVENTS
{-| -}
onMouseDown : msg -> Attribute msg
onMouseDown =
Internal.Attr << Html.Events.onMouseDown
{-| -}
onMouseUp : msg -> Attribute msg
onMouseUp =
Internal.Attr << Html.Events.onMouseUp
{-| -}
onClick : msg -> Attribute msg
onClick =
Internal.Attr << Html.Events.onClick
{-| -}
onDoubleClick : msg -> Attribute msg
onDoubleClick =
Internal.Attr << Html.Events.onDoubleClick
{-| -}
onMouseEnter : msg -> Attribute msg
onMouseEnter =
Internal.Attr << Html.Events.onMouseEnter
{-| -}
onMouseLeave : msg -> Attribute msg
onMouseLeave =
Internal.Attr << Html.Events.onMouseLeave
{-| -}
onMouseMove : msg -> Attribute msg
onMouseMove msg =
on "mousemove" (Json.succeed msg)
-- onClickWith
-- { button = primary
-- , send = localCoords Button
-- }
-- type alias Click =
-- { button : Button
-- , send : Track
-- }
-- type Button = Primary | Secondary
-- type Track
-- = ElementCoords
-- | PageCoords
-- | ScreenCoords
-- |
{-| -}
onClickCoords : (Coords -> msg) -> Attribute msg
onClickCoords msg =
on "click" (Json.map msg localCoords)
{-| -}
onClickScreenCoords : (Coords -> msg) -> Attribute msg
onClickScreenCoords msg =
on "click" (Json.map msg screenCoords)
{-| -}
onClickPageCoords : (Coords -> msg) -> Attribute msg
onClickPageCoords msg =
on "click" (Json.map msg pageCoords)
{-| -}
onMouseCoords : (Coords -> msg) -> Attribute msg
onMouseCoords msg =
on "mousemove" (Json.map msg localCoords)
{-| -}
onMouseScreenCoords : (Coords -> msg) -> Attribute msg
onMouseScreenCoords msg =
on "mousemove" (Json.map msg screenCoords)
{-| -}
onMousePageCoords : (Coords -> msg) -> Attribute msg
onMousePageCoords msg =
on "mousemove" (Json.map msg pageCoords)
type alias Coords =
{ x : Int
, y : Int
}
screenCoords : Json.Decoder Coords
screenCoords =
Json.map2 Coords
(Json.field "screenX" Json.int)
(Json.field "screenY" Json.int)
{-| -}
localCoords : Json.Decoder Coords
localCoords =
Json.map2 Coords
(Json.field "offsetX" Json.int)
(Json.field "offsetY" Json.int)
pageCoords : Json.Decoder Coords
pageCoords =
Json.map2 Coords
(Json.field "pageX" Json.int)
(Json.field "pageY" Json.int)
-- FOCUS EVENTS
{-| -}
onLoseFocus : msg -> Attribute msg
onLoseFocus =
Internal.Attr << Html.Events.onBlur
{-| -}
onFocus : msg -> Attribute msg
onFocus =
Internal.Attr << Html.Events.onFocus
-- CUSTOM EVENTS
{-| Create a custom event listener. Normally this will not be necessary, but
you have the power! Here is how `onClick` is defined for example:
import Json.Decode as Json
onClick : msg -> Attribute msg
onClick message =
on "click" (Json.succeed message)
The first argument is the event name in the same format as with JavaScript's
[`addEventListener`][aEL] function.
The second argument is a JSON decoder. Read more about these [here][decoder].
When an event occurs, the decoder tries to turn the event object into an Elm
value. If successful, the value is routed to your `update` function. In the
case of `onClick` we always just succeed with the given `message`.
If this is confusing, work through the [Elm Architecture Tutorial][tutorial].
It really does help!
[aEL]: <https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener>
[decoder]: <http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode>
[tutorial]: <https://github.com/evancz/elm-architecture-tutorial/>
-}
on : String -> Json.Decoder msg -> Attribute msg
on event decode =
Internal.Attr <| Html.Events.on event decode
-- {-| Same as `on` but you can set a few options.
-- -}
-- onWithOptions : String -> Html.Events.Options -> Json.Decoder msg -> Attribute msg
-- onWithOptions event options decode =
-- Internal.Attr <| Html.Events.onWithOptions event options decode
-- COMMON DECODERS
{-| A `Json.Decoder` for grabbing `event.target.value`. We use this to define
`onInput` as follows:
import Json.Decode as Json
onInput : (String -> msg) -> Attribute msg
onInput tagger =
on "input" (Json.map tagger targetValue)
You probably will never need this, but hopefully it gives some insights into
how to make custom event handlers.
-}
targetValue : Json.Decoder String
targetValue =
Json.at [ "target", "value" ] Json.string
{-| A `Json.Decoder` for grabbing `event.target.checked`. We use this to define
`onCheck` as follows:
import Json.Decode as Json
onCheck : (Bool -> msg) -> Attribute msg
onCheck tagger =
on "input" (Json.map tagger targetChecked)
-}
targetChecked : Json.Decoder Bool
targetChecked =
Json.at [ "target", "checked" ] Json.bool
{-| A `Json.Decoder` for grabbing `event.keyCode`. This helps you define
keyboard listeners like this:
import Json.Decode as Json
onKeyUp : (Int -> msg) -> Attribute msg
onKeyUp tagger =
on "keyup" (Json.map tagger keyCode)
**Note:** It looks like the spec is moving away from `event.keyCode` and
towards `event.key`. Once this is supported in more browsers, we may add
helpers here for `onKeyUp`, `onKeyDown`, `onKeyPress`, etc.
-}
keyCode : Json.Decoder Int
keyCode =
Json.field "keyCode" Json.int

View File

@ -0,0 +1,525 @@
module Element.Font exposing
( color, size
, family, Font, typeface, serif, sansSerif, monospace
, external
, alignLeft, alignRight, center, justify, letterSpacing, wordSpacing
, underline, strike, italic, unitalicized
, heavy, extraBold, bold, semiBold, medium, regular, light, extraLight, hairline
, Variant, variant, variantList, smallCaps, slashedZero, ligatures, ordinal, tabularNumbers, stackedFractions, diagonalFractions, swash, feature, indexed
, glow, shadow
)
{-|
import Element
import Element.Font as Font
view =
Element.el
[ Font.color (Element.rgb 0 0 1)
, Font.size 18
, Font.family
[ Font.typeface "Open Sans"
, Font.sansSerif
]
]
(Element.text "Woohoo, I'm stylish text")
**Note:** `Font.color`, `Font.size`, and `Font.family` are inherited, meaning you can set them at the top of your view and all subsequent nodes will have that value.
**Other Note:** If you're looking for something like `line-height`, it's handled by `Element.spacing` on a `paragraph`.
@docs color, size
## Typefaces
@docs family, Font, typeface, serif, sansSerif, monospace
@docs external
## Alignment and Spacing
@docs alignLeft, alignRight, center, justify, letterSpacing, wordSpacing
## Font Styles
@docs underline, strike, italic, unitalicized
## Font Weight
@docs heavy, extraBold, bold, semiBold, medium, regular, light, extraLight, hairline
## Variants
@docs Variant, variant, variantList, smallCaps, slashedZero, ligatures, ordinal, tabularNumbers, stackedFractions, diagonalFractions, swash, feature, indexed
## Shadows
@docs glow, shadow
-}
import Element exposing (Attr, Attribute, Color)
import Internal.Flag as Flag
import Internal.Model as Internal
import Internal.Style exposing (classes)
{-| -}
type alias Font =
Internal.Font
{-| -}
color : Color -> Attr decorative msg
color fontColor =
Internal.StyleClass
Flag.fontColor
(Internal.Colored
("fc-" ++ Internal.formatColorClass fontColor)
"color"
fontColor
)
{-|
import Element
import Element.Font as Font
myElement =
Element.el
[ Font.family
[ Font.typeface "Helvetica"
, Font.sansSerif
]
]
(text "")
-}
family : List Font -> Attribute msg
family families =
Internal.StyleClass
Flag.fontFamily
(Internal.FontFamily
(List.foldl Internal.renderFontClassName "ff-" families)
families
)
{-| -}
serif : Font
serif =
Internal.Serif
{-| -}
sansSerif : Font
sansSerif =
Internal.SansSerif
{-| -}
monospace : Font
monospace =
Internal.Monospace
{-| -}
typeface : String -> Font
typeface =
Internal.Typeface
{-| -}
type alias Adjustment =
{ capital : Float
, lowercase : Float
, baseline : Float
, descender : Float
}
{-| -}
with :
{ name : String
, adjustment : Maybe Adjustment
, variants : List Variant
}
-> Font
with =
Internal.FontWith
{-| -}
sizeByCapital : Attribute msg
sizeByCapital =
Internal.htmlClass classes.sizeByCapital
{-| -}
full : Attribute msg
full =
Internal.htmlClass classes.fullSize
{-| **Note** it's likely that `Font.external` will cause a flash on your page on loading.
To bypass this, import your fonts using a separate stylesheet and just use `Font.typeface`.
It's likely that `Font.external` will be removed or redesigned in the future to avoid the flashing.
`Font.external` can be used to import font files. Let's say you found a neat font on <http://fonts.google.com>:
import Element
import Element.Font as Font
view =
Element.el
[ Font.family
[ Font.external
{ name = "Roboto"
, url = "https://fonts.googleapis.com/css?family=Roboto"
}
, Font.sansSerif
]
]
(Element.text "Woohoo, I'm stylish text")
-}
external : { url : String, name : String } -> Font
external { url, name } =
Internal.ImportFont name url
{-| Font sizes are always given as `px`.
-}
size : Int -> Attr decorative msg
size i =
Internal.StyleClass Flag.fontSize (Internal.FontSize i)
{-| In `px`.
-}
letterSpacing : Float -> Attribute msg
letterSpacing offset =
Internal.StyleClass Flag.letterSpacing <|
Internal.Single
("ls-" ++ Internal.floatClass offset)
"letter-spacing"
(String.fromFloat offset ++ "px")
{-| In `px`.
-}
wordSpacing : Float -> Attribute msg
wordSpacing offset =
Internal.StyleClass Flag.wordSpacing <|
Internal.Single ("ws-" ++ Internal.floatClass offset) "word-spacing" (String.fromFloat offset ++ "px")
{-| Align the font to the left.
-}
alignLeft : Attribute msg
alignLeft =
Internal.Class Flag.fontAlignment classes.textLeft
{-| Align the font to the right.
-}
alignRight : Attribute msg
alignRight =
Internal.Class Flag.fontAlignment classes.textRight
{-| Center align the font.
-}
center : Attribute msg
center =
Internal.Class Flag.fontAlignment classes.textCenter
{-| -}
justify : Attribute msg
justify =
Internal.Class Flag.fontAlignment classes.textJustify
-- {-| -}
-- justifyAll : Attribute msg
-- justifyAll =
-- Internal.class classesTextJustifyAll
{-| -}
underline : Attribute msg
underline =
Internal.htmlClass classes.underline
{-| -}
strike : Attribute msg
strike =
Internal.htmlClass classes.strike
{-| -}
italic : Attribute msg
italic =
Internal.htmlClass classes.italic
{-| -}
bold : Attribute msg
bold =
Internal.Class Flag.fontWeight classes.bold
{-| -}
light : Attribute msg
light =
Internal.Class Flag.fontWeight classes.textLight
{-| -}
hairline : Attribute msg
hairline =
Internal.Class Flag.fontWeight classes.textThin
{-| -}
extraLight : Attribute msg
extraLight =
Internal.Class Flag.fontWeight classes.textExtraLight
{-| -}
regular : Attribute msg
regular =
Internal.Class Flag.fontWeight classes.textNormalWeight
{-| -}
semiBold : Attribute msg
semiBold =
Internal.Class Flag.fontWeight classes.textSemiBold
{-| -}
medium : Attribute msg
medium =
Internal.Class Flag.fontWeight classes.textMedium
{-| -}
extraBold : Attribute msg
extraBold =
Internal.Class Flag.fontWeight classes.textExtraBold
{-| -}
heavy : Attribute msg
heavy =
Internal.Class Flag.fontWeight classes.textHeavy
{-| This will reset bold and italic.
-}
unitalicized : Attribute msg
unitalicized =
Internal.htmlClass classes.textUnitalicized
{-| -}
shadow :
{ offset : ( Float, Float )
, blur : Float
, color : Color
}
-> Attr decorative msg
shadow shade =
Internal.StyleClass Flag.txtShadows <|
Internal.Single (Internal.textShadowClass shade) "text-shadow" (Internal.formatTextShadow shade)
{-| A glow is just a simplified shadow.
-}
glow : Color -> Float -> Attr decorative msg
glow clr i =
let
shade =
{ offset = ( 0, 0 )
, blur = i * 2
, color = clr
}
in
Internal.StyleClass Flag.txtShadows <|
Internal.Single (Internal.textShadowClass shade) "text-shadow" (Internal.formatTextShadow shade)
{- Variants -}
{-| -}
type alias Variant =
Internal.Variant
{-| You can use this to set a single variant on an element itself such as:
el
[ Font.variant Font.smallCaps
]
(text "rendered with smallCaps")
**Note** These will **not** stack. If you want multiple variants, you should use `Font.variantList`.
-}
variant : Variant -> Attribute msg
variant var =
case var of
Internal.VariantActive name ->
Internal.Class Flag.fontVariant ("v-" ++ name)
Internal.VariantOff name ->
Internal.Class Flag.fontVariant ("v-" ++ name ++ "-off")
Internal.VariantIndexed name index ->
Internal.StyleClass Flag.fontVariant <|
Internal.Single ("v-" ++ name ++ "-" ++ String.fromInt index)
"font-feature-settings"
("\"" ++ name ++ "\" " ++ String.fromInt index)
isSmallCaps x =
case x of
Internal.VariantActive feat ->
feat == "smcp"
_ ->
False
{-| -}
variantList : List Variant -> Attribute msg
variantList vars =
let
features =
vars
|> List.map Internal.renderVariant
hasSmallCaps =
List.any isSmallCaps vars
name =
if hasSmallCaps then
vars
|> List.map Internal.variantName
|> String.join "-"
|> (\x -> x ++ "-sc")
else
vars
|> List.map Internal.variantName
|> String.join "-"
featureString =
String.join ", " features
in
Internal.StyleClass Flag.fontVariant <|
Internal.Style ("v-" ++ name)
[ Internal.Property "font-feature-settings" featureString
, Internal.Property "font-variant"
(if hasSmallCaps then
"small-caps"
else
"normal"
)
]
{-| [Small caps](https://en.wikipedia.org/wiki/Small_caps) are rendered using uppercase glyphs, but at the size of lowercase glyphs.
-}
smallCaps : Variant
smallCaps =
Internal.VariantActive "smcp"
{-| Add a slash when rendering `0`
-}
slashedZero : Variant
slashedZero =
Internal.VariantActive "zero"
{-| -}
ligatures : Variant
ligatures =
Internal.VariantActive "liga"
{-| Oridinal markers like `1st` and `2nd` will receive special glyphs.
-}
ordinal : Variant
ordinal =
Internal.VariantActive "ordn"
{-| Number figures will each take up the same space, allowing them to be easily aligned, such as in tables.
-}
tabularNumbers : Variant
tabularNumbers =
Internal.VariantActive "tnum"
{-| Render fractions with the numerator stacked on top of the denominator.
-}
stackedFractions : Variant
stackedFractions =
Internal.VariantActive "afrc"
{-| Render fractions
-}
diagonalFractions : Variant
diagonalFractions =
Internal.VariantActive "frac"
{-| -}
swash : Int -> Variant
swash =
Internal.VariantIndexed "swsh"
{-| Set a feature by name and whether it should be on or off.
Feature names are four-letter names as defined in the [OpenType specification](https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist).
-}
feature : String -> Bool -> Variant
feature name on =
if on then
Internal.VariantIndexed name 1
else
Internal.VariantIndexed name 0
{-| A font variant might have multiple versions within the font.
In these cases we need to specify the index of the version we want.
-}
indexed : String -> Int -> Variant
indexed name on =
Internal.VariantIndexed name on

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
module Element.Keyed exposing (el, column, row)
{-| Notes from the `Html.Keyed` on how keyed works:
---
A keyed node helps optimize cases where children are getting added, moved, removed, etc. Common examples include:
- The user can delete items from a list.
- The user can create new items in a list.
- You can sort a list based on name or date or whatever.
When you use a keyed node, every child is paired with a string identifier. This makes it possible for the underlying diffing algorithm to reuse nodes more efficiently.
This means if a key is changed between renders, then the diffing step will be skipped and the node will be forced to rerender.
---
@docs el, column, row
-}
import Element exposing (Attribute, Element, fill, height, shrink, width)
import Internal.Model as Internal
import Internal.Style exposing (classes)
{-| -}
el : List (Attribute msg) -> ( String, Element msg ) -> Element msg
el attrs child =
Internal.element
Internal.asEl
Internal.div
(width shrink
:: height shrink
:: attrs
)
(Internal.Keyed [ child ])
{-| -}
row : List (Attribute msg) -> List ( String, Element msg ) -> Element msg
row attrs children =
Internal.element
Internal.asRow
Internal.div
(Internal.htmlClass (classes.contentLeft ++ " " ++ classes.contentCenterY)
:: width shrink
:: height shrink
:: attrs
)
(Internal.Keyed children)
{-| -}
column : List (Attribute msg) -> List ( String, Element msg ) -> Element msg
column attrs children =
Internal.element
Internal.asColumn
Internal.div
(Internal.htmlClass
(classes.contentTop
++ " "
++ classes.contentLeft
)
:: height shrink
:: width shrink
:: attrs
)
(Internal.Keyed children)

View File

@ -0,0 +1,117 @@
module Element.Lazy exposing (lazy, lazy2, lazy3, lazy4, lazy5)
{-| Same as `Html.lazy`. In case you're unfamiliar, here's a note from the `Html` library!
---
Since all Elm functions are pure we have a guarantee that the same input
will always result in the same output. This module gives us tools to be lazy
about building `Html` that utilize this fact.
Rather than immediately applying functions to their arguments, the `lazy`
functions just bundle the function and arguments up for later. When diffing
the old and new virtual DOM, it checks to see if all the arguments are equal
by reference. If so, it skips calling the function!
This is a really cheap test and often makes things a lot faster, but definitely
benchmark to be sure!
---
@docs lazy, lazy2, lazy3, lazy4, lazy5
-}
import Internal.Model exposing (..)
import VirtualDom
{-| -}
lazy : (a -> Element msg) -> a -> Element msg
lazy fn a =
Unstyled <| VirtualDom.lazy3 apply1 fn a
{-| -}
lazy2 : (a -> b -> Element msg) -> a -> b -> Element msg
lazy2 fn a b =
Unstyled <| VirtualDom.lazy4 apply2 fn a b
{-| -}
lazy3 : (a -> b -> c -> Element msg) -> a -> b -> c -> Element msg
lazy3 fn a b c =
Unstyled <| VirtualDom.lazy5 apply3 fn a b c
{-| -}
lazy4 : (a -> b -> c -> d -> Element msg) -> a -> b -> c -> d -> Element msg
lazy4 fn a b c d =
Unstyled <| VirtualDom.lazy6 apply4 fn a b c d
{-| -}
lazy5 : (a -> b -> c -> d -> e -> Element msg) -> a -> b -> c -> d -> e -> Element msg
lazy5 fn a b c d e =
Unstyled <| VirtualDom.lazy7 apply5 fn a b c d e
apply1 fn a =
embed (fn a)
apply2 fn a b =
embed (fn a b)
apply3 fn a b c =
embed (fn a b c)
apply4 fn a b c d =
embed (fn a b c d)
apply5 fn a b c d e =
embed (fn a b c d e)
{-| -}
embed : Element msg -> LayoutContext -> VirtualDom.Node msg
embed x =
case x of
Unstyled html ->
html
Styled styled ->
styled.html
(Internal.Model.OnlyDynamic
{ hover = AllowHover
, focus =
{ borderColor = Nothing
, shadow = Nothing
, backgroundColor = Nothing
}
, mode = Layout
}
styled.styles
)
-- -- (Just
-- -- (toStyleSheetString
-- { hover = AllowHover
-- , focus =
-- { borderColor = Nothing
-- , shadow = Nothing
-- , backgroundColor = Nothing
-- }
-- , mode = Layout
-- }
-- -- styled.styles
-- -- )
-- -- )
Text text ->
always (VirtualDom.text text)
Empty ->
always (VirtualDom.text "")

View File

@ -0,0 +1,107 @@
module Element.Region exposing
( mainContent, navigation, heading, aside, footer
, description
, announce, announceUrgently
)
{-| This module is meant to make accessibility easy!
These are sign posts that accessibility software like screen readers can use to navigate your app.
All you have to do is add them to elements in your app where you see fit.
Here's an example of annotating your navigation region:
import Element.Region as Region
myNavigation =
Element.row [ Region.navigation ]
[-- ..your navigation links
]
@docs mainContent, navigation, heading, aside, footer
@docs description
@docs announce, announceUrgently
-}
import Element exposing (Attribute)
import Internal.Model as Internal exposing (Description(..))
{-| -}
mainContent : Attribute msg
mainContent =
Internal.Describe Main
{-| -}
aside : Attribute msg
aside =
Internal.Describe Complementary
{-| -}
navigation : Attribute msg
navigation =
Internal.Describe Navigation
-- form : Attribute msg
-- form =
-- Internal.Describe Form
-- search : Attribute msg
-- search =
-- Internal.Describe Search
{-| -}
footer : Attribute msg
footer =
Internal.Describe ContentInfo
{-| This will mark an element as `h1`, `h2`, etc where possible.
Though it's also smart enough to not conflict with existing nodes.
So, this code
link [ Region.heading 1 ]
{ url = "http://fruits.com"
, label = text "Best site ever"
}
will generate
<a href="http://fruits.com">
<h1>Best site ever</h1>
</a>
-}
heading : Int -> Attribute msg
heading =
Internal.Describe << Heading
{-| Screen readers will announce changes to this element and potentially interrupt any other announcement.
-}
announceUrgently : Attribute msg
announceUrgently =
Internal.Describe LiveAssertive
{-| Screen readers will announce when changes to this element are made.
-}
announce : Attribute msg
announce =
Internal.Describe LivePolite
{-| -}
description : String -> Attribute msg
description =
Internal.Describe << Internal.Label

View File

@ -0,0 +1,325 @@
module Internal.Flag exposing
( Field(..)
, Flag(..)
, active
, add
, alignBottom
, alignRight
, behind
, bgColor
, bgGradient
, bgImage
, borderColor
, borderRound
, borderStyle
, borderWidth
, centerX
, centerY
, cursor
, flag
, focus
, fontAlignment
, fontColor
, fontFamily
, fontSize
, fontVariant
, fontWeight
, gridPosition
, gridTemplate
, height
, heightBetween
, heightContent
, heightFill
, heightTextAreaContent
, hover
, letterSpacing
, merge
, moveX
, moveY
, none
, overflow
, padding
, present
, rotate
, scale
, shadows
, spacing
, transparency
, txtShadows
, value
, width
, widthBetween
, widthContent
, widthFill
, wordSpacing
, xAlign
, yAlign
)
{-| -}
import Bitwise
type Field
= Field Int Int
type Flag
= Flag Int
| Second Int
none : Field
none =
Field 0 0
value myFlag =
case myFlag of
Flag first ->
round (logBase 2 (toFloat first))
Second second ->
round (logBase 2 (toFloat second)) + 32
{-| If the query is in the truth, return True
-}
present : Flag -> Field -> Bool
present myFlag (Field fieldOne fieldTwo) =
case myFlag of
Flag first ->
Bitwise.and first fieldOne == first
Second second ->
Bitwise.and second fieldTwo == second
{-| Add a flag to a field.
-}
add : Flag -> Field -> Field
add myFlag (Field one two) =
case myFlag of
Flag first ->
Field (Bitwise.or first one) two
Second second ->
Field one (Bitwise.or second two)
{-| Generally you want to use `add`, which keeps a distinction between Fields and Flags.
Merging will combine two fields
-}
merge : Field -> Field -> Field
merge (Field one two) (Field three four) =
Field (Bitwise.or one three) (Bitwise.or two four)
flag : Int -> Flag
flag i =
if i > 31 then
Second
(Bitwise.shiftLeftBy (i - 32) 1)
else
Flag
(Bitwise.shiftLeftBy i 1)
{- Used for Style invalidation -}
transparency =
flag 0
padding =
flag 2
spacing =
flag 3
fontSize =
flag 4
fontFamily =
flag 5
width =
flag 6
height =
flag 7
bgColor =
flag 8
bgImage =
flag 9
bgGradient =
flag 10
borderStyle =
flag 11
fontAlignment =
flag 12
fontWeight =
flag 13
fontColor =
flag 14
wordSpacing =
flag 15
letterSpacing =
flag 16
borderRound =
flag 17
txtShadows =
flag 18
shadows =
flag 19
overflow =
flag 20
cursor =
flag 21
scale =
flag 23
rotate =
flag 24
moveX =
flag 25
moveY =
flag 26
borderWidth =
flag 27
borderColor =
flag 28
yAlign =
flag 29
xAlign =
flag 30
focus =
flag 31
active =
flag 32
hover =
flag 33
gridTemplate =
flag 34
gridPosition =
flag 35
{- Notes -}
heightContent =
flag 36
heightFill =
flag 37
widthContent =
flag 38
widthFill =
flag 39
alignRight =
flag 40
alignBottom =
flag 41
centerX =
flag 42
centerY =
flag 43
widthBetween =
flag 44
heightBetween =
flag 45
behind =
flag 46
heightTextAreaContent =
flag 47
fontVariant =
flag 48

View File

@ -0,0 +1,270 @@
module Internal.Grid exposing (Around, Layout(..), PositionedElement, RelativePosition(..), build, createGrid, getWidth, relative)
{-| Relative positioning within a grid.
A relatively positioned grid, means a 3x3 grid with the primary element in the center.
-}
import Element
import Internal.Flag as Flag
import Internal.Model as Internal
type RelativePosition
= OnRight
| OnLeft
| Above
| Below
| InFront
type Layout
= GridElement
| Row
| Column
type alias Around alignment msg =
{ right : Maybe (PositionedElement alignment msg)
, left : Maybe (PositionedElement alignment msg)
, primary : ( Maybe String, List (Internal.Attribute alignment msg), List (Internal.Element msg) )
-- , primaryWidth : Internal.Length
, defaultWidth : Internal.Length
, below : Maybe (PositionedElement alignment msg)
, above : Maybe (PositionedElement alignment msg)
, inFront : Maybe (PositionedElement alignment msg)
}
type alias PositionedElement alignment msg =
{ layout : Layout
, child : List (Internal.Element msg)
, attrs : List (Internal.Attribute alignment msg)
, width : Int
, height : Int
}
relative : Maybe String -> List (Internal.Attribute alignment msg) -> Around alignment msg -> Internal.Element msg
relative node attributes around =
let
( sX, sY ) =
Internal.getSpacing attributes ( 7, 7 )
make positioned =
Internal.element Internal.noStyleSheet
Internal.asEl
Nothing
positioned.attrs
(Internal.Unkeyed positioned.child)
( template, children ) =
createGrid ( sX, sY ) around
in
Internal.element Internal.noStyleSheet
Internal.asGrid
node
(template ++ attributes)
(Internal.Unkeyed
children
)
createGrid : ( Int, Int ) -> Around alignment msg -> ( List (Internal.Attribute alignment msg1), List (Element.Element msg) )
createGrid ( spacingX, spacingY ) nearby =
let
rowCount =
List.sum
[ 1
, if Nothing == nearby.above then
0
else
1
, if Nothing == nearby.below then
0
else
1
]
colCount =
List.sum
[ 1
, if Nothing == nearby.left then
0
else
1
, if Nothing == nearby.right then
0
else
1
]
rows =
if nearby.above == Nothing then
{ above = 0
, primary = 1
, below = 2
}
else
{ above = 1
, primary = 2
, below = 3
}
columns =
if Nothing == nearby.left then
{ left = 0
, primary = 1
, right = 2
}
else
{ left = 1
, primary = 2
, right = 3
}
rowCoord pos =
case pos of
Above ->
rows.above
Below ->
rows.below
OnRight ->
rows.primary
OnLeft ->
rows.primary
InFront ->
rows.primary
colCoord pos =
case pos of
Above ->
columns.primary
Below ->
columns.primary
OnRight ->
columns.right
OnLeft ->
columns.left
InFront ->
columns.primary
place pos el =
build (rowCoord pos) (colCoord pos) spacingX spacingY el
in
( [ Internal.StyleClass Flag.gridTemplate
(Internal.GridTemplateStyle
{ spacing = ( Internal.Px spacingX, Internal.Px spacingY )
, columns =
List.filterMap identity
[ nearby.left
|> Maybe.map (\el -> Maybe.withDefault nearby.defaultWidth (getWidth el.attrs))
, nearby.primary
|> (\( node, attrs, el ) -> getWidth attrs)
|> Maybe.withDefault nearby.defaultWidth
|> Just
, nearby.right
|> Maybe.map (\el -> Maybe.withDefault nearby.defaultWidth (getWidth el.attrs))
]
, rows = List.map (always Internal.Content) (List.range 1 rowCount)
}
)
]
, List.filterMap identity
[ Just <|
case nearby.primary of
( primaryNode, primaryAttrs, primaryChildren ) ->
Internal.element Internal.noStyleSheet
Internal.asEl
primaryNode
(Internal.StyleClass Flag.gridPosition
(Internal.GridPosition
{ row = rows.primary
, col = columns.primary
, width = 1
, height = 1
}
)
:: primaryAttrs
)
(Internal.Unkeyed primaryChildren)
, Maybe.map (place OnLeft) nearby.left
, Maybe.map (place OnRight) nearby.right
, Maybe.map (place Above) nearby.above
, Maybe.map (place Below) nearby.below
, Maybe.map (place InFront) nearby.inFront
]
)
build : Int -> Int -> Int -> Int -> { a | attrs : List (Internal.Attribute alignment msg), height : Int, layout : Layout, width : Int, child : List (Internal.Element msg) } -> Internal.Element msg
build rowCoord colCoord spacingX spacingY positioned =
let
attributes =
Internal.StyleClass Flag.gridPosition
(Internal.GridPosition
{ row = rowCoord
, col = colCoord
, width = positioned.width
, height = positioned.height
}
)
:: Internal.StyleClass Flag.spacing (Internal.SpacingStyle spacingX spacingY)
:: positioned.attrs
in
case positioned.layout of
GridElement ->
Internal.element Internal.noStyleSheet
Internal.asEl
Nothing
attributes
(Internal.Unkeyed <| positioned.child)
Row ->
Internal.element Internal.noStyleSheet
Internal.asRow
Nothing
attributes
(Internal.Unkeyed positioned.child)
Column ->
Internal.element Internal.noStyleSheet
Internal.asColumn
Nothing
attributes
(Internal.Unkeyed positioned.child)
getWidth : List (Internal.Attribute align msg) -> Maybe Internal.Length
getWidth attrs =
let
widthPlease attr found =
case found of
Just x ->
Just x
Nothing ->
case attr of
Internal.Width w ->
Just w
_ ->
Nothing
in
List.foldr widthPlease Nothing attrs

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

307
generator/src/cli.js Normal file
View File

@ -0,0 +1,307 @@
#!/usr/bin/env node
const cliVersion = require("../../package.json").version;
const indexTemplate = require("./index-template.js");
const util = require("util");
const fs = require("./dir-helpers.js");
const path = require("path");
const seo = require("./seo-renderer.js");
const exec = util.promisify(require("child_process").exec);
const spawnCallback = require("child_process").spawn;
const codegen = require("./codegen.js");
const generateManifest = require("./generate-manifest.js");
const DIR_PATH = path.join(process.cwd());
const OUTPUT_FILE_NAME = "elm.js";
let foundErrors = false;
process.on("unhandledRejection", (error) => {
console.error(error);
process.exit(1);
});
const ELM_FILE_PATH = path.join(
DIR_PATH,
"./elm-stuff/elm-pages",
OUTPUT_FILE_NAME
);
async function ensureRequiredDirs() {
fs.tryMkdir(`dist`);
}
async function run() {
await ensureRequiredDirs();
XMLHttpRequest = require("xhr2");
await codegen.generate();
await compileCliApp();
copyAssets();
compileElm();
runElmApp();
}
function runElmApp() {
process.on("beforeExit", (code) => {
if (foundErrors) {
process.exit(1);
} else {
process.exit(0);
}
});
return new Promise((resolve, _) => {
const mode /** @type { "dev" | "prod" } */ = "elm-to-html-beta";
const staticHttpCache = {};
const app = require(ELM_FILE_PATH).Elm.Main.init({
flags: { secrets: process.env, mode, staticHttpCache },
});
app.ports.toJsPort.subscribe((/** @type { FromElm } */ fromElm) => {
if (fromElm.command === "log") {
console.log(fromElm.value);
} else if (fromElm.tag === "InitialData") {
fs.writeFile(
`dist/manifest.json`,
JSON.stringify(generateManifest(fromElm.args[0].manifest))
);
generateFiles(fromElm.args[0].filesToGenerate);
} else if (fromElm.tag === "PageProgress") {
outputString(fromElm);
} else if (fromElm.tag === "Errors") {
console.error(fromElm.args[0]);
foundErrors = true;
} else {
console.log(fromElm);
throw "Unknown port tag.";
}
});
});
}
/**
* @param {{ path: string; content: string; }[]} filesToGenerate
*/
async function generateFiles(filesToGenerate) {
filesToGenerate.forEach(async ({ path: pathToGenerate, content }) => {
const fullPath = `dist/${pathToGenerate}`;
console.log(`Generating file /${pathToGenerate}`);
await fs.tryMkdir(path.dirname(fullPath));
fs.writeFile(fullPath, content);
});
}
/**
* @param {string} route
*/
function cleanRoute(route) {
return route.replace(/(^\/|\/$)/, "");
}
/**
* @param {string} elmPath
*/
async function elmToEsm(elmPath) {
const elmEs3 = await fs.readFile(elmPath, "utf8");
const elmEsm =
"\n" +
"const scope = {};\n" +
elmEs3.replace("}(this));", "}(scope));") +
"export const { Elm } = scope;\n" +
"\n";
await fs.writeFile(elmPath, elmEsm);
}
/**
* @param {string} cleanedRoute
*/
function pathToRoot(cleanedRoute) {
return cleanedRoute === ""
? cleanedRoute
: cleanedRoute
.split("/")
.map((_) => "..")
.join("/")
.replace(/\.$/, "./");
}
/**
* @param {string} route
*/
function baseRoute(route) {
const cleanedRoute = cleanRoute(route);
return cleanedRoute === "" ? "./" : pathToRoot(route);
}
async function outputString(/** @type { PageProgress } */ fromElm) {
const args = fromElm.args[0];
console.log(`Pre-rendered /${args.route}`);
let contentJson = {};
contentJson["body"] = args.body;
contentJson["staticData"] = args.contentJson;
const normalizedRoute = args.route.replace(/index$/, "");
// await fs.mkdir(`./dist/${normalizedRoute}`, { recursive: true });
await fs.tryMkdir(`./dist/${normalizedRoute}`);
fs.writeFile(`dist/${normalizedRoute}/index.html`, wrapHtml(args));
fs.writeFile(
`dist/${normalizedRoute}/content.json`,
JSON.stringify(contentJson)
);
}
async function compileElm() {
const outputPath = `dist/elm.js`;
await spawnElmMake("src/Main.elm", outputPath);
await elmToEsm(path.join(process.cwd(), outputPath));
runTerser(outputPath);
}
function spawnElmMake(elmEntrypointPath, outputPath, cwd) {
return new Promise((resolve, reject) => {
const fullOutputPath = cwd ? path.join(cwd, outputPath) : outputPath;
if (fs.existsSync(fullOutputPath)) {
fs.rmSync(fullOutputPath, {
force: true /* ignore errors if file doesn't exist */,
});
}
const subprocess = spawnCallback(
`elm-optimize-level-2`,
[elmEntrypointPath, "--output", outputPath],
{
// ignore stdout
stdio: ["inherit", "ignore", "inherit"],
cwd: cwd,
}
);
subprocess.on("close", (code) => {
const fileOutputExists = fs.existsSync(fullOutputPath);
if (code == 0 && fileOutputExists) {
resolve();
} else {
reject();
process.exit(1);
}
});
});
}
/**
* @param {string} filePath
*/
async function runTerser(filePath) {
await shellCommand(
`npx terser ${filePath} --module --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | npx terser --module --mangle --output=${filePath}`
);
}
async function copyAssets() {
fs.writeFile("dist/elm-pages.js", indexTemplate);
fs.copyFile("beta-index.js", "dist/index.js");
fs.copyFile("beta-style.css", "dist/style.css");
fs.copyDirFlat("static", "dist");
fs.tryMkdir("dist/images");
fs.copyDirNested("images", "dist/images");
}
async function compileCliApp() {
await spawnElmMake("../../src/Main.elm", "elm.js", "./elm-stuff/elm-pages");
const elmFileContent = await fs.readFile(ELM_FILE_PATH, "utf-8");
await fs.writeFile(
ELM_FILE_PATH,
elmFileContent.replace(
/return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_JSON_STRINGIFY.\)/g,
"return x"
)
);
}
run();
/**
* @param {string} command
*/
function shellCommand(command) {
const promise = exec(command, { stdio: "inherit" });
promise.then((output) => {
if (output.stdout) {
console.log(output.stdout);
}
if (output.stderr) {
throw output.stderr;
}
});
return promise;
}
/** @typedef { { route : string; contentJson : string; head : SeoTag[]; html: string; body: string; } } FromElm */
/** @typedef {HeadTag | JsonLdTag} SeoTag */
/** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */
/** @typedef {{ contents: Object; type: 'json-ld' }} JsonLdTag */
/** @typedef { { tag : 'PageProgress'; args : Arg[] } } PageProgress */
/** @typedef {
{
body: string;
head: any[];
errors: any[];
contentJson: any[];
html: string;
route: string;
title: string;
}
} Arg
*/
function wrapHtml(/** @type { Arg } */ fromElm) {
/*html*/
return `<!DOCTYPE html>
<html lang="en">
<head>
<link rel="preload" href="content.json" as="fetch" crossorigin="">
<link rel="stylesheet" href="/style.css"></link>
<link rel="preload" href="/elm-pages.js" as="script">
<link rel="preload" href="/index.js" as="script">
<link rel="preload" href="/elm.js" as="script">
<link rel="preload" href="/elm.js" as="script">
<script defer="defer" src="/elm.js" type="module"></script>
<script defer="defer" src="/elm-pages.js" type="module"></script>
<base href="${baseRoute(fromElm.route)}">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.getRegistrations().then(function(registrations) {
for (let registration of registrations) {
registration.unregister()
}
})
});
}
</script>
<title>${fromElm.title}</title>
<meta name="generator" content="elm-pages v${cliVersion}">
<link rel="manifest" href="manifest.json">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
${seo.toString(fromElm.head)}
<body>
<div data-url="" display="none"></div>
${fromElm.html}
</body>
</html>
`;
}

75
generator/src/codegen.js Normal file
View File

@ -0,0 +1,75 @@
const fs = require("fs");
const copyModifiedElmJson = require("./rewrite-elm-json.js");
const { elmPagesCliFile, elmPagesUiFile } = require("./elm-file-constants.js");
const path = require("path");
const { ensureDirSync, deleteIfExists } = require("./file-helpers.js");
const globby = require("globby");
const parseFrontmatter = require("./frontmatter.js");
const generateRecords = require("./generate-records.js");
async function generate() {
global.builtAt = new Date();
global.staticHttpCache = {};
const markdownContent = globby
.sync(["content/**/*.*"], {})
.map(unpackFile)
.map(({ path, contents }) => {
return parseMarkdown(path, contents);
});
const routes = toRoutes(markdownContent);
await writeFiles(markdownContent);
}
function unpackFile(path) {
return { path, contents: fs.readFileSync(path).toString() };
}
function toRoutes(entries) {
return entries.map(toRoute);
}
function toRoute(entry) {
let fullPath = entry.path
.replace(/(index)?\.[^/.]+$/, "")
.split("/")
.filter((item) => item !== "")
.slice(1);
return fullPath.join("/");
}
async function writeFiles(markdownContent) {
const staticRoutes = await generateRecords();
ensureDirSync("./elm-stuff");
ensureDirSync("./gen");
ensureDirSync("./elm-stuff/elm-pages");
// prevent compilation errors if migrating from previous elm-pages version
deleteIfExists("./elm-stuff/elm-pages/Pages/ContentCache.elm");
deleteIfExists("./elm-stuff/elm-pages/Pages/Platform.elm");
const uiFileContent = elmPagesUiFile(staticRoutes, markdownContent);
fs.writeFileSync("./gen/Pages.elm", uiFileContent);
// write `Pages.elm` with cli interface
fs.writeFileSync(
"./elm-stuff/elm-pages/Pages.elm",
elmPagesCliFile(staticRoutes, markdownContent)
);
// write modified elm.json to elm-stuff/elm-pages/
copyModifiedElmJson();
}
function parseMarkdown(path, fileContents) {
const { content, data } = parseFrontmatter(path, fileContents);
return {
path,
metadata: JSON.stringify(data),
body: content,
};
}
module.exports = { generate };

View File

@ -23,6 +23,8 @@ function runElm(/** @type string */ mode) {
if (payload.tag === "Success") {
global.staticHttpCache = payload.args[0].staticHttpCache;
resolve(payload.args[0])
} else if (payload.command === "log") {
console.log(payload.value);
} else {
reject(payload.args[0])
}

48
generator/src/copy-dir.js Normal file
View File

@ -0,0 +1,48 @@
const util = require("util");
const fsSync = require("fs");
const fs = {
writeFile: util.promisify(fsSync.writeFile),
mkdir: util.promisify(fsSync.mkdir),
readFile: util.promisify(fsSync.readFile),
copyFile: util.promisify(fsSync.copyFile),
readdir: util.promisify(fsSync.readdir),
};
const path = require("path");
/**
* @param {string} srcDirectory
* @param {string} destDir
*/
async function copyDirFlat(srcDirectory, destDir) {
const items = await fs.readdir(srcDirectory);
items.forEach(function (childItemName) {
copyDirNested(
path.join(srcDirectory, childItemName),
path.join(destDir, childItemName)
);
});
}
/**
* @param {string} src
* @param {string} dest
*/
async function copyDirNested(src, dest) {
var exists = fsSync.existsSync(src);
var stats = exists && fsSync.statSync(src);
var isDirectory = exists && stats.isDirectory();
if (isDirectory) {
await fs.mkdir(dest);
const items = await fs.readdir(src);
items.forEach(function (childItemName) {
copyDirNested(
path.join(src, childItemName),
path.join(dest, childItemName)
);
});
} else {
fs.copyFile(src, dest);
}
}
module.exports = { copyDirFlat, copyDirNested };

View File

@ -0,0 +1,72 @@
const util = require("util");
const fsSync = require("fs");
const fs = {
writeFile: util.promisify(fsSync.writeFile),
rmSync: util.promisify(fsSync.unlinkSync),
mkdir: util.promisify(fsSync.mkdir),
readFile: util.promisify(fsSync.readFile),
copyFile: util.promisify(fsSync.copyFile),
exists: util.promisify(fsSync.exists),
existsSync: fsSync.existsSync,
readdir: util.promisify(fsSync.readdir),
};
/**
* @param {import("fs").PathLike} dirName
*/
async function tryMkdir(dirName) {
const exists = await fs.exists(dirName);
if (!exists) {
await fs.mkdir(dirName, { recursive: true });
}
}
const path = require("path");
/**
* @param {string} srcDirectory
* @param {string} destDir
*/
async function copyDirFlat(srcDirectory, destDir) {
const items = await fs.readdir(srcDirectory);
items.forEach(function (childItemName) {
copyDirNested(
path.join(srcDirectory, childItemName),
path.join(destDir, childItemName)
);
});
}
/**
* @param {string} src
* @param {string} dest
*/
async function copyDirNested(src, dest) {
var exists = fsSync.existsSync(src);
var stats = exists && fsSync.statSync(src);
var isDirectory = exists && stats.isDirectory();
if (isDirectory) {
await tryMkdir(dest);
const items = await fs.readdir(src);
items.forEach(function (childItemName) {
copyDirNested(
path.join(src, childItemName),
path.join(dest, childItemName)
);
});
} else {
fs.copyFile(src, dest);
}
}
module.exports = {
writeFile: fs.writeFile,
readFile: fs.readFile,
copyFile: fs.copyFile,
exists: fs.exists,
tryMkdir,
copyDirFlat,
copyDirNested,
rmSync: fs.rmSync,
existsSync: fs.existsSync,
};

View File

@ -0,0 +1,21 @@
/**
* @param {{ name: string; short_name: string; description: string; display: string; orientation: string; serviceworker: { scope: string; }; start_url: string; background_color: string; theme_color: string; }} config
*/
function generate(config) {
return {
name: config.name,
short_name: config.short_name,
description: config.description,
dir: "auto",
lang: "en-US",
display: config.display,
orientation: config.orientation,
scope: config.serviceworker.scope,
start_url: `/${config.start_url}`,
background_color: config.background_color,
theme_color: config.theme_color,
icons: config.icons,
};
}
module.exports = generate;

View File

@ -0,0 +1,165 @@
module.exports = `import { Elm } from "/elm.js";
import userInit from "/index.js";
let prefetchedPages;
let initialLocationHash;
let elmViewRendered = false;
function pagesInit(
/** @type { mainElmModule: { init: any } } */ { mainElmModule }
) {
prefetchedPages = [window.location.pathname];
initialLocationHash = document.location.hash.replace(/^#/, "");
return new Promise(function (resolve, reject) {
document.addEventListener("DOMContentLoaded", (_) => {
new MutationObserver(function () {
elmViewRendered = true;
}).observe(document.body, {
attributes: true,
childList: true,
subtree: true,
});
loadContentAndInitializeApp(mainElmModule).then(resolve, reject);
});
});
}
function loadContentAndInitializeApp(
/** @type { init: any } */ mainElmModule
) {
const path = window.location.pathname.replace(/(\w)$/, "$1/");
return Promise.all([
httpGet(\`\${window.location.origin}\${path}content.json\`),
]).then(function (/** @type {[JSON]} */ [contentJson]) {
const app = mainElmModule.init({
flags: {
secrets: null,
baseUrl: document.baseURI,
isPrerendering: false,
isDevServer: false,
isElmDebugMode: false,
contentJson,
},
});
app.ports.toJsPort.subscribe((
/** @type { { allRoutes: string[] } } */ fromElm
) => {
window.allRoutes = fromElm.allRoutes.map(
(route) => new URL(route, document.baseURI).href
);
setupLinkPrefetching();
});
return app;
});
}
function setupLinkPrefetching() {
new MutationObserver(observeFirstRender).observe(document.body, {
attributes: true,
childList: true,
subtree: true,
});
}
function loadNamedAnchor() {
if (initialLocationHash !== "") {
const namedAnchor = document.querySelector(\`[name=\${initialLocationHash}]\`);
namedAnchor && namedAnchor.scrollIntoView();
}
}
function observeFirstRender(
/** @type {MutationRecord[]} */ mutationList,
/** @type {MutationObserver} */ firstRenderObserver
) {
loadNamedAnchor();
for (let mutation of mutationList) {
if (mutation.type === "childList") {
setupLinkPrefetchingHelp();
}
}
firstRenderObserver.disconnect();
new MutationObserver(observeUrlChanges).observe(document.body.children[0], {
attributes: true,
childList: false,
subtree: false,
});
}
function observeUrlChanges(
/** @type {MutationRecord[]} */ mutationList,
/** @type {MutationObserver} */ _theObserver
) {
for (let mutation of mutationList) {
if (
mutation.type === "attributes" &&
mutation.attributeName === "data-url"
) {
setupLinkPrefetchingHelp();
}
}
}
function setupLinkPrefetchingHelp(
/** @type {MutationObserver} */ _mutationList,
/** @type {MutationObserver} */ _theObserver
) {
const links = document.querySelectorAll("a");
links.forEach((link) => {
// console.log(link.pathname);
link.addEventListener("mouseenter", function (event) {
if (event && event.target && event.target instanceof HTMLAnchorElement) {
prefetchIfNeeded(event.target);
} else {
// console.log("Couldn't prefetch with event", event);
}
});
});
}
function prefetchIfNeeded(/** @type {HTMLAnchorElement} */ target) {
if (target.host === window.location.host) {
if (prefetchedPages.includes(target.pathname)) {
// console.log("Already preloaded", target.href);
// console.log("Not a known route, skipping preload", target.pathname);
} else if (
!allRoutes.includes(new URL(target.pathname, document.baseURI).href)
) {
} else {
prefetchedPages.push(target.pathname);
// console.log("Preloading...", target.pathname);
const link = document.createElement("link");
link.setAttribute("as", "fetch");
link.setAttribute("rel", "prefetch");
link.setAttribute("href", origin + target.pathname + "/content.json");
document.head.appendChild(link);
}
}
}
function httpGet(/** @type string */ theUrl) {
return new Promise(function (resolve, reject) {
const xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
resolve(JSON.parse(xmlHttp.responseText));
};
xmlHttp.onerror = reject;
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
});
}
userInit(
pagesInit({
mainElmModule: Elm.Main,
})
);
`;

View File

@ -0,0 +1,60 @@
const elmPagesVersion = "TODO";
module.exports = { toString };
function toString(/** @type { SeoTag[] } */ tags) {
// appendTag({
// type: "head",
// name: "meta",
// attributes: [
// ["name", "generator"],
// ["content", `elm-pages v${elmPagesVersion}`],
// ],
// });
const generatorTag /** @type { HeadTag } */ = {
type: "head",
name: "meta",
attributes: [
["name", "generator"],
["content", `elm-pages v${elmPagesVersion}`],
],
};
// tags.concat([generatorTag]);
return tags
.map((headTag) => {
if (headTag.type === "head") {
return appendTag(headTag);
} else if (headTag.type === "json-ld") {
return appendJsonLdTag(headTag);
} else {
throw new Error(`Unknown tag type ${JSON.stringify(headTag)}`);
}
})
.join("\n");
}
/** @typedef {HeadTag | JsonLdTag} SeoTag */
/** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */
function appendTag(/** @type {HeadTag} */ tagDetails) {
// const meta = document.createElement(tagDetails.name);
const tagsString = tagDetails.attributes.map(([name, value]) => {
// meta.setAttribute(name, value);
return `${name}="${value}"`;
});
return ` <${tagDetails.name} ${tagsString.join(" ")} />`;
// document.getElementsByTagName("head")[0].appendChild(meta);
}
/** @typedef {{ contents: Object; type: 'json-ld' }} JsonLdTag */
function appendJsonLdTag(/** @type {JsonLdTag} */ tagDetails) {
// let jsonLdScript = document.createElement("script");
// jsonLdScript.type = "application/ld+json";
// jsonLdScript.innerHTML = JSON.stringify(tagDetails.contents);
// document.getElementsByTagName("head")[0].appendChild(jsonLdScript);
return `<script type="application/ld+json">
${JSON.stringify(tagDetails.contents)}
</script>`;
}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "esnext",
"noImplicitAny": true,
"removeComments": true,
"strictNullChecks": true,

366
package-lock.json generated
View File

@ -2116,14 +2116,12 @@
"@types/istanbul-lib-coverage": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.2.tgz",
"integrity": "sha512-rsZg7eL+Xcxsxk2XlBt9KcG8nOp9iYdKCOikY9x2RFJCyOdNj4MKPQty0e8oZr29vVAzKXr1BmR+kZauti3o1w==",
"dev": true
"integrity": "sha512-rsZg7eL+Xcxsxk2XlBt9KcG8nOp9iYdKCOikY9x2RFJCyOdNj4MKPQty0e8oZr29vVAzKXr1BmR+kZauti3o1w=="
},
"@types/istanbul-lib-report": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
"integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "*"
}
@ -2132,7 +2130,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz",
"integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "*",
"@types/istanbul-lib-report": "*"
@ -2277,8 +2274,7 @@
"@types/node": {
"version": "12.12.38",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.38.tgz",
"integrity": "sha512-75eLjX0pFuTcUXnnWmALMzzkYorjND0ezNEycaKesbUBg9eGZp4GHPuDmkRc4mQQvIpe29zrzATNRA6hkYqwmA==",
"dev": true
"integrity": "sha512-75eLjX0pFuTcUXnnWmALMzzkYorjND0ezNEycaKesbUBg9eGZp4GHPuDmkRc4mQQvIpe29zrzATNRA6hkYqwmA=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@ -2417,7 +2413,6 @@
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
}
@ -2425,8 +2420,7 @@
"@types/yargs-parser": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz",
"integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==",
"dev": true
"integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
},
"@webassemblyjs/ast": {
"version": "1.9.0",
@ -3906,6 +3900,14 @@
"pkg-up": "^2.0.0"
}
},
"bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"requires": {
"fast-json-stable-stringify": "2.x"
}
},
"bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@ -4323,8 +4325,7 @@
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="
},
"cipher-base": {
"version": "1.0.4",
@ -5454,6 +5455,74 @@
"elm-hot": "^1.1.4"
}
},
"elm-optimize-level-2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/elm-optimize-level-2/-/elm-optimize-level-2-0.1.4.tgz",
"integrity": "sha512-3pWWNCurTBfjTfRQViECVUaK+ygwp4gQ0KAPKpYYlGl2KmlBy0JqRcgcSXIgqO4C9Tmeb+20J1qZplCaY/2r/w==",
"requires": {
"chalk": "^4.1.0",
"commander": "^6.0.0",
"node-elm-compiler": "^5.0.4",
"ts-jest": "^26.2.0",
"ts-union": "^2.2.1",
"typescript": "^3.9.7"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"commander": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.1.0.tgz",
"integrity": "sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw=="
}
}
},
"elm-test": {
"version": "0.19.1-revision2",
"resolved": "https://registry.npmjs.org/elm-test/-/elm-test-0.19.1-revision2.tgz",
@ -7269,6 +7338,30 @@
"param-case": "^3.0.3",
"relateurl": "^0.2.7",
"terser": "^4.6.3"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"terser": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
}
}
}
}
},
"html-webpack-plugin": {
@ -8059,7 +8152,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
"integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
"dev": true,
"requires": {
"ci-info": "^2.0.0"
}
@ -10506,6 +10598,11 @@
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
"integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM="
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@ -10601,6 +10698,11 @@
"semver": "^5.6.0"
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
},
"makeerror": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
@ -12412,8 +12514,7 @@
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"read-chunk": {
"version": "1.0.1",
@ -14382,24 +14483,27 @@
}
},
"terser": {
"version": "4.6.13",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.13.tgz",
"integrity": "sha512-wMvqukYgVpQlymbnNbabVZbtM6PN63AzqexpwJL8tbh/mRT9LE5o+ruVduAGL7D6Fpjl+Q+06U5I9Ul82odAhw==",
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.3.7.tgz",
"integrity": "sha512-lJbKdfxWvjpV330U4PBZStCT9h3N9A4zZVA5Y4k9sCWXknrpdyxi1oMsRKLmQ/YDMDxSBKIh88v0SkdhdqX06w==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
"source-map": "~0.7.2",
"source-map-support": "~0.5.19"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
}
}
},
@ -14444,6 +14548,11 @@
"unique-filename": "^1.1.1"
}
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"find-cache-dir": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
@ -14536,6 +14645,16 @@
"figgy-pudding": "^3.5.1",
"minipass": "^3.1.1"
}
},
"terser": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
}
}
}
},
@ -14739,6 +14858,190 @@
"glob": "^7.1.2"
}
},
"ts-jest": {
"version": "26.4.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.1.tgz",
"integrity": "sha512-F4aFq01aS6mnAAa0DljNmKr/Kk9y4HVZ1m6/rtJ0ED56cuxINGq3Q9eVAh+z5vcYKe5qnTMvv90vE8vUMFxomg==",
"requires": {
"@types/jest": "26.x",
"bs-logger": "0.x",
"buffer-from": "1.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^26.1.0",
"json5": "2.x",
"lodash.memoize": "4.x",
"make-error": "1.x",
"mkdirp": "1.x",
"semver": "7.x",
"yargs-parser": "20.x"
},
"dependencies": {
"@jest/types": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz",
"integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==",
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^1.1.1",
"@types/yargs": "^15.0.0",
"chalk": "^3.0.0"
}
},
"@types/jest": {
"version": "26.0.14",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.14.tgz",
"integrity": "sha512-Hz5q8Vu0D288x3iWXePSn53W7hAjP0H7EQ6QvDO9c7t46mR0lNOLlfuwQ+JkVxuhygHzlzPX+0jKdA3ZgSh+Vg==",
"requires": {
"jest-diff": "^25.2.1",
"pretty-format": "^25.2.1"
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"diff-sequences": {
"version": "25.2.6",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz",
"integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"jest-diff": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz",
"integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==",
"requires": {
"chalk": "^3.0.0",
"diff-sequences": "^25.2.6",
"jest-get-type": "^25.2.6",
"pretty-format": "^25.5.0"
}
},
"jest-get-type": {
"version": "25.2.6",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz",
"integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig=="
},
"jest-util": {
"version": "26.6.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.0.tgz",
"integrity": "sha512-/cUGqcnKeZMjvTQLfJo65nBOEZ/k0RB/8usv2JpfYya05u0XvBmKkIH5o5c4nCh9DD61B1YQjMGGqh1Ha0aXdg==",
"requires": {
"@jest/types": "^26.6.0",
"@types/node": "*",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.4",
"is-ci": "^2.0.0",
"micromatch": "^4.0.2"
},
"dependencies": {
"@jest/types": {
"version": "26.6.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.0.tgz",
"integrity": "sha512-8pDeq/JVyAYw7jBGU83v8RMYAkdrRxLG3BGnAJuqaQAUd6GWBmND2uyl+awI88+hit48suLoLjNFtR+ZXxWaYg==",
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^15.0.0",
"chalk": "^4.0.0"
}
},
"@types/istanbul-reports": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz",
"integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==",
"requires": {
"@types/istanbul-lib-report": "*"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"pretty-format": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz",
"integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==",
"requires": {
"@jest/types": "^25.5.0",
"ansi-regex": "^5.0.0",
"ansi-styles": "^4.0.0",
"react-is": "^16.12.0"
}
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
},
"yargs-parser": {
"version": "20.2.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz",
"integrity": "sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww=="
}
}
},
"ts-union": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/ts-union/-/ts-union-2.3.0.tgz",
"integrity": "sha512-OP+W9WoYvGlOMjc90D6nYz60jU1zQlXAg3VBtuSoMDejY94PaORkya9HtHjaaqqwA4I5/hN38fmKK0nSWj7jPg=="
},
"tslib": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz",
@ -15242,6 +15545,11 @@
}
}
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"extend-shallow": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@ -15349,6 +15657,18 @@
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
"worker-farm": "^1.7.0"
},
"dependencies": {
"terser": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
}
}
}
},
"to-regex-range": {

View File

@ -2,6 +2,7 @@
"name": "elm-pages",
"version": "1.4.3",
"homepage": "http://elm-pages.com",
"moduleResolution": "node",
"description": "Type-safe static sites, written in pure elm with your own custom elm-markup syntax.",
"main": "index.js",
"scripts": {
@ -27,6 +28,7 @@
"css-loader": "^3.2.0",
"elm": "^0.19.1-3",
"elm-hot-webpack-loader": "^1.1.2",
"elm-optimize-level-2": "^0.1.4",
"elm-webpack-loader": "^6.0.0",
"express": "^4.17.1",
"favicons-webpack-plugin": "^3.0.0",
@ -63,6 +65,7 @@
"elm-format": "^0.8.2",
"elm-test": "^0.19.1-revision2",
"jest": "^26.0.1",
"terser": "^5.3.7",
"typescript": "^3.6.3"
},
"files": [
@ -71,6 +74,7 @@
"generator/src/"
],
"bin": {
"elm-pages": "generator/src/elm-pages.js"
"elm-pages": "generator/src/elm-pages.js",
"elm-pages-beta": "generator/src/cli.js"
}
}

75
plugins/Cloudinary.elm Normal file
View File

@ -0,0 +1,75 @@
module Cloudinary exposing (url, urlSquare)
import MimeType
import Pages.ImagePath as ImagePath exposing (ImagePath)
url :
String
-> Maybe MimeType.MimeImage
-> Int
-> ImagePath pathKey
url asset format width =
let
base =
"https://res.cloudinary.com/dillonkearns/image/upload"
fetch_format =
case format of
Just MimeType.Png ->
"png"
Just (MimeType.OtherImage "webp") ->
"webp"
Just _ ->
"auto"
Nothing ->
"auto"
transforms =
[ "c_pad"
, "w_" ++ String.fromInt width
, "q_auto"
, "f_" ++ fetch_format
]
|> String.join ","
in
ImagePath.external (base ++ "/" ++ transforms ++ "/" ++ asset)
urlSquare :
String
-> Maybe MimeType.MimeImage
-> Int
-> ImagePath pathKey
urlSquare asset format width =
let
base =
"https://res.cloudinary.com/dillonkearns/image/upload"
fetch_format =
case format of
Just MimeType.Png ->
"png"
Just (MimeType.OtherImage "webp") ->
"webp"
Just _ ->
"auto"
Nothing ->
"auto"
transforms =
[ "c_pad"
, "w_" ++ String.fromInt width
, "h_" ++ String.fromInt width
, "q_auto"
, "f_" ++ fetch_format
]
|> String.join ","
in
ImagePath.external (base ++ "/" ++ transforms ++ "/" ++ asset)

0
src/EncodeHelpers.elm Normal file
View File

View File

@ -4,6 +4,7 @@ module Head exposing
, structuredData
, AttributeValue
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw
, appleTouchIcon, icon
, toJson, canonicalLink
)
@ -31,13 +32,20 @@ writing a plugin package to extend `elm-pages`.
@docs currentPageFullUrl, fullImageUrl, fullPageUrl, raw
## Icons
@docs appleTouchIcon, icon
## Functions for use by generated code
@docs toJson, canonicalLink
-}
import Codec exposing (Codec)
import Json.Encode
import MimeType
import Pages.ImagePath as ImagePath exposing (ImagePath)
import Pages.Internal.String as String
import Pages.PagePath as PagePath exposing (PagePath)
@ -162,7 +170,7 @@ raw value =
-}
fullImageUrl : ImagePath pathKey -> AttributeValue pathKey
fullImageUrl value =
FullUrl (ImagePath.toString value)
FullImageUrl value
{-| Create an `AttributeValue` from a `PagePath`.
@ -189,6 +197,7 @@ currentPageFullUrl =
type AttributeValue pathKey
= Raw String
| FullUrl String
| FullImageUrl (ImagePath pathKey)
| FullUrlToCurrentPage
@ -230,6 +239,76 @@ rssLink url =
]
{-| -}
icon : List ( Int, Int ) -> MimeType.MimeImage -> ImagePath pathKey -> Tag pathKey
icon sizes imageMimeType image =
-- TODO allow "any" for sizes value
[ ( "rel", raw "icon" |> Just )
, ( "sizes"
, sizes
|> nonEmptyList
|> Maybe.map sizesToString
|> Maybe.map raw
)
, ( "type", imageMimeType |> MimeType.Image |> MimeType.toString |> raw |> Just )
, ( "href", fullImageUrl image |> Just )
]
|> filterMaybeValues
|> node "link"
nonEmptyList : List a -> Maybe (List a)
nonEmptyList list =
if List.isEmpty list then
Nothing
else
Just list
{-| Note: the type must be png.
See <https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html>.
If a size is provided, it will be turned into square dimensions as per the recommendations here: <https://developers.google.com/web/fundamentals/design-and-ux/browser-customization/#safari>
Images must be png's, and non-transparent images are recommended. Current recommended dimensions are 180px and 192px.
-}
appleTouchIcon : Maybe Int -> ImagePath pathKey -> Tag pathKey
appleTouchIcon maybeSize image =
[ ( "rel", raw "apple-touch-icon" |> Just )
, ( "sizes"
, maybeSize
|> Maybe.map (\size -> sizesToString [ ( size, size ) ])
|> Maybe.map raw
)
, ( "href", fullImageUrl image |> Just )
]
|> filterMaybeValues
|> node "link"
filterMaybeValues : List ( String, Maybe a ) -> List ( String, a )
filterMaybeValues list =
list
|> List.filterMap
(\( key, maybeValue ) ->
case maybeValue of
Just value ->
Just ( key, value )
Nothing ->
Nothing
)
sizesToString : List ( Int, Int ) -> String
sizesToString sizes =
sizes
|> List.map (\( x, y ) -> String.fromInt x ++ "x" ++ String.fromInt y)
|> String.join " "
{-| Add a link to the site's RSS feed.
Example:
@ -325,6 +404,9 @@ encodeProperty canonicalSiteUrl currentPagePath ( name, value ) =
FullUrlToCurrentPage ->
Json.Encode.list Json.Encode.string [ name, joinPaths canonicalSiteUrl currentPagePath ]
FullImageUrl imagePath ->
Json.Encode.list Json.Encode.string [ name, ImagePath.toAbsoluteUrl canonicalSiteUrl imagePath ]
joinPaths : String -> String -> String
joinPaths base path =

View File

@ -10,6 +10,7 @@ module Pages.ContentCache exposing
, lookup
, lookupMetadata
, pagesWithErrors
, parseContent
, pathForUrl
, routesForCache
, update
@ -24,6 +25,7 @@ import Json.Decode as Decode
import Pages.Document as Document exposing (Document)
import Pages.Internal.String as String
import Pages.PagePath as PagePath exposing (PagePath)
import RequestsAndPending exposing (RequestsAndPending)
import Task exposing (Task)
import TerminalText as Terminal
import Url exposing (Url)
@ -49,7 +51,7 @@ type Entry metadata view
= NeedContent String metadata
| Unparsed String metadata (ContentJson String)
-- TODO need to have an UnparsedMarkup entry type so the right parser is applied
| Parsed metadata (ContentJson (Result ParseError view))
| Parsed metadata String (ContentJson (Result ParseError view))
type alias ParseError =
@ -76,7 +78,7 @@ getMetadata entry =
Unparsed extension metadata _ ->
metadata
Parsed metadata _ ->
Parsed metadata body _ ->
metadata
@ -88,7 +90,7 @@ pagesWithErrors cache =
List.filterMap
(\( path, value ) ->
case value of
Parsed metadata { body } ->
Parsed metadata rawBody { body } ->
case body of
Err parseError ->
createBuildError path parseError |> Just
@ -167,6 +169,7 @@ parseMetadata maybeInitialPageContent document content =
Just { contentJson, initialUrl } ->
if normalizePath initialUrl.path == (String.join "/" path |> normalizePath) then
Parsed metadata
contentJson.body
{ body = renderer contentJson.body
, staticData = contentJson.staticData
}
@ -182,6 +185,7 @@ parseMetadata maybeInitialPageContent document content =
-- TODO use types to make this more semantic
Just bodyFromCli ->
Parsed metadata
bodyFromCli
{ body = renderer bodyFromCli
, staticData = Dict.empty
}
@ -375,7 +379,7 @@ lazyLoad document urls cacheResult =
urls
|> Task.succeed
Parsed _ _ ->
Parsed _ _ _ ->
Task.succeed cacheResult
Nothing ->
@ -423,7 +427,7 @@ httpTask url =
type alias ContentJson body =
{ body : body
, staticData : Dict String String
, staticData : RequestsAndPending
}
@ -431,7 +435,7 @@ contentJsonDecoder : Decode.Decoder (ContentJson String)
contentJsonDecoder =
Decode.map2 ContentJson
(Decode.field "body" Decode.string)
(Decode.field "staticData" (Decode.dict Decode.string))
(Decode.field "staticData" RequestsAndPending.decoder)
update :
@ -447,11 +451,12 @@ update cacheResult renderer urls rawContent =
(pathForUrl urls)
(\entry ->
case entry of
Just (Parsed metadata view) ->
Just (Parsed metadata rawBody view) ->
entry
Just (Unparsed extension metadata content) ->
Parsed metadata
content.body
{ body = renderer content.body
, staticData = content.staticData
}
@ -459,6 +464,7 @@ update cacheResult renderer urls rawContent =
Just (NeedContent extension metadata) ->
Parsed metadata
rawContent.body
{ body = renderer rawContent.body
, staticData = rawContent.staticData
}
@ -526,6 +532,6 @@ lookupMetadata pathKey content urls =
Unparsed _ metadata _ ->
( pagePath, metadata )
Parsed metadata _ ->
Parsed metadata body _ ->
( pagePath, metadata )
)

View File

@ -1,5 +1,5 @@
module Pages.ImagePath exposing
( ImagePath, toString, external, dimensions, Dimensions
( ImagePath, toString, toAbsoluteUrl, external, dimensions, Dimensions
, build
)
@ -39,7 +39,7 @@ or
-- ImagePath.toString helloWorldPostPath
-- => "images/profile-photos/dillon.jpg"
@docs ImagePath, toString, external, dimensions, Dimensions
@docs ImagePath, toString, toAbsoluteUrl, external, dimensions, Dimensions
## Functions for code generation only
@ -50,6 +50,8 @@ Don't bother using these.
-}
import Path
{-| There are only two ways to get an `ImagePath`:
@ -101,6 +103,20 @@ toString path =
url
{-| Gives you the image's absolute URL as a String. This is useful for constructing `<img>` tags:
-}
toAbsoluteUrl : String -> ImagePath key -> String
toAbsoluteUrl canonicalSiteUrl path =
case path of
Internal rawPath _ ->
Path.join
canonicalSiteUrl
(String.join "/" rawPath)
External url ->
url
{-| This is not useful except for the internal generated code to construct an `ImagePath`.
-}
build : key -> List String -> Dimensions -> ImagePath key

View File

@ -21,6 +21,7 @@ import Pages.Manifest as Manifest
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp
import Pages.StaticHttpRequest as StaticHttpRequest
import RequestsAndPending exposing (RequestsAndPending)
import Result.Extra
import Task exposing (Task)
import Url exposing (Url)
@ -37,8 +38,8 @@ type alias Content =
List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
type alias Program userModel userMsg metadata view =
Platform.Program Flags (Model userModel userMsg metadata view) (Msg userMsg metadata view)
type alias Program userModel userMsg metadata view pathKey =
Platform.Program Flags (Model userModel userMsg metadata view pathKey) (Msg userMsg metadata view)
mainView :
@ -117,7 +118,7 @@ pageViewOrError pathKey viewFn model cache =
case ContentCache.lookup pathKey cache urls of
Just ( pagePath, entry ) ->
case entry of
ContentCache.Parsed metadata viewResult ->
ContentCache.Parsed metadata body viewResult ->
let
viewFnResult =
{ path = pagePath, frontmatter = metadata }
@ -250,7 +251,7 @@ type alias Flags =
type alias ContentJson =
{ body : String
, staticData : Dict String String
, staticData : RequestsAndPending
}
@ -258,7 +259,7 @@ contentJsonDecoder : Decode.Decoder ContentJson
contentJsonDecoder =
Decode.map2 ContentJson
(Decode.field "body" Decode.string)
(Decode.field "staticData" (Decode.dict Decode.string))
(Decode.field "staticData" RequestsAndPending.decoder)
init :
@ -453,9 +454,9 @@ type AppMsg userMsg metadata view
| StartingHotReload
type Model userModel userMsg metadata view
type Model userModel userMsg metadata view pathKey
= Model (ModelDetails userModel metadata view)
| CliModel Pages.Internal.Platform.Cli.Model
| CliModel (Pages.Internal.Platform.Cli.Model pathKey metadata)
type alias ModelDetails userModel metadata view =
@ -576,7 +577,7 @@ update content allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChange
case ContentCache.lookup pathKey updatedCache urls of
Just ( pagePath, entry ) ->
case entry of
ContentCache.Parsed frontmatter viewResult ->
ContentCache.Parsed frontmatter body viewResult ->
headFn pagePath frontmatter viewResult.staticData
|> Result.map .head
|> Result.toMaybe
@ -632,7 +633,7 @@ update content allRoutes canonicalSiteUrl viewFunction pathKey maybeOnPageChange
case ContentCache.lookup pathKey updatedCache urls of
Just ( pagePath, entry ) ->
case entry of
ContentCache.Parsed metadata viewResult ->
ContentCache.Parsed metadata rawBody viewResult ->
Just metadata
ContentCache.NeedContent string metadata ->
@ -772,7 +773,7 @@ application :
)
}
-- -> Program userModel userMsg metadata view
-> Platform.Program Flags (Model userModel userMsg metadata view) (Msg userMsg metadata view)
-> Platform.Program Flags (Model userModel userMsg metadata view pathKey) (Msg userMsg metadata view)
application config =
Browser.application
{ init =
@ -930,7 +931,7 @@ cliApplication :
-> userMsg
)
}
-> Program userModel userMsg metadata view
-> Program userModel userMsg metadata view pathKey
cliApplication =
Pages.Internal.Platform.Cli.cliApplication CliMsg
(\msg ->

View File

@ -12,6 +12,8 @@ module Pages.Internal.Platform.Cli exposing
import BuildError exposing (BuildError)
import Codec exposing (Codec)
import Dict exposing (Dict)
import ElmHtml.InternalTypes exposing (decodeElmHtml)
import ElmHtml.ToString exposing (FormatOptions, defaultFormatOptions, nodeToStringWithOptions)
import Head
import Html exposing (Html)
import Http
@ -20,17 +22,20 @@ import Json.Encode
import Pages.ContentCache as ContentCache exposing (ContentCache)
import Pages.Document
import Pages.Http
import Pages.Internal.ApplicationType as ApplicationType
import Pages.Internal.Platform.Effect as Effect exposing (Effect)
import Pages.Internal.Platform.Mode as Mode exposing (Mode)
import Pages.Internal.Platform.StaticResponses as StaticResponses exposing (StaticResponses)
import Pages.Internal.Platform.ToJsPayload as ToJsPayload exposing (ToJsPayload)
import Pages.Internal.Platform.ToJsPayload as ToJsPayload exposing (ToJsPayload, ToJsSuccessPayload)
import Pages.Internal.StaticHttpBody as StaticHttpBody
import Pages.Manifest as Manifest
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp exposing (RequestDetails)
import Pages.StaticHttpRequest as StaticHttpRequest
import SecretsDict exposing (SecretsDict)
import Task
import TerminalText as Terminal
import Url
type alias FileToGenerate =
@ -54,17 +59,20 @@ type alias Flags =
Decode.Value
type alias Model =
type alias Model pathKey metadata =
{ staticResponses : StaticResponses
, secrets : SecretsDict
, errors : List BuildError
, allRawResponses : Dict String (Maybe String)
, mode : Mode
, pendingRequests : List { masked : RequestDetails, unmasked : RequestDetails }
, unprocessedPages : List ( PagePath pathKey, metadata )
}
type Msg
= GotStaticHttpResponse { request : { masked : RequestDetails, unmasked : RequestDetails }, response : Result Pages.Http.Error String }
| Continue
type alias Config pathKey userMsg userModel metadata view =
@ -128,8 +136,8 @@ type alias Config pathKey userMsg userModel metadata view =
cliApplication :
(Msg -> msg)
-> (msg -> Maybe Msg)
-> (Model -> model)
-> (model -> Maybe Model)
-> (Model pathKey metadata -> model)
-> (model -> Maybe (Model pathKey metadata))
-> Config pathKey userMsg userModel metadata view
-> Platform.Program Flags model msg
cliApplication cliMsgConstructor narrowMsg toModel fromModel config =
@ -147,13 +155,13 @@ cliApplication cliMsgConstructor narrowMsg toModel fromModel config =
{ init =
\flags ->
init toModel contentCache siteMetadata config flags
|> Tuple.mapSecond (perform cliMsgConstructor config.toJsPort)
|> Tuple.mapSecond (perform config cliMsgConstructor config.toJsPort)
, update =
\msg model ->
case ( narrowMsg msg, fromModel model ) of
( Just cliMsg, Just cliModel ) ->
update siteMetadata config cliMsg cliModel
|> Tuple.mapSecond (perform cliMsgConstructor config.toJsPort)
update contentCache siteMetadata config cliMsg cliModel
|> Tuple.mapSecond (perform config cliMsgConstructor config.toJsPort)
|> Tuple.mapFirst toModel
_ ->
@ -162,21 +170,49 @@ cliApplication cliMsgConstructor narrowMsg toModel fromModel config =
}
perform : (Msg -> msg) -> (Json.Encode.Value -> Cmd Never) -> Effect pathKey -> Cmd msg
perform cliMsgConstructor toJsPort effect =
viewRenderer : Html msg -> String
viewRenderer html =
let
options =
{ defaultFormatOptions | newLines = False, indent = 0 }
in
viewDecoder options html
viewDecoder : FormatOptions -> Html msg -> String
viewDecoder options viewHtml =
case
Decode.decodeValue
(decodeElmHtml (\_ _ -> Decode.succeed ()))
(asJsonView viewHtml)
of
Ok str ->
nodeToStringWithOptions options str
Err err ->
"Error: " ++ Decode.errorToString err
asJsonView : Html msg -> Decode.Value
asJsonView x =
Json.Encode.string "REPLACE_ME_WITH_JSON_STRINGIFY"
perform : Config pathKey userMsg userModel metadata view -> (Msg -> msg) -> (Json.Encode.Value -> Cmd Never) -> Effect pathKey -> Cmd msg
perform config cliMsgConstructor toJsPort effect =
case effect of
Effect.NoEffect ->
Cmd.none
Effect.SendJsData value ->
value
|> Codec.encoder ToJsPayload.toJsCodec
|> Codec.encoder (ToJsPayload.toJsCodec config.canonicalSiteUrl)
|> toJsPort
|> Cmd.map never
Effect.Batch list ->
list
|> List.map (perform cliMsgConstructor toJsPort)
|> List.map (perform config cliMsgConstructor toJsPort)
|> Cmd.batch
Effect.FetchHttp ({ unmasked, masked } as requests) ->
@ -184,31 +220,80 @@ perform cliMsgConstructor toJsPort effect =
-- _ =
-- Debug.log "Fetching" masked.url
-- in
Http.request
{ method = unmasked.method
, url = unmasked.url
, headers = unmasked.headers |> List.map (\( key, value ) -> Http.header key value)
, body =
case unmasked.body of
StaticHttpBody.EmptyBody ->
Http.emptyBody
Cmd.batch
[ Http.request
{ method = unmasked.method
, url = unmasked.url
, headers = unmasked.headers |> List.map (\( key, value ) -> Http.header key value)
, body =
case unmasked.body of
StaticHttpBody.EmptyBody ->
Http.emptyBody
StaticHttpBody.StringBody contentType string ->
Http.stringBody contentType string
StaticHttpBody.StringBody contentType string ->
Http.stringBody contentType string
StaticHttpBody.JsonBody value ->
Http.jsonBody value
, expect =
Pages.Http.expectString
(\response ->
(GotStaticHttpResponse >> cliMsgConstructor)
{ request = requests
, response = response
}
)
, timeout = Nothing
, tracker = Nothing
}
StaticHttpBody.JsonBody value ->
Http.jsonBody value
, expect =
Pages.Http.expectString
(\response ->
(GotStaticHttpResponse >> cliMsgConstructor)
{ request = requests
, response = response
}
)
, timeout = Nothing
, tracker = Nothing
}
, toJsPort
(Json.Encode.object
[ ( "command", Json.Encode.string "log" )
, ( "value", Json.Encode.string ("Fetching " ++ masked.url) )
]
)
|> Cmd.map never
]
Effect.SendSinglePage info ->
let
currentPagePath =
case info of
ToJsPayload.PageProgress toJsSuccessPayloadNew ->
toJsSuccessPayloadNew.route
_ ->
""
in
Cmd.batch
[ info
|> Codec.encoder (ToJsPayload.successCodecNew2 config.canonicalSiteUrl currentPagePath)
|> toJsPort
|> Cmd.map never
, Task.succeed ()
|> Task.perform (\_ -> Continue)
|> Cmd.map cliMsgConstructor
]
Effect.Continue ->
Cmd.none
encodeFilesToGenerate list =
list
|> Json.Encode.list
(\item ->
Json.Encode.object
[ ( "path", item.path |> String.join "/" |> Json.Encode.string )
, ( "content", item.content |> Json.Encode.string )
]
)
--Task.succeed ()
-- |> Task.perform (\_ -> Continue)
-- |> Cmd.map cliMsgConstructor
flagsDecoder :
@ -237,7 +322,7 @@ flagsDecoder =
init :
(Model -> model)
(Model pathKey metadata -> model)
-> ContentCache.ContentCache metadata view
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Config pathKey userMsg userModel metadata view
@ -246,77 +331,16 @@ init :
init toModel contentCache siteMetadata config flags =
case Decode.decodeValue flagsDecoder flags of
Ok { secrets, mode, staticHttpCache } ->
case contentCache of
Ok _ ->
case ContentCache.pagesWithErrors contentCache of
[] ->
let
requests =
Result.andThen
(\metadata ->
staticResponseForPage metadata config.view
)
siteMetadata
staticResponses : StaticResponses
staticResponses =
case requests of
Ok okRequests ->
StaticResponses.init staticHttpCache siteMetadata config okRequests
Err errors ->
-- TODO need to handle errors better?
StaticResponses.init staticHttpCache siteMetadata config []
in
StaticResponses.nextStep config siteMetadata mode secrets staticHttpCache [] staticResponses
|> nextStepToEffect (Model staticResponses secrets [] staticHttpCache mode)
|> Tuple.mapFirst toModel
pageErrors ->
let
requests =
Result.andThen
(\metadata ->
staticResponseForPage metadata config.view
)
siteMetadata
staticResponses : StaticResponses
staticResponses =
case requests of
Ok okRequests ->
StaticResponses.init staticHttpCache siteMetadata config okRequests
Err errors ->
-- TODO need to handle errors better?
StaticResponses.init staticHttpCache siteMetadata config []
in
updateAndSendPortIfDone
config
siteMetadata
(Model
staticResponses
secrets
pageErrors
staticHttpCache
mode
)
toModel
Err metadataParserErrors ->
updateAndSendPortIfDone
config
siteMetadata
(Model StaticResponses.error
secrets
(metadataParserErrors |> List.map Tuple.second)
staticHttpCache
mode
)
toModel
case mode of
--Mode.ElmToHtmlBeta ->
-- elmToHtmlBetaInit { secrets = secrets, mode = mode, staticHttpCache = staticHttpCache } toModel contentCache siteMetadata config flags
--
_ ->
initLegacy { secrets = secrets, mode = mode, staticHttpCache = staticHttpCache } toModel contentCache siteMetadata config flags
Err error ->
updateAndSendPortIfDone
contentCache
config
siteMetadata
(Model StaticResponses.error
@ -328,36 +352,161 @@ init toModel contentCache siteMetadata config flags =
]
Dict.empty
Mode.Dev
[]
(siteMetadata |> Result.withDefault [])
)
toModel
elmToHtmlBetaInit { secrets, mode, staticHttpCache } toModel contentCache siteMetadata config flags =
--case flags of
--init toModel contentCache siteMetadata config flags
--|> Tuple.mapSecond (perform cliMsgConstructor config.toJsPort)
--|> Tuple.mapSecond
-- (\cmd ->
--Cmd.map AppMsg
--Cmd.none
( toModel
(Model StaticResponses.error
secrets
[]
--(metadataParserErrors |> List.map Tuple.second)
staticHttpCache
mode
[]
)
, Effect.NoEffect
--, { html =
-- Html.div []
-- [ Html.text "Hello!!!!!" ]
-- |> viewRenderer
-- }
-- |> Effect.SendSinglePage
)
--)
initLegacy { secrets, mode, staticHttpCache } toModel contentCache siteMetadata config flags =
case contentCache of
Ok _ ->
case ContentCache.pagesWithErrors contentCache of
[] ->
let
requests =
Result.andThen
(\metadata ->
staticResponseForPage metadata config.view
)
siteMetadata
staticResponses : StaticResponses
staticResponses =
case requests of
Ok okRequests ->
StaticResponses.init staticHttpCache siteMetadata config okRequests
Err errors ->
-- TODO need to handle errors better?
StaticResponses.init staticHttpCache siteMetadata config []
in
StaticResponses.nextStep config siteMetadata (siteMetadata |> Result.map (List.take 1)) mode secrets staticHttpCache [] staticResponses
|> nextStepToEffect contentCache config (Model staticResponses secrets [] staticHttpCache mode [] (siteMetadata |> Result.withDefault []))
|> Tuple.mapFirst toModel
pageErrors ->
let
requests =
Result.andThen
(\metadata ->
staticResponseForPage metadata config.view
)
siteMetadata
staticResponses : StaticResponses
staticResponses =
case requests of
Ok okRequests ->
StaticResponses.init staticHttpCache siteMetadata config okRequests
Err errors ->
-- TODO need to handle errors better?
StaticResponses.init staticHttpCache siteMetadata config []
in
updateAndSendPortIfDone
contentCache
config
siteMetadata
(Model
staticResponses
secrets
pageErrors
staticHttpCache
mode
[]
(siteMetadata |> Result.withDefault [])
)
toModel
Err metadataParserErrors ->
updateAndSendPortIfDone
contentCache
config
siteMetadata
(Model StaticResponses.error
secrets
(metadataParserErrors |> List.map Tuple.second)
staticHttpCache
mode
[]
(siteMetadata |> Result.withDefault [])
)
toModel
updateAndSendPortIfDone :
Config pathKey userMsg userModel metadata view
ContentCache.ContentCache metadata view
-> Config pathKey userMsg userModel metadata view
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Model
-> (Model -> model)
-> Model pathKey metadata
-> (Model pathKey metadata -> model)
-> ( model, Effect pathKey )
updateAndSendPortIfDone config siteMetadata model toModel =
updateAndSendPortIfDone contentCache config siteMetadata model toModel =
let
nextToProcess =
drop1 model
in
StaticResponses.nextStep
config
siteMetadata
(Ok nextToProcess)
model.mode
model.secrets
model.allRawResponses
model.errors
model.staticResponses
|> nextStepToEffect model
|> nextStepToEffect contentCache config model
|> Tuple.mapFirst toModel
drop1 model =
List.take 1 model.unprocessedPages
--, { model | unprocessedPages = List.drop 1 model.unprocessedPages }
update :
Result (List BuildError) (List ( PagePath pathKey, metadata ))
ContentCache.ContentCache metadata view
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Config pathKey userMsg userModel metadata view
-> Msg
-> Model
-> ( Model, Effect pathKey )
update siteMetadata config msg model =
-> Model pathKey metadata
-> ( Model pathKey metadata, Effect pathKey )
update contentCache siteMetadata config msg model =
case msg of
GotStaticHttpResponse { request, response } ->
let
@ -367,7 +516,11 @@ update siteMetadata config msg model =
updatedModel =
(case response of
Ok okResponse ->
model
{ model
| pendingRequests =
model.pendingRequests
|> List.filter (\pending -> pending /= request)
}
Err error ->
{ model
@ -410,29 +563,298 @@ update siteMetadata config msg model =
{ request = request
, response = Result.mapError (\_ -> ()) response
}
nextToProcess =
drop1 updatedModel
in
StaticResponses.nextStep config
siteMetadata
(Ok nextToProcess)
updatedModel.mode
updatedModel.secrets
updatedModel.allRawResponses
updatedModel.errors
updatedModel.staticResponses
|> nextStepToEffect updatedModel
|> nextStepToEffect contentCache config updatedModel
Continue ->
-- TODO
let
--_ =
-- Debug.log "Continuing..." (List.length model.unprocessedPages)
updatedModel =
model
--|> popProcessedRequest
nextToProcess =
drop1 model
in
StaticResponses.nextStep config
siteMetadata
(Ok nextToProcess)
updatedModel.mode
updatedModel.secrets
updatedModel.allRawResponses
updatedModel.errors
updatedModel.staticResponses
|> nextStepToEffect contentCache config updatedModel
nextStepToEffect : Model -> StaticResponses.NextStep pathKey -> ( Model, Effect pathKey )
nextStepToEffect model nextStep =
nextStepToEffect :
ContentCache.ContentCache metadata view
-> Config pathKey userMsg userModel metadata view
-> Model pathKey metadata
-> StaticResponses.NextStep pathKey
-> ( Model pathKey metadata, Effect pathKey )
nextStepToEffect contentCache config model nextStep =
case nextStep of
StaticResponses.Continue updatedAllRawResponses httpRequests ->
( { model | allRawResponses = updatedAllRawResponses }
, httpRequests
let
nextAndPending =
model.pendingRequests ++ httpRequests
doNow =
nextAndPending
pending =
[]
in
( { model
| allRawResponses = updatedAllRawResponses
, pendingRequests = pending
}
, doNow
|> List.map Effect.FetchHttp
|> Effect.Batch
)
StaticResponses.Finish toJsPayload ->
( model, Effect.SendJsData toJsPayload )
case model.mode of
Mode.ElmToHtmlBeta ->
let
siteMetadata =
contentCache
--|> Debug.log "contentCache"
|> Result.map
(\cache -> cache |> ContentCache.extractMetadata config.pathKey)
|> Result.mapError (List.map Tuple.second)
--viewFnResult =
-- --currentPage
-- config.view siteMetadata currentPage
-- --(contentCache
-- -- |> Result.map (ContentCache.extractMetadata pathKey)
-- -- |> Result.withDefault []
-- -- -- TODO handle error better
-- --)
-- |> (\request ->
-- StaticHttpRequest.resolve ApplicationType.Browser request viewResult.staticData
-- )
--makeItWork :
-- StaticHttp.Request
-- { view :
-- userModel
-- -> view
-- -> { title : String, body : Html userMsg }
-- , head : List (Head.Tag pathKey)
-- }
-- -> { title : String, body : Html userMsg }
in
case siteMetadata of
Ok pages ->
let
sendManifestIfNeeded =
if List.length model.unprocessedPages == List.length pages then
case toJsPayload of
ToJsPayload.Success value ->
Effect.SendSinglePage
(ToJsPayload.InitialData
{ manifest = value.manifest
, filesToGenerate = value.filesToGenerate
}
)
ToJsPayload.Errors errorMessage ->
Effect.SendJsData toJsPayload
else
Effect.NoEffect
in
model.unprocessedPages
|> List.take 1
--|> Debug.log "@@@ pages"
|> List.filterMap
(\pageAndMetadata ->
case toJsPayload of
ToJsPayload.Success value ->
sendSinglePageProgress value siteMetadata config contentCache model pageAndMetadata
|> Just
ToJsPayload.Errors errorMessage ->
Nothing
)
|> Effect.Batch
|> (\cmd -> ( model |> popProcessedRequest, Effect.Batch [ cmd, sendManifestIfNeeded ] ))
Err error ->
( model
, Effect.SendJsData
(ToJsPayload.Errors <|
BuildError.errorsToString error
)
)
_ ->
( model, Effect.SendJsData toJsPayload )
sendSinglePageProgress :
ToJsSuccessPayload pathKey
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Config pathKey userMsg userModel metadata view
-> ContentCache metadata view
-> Model pathKey metadata
-> ( PagePath pathKey, metadata )
-> Effect pathKey
sendSinglePageProgress toJsPayload siteMetadata config contentCache model =
\( page, metadata ) ->
let
makeItWork : StaticHttpRequest.Request staticData -> Result BuildError staticData
makeItWork request =
StaticHttpRequest.resolve ApplicationType.Browser request (staticData |> Dict.map (\k v -> Just v))
|> Result.mapError (StaticHttpRequest.toBuildError (page |> PagePath.toString))
staticData =
toJsPayload.pages
|> Dict.get (PagePath.toString page)
|> Maybe.withDefault Dict.empty
viewRequest :
StaticHttp.Request
{ view :
userModel
-> view
-> { title : String, body : Html userMsg }
, head : List (Head.Tag pathKey)
}
viewRequest =
config.view (siteMetadata |> Result.withDefault []) currentPage
twoThings : Result BuildError { view : userModel -> view -> { title : String, body : Html userMsg }, head : List (Head.Tag pathKey) }
twoThings =
viewRequest |> makeItWork
renderer value =
ContentCache.parseContent "md" value config.document
updatedCache =
ContentCache.update contentCache
renderer
urls
{ body = "", staticData = model.allRawResponses }
currentPage : { path : PagePath pathKey, frontmatter : metadata }
currentPage =
{ path = page, frontmatter = metadata }
pageModel : userModel
pageModel =
config.init
(Just
{ path =
{ path = currentPage.path
, query = Nothing
, fragment = Nothing
}
, metadata = metadata
}
)
|> Tuple.first
currentUrl =
{ protocol = Url.Https
, host = config.canonicalSiteUrl
, port_ = Nothing
, path = currentPage.path |> PagePath.toString
, query = Nothing
, fragment = Nothing
}
urls =
{ currentUrl = currentUrl
, baseUrl =
{ protocol = Url.Https
, host = config.canonicalSiteUrl
, port_ = Nothing
, path = ""
, query = Nothing
, fragment = Nothing
}
}
value2 =
case ContentCache.lookup config.pathKey updatedCache urls of
Just ( path, ContentCache.Parsed frontmatter unparsedBody viewResult ) ->
viewResult.body
|> Result.map
(\body ->
{ body = body
, viewResult = viewResult
, unparsedBody = unparsedBody
}
)
|> Result.mapError
(\parseError ->
{ title = "Internal Error"
, message = [ Terminal.text parseError ]
, fatal = True
}
)
_ ->
Err
{ title = "Internal Error"
, message = [ Terminal.text "Unable to lookup value in ContentCache." ]
, fatal = True
}
in
case Result.map2 Tuple.pair twoThings value2 of
Ok ( success, lookedUp ) ->
let
viewValue =
success.view pageModel lookedUp.body
in
{ route = page |> PagePath.toString
, contentJson =
toJsPayload.pages
|> Dict.get (PagePath.toString page)
|> Maybe.withDefault Dict.empty
, html = viewValue.body |> viewRenderer
, errors = []
, head = success.head
, title = viewValue.title
, body = lookedUp.unparsedBody
}
|> sendProgress
Err error ->
error
|> BuildError.errorToString
|> ToJsPayload.Errors
|> Effect.SendJsData
popProcessedRequest model =
{ model | unprocessedPages = List.drop 1 model.unprocessedPages }
sendProgress : ToJsPayload.ToJsSuccessPayloadNew pathKey -> Effect pathKey
sendProgress singlePage =
Effect.Batch
[ singlePage |> ToJsPayload.PageProgress |> Effect.SendSinglePage
--, Effect.Continue
]
staticResponseForPage :

View File

@ -1,6 +1,7 @@
module Pages.Internal.Platform.Effect exposing (..)
import Pages.Internal.Platform.ToJsPayload exposing (ToJsPayload)
import Pages.Internal.Platform.ToJsPayload exposing (FileToGenerate, ToJsPayload, ToJsSuccessPayloadNew, ToJsSuccessPayloadNewCombined)
import Pages.Manifest as Manifest
import Pages.StaticHttp exposing (RequestDetails)
@ -9,3 +10,5 @@ type Effect pathKey
| SendJsData (ToJsPayload pathKey)
| FetchHttp { masked : RequestDetails, unmasked : RequestDetails }
| Batch (List (Effect pathKey))
| SendSinglePage (ToJsSuccessPayloadNewCombined pathKey)
| Continue

View File

@ -6,6 +6,7 @@ import Json.Decode as Decode
type Mode
= Prod
| Dev
| ElmToHtmlBeta
modeDecoder =
@ -15,6 +16,9 @@ modeDecoder =
if mode == "prod" then
Decode.succeed Prod
else if mode == "elm-to-html-beta" then
Decode.succeed ElmToHtmlBeta
else
Decode.succeed Dev
)

View File

@ -11,6 +11,8 @@ import Pages.PagePath as PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp exposing (RequestDetails)
import Pages.StaticHttp.Request as HashRequest
import Pages.StaticHttpRequest as StaticHttpRequest
import RequestsAndPending exposing (RequestsAndPending)
import Result.Extra
import Secrets
import SecretsDict exposing (SecretsDict)
import Set
@ -110,23 +112,9 @@ init staticHttpCache siteMetadataResult config list =
let
entry =
NotFetched (staticRequest |> StaticHttp.map (\_ -> ())) Dict.empty
updatedEntry =
staticHttpCache
|> dictCompact
|> Dict.toList
|> List.foldl
(\( hashedRequest, response ) entrySoFar ->
entrySoFar
|> addEntry
staticHttpCache
hashedRequest
(Ok response)
)
entry
in
( PagePath.toString path
, updatedEntry
, entry
)
)
|> List.append [ generateFilesStaticRequest ]
@ -160,82 +148,17 @@ update newEntry model =
in
{ model
| allRawResponses = updatedAllResponses
, staticResponses =
case model.staticResponses of
StaticResponses staticResponses ->
staticResponses
|> Dict.map
(\pageUrl entry ->
case entry of
NotFetched request rawResponses ->
let
realUrls =
updatedAllResponses
|> dictCompact
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|> Tuple.second
|> List.map Secrets.maskedLookup
|> List.map HashRequest.hash
includesUrl =
List.member
(HashRequest.hash newEntry.request.masked)
realUrls
in
if includesUrl then
let
updatedRawResponses =
Dict.insert
(HashRequest.hash newEntry.request.masked)
newEntry.response
rawResponses
in
NotFetched request updatedRawResponses
else
entry
)
|> StaticResponses
}
addEntry :
Dict String (Maybe String)
-> String
-> Result () String
-> StaticHttpResult
-> StaticHttpResult
addEntry globalRawResponses hashedRequest rawResponse ((NotFetched request rawResponses) as entry) =
let
realUrls =
globalRawResponses
|> dictCompact
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|> Tuple.second
|> List.map Secrets.maskedLookup
|> List.map HashRequest.hash
includesUrl =
List.member
hashedRequest
realUrls
in
if includesUrl then
let
updatedRawResponses =
Dict.insert
hashedRequest
rawResponse
rawResponses
in
NotFetched request updatedRawResponses
else
entry
dictCompact : Dict String (Maybe a) -> Dict String a
dictCompact dict =
dict
|> Dict.Extra.filterMap (\key value -> value)
encode : Mode -> StaticResponses -> Dict String (Dict String String)
encode mode (StaticResponses staticResponses) =
encode : RequestsAndPending -> Mode -> StaticResponses -> Dict String (Dict String String)
encode requestsAndPending mode (StaticResponses staticResponses) =
staticResponses
|> Dict.filter
(\key value ->
@ -244,36 +167,19 @@ encode mode (StaticResponses staticResponses) =
|> Dict.map
(\path result ->
case result of
NotFetched request rawResponsesDict ->
let
relevantResponses =
Dict.map
(\_ ->
-- TODO avoid running this code at all if there are errors here
Result.withDefault ""
)
rawResponsesDict
strippedResponses : Dict String String
strippedResponses =
-- TODO should this return an Err and handle that here?
StaticHttpRequest.strippedResponses ApplicationType.Cli request relevantResponses
in
NotFetched request _ ->
case mode of
Mode.Dev ->
relevantResponses
StaticHttpRequest.strippedResponses ApplicationType.Cli request requestsAndPending
Mode.Prod ->
strippedResponses
StaticHttpRequest.strippedResponses ApplicationType.Cli request requestsAndPending
Mode.ElmToHtmlBeta ->
StaticHttpRequest.strippedResponses ApplicationType.Cli request requestsAndPending
)
dictCompact : Dict String (Maybe a) -> Dict String a
dictCompact dict =
dict
|> Dict.Extra.filterMap (\key value -> value)
cliDictKey : String
cliDictKey =
"////elm-pages-CLI////"
@ -305,18 +211,20 @@ nextStep :
)
}
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Mode
-> SecretsDict
-> Dict String (Maybe String)
-> RequestsAndPending
-> List BuildError
-> StaticResponses
-> NextStep pathKey
nextStep config siteMetadata mode secrets allRawResponses errors (StaticResponses staticResponses) =
nextStep config allSiteMetadata siteMetadata mode secrets allRawResponses errors (StaticResponses staticResponses) =
let
metadataForGenerateFiles =
siteMetadata
allSiteMetadata
|> Result.withDefault []
|> List.map
-- TODO extract helper function that processes next step *for a single page* at a time
(\( pagePath, metadata ) ->
let
contentForPage =
@ -353,7 +261,7 @@ nextStep config siteMetadata mode secrets allRawResponses errors (StaticResponse
resolvedGenerateFilesResult =
StaticHttpRequest.resolve ApplicationType.Cli
(config.generateFiles metadataForGenerateFiles)
(allRawResponses |> Dict.Extra.filterMap (\key value -> value))
(allRawResponses |> Dict.Extra.filterMap (\key value -> Just value))
generatedOkayFiles : List { path : List String, content : String }
generatedOkayFiles =
@ -400,41 +308,28 @@ nextStep config siteMetadata mode secrets allRawResponses errors (StaticResponse
case entry of
NotFetched request rawResponses ->
let
usableRawResponses : Dict String String
usableRawResponses =
rawResponses
|> Dict.Extra.filterMap
(\key value ->
value
|> Result.map Just
|> Result.withDefault Nothing
)
staticRequestsStatus =
allRawResponses
|> StaticHttpRequest.cacheRequestResolution ApplicationType.Cli request
hasPermanentError =
usableRawResponses
|> StaticHttpRequest.permanentError ApplicationType.Cli request
|> isJust
case staticRequestsStatus of
StaticHttpRequest.HasPermanentError _ ->
True
_ ->
False
hasPermanentHttpError =
not (List.isEmpty errors)
--|> List.any
-- (\error ->
-- case error of
-- FailedStaticHttpRequestError _ ->
-- True
--
-- _ ->
-- False
-- )
( allUrlsKnown, knownUrlsToFetch ) =
StaticHttpRequest.resolveUrls
ApplicationType.Cli
request
(rawResponses
|> Dict.map (\key value -> value |> Result.withDefault "")
|> Dict.union (allRawResponses |> Dict.Extra.filterMap (\_ value -> value))
)
case staticRequestsStatus of
StaticHttpRequest.Incomplete newUrlsToFetch ->
( False, newUrlsToFetch )
_ ->
( True, [] )
fetchedAllKnownUrls =
(rawResponses
@ -461,24 +356,26 @@ nextStep config siteMetadata mode secrets allRawResponses errors (StaticResponse
staticResponses
|> Dict.toList
|> List.concatMap
(\( path, NotFetched request rawResponses ) ->
(\( path, NotFetched request _ ) ->
let
usableRawResponses : Dict String String
usableRawResponses =
rawResponses
|> Dict.Extra.filterMap
(\key value ->
value
|> Result.map Just
|> Result.withDefault Nothing
)
maybePermanentError =
StaticHttpRequest.permanentError
staticRequestsStatus =
StaticHttpRequest.cacheRequestResolution
ApplicationType.Cli
request
usableRawResponses
usableRawResponses : RequestsAndPending
usableRawResponses =
allRawResponses
maybePermanentError =
case staticRequestsStatus of
StaticHttpRequest.HasPermanentError theError ->
Just theError
_ ->
Nothing
decoderErrors =
maybePermanentError
|> Maybe.map (StaticHttpRequest.toBuildError path)
@ -495,7 +392,7 @@ nextStep config siteMetadata mode secrets allRawResponses errors (StaticResponse
staticResponses
|> Dict.toList
|> List.map
(\( path, NotFetched request rawResponses ) ->
(\( path, NotFetched request _ ) ->
( path, request )
)
in
@ -546,7 +443,7 @@ nextStep config siteMetadata mode secrets allRawResponses errors (StaticResponse
else
ToJsPayload.toJsPayload
(encode mode (StaticResponses staticResponses))
(encode allRawResponses mode (StaticResponses staticResponses))
config.manifest
generatedOkayFiles
allRawResponses
@ -561,10 +458,10 @@ performStaticHttpRequests :
-> Result (List BuildError) (List { unmasked : RequestDetails, masked : RequestDetails })
performStaticHttpRequests allRawResponses secrets staticRequests =
staticRequests
-- TODO look for performance bottleneck in this double nesting
|> List.map
(\( pagePath, request ) ->
allRawResponses
|> dictCompact
|> StaticHttpRequest.resolveUrls ApplicationType.Cli request
|> Tuple.second
)

View File

@ -3,6 +3,7 @@ module Pages.Internal.Platform.ToJsPayload exposing (..)
import BuildError
import Codec exposing (Codec)
import Dict exposing (Dict)
import Head
import Json.Decode as Decode
import Json.Encode
import Pages.ImagePath as ImagePath
@ -25,6 +26,17 @@ type alias ToJsSuccessPayload pathKey =
}
type alias ToJsSuccessPayloadNew pathKey =
{ route : String
, html : String
, contentJson : Dict String String
, errors : List String
, head : List (Head.Tag pathKey)
, body : String
, title : String
}
type alias FileToGenerate =
{ path : List String
, content : String
@ -61,8 +73,8 @@ toJsPayload encodedStatic manifest generated allRawResponses allErrors =
Errors <| BuildError.errorsToString allErrors
toJsCodec : Codec (ToJsPayload pathKey)
toJsCodec =
toJsCodec : String -> Codec (ToJsPayload pathKey)
toJsCodec canonicalSiteUrl =
Codec.custom
(\errorsTag success value ->
case value of
@ -73,7 +85,7 @@ toJsCodec =
success (ToJsSuccessPayload pages manifest filesToGenerate staticHttpCache errors)
)
|> Codec.variant1 "Errors" Errors Codec.string
|> Codec.variant1 "Success" Success successCodec
|> Codec.variant1 "Success" Success (successCodec canonicalSiteUrl)
|> Codec.buildCustom
@ -90,18 +102,19 @@ stubManifest =
, startUrl = PagePath.external ""
, shortName = Just "elm-pages"
, sourceIcon = ImagePath.external ""
, icons = []
}
successCodec : Codec (ToJsSuccessPayload pathKey)
successCodec =
successCodec : String -> Codec (ToJsSuccessPayload pathKey)
successCodec canonicalSiteUrl =
Codec.object ToJsSuccessPayload
|> Codec.field "pages"
.pages
(Codec.dict (Codec.dict Codec.string))
|> Codec.field "manifest"
.manifest
(Codec.build Manifest.toJson (Decode.succeed stubManifest))
(Codec.build (Manifest.toJson canonicalSiteUrl) (Decode.succeed stubManifest))
|> Codec.field "filesToGenerate"
.filesToGenerate
(Codec.build
@ -127,3 +140,93 @@ successCodec =
(Codec.dict Codec.string)
|> Codec.field "errors" .errors (Codec.list Codec.string)
|> Codec.buildObject
successCodecNew : String -> String -> Codec (ToJsSuccessPayloadNew pathKey)
successCodecNew canonicalSiteUrl currentPagePath =
Codec.object ToJsSuccessPayloadNew
|> Codec.field "route"
.route
Codec.string
|> Codec.field "html"
.html
Codec.string
|> Codec.field "contentJson"
.contentJson
(Codec.dict Codec.string)
|> Codec.field "errors" .errors (Codec.list Codec.string)
|> Codec.field "head" .head (Codec.list (headCodec canonicalSiteUrl currentPagePath))
|> Codec.field "body" .body Codec.string
|> Codec.field "title" .title Codec.string
|> Codec.buildObject
headCodec : String -> String -> Codec (Head.Tag pathKey)
headCodec canonicalSiteUrl currentPagePath =
Codec.build (Head.toJson canonicalSiteUrl currentPagePath)
(Decode.succeed (Head.canonicalLink Nothing))
type ToJsSuccessPayloadNewCombined pathKey
= PageProgress (ToJsSuccessPayloadNew pathKey)
| InitialData (InitialDataRecord pathKey)
type alias InitialDataRecord pathKey =
{ filesToGenerate : List FileToGenerate
, manifest : Manifest.Config pathKey
}
successCodecNew2 : String -> String -> Codec (ToJsSuccessPayloadNewCombined pathKey)
successCodecNew2 canonicalSiteUrl currentPagePath =
Codec.custom
(\success initialData value ->
case value of
PageProgress payload ->
success payload
InitialData payload ->
initialData payload
)
|> Codec.variant1 "PageProgress" PageProgress (successCodecNew canonicalSiteUrl currentPagePath)
|> Codec.variant1 "InitialData" InitialData (initialDataCodec canonicalSiteUrl)
|> Codec.buildCustom
manifestCodec : String -> Codec (Manifest.Config pathKey)
manifestCodec canonicalSiteUrl =
Codec.build (Manifest.toJson canonicalSiteUrl) (Decode.succeed stubManifest)
filesToGenerateCodec : Codec (List { path : List String, content : String })
filesToGenerateCodec =
Codec.build
(\list ->
list
|> Json.Encode.list
(\item ->
Json.Encode.object
[ ( "path", item.path |> String.join "/" |> Json.Encode.string )
, ( "content", item.content |> Json.Encode.string )
]
)
)
(Decode.list
(Decode.map2 (\path content -> { path = path, content = content })
(Decode.string |> Decode.map (String.split "/") |> Decode.field "path")
(Decode.string |> Decode.field "content")
)
)
initialDataCodec : String -> Codec (InitialDataRecord pathKey)
initialDataCodec canonicalSiteUrl =
Codec.object InitialDataRecord
|> Codec.field "filesToGenerate"
.filesToGenerate
filesToGenerateCodec
|> Codec.field "manifest"
.manifest
(manifestCodec canonicalSiteUrl)
|> Codec.buildObject

View File

@ -1,6 +1,6 @@
module Pages.Manifest exposing
( Config
, DisplayMode(..), Orientation(..)
( Config, Icon
, DisplayMode(..), Orientation(..), IconPurpose(..)
, toJson
)
@ -44,12 +44,12 @@ You pass your `Pages.Manifest.Config` record into the `Pages.application` functi
, canonicalSiteUrl = canonicalSiteUrl
}
@docs Config
@docs Config, Icon
## Config options
@docs DisplayMode, Orientation
@docs DisplayMode, Orientation, IconPurpose
## Functions for use by the generated code (`Pages.elm`)
@ -61,6 +61,7 @@ You pass your `Pages.Manifest.Config` record into the `Pages.application` functi
import Color exposing (Color)
import Color.Convert
import Json.Encode as Encode
import MimeType
import Pages.ImagePath as ImagePath exposing (ImagePath)
import Pages.Manifest.Category as Category exposing (Category)
import Pages.PagePath as PagePath exposing (PagePath)
@ -70,7 +71,6 @@ import Pages.PagePath as PagePath exposing (PagePath)
{- TODO serviceworker https://developer.mozilla.org/en-US/docs/Web/Manifest/serviceworker
This is mandatory... need to process this in a special way
-}
-- TODO icons https://developer.mozilla.org/en-US/docs/Web/Manifest/icons
-- TODO use language https://developer.mozilla.org/en-US/docs/Web/Manifest/lang
@ -166,9 +166,33 @@ type alias Config pathKey =
-- https://developer.mozilla.org/en-US/docs/Web/Manifest/short_name
, shortName : Maybe String
, sourceIcon : ImagePath pathKey
, icons : List (Icon pathKey)
}
{-| <https://developer.mozilla.org/en-US/docs/Web/Manifest/icons>
-}
type alias Icon pathKey =
{ src : ImagePath pathKey
, sizes : List ( Int, Int )
, mimeType : Maybe MimeType.MimeImage
, purposes : List IconPurpose
}
{-| <https://w3c.github.io/manifest/#dfn-icon-purposes>
-}
type IconPurpose
= IconPurposeMonochrome
| IconPurposeMaskable
| IconPurposeAny
square : Int -> ( Int, Int )
square x =
( x, x )
displayModeToAttribute : DisplayMode -> String
displayModeToAttribute displayMode =
case displayMode of
@ -185,17 +209,68 @@ displayModeToAttribute displayMode =
"browser"
encodeIcon : String -> Icon pathKey -> Encode.Value
encodeIcon canonicalSiteUrl icon =
encodeMaybeObject
[ ( "src", icon.src |> ImagePath.toAbsoluteUrl canonicalSiteUrl |> Encode.string |> Just )
, ( "type", icon.mimeType |> Maybe.map MimeType.Image |> Maybe.map MimeType.toString |> Maybe.map Encode.string )
, ( "sizes", icon.sizes |> nonEmptyList |> Maybe.map sizesString |> Maybe.map Encode.string )
, ( "purpose", icon.purposes |> nonEmptyList |> Maybe.map purposesString |> Maybe.map Encode.string )
]
purposesString : List IconPurpose -> String
purposesString purposes =
purposes
|> List.map purposeToString
|> String.join " "
purposeToString : IconPurpose -> String
purposeToString purpose =
case purpose of
IconPurposeMonochrome ->
"monochrome"
IconPurposeMaskable ->
"maskable"
IconPurposeAny ->
"any"
sizesString : List ( Int, Int ) -> String
sizesString sizes =
sizes
|> List.map (\( x, y ) -> String.fromInt x ++ "x" ++ String.fromInt y)
|> String.join " "
nonEmptyList : List a -> Maybe (List a)
nonEmptyList list =
if List.isEmpty list then
Nothing
else
Just list
{-| Feel free to use this, but in 99% of cases you won't need it. The generated
code will run this for you to generate your `manifest.json` file automatically!
-}
toJson : Config pathKey -> Encode.Value
toJson config =
toJson : String -> Config pathKey -> Encode.Value
toJson canonicalSiteUrl config =
[ ( "sourceIcon"
, config.sourceIcon
|> ImagePath.toString
|> Encode.string
|> Just
)
, ( "icons"
, config.icons
|> Encode.list (encodeIcon canonicalSiteUrl)
|> Just
)
, ( "background_color"
, config.backgroundColor
|> Maybe.map Color.Convert.colorToHex
@ -267,6 +342,12 @@ toJson config =
|> Just
)
]
|> encodeMaybeObject
encodeMaybeObject : List ( String, Maybe Encode.Value ) -> Encode.Value
encodeMaybeObject list =
list
|> List.filterMap
(\( key, maybeValue ) ->
case maybeValue of

View File

@ -302,7 +302,7 @@ withFileGenerator generateFiles (Builder config) =
{-| When you're done with your builder pipeline, you complete it with `Pages.Platform.toProgram`.
-}
toProgram : Builder pathKey model msg metadata view -> Program model msg metadata view
toProgram : Builder pathKey model msg metadata view -> Program model msg metadata view pathKey
toProgram (Builder config) =
application
{ init = config.init
@ -371,7 +371,7 @@ application :
, canonicalSiteUrl : String
, internals : Pages.Internal.Internal pathKey
}
-> Program model msg metadata view
-> Program model msg metadata view pathKey
application config =
(case config.internals.applicationType of
Pages.Internal.Browser ->
@ -399,8 +399,8 @@ application config =
{-| The `Program` type for an `elm-pages` app.
-}
type alias Program model msg metadata view =
Pages.Internal.Platform.Program model msg metadata view
type alias Program model msg metadata view pathKey =
Pages.Internal.Platform.Program model msg metadata view pathKey
{-| -}

View File

@ -86,6 +86,7 @@ import Pages.Internal.StaticHttpBody as Body
import Pages.Secrets
import Pages.StaticHttp.Request as HashRequest
import Pages.StaticHttpRequest exposing (Request(..))
import RequestsAndPending exposing (RequestsAndPending)
import Secrets
@ -160,7 +161,10 @@ map fn requestInfo =
( urls
, \appType rawResponses ->
lookupFn appType rawResponses
|> Result.map (\( partiallyStripped, nextRequest ) -> ( partiallyStripped, map fn nextRequest ))
|> Result.map
(\( partiallyStripped, nextRequest ) ->
( partiallyStripped, map fn nextRequest )
)
)
Done value ->
@ -241,33 +245,17 @@ map2 fn request1 request2 =
case ( request1, request2 ) of
( Request ( urls1, lookupFn1 ), Request ( urls2, lookupFn2 ) ) ->
let
value : ApplicationType -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, Request c )
value : ApplicationType -> RequestsAndPending -> Result Pages.StaticHttpRequest.Error ( Dict String String, Request c )
value appType rawResponses =
let
value1 =
lookupFn1 appType rawResponses
|> Result.map Tuple.second
case ( lookupFn1 appType rawResponses, lookupFn2 appType rawResponses ) of
( Ok ( newDict1, newValue1 ), Ok ( newDict2, newValue2 ) ) ->
Ok ( combineReducedDicts newDict1 newDict2, map2 fn newValue1 newValue2 )
value2 =
lookupFn2 appType rawResponses
|> Result.map Tuple.second
( Err error, _ ) ->
Err error
dict1 =
lookupFn1 appType rawResponses
|> Result.map Tuple.first
|> Result.withDefault Dict.empty
dict2 =
lookupFn2 appType rawResponses
|> Result.map Tuple.first
|> Result.withDefault Dict.empty
in
Result.map2
(\thing1 thing2 ->
( combineReducedDicts dict1 dict2, map2 fn thing1 thing2 )
)
value1
value2
( _, Err error ) ->
Err error
in
Request
( urls1 ++ urls2
@ -278,44 +266,22 @@ map2 fn request1 request2 =
Request
( urls1
, \appType rawResponses ->
let
value1 =
lookupFn1 appType rawResponses
|> Result.map Tuple.second
dict1 =
lookupFn1 appType rawResponses
|> Result.map Tuple.first
|> Result.withDefault Dict.empty
in
Result.map2
(\thing1 thing2 ->
( dict1, map2 fn thing1 thing2 )
)
value1
(Ok (Done value2))
lookupFn1 appType rawResponses
|> Result.map
(\( dict1, value1 ) ->
( dict1, map2 fn value1 (Done value2) )
)
)
( Done value2, Request ( urls1, lookupFn1 ) ) ->
Request
( urls1
, \appType rawResponses ->
let
value1 =
lookupFn1 appType rawResponses
|> Result.map Tuple.second
dict1 =
lookupFn1 appType rawResponses
|> Result.map Tuple.first
|> Result.withDefault Dict.empty
in
Result.map2
(\thing1 thing2 ->
( dict1, map2 fn thing1 thing2 )
)
(Ok (Done value2))
value1
lookupFn1 appType rawResponses
|> Result.map
(\( dict1, value1 ) ->
( dict1, map2 fn (Done value2) value1 )
)
)
( Done value1, Done value2 ) ->
@ -339,20 +305,26 @@ combineReducedDicts dict1 dict2 =
)
lookup : ApplicationType -> Pages.StaticHttpRequest.Request value -> Dict String String -> Result Pages.StaticHttpRequest.Error ( Dict String String, value )
lookup appType requestInfo rawResponses =
lookup : ApplicationType -> Pages.StaticHttpRequest.Request value -> RequestsAndPending -> Result Pages.StaticHttpRequest.Error ( Dict String String, value )
lookup =
lookupHelp Dict.empty
lookupHelp : Dict String String -> ApplicationType -> Pages.StaticHttpRequest.Request value -> RequestsAndPending -> Result Pages.StaticHttpRequest.Error ( Dict String String, value )
lookupHelp strippedSoFar appType requestInfo rawResponses =
case requestInfo of
Request ( urls, lookupFn ) ->
lookupFn appType rawResponses
|> Result.andThen
(\( strippedResponses, nextRequest ) ->
lookup appType
lookupHelp (Dict.union strippedResponses strippedSoFar)
appType
(addUrls urls nextRequest)
strippedResponses
rawResponses
)
Done value ->
Ok ( rawResponses, value )
Ok ( strippedSoFar, value )
addUrls : List (Pages.Secrets.Value HashRequest.Request) -> Pages.StaticHttpRequest.Request value -> Pages.StaticHttpRequest.Request value
@ -440,7 +412,7 @@ succeed value =
Request
( []
, \appType rawResponses ->
Ok ( rawResponses, Done value )
Ok ( Dict.empty, Done value )
)
@ -595,12 +567,12 @@ unoptimizedRequest requestWithSecrets expect =
case appType of
ApplicationType.Cli ->
rawResponseDict
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> RequestsAndPending.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> (\maybeResponse ->
case maybeResponse of
Just rawResponse ->
Ok
( rawResponseDict
( Dict.singleton (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) rawResponse
, rawResponse
)
@ -650,12 +622,12 @@ unoptimizedRequest requestWithSecrets expect =
ApplicationType.Browser ->
rawResponseDict
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> RequestsAndPending.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> (\maybeResponse ->
case maybeResponse of
Just rawResponse ->
Ok
( rawResponseDict
( Dict.singleton (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) rawResponse
, rawResponse
)
@ -690,13 +662,12 @@ unoptimizedRequest requestWithSecrets expect =
( [ requestWithSecrets ]
, \appType rawResponseDict ->
rawResponseDict
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> RequestsAndPending.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> (\maybeResponse ->
case maybeResponse of
Just rawResponse ->
Ok
( rawResponseDict
-- |> Dict.update url (\maybeValue -> Just """{"fake": 123}""")
( Dict.singleton (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) rawResponse
, rawResponse
)
@ -710,7 +681,6 @@ unoptimizedRequest requestWithSecrets expect =
(\( strippedResponses, rawResponse ) ->
rawResponse
|> Json.Decode.decodeString decoder
-- |> Result.mapError Json.Decode.Exploration.errorsToString
|> (\decodeResult ->
case decodeResult of
Err error ->
@ -740,13 +710,12 @@ unoptimizedRequest requestWithSecrets expect =
( [ requestWithSecrets ]
, \appType rawResponseDict ->
rawResponseDict
|> Dict.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> RequestsAndPending.get (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
|> (\maybeResponse ->
case maybeResponse of
Just rawResponse ->
Ok
( rawResponseDict
-- |> Dict.update url (\maybeValue -> Just """{"fake": 123}""")
( Dict.singleton (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) rawResponse
, rawResponse
)
@ -765,9 +734,7 @@ unoptimizedRequest requestWithSecrets expect =
|> Result.map
(\finalRequest ->
( strippedResponses
|> Dict.insert
(Secrets.maskedLookup requestWithSecrets |> HashRequest.hash)
rawResponse
|> Dict.insert (Secrets.maskedLookup requestWithSecrets |> HashRequest.hash) rawResponse
, finalRequest
)
)

View File

@ -1,31 +1,38 @@
module Pages.StaticHttpRequest exposing (Error(..), Request(..), permanentError, resolve, resolveUrls, strippedResponses, toBuildError, urls)
module Pages.StaticHttpRequest exposing (Error(..), Request(..), Status(..), cacheRequestResolution, permanentError, resolve, resolveUrls, strippedResponses, toBuildError, urls)
import BuildError exposing (BuildError)
import Dict exposing (Dict)
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
import Pages.Internal.StaticHttpBody as StaticHttpBody
import Pages.StaticHttp.Request
import RequestsAndPending exposing (RequestsAndPending)
import Secrets
import TerminalText as Terminal
type Request value
= Request ( List (Secrets.Value Pages.StaticHttp.Request.Request), ApplicationType -> Dict String String -> Result Error ( Dict String String, Request value ) )
= Request ( List (Secrets.Value Pages.StaticHttp.Request.Request), ApplicationType -> RequestsAndPending -> Result Error ( Dict String String, Request value ) )
| Done value
strippedResponses : ApplicationType -> Request value -> Dict String String -> Dict String String
strippedResponses appType request rawResponses =
strippedResponses : ApplicationType -> Request value -> RequestsAndPending -> Dict String String
strippedResponses =
strippedResponsesHelp Dict.empty
strippedResponsesHelp : Dict String String -> ApplicationType -> Request value -> RequestsAndPending -> Dict String String
strippedResponsesHelp usedSoFar appType request rawResponses =
case request of
Request ( list, lookupFn ) ->
case lookupFn appType rawResponses of
Err error ->
rawResponses
usedSoFar
Ok ( partiallyStrippedResponses, followupRequest ) ->
strippedResponses appType followupRequest partiallyStrippedResponses
strippedResponsesHelp (Dict.union usedSoFar partiallyStrippedResponses) appType followupRequest rawResponses
Done value ->
rawResponses
usedSoFar
type Error
@ -78,7 +85,7 @@ toBuildError path error =
}
permanentError : ApplicationType -> Request value -> Dict String String -> Maybe Error
permanentError : ApplicationType -> Request value -> RequestsAndPending -> Maybe Error
permanentError appType request rawResponses =
case request of
Request ( urlList, lookupFn ) ->
@ -101,7 +108,7 @@ permanentError appType request rawResponses =
Nothing
resolve : ApplicationType -> Request value -> Dict String String -> Result Error value
resolve : ApplicationType -> Request value -> RequestsAndPending -> Result Error value
resolve appType request rawResponses =
case request of
Request ( urlList, lookupFn ) ->
@ -116,19 +123,62 @@ resolve appType request rawResponses =
Ok value
resolveUrls : ApplicationType -> Request value -> Dict String String -> ( Bool, List (Secrets.Value Pages.StaticHttp.Request.Request) )
resolveUrls : ApplicationType -> Request value -> RequestsAndPending -> ( Bool, List (Secrets.Value Pages.StaticHttp.Request.Request) )
resolveUrls appType request rawResponses =
case request of
Request ( urlList, lookupFn ) ->
case lookupFn appType rawResponses of
Ok ( partiallyStrippedResponses, nextRequest ) ->
Ok ( _, nextRequest ) ->
resolveUrls appType nextRequest rawResponses
|> Tuple.mapSecond ((++) urlList)
Err error ->
Err _ ->
( False
, urlList
)
Done value ->
Done _ ->
( True, [] )
cacheRequestResolution :
ApplicationType
-> Request value
-> RequestsAndPending
-> Status value
cacheRequestResolution =
cacheRequestResolutionHelp []
type Status value
= Incomplete (List (Secrets.Value Pages.StaticHttp.Request.Request))
| HasPermanentError Error
| Complete value -- TODO include stripped responses?
cacheRequestResolutionHelp :
List (Secrets.Value Pages.StaticHttp.Request.Request)
-> ApplicationType
-> Request value
-> RequestsAndPending
-> Status value
cacheRequestResolutionHelp foundUrls appType request rawResponses =
case request of
Request ( urlList, lookupFn ) ->
case lookupFn appType rawResponses of
Ok ( partiallyStrippedResponses, nextRequest ) ->
cacheRequestResolutionHelp urlList appType nextRequest rawResponses
Err error ->
case error of
MissingHttpResponse string ->
Incomplete (urlList ++ foundUrls)
DecoderError string ->
HasPermanentError error
UserCalledStaticHttpFail string ->
HasPermanentError error
Done value ->
Complete value

8
src/Path.elm Normal file
View File

@ -0,0 +1,8 @@
module Path exposing (..)
import Pages.Internal.String as String
join : String -> String -> String
join base path =
String.chopEnd "/" base ++ "/" ++ String.chopStart "/" path

View File

@ -0,0 +1,31 @@
module RequestsAndPending exposing (..)
import Dict exposing (Dict)
import Json.Decode as Decode
import List.Extra as Dict
type alias RequestsAndPending =
Dict String (Maybe String)
init : RequestsAndPending
init =
Dict.empty
get : String -> RequestsAndPending -> Maybe String
get key requestsAndPending =
requestsAndPending
|> Dict.get key
|> Maybe.andThen identity
insert : String -> String -> RequestsAndPending -> RequestsAndPending
insert key value requestsAndPending =
Dict.insert key (Just value) requestsAndPending
decoder : Decode.Decoder RequestsAndPending
decoder =
Decode.dict (Decode.string |> Decode.map Just)

View File

@ -0,0 +1,442 @@
module BetaStaticHttpRequestsTests exposing (all)
import Codec
import Dict exposing (Dict)
import Expect
import Html
import Json.Decode as JD
import Json.Encode as Encode
import OptimizedDecoder as Decode exposing (Decoder)
import Pages.ContentCache as ContentCache
import Pages.Document as Document
import Pages.ImagePath as ImagePath
import Pages.Internal.Platform.Cli as Main exposing (..)
import Pages.Internal.Platform.Effect as Effect exposing (Effect)
import Pages.Internal.Platform.ToJsPayload as ToJsPayload exposing (ToJsPayload)
import Pages.Internal.StaticHttpBody as StaticHttpBody
import Pages.Manifest as Manifest
import Pages.PagePath as PagePath
import Pages.StaticHttp as StaticHttp
import Pages.StaticHttp.Request as Request
import PagesHttp
import ProgramTest exposing (ProgramTest)
import Regex
import Secrets
import SimulatedEffect.Cmd
import SimulatedEffect.Http as Http
import SimulatedEffect.Ports
import SimulatedEffect.Task
import Test exposing (Test, describe, only, skip, test)
import Test.Http
all : Test
all =
describe "Beta Static Http Requests"
[ test "port is sent out once all requests are finished" <|
\() ->
start
[ ( [ "elm-pages" ]
, StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
)
]
|> ProgramTest.simulateHttpOk
"GET"
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86 }"""
|> expectSuccess
[ ( "elm-pages"
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
)
]
, test "two pages" <|
\() ->
start
[ ( [ "elm-pages" ]
, StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") starDecoder
)
, ( [ "elm-pages-starter" ]
, StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages-starter") starDecoder
)
]
|> ProgramTest.simulateHttpOk
"GET"
"https://api.github.com/repos/dillonkearns/elm-pages"
"""{ "stargazer_count": 86 }"""
|> ProgramTest.simulateHttpOk
"GET"
"https://api.github.com/repos/dillonkearns/elm-pages-starter"
"""{ "stargazer_count": 49 }"""
|> expectSuccess
[ ( "elm-pages"
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages"
, """{"stargazer_count":86}"""
)
]
)
, ( "elm-pages-starter"
, [ ( get "https://api.github.com/repos/dillonkearns/elm-pages-starter"
, """{"stargazer_count":49}"""
)
]
)
]
]
start : List ( List String, StaticHttp.Request a ) -> ProgramTest (Main.Model PathKey ()) Main.Msg (Effect PathKey)
start pages =
startWithHttpCache (Ok ()) [] pages
startWithHttpCache :
Result String ()
-> List ( Request.Request, String )
-> List ( List String, StaticHttp.Request a )
-> ProgramTest (Main.Model PathKey ()) Main.Msg (Effect PathKey)
startWithHttpCache =
startLowLevel (StaticHttp.succeed [])
startLowLevel :
StaticHttp.Request
(List
(Result String
{ path : List String
, content : String
}
)
)
-> Result String ()
-> List ( Request.Request, String )
-> List ( List String, StaticHttp.Request a )
-> ProgramTest (Main.Model PathKey ()) Main.Msg (Effect PathKey)
startLowLevel generateFiles documentBodyResult staticHttpCache pages =
let
document =
Document.fromList
[ Document.parser
{ extension = "md"
, metadata = JD.succeed ()
, body = \_ -> documentBodyResult
}
]
content =
pages
|> List.map
(\( path, _ ) ->
( path, { extension = "md", frontMatter = "null", body = Just "" } )
)
contentCache =
ContentCache.init document content Nothing
siteMetadata =
contentCache
|> Result.map
(\cache -> cache |> ContentCache.extractMetadata PathKey)
|> Result.mapError (List.map Tuple.second)
config =
{ toJsPort = toJsPort
, fromJsPort = fromJsPort
, manifest = manifest
, generateFiles = \_ -> generateFiles
, init = \_ -> ( (), Cmd.none )
, update = \_ _ -> ( (), Cmd.none )
, view =
\allFrontmatter page ->
let
thing =
pages
|> Dict.fromList
|> Dict.get
(page.path
|> PagePath.toString
|> String.split "/"
|> List.filter (\pathPart -> pathPart /= "")
)
in
case thing of
Just request ->
request
|> StaticHttp.map
(\staticData -> { view = \model viewForPage -> { title = "Title", body = Html.text "" }, head = [] })
Nothing ->
Debug.todo "Couldn't find page"
, subscriptions = \_ -> Sub.none
, document = document
, content =
[ ( [ "elm-pages" ]
, { extension = "md", frontMatter = "{}", body = Nothing }
)
]
, canonicalSiteUrl = canonicalSiteUrl
, pathKey = PathKey
, onPageChange = Just (\_ -> ())
}
encodedFlags =
--{"secrets":
-- {"API_KEY": "ABCD1234","BEARER": "XYZ789"}, "mode": "prod", "staticHttpCache": {}
-- }
Encode.object
[ ( "secrets"
, [ ( "API_KEY", "ABCD1234" )
, ( "BEARER", "XYZ789" )
]
|> Dict.fromList
|> Encode.dict identity Encode.string
)
, ( "mode", Encode.string "elm-to-html-beta" )
, ( "staticHttpCache", encodedStaticHttpCache )
]
encodedStaticHttpCache =
staticHttpCache
|> List.map
(\( request, httpResponseString ) ->
( Request.hash request, Encode.string httpResponseString )
)
|> Encode.object
in
{-
(Model -> model)
-> ContentCache.ContentCache metadata view
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Config pathKey userMsg userModel metadata view
-> Decode.Value
-> ( model, Effect pathKey )
-}
ProgramTest.createDocument
{ init = Main.init identity contentCache siteMetadata config
, update = Main.update contentCache siteMetadata config
, view = \_ -> { title = "", body = [] }
}
|> ProgramTest.withSimulatedEffects simulateEffects
|> ProgramTest.start (flags (Encode.encode 0 encodedFlags))
canonicalSiteUrl =
""
flags : String -> JD.Value
flags jsonString =
case JD.decodeString JD.value jsonString of
Ok value ->
value
Err _ ->
Debug.todo "Invalid JSON value."
simulateEffects : Effect PathKey -> ProgramTest.SimulatedEffect Main.Msg
simulateEffects effect =
case effect of
Effect.NoEffect ->
SimulatedEffect.Cmd.none
Effect.SendJsData value ->
SimulatedEffect.Ports.send "toJsPort" (value |> Codec.encoder (ToJsPayload.toJsCodec canonicalSiteUrl))
-- toJsPort value |> Cmd.map never
Effect.Batch list ->
list
|> List.map simulateEffects
|> SimulatedEffect.Cmd.batch
Effect.FetchHttp ({ unmasked, masked } as requests) ->
Http.request
{ method = unmasked.method
, url = unmasked.url
, headers = unmasked.headers |> List.map (\( key, value ) -> Http.header key value)
, body =
case unmasked.body of
StaticHttpBody.EmptyBody ->
Http.emptyBody
StaticHttpBody.StringBody contentType string ->
Http.stringBody contentType string
StaticHttpBody.JsonBody value ->
Http.jsonBody value
, expect =
PagesHttp.expectString
(\response ->
GotStaticHttpResponse
{ request = requests
, response = response
}
)
, timeout = Nothing
, tracker = Nothing
}
Effect.SendSinglePage info ->
SimulatedEffect.Cmd.batch
[ info
|> Codec.encoder (ToJsPayload.successCodecNew2 "" "")
|> SimulatedEffect.Ports.send "toJsPort"
, SimulatedEffect.Task.succeed ()
|> SimulatedEffect.Task.perform (\_ -> Main.Continue)
]
Effect.Continue ->
SimulatedEffect.Cmd.none
--SimulatedEffect.Task.succeed ()
-- |> SimulatedEffect.Task.perform (\_ -> Main.Continue)
expectErrorsPort : String -> List (ToJsPayload pathKey) -> Expect.Expectation
expectErrorsPort expectedPlainString actualPorts =
case actualPorts of
[ ToJsPayload.Errors actualRichTerminalString ] ->
actualRichTerminalString
|> normalizeErrorExpectEqual expectedPlainString
[] ->
Expect.fail "Expected single error port. Didn't receive any ports."
_ ->
Expect.fail <| "Expected single error port. Got\n" ++ String.join "\n\n" (List.map Debug.toString actualPorts)
expectNonfatalErrorsPort : String -> List (ToJsPayload pathKey) -> Expect.Expectation
expectNonfatalErrorsPort expectedPlainString actualPorts =
case actualPorts of
[ ToJsPayload.Success successPayload ] ->
successPayload.errors
|> String.join "\n\n"
|> normalizeErrorExpectEqual expectedPlainString
_ ->
Expect.fail <| "Expected single non-fatal error port. Got\n" ++ String.join "\n\n" (List.map Debug.toString actualPorts)
normalizeErrorExpectEqual : String -> String -> Expect.Expectation
normalizeErrorExpectEqual expectedPlainString actualRichTerminalString =
actualRichTerminalString
|> Regex.replace
(Regex.fromString "\u{001B}\\[[0-9;]+m"
|> Maybe.withDefault Regex.never
)
(\_ -> "")
|> Expect.equal expectedPlainString
normalizeErrorsExpectEqual : List String -> List String -> Expect.Expectation
normalizeErrorsExpectEqual expectedPlainStrings actualRichTerminalStrings =
actualRichTerminalStrings
|> List.map
(Regex.replace
(Regex.fromString "\u{001B}\\[[0-9;]+m"
|> Maybe.withDefault Regex.never
)
(\_ -> "")
)
|> Expect.equalLists expectedPlainStrings
toJsPort foo =
Cmd.none
fromJsPort =
Sub.none
type PathKey
= PathKey
manifest : Manifest.Config PathKey
manifest =
{ backgroundColor = Nothing
, categories = []
, displayMode = Manifest.Standalone
, orientation = Manifest.Portrait
, description = "elm-pages - A statically typed site generator."
, iarcRatingId = Nothing
, name = "elm-pages docs"
, themeColor = Nothing
, startUrl = PagePath.external ""
, shortName = Just "elm-pages"
, sourceIcon = ImagePath.external ""
, icons = []
}
starDecoder : Decoder Int
starDecoder =
Decode.field "stargazer_count" Decode.int
expectSuccess : List ( String, List ( Request.Request, String ) ) -> ProgramTest model msg effect -> Expect.Expectation
expectSuccess expectedRequests previous =
previous
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder (ToJsPayload.successCodecNew2 "" ""))
(\portPayloads ->
portPayloads
|> List.filterMap
(\portPayload ->
case portPayload of
ToJsPayload.PageProgress value ->
Just ( value.route, value.contentJson )
ToJsPayload.InitialData record ->
Nothing
)
|> Dict.fromList
|> Expect.equalDicts
(expectedRequests
|> List.map
(\( url, requests ) ->
( url
, requests
|> List.map
(\( request, response ) ->
( Request.hash request, response )
)
|> Dict.fromList
)
)
|> Dict.fromList
)
)
expectError : List String -> ProgramTest model msg effect -> Expect.Expectation
expectError expectedErrors previous =
previous
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder (ToJsPayload.successCodecNew2 "" ""))
(\value ->
case value of
[ ToJsPayload.PageProgress portPayload ] ->
portPayload.errors
|> normalizeErrorsExpectEqual expectedErrors
_ ->
Expect.fail ("Expected ports to be called once, but instead there were " ++ String.fromInt (List.length value) ++ " calls.")
)
get : String -> Request.Request
get url =
{ method = "GET"
, url = url
, headers = []
, body = StaticHttp.emptyBody
}

View File

@ -25,6 +25,7 @@ import Secrets
import SimulatedEffect.Cmd
import SimulatedEffect.Http as Http
import SimulatedEffect.Ports
import SimulatedEffect.Task
import Test exposing (Test, describe, only, skip, test)
import Test.Http
@ -337,7 +338,7 @@ all =
"This is a raw text file."
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.toJsCodec canonicalSiteUrl))
(expectErrorsPort
"""-- STATIC HTTP DECODING ERROR ----------------------------------------------------- elm-pages
@ -476,7 +477,7 @@ String was not uppercased"""
"""{ "stargazer_count": 86 }"""
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.toJsCodec canonicalSiteUrl))
(expectErrorsPort
"""-- STATIC HTTP DECODING ERROR ----------------------------------------------------- elm-pages
@ -521,7 +522,7 @@ I encountered some errors while decoding this JSON:
""" "continuation-url" """
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.toJsCodec canonicalSiteUrl))
(expectErrorsPort
"""-- MISSING SECRET ----------------------------------------------------- elm-pages
@ -550,14 +551,20 @@ So maybe MISSING should be API_KEY"""
)
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.toJsCodec canonicalSiteUrl))
(expectErrorsPort """-- STATIC HTTP ERROR ----------------------------------------------------- elm-pages
I got an error making an HTTP request to this URL: https://api.github.com/repos/dillonkearns/elm-pages
Bad status: 404
Status message: TODO: if you need this, please report to https://github.com/avh4/elm-program-test/issues
Body: """)
Body:
-- STATIC HTTP DECODING ERROR ----------------------------------------------------- elm-pages
Payload sent back invalid JSON""")
, test "uses real secrets to perform request and masked secrets to store and lookup response" <|
\() ->
start
@ -763,7 +770,7 @@ Found an unhandled HTML tag in markdown doc."""
]
start : List ( List String, StaticHttp.Request a ) -> ProgramTest Main.Model Main.Msg (Effect PathKey)
start : List ( List String, StaticHttp.Request a ) -> ProgramTest (Main.Model PathKey ()) Main.Msg (Effect PathKey)
start pages =
startWithHttpCache (Ok ()) [] pages
@ -772,7 +779,7 @@ startWithHttpCache :
Result String ()
-> List ( Request.Request, String )
-> List ( List String, StaticHttp.Request a )
-> ProgramTest Main.Model Main.Msg (Effect PathKey)
-> ProgramTest (Main.Model PathKey ()) Main.Msg (Effect PathKey)
startWithHttpCache =
startLowLevel (StaticHttp.succeed [])
@ -789,7 +796,7 @@ startLowLevel :
-> Result String ()
-> List ( Request.Request, String )
-> List ( List String, StaticHttp.Request a )
-> ProgramTest Main.Model Main.Msg (Effect PathKey)
-> ProgramTest (Main.Model PathKey ()) Main.Msg (Effect PathKey)
startLowLevel generateFiles documentBodyResult staticHttpCache pages =
let
document =
@ -848,7 +855,7 @@ startLowLevel generateFiles documentBodyResult staticHttpCache pages =
, subscriptions = \_ -> Sub.none
, document = document
, content = []
, canonicalSiteUrl = ""
, canonicalSiteUrl = canonicalSiteUrl
, pathKey = PathKey
, onPageChange = Just (\_ -> ())
}
@ -887,7 +894,7 @@ startLowLevel generateFiles documentBodyResult staticHttpCache pages =
-}
ProgramTest.createDocument
{ init = Main.init identity contentCache siteMetadata config
, update = Main.update siteMetadata config
, update = Main.update contentCache siteMetadata config
, view = \_ -> { title = "", body = [] }
}
|> ProgramTest.withSimulatedEffects simulateEffects
@ -911,7 +918,7 @@ simulateEffects effect =
SimulatedEffect.Cmd.none
Effect.SendJsData value ->
SimulatedEffect.Ports.send "toJsPort" (value |> Codec.encoder ToJsPayload.toJsCodec)
SimulatedEffect.Ports.send "toJsPort" (value |> Codec.encoder (ToJsPayload.toJsCodec canonicalSiteUrl))
-- toJsPort value |> Cmd.map never
Effect.Batch list ->
@ -946,6 +953,20 @@ simulateEffects effect =
, tracker = Nothing
}
Effect.SendSinglePage info ->
SimulatedEffect.Cmd.batch
[ info
|> Codec.encoder (ToJsPayload.successCodecNew2 "" "")
|> SimulatedEffect.Ports.send "toJsPort"
, SimulatedEffect.Task.succeed ()
|> SimulatedEffect.Task.perform (\_ -> Main.Continue)
]
Effect.Continue ->
--SimulatedEffect.Task.succeed ()
-- |> SimulatedEffect.Task.perform (\_ -> Continue)
SimulatedEffect.Cmd.none
expectErrorsPort : String -> List (ToJsPayload pathKey) -> Expect.Expectation
expectErrorsPort expectedPlainString actualPorts =
@ -954,6 +975,9 @@ expectErrorsPort expectedPlainString actualPorts =
actualRichTerminalString
|> normalizeErrorExpectEqual expectedPlainString
[] ->
Expect.fail "Expected single error port. Didn't receive any ports."
_ ->
Expect.fail <| "Expected single error port. Got\n" ++ String.join "\n\n" (List.map Debug.toString actualPorts)
@ -1019,6 +1043,7 @@ manifest =
, startUrl = PagePath.external ""
, shortName = Just "elm-pages"
, sourceIcon = ImagePath.external ""
, icons = []
}
@ -1037,7 +1062,7 @@ expectSuccessNew expectedRequests expectations previous =
previous
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.toJsCodec canonicalSiteUrl))
(\value ->
case value of
(ToJsPayload.Success portPayload) :: rest ->
@ -1077,7 +1102,7 @@ expectError expectedErrors previous =
previous
|> ProgramTest.expectOutgoingPortValues
"toJsPort"
(Codec.decoder ToJsPayload.toJsCodec)
(Codec.decoder (ToJsPayload.toJsCodec canonicalSiteUrl))
(\value ->
case value of
[ ToJsPayload.Success portPayload ] ->
@ -1092,6 +1117,10 @@ expectError expectedErrors previous =
)
canonicalSiteUrl =
""
get : String -> Request.Request
get url =
{ method = "GET"

View File

@ -21,7 +21,7 @@ requestsDict requestMap =
|> List.map
(\( request, response ) ->
( request |> Request.hash
, response
, Just response
)
)
|> Dict.fromList

View File

@ -31,7 +31,7 @@ all =
[ test "andThen" <|
\() ->
StaticResponses.init Dict.empty (Ok []) config []
|> StaticResponses.nextStep config (Ok []) Mode.Dev (SecretsDict.unmasked Dict.empty) Dict.empty []
|> StaticResponses.nextStep config (Ok []) (Ok []) Mode.Dev (SecretsDict.unmasked Dict.empty) Dict.empty []
|> Expect.equal
(StaticResponses.Finish
(ToJsPayload.Success