mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2024-12-28 22:37:08 +03:00
Merge branch 'master' into template-modules
# Conflicts: # .gitignore # examples/docs/src/Main.elm
This commit is contained in:
commit
3442cf02a8
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [dillonkearns]
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,4 +4,4 @@ dist/
|
||||
.cache/
|
||||
generator/src/Main.js
|
||||
elm-pages-*.tgz
|
||||
coverage
|
||||
coverage
|
||||
|
2
elm.json
2
elm.json
@ -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",
|
||||
|
8
examples/docs/beta-index.js
Normal file
8
examples/docs/beta-index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import * as elmOembed from "/elm-oembed.js";
|
||||
// import "./lib/native-shim.js";
|
||||
|
||||
export default function (elmLoaded) {
|
||||
document.addEventListener("DOMContentLoaded", function (event) {
|
||||
elmOembed.setup();
|
||||
});
|
||||
}
|
41
examples/docs/beta-style.css
Normal file
41
examples/docs/beta-style.css
Normal file
@ -0,0 +1,41 @@
|
||||
@import "/syntax.css";
|
||||
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto|Roboto+Mono&display=swap");
|
||||
@import url("https://use.fontawesome.com/releases/v5.9.0/css/all.css");
|
||||
|
||||
.dotted-line {
|
||||
-webkit-animation: animation-yweh2o 400ms linear infinite;
|
||||
animation: animation-yweh2o 400ms linear infinite;
|
||||
}
|
||||
@-webkit-keyframes animation-yweh2o {
|
||||
to {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
}
|
||||
@keyframes animation-yweh2o {
|
||||
to {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
@media all and (max-width: 600px) {
|
||||
.navbar-title {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.navbar-title svg {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.responsive-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 600px) {
|
||||
.responsive-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
@ -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",
|
||||
}
|
||||
---
|
||||
|
@ -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",
|
||||
}
|
||||
---
|
||||
|
@ -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",
|
||||
}
|
||||
---
|
||||
|
@ -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",
|
||||
}
|
||||
---
|
||||
|
@ -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.
|
||||
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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."
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
149
examples/docs/static/elm-oembed.js
vendored
Normal file
@ -0,0 +1,149 @@
|
||||
export function setup() {
|
||||
customElements.define(
|
||||
"oembed-element",
|
||||
class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
let shadow = this.attachShadow({ mode: "closed" });
|
||||
const urlAttr = this.getAttribute("url");
|
||||
if (urlAttr) {
|
||||
renderOembed(shadow, urlAttr, {
|
||||
maxwidth: this.getAttribute("maxwidth"),
|
||||
maxheight: this.getAttribute("maxheight"),
|
||||
});
|
||||
} else {
|
||||
const discoverUrl = this.getAttribute("discover-url");
|
||||
if (discoverUrl) {
|
||||
getDiscoverUrl(discoverUrl, function (discoveredUrl) {
|
||||
if (discoveredUrl) {
|
||||
renderOembed(shadow, discoveredUrl, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ShadowRoot} shadow
|
||||
* @param {string} urlToEmbed
|
||||
* @param {{maxwidth: string?; maxheight: string?}?} options
|
||||
*/
|
||||
function renderOembed(shadow, urlToEmbed, options) {
|
||||
let apiUrlBuilder = new URL(
|
||||
`https://cors-anywhere.herokuapp.com/${urlToEmbed}`
|
||||
);
|
||||
if (options && options.maxwidth) {
|
||||
apiUrlBuilder.searchParams.set("maxwidth", options.maxwidth);
|
||||
}
|
||||
if (options && options.maxheight) {
|
||||
apiUrlBuilder.searchParams.set("maxheight", options.maxheight);
|
||||
}
|
||||
const apiUrl = apiUrlBuilder.toString();
|
||||
httpGetAsync(apiUrl, (rawResponse) => {
|
||||
const response = JSON.parse(rawResponse);
|
||||
|
||||
switch (response.type) {
|
||||
case "rich":
|
||||
tryRenderingHtml(shadow, response);
|
||||
break;
|
||||
case "video":
|
||||
tryRenderingHtml(shadow, response);
|
||||
break;
|
||||
case "photo":
|
||||
let img = document.createElement("img");
|
||||
img.setAttribute("src", response.url);
|
||||
if (options) {
|
||||
img.setAttribute(
|
||||
"style",
|
||||
`max-width: ${options.maxwidth}px; max-height: ${options.maxheight}px;`
|
||||
);
|
||||
}
|
||||
shadow.appendChild(img);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
height: ?number;
|
||||
width: ?number;
|
||||
html: any;
|
||||
}} response
|
||||
* @param {ShadowRoot} shadow
|
||||
*/
|
||||
function tryRenderingHtml(shadow, response) {
|
||||
if (response && typeof response.html) {
|
||||
let iframe = createIframe(response);
|
||||
shadow.appendChild(iframe);
|
||||
setTimeout(() => {
|
||||
let refetchedIframe = shadow.querySelector("iframe");
|
||||
if (refetchedIframe && !response.height) {
|
||||
refetchedIframe.setAttribute(
|
||||
"height",
|
||||
// @ts-ignore
|
||||
(iframe.contentWindow.document.body.scrollHeight + 10).toString()
|
||||
);
|
||||
}
|
||||
if (refetchedIframe && !response.width) {
|
||||
refetchedIframe.setAttribute(
|
||||
"width",
|
||||
// @ts-ignore
|
||||
(iframe.contentWindow.document.body.scrollWidth + 10).toString()
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ height: number?; width: number?; html: string; }} response
|
||||
* @returns {HTMLIFrameElement}
|
||||
*/
|
||||
function createIframe(response) {
|
||||
let iframe = document.createElement("iframe");
|
||||
iframe.setAttribute("border", "0");
|
||||
iframe.setAttribute("frameborder", "0");
|
||||
iframe.setAttribute("height", ((response.height || 500) + 20).toString());
|
||||
iframe.setAttribute("width", ((response.width || 500) + 20).toString());
|
||||
iframe.setAttribute("style", "max-width: 100%;");
|
||||
iframe.srcdoc = response.html;
|
||||
return iframe;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {{ (discoveredUrl: string?): void;}} callback
|
||||
*/
|
||||
function getDiscoverUrl(url, callback) {
|
||||
let apiUrl = new URL(
|
||||
`https://cors-anywhere.herokuapp.com/${url}`
|
||||
).toString();
|
||||
httpGetAsync(apiUrl, function (response) {
|
||||
let dom = document.createElement("html");
|
||||
dom.innerHTML = response;
|
||||
/** @type {HTMLLinkElement | null} */ const oembedTag = dom.querySelector(
|
||||
'link[type="application/json+oembed"]'
|
||||
);
|
||||
callback(oembedTag && oembedTag.href);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} theUrl
|
||||
* @param {{ (rawResponse: string): void }} callback
|
||||
*/
|
||||
function httpGetAsync(theUrl, callback) {
|
||||
var xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.onreadystatechange = function () {
|
||||
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
|
||||
callback(xmlHttp.responseText);
|
||||
};
|
||||
xmlHttp.open("GET", theUrl, true); // true for asynchronous
|
||||
xmlHttp.send(null);
|
||||
}
|
||||
}
|
43
examples/docs/static/syntax.css
vendored
Normal file
43
examples/docs/static/syntax.css
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
pre.elmsh {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
code.elmsh {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Roboto Mono' !important;
|
||||
font-size: 20px !important;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.elmsh-line:before {
|
||||
/* content: attr(data-elmsh-lc); */
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
width: 40px;
|
||||
padding: 0 20px 0 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.elmsh {
|
||||
color: #f8f8f2;
|
||||
background: #000;
|
||||
}
|
||||
.elmsh-hl {background: #343434;}
|
||||
.elmsh-add {background: #003800;}
|
||||
.elmsh-del {background: #380000;}
|
||||
.elmsh-comm {color: #75715e;}
|
||||
.elmsh1 {color: #ae81ff;}
|
||||
.elmsh2 {color: #e6db74;}
|
||||
.elmsh3 {color: #66d9ef;}
|
||||
.elmsh4 {color: #f92672;}
|
||||
.elmsh5 {color: #a6e22e;}
|
||||
.elmsh6 {color: #ae81ff;}
|
||||
.elmsh7 {color: #fd971f;}
|
||||
|
@ -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
2
examples/simple/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
gen/
|
||||
|
9
examples/simple/beta-index.js
Normal file
9
examples/simple/beta-index.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
3
examples/simple/beta-style.css
Normal file
3
examples/simple/beta-style.css
Normal file
@ -0,0 +1,3 @@
|
||||
h1 {
|
||||
color: red;
|
||||
}
|
7
examples/simple/content/elm-markdown.md
Normal file
7
examples/simple/content/elm-markdown.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Hello from another page.
|
||||
type: page
|
||||
repo: elm-markdown
|
||||
---
|
||||
|
||||
This is another page
|
8
examples/simple/content/index.md
Normal file
8
examples/simple/content/index.md
Normal 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
64
examples/simple/elm.json
Normal 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": {}
|
||||
}
|
||||
}
|
7
examples/simple/generate-content.sh
Executable file
7
examples/simple/generate-content.sh
Executable 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
|
BIN
examples/simple/images/icon-png.png
Normal file
BIN
examples/simple/images/icon-png.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 976 B |
49
examples/simple/lib/native-shim.js
Normal file
49
examples/simple/lib/native-shim.js
Normal 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);
|
||||
})();
|
21
examples/simple/package.json
Normal file
21
examples/simple/package.json
Normal 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"
|
||||
}
|
||||
}
|
48
examples/simple/src/Data/Author.elm
Normal file
48
examples/simple/src/Data/Author.elm
Normal 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 }
|
70
examples/simple/src/DocSidebar.elm
Normal file
70
examples/simple/src/DocSidebar.elm
Normal 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
|
91
examples/simple/src/DocumentSvg.elm
Normal file
91
examples/simple/src/DocumentSvg.elm
Normal file
@ -0,0 +1,91 @@
|
||||
module DocumentSvg exposing (view)
|
||||
|
||||
import Color
|
||||
import Element exposing (Element)
|
||||
import Svg exposing (..)
|
||||
import Svg.Attributes exposing (..)
|
||||
|
||||
|
||||
strokeColor =
|
||||
-- "url(#grad1)"
|
||||
"black"
|
||||
|
||||
|
||||
pageTextColor =
|
||||
"black"
|
||||
|
||||
|
||||
fillColor =
|
||||
"url(#grad1)"
|
||||
|
||||
|
||||
|
||||
-- "none"
|
||||
|
||||
|
||||
fillGradient =
|
||||
gradient
|
||||
(Color.rgb255 5 117 230)
|
||||
(Color.rgb255 0 242 96)
|
||||
|
||||
|
||||
|
||||
-- (Color.rgb255 252 0 255)
|
||||
-- (Color.rgb255 0 219 222)
|
||||
-- (Color.rgb255 255 93 194)
|
||||
-- (Color.rgb255 255 150 250)
|
||||
|
||||
|
||||
gradient color1 color2 =
|
||||
linearGradient [ id "grad1", x1 "0%", y1 "0%", x2 "100%", y2 "0%" ]
|
||||
[ stop
|
||||
[ offset "10%"
|
||||
, Svg.Attributes.style ("stop-color:" ++ Color.toCssString color1 ++ ";stop-opacity:1")
|
||||
]
|
||||
[]
|
||||
, stop [ offset "100%", Svg.Attributes.style ("stop-color:" ++ Color.toCssString color2 ++ ";stop-opacity:1") ] []
|
||||
]
|
||||
|
||||
|
||||
view : Element msg
|
||||
view =
|
||||
svg
|
||||
[ version "1.1"
|
||||
, viewBox "251.0485 144.52063 56.114286 74.5"
|
||||
, width "56.114286"
|
||||
, height "74.5"
|
||||
, Svg.Attributes.width "30px"
|
||||
]
|
||||
[ defs []
|
||||
[ fillGradient ]
|
||||
, metadata [] []
|
||||
, g
|
||||
[ id "Canvas_11"
|
||||
, stroke "none"
|
||||
, fill fillColor
|
||||
, strokeOpacity "1"
|
||||
, fillOpacity "1"
|
||||
, strokeDasharray "none"
|
||||
]
|
||||
[ g [ id "Canvas_11: Layer 1" ]
|
||||
[ g [ id "Group_38" ]
|
||||
[ g [ id "Graphic_32" ]
|
||||
[ Svg.path
|
||||
[ d "M 252.5485 146.02063 L 252.5485 217.52063 L 305.66277 217.52063 L 305.66277 161.68254 L 290.00087 146.02063 Z"
|
||||
, stroke strokeColor
|
||||
, strokeLinecap "round"
|
||||
, strokeLinejoin "round"
|
||||
, strokeWidth "3"
|
||||
]
|
||||
[]
|
||||
]
|
||||
, g [ id "Line_34" ] [ line [ x1 "266.07286", y1 "182.8279", x2 "290.75465", y2 "183.00997", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_35" ] [ line [ x1 "266.07286", y1 "191.84156", x2 "290.75465", y2 "192.02363", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_36" ] [ line [ x1 "266.07286", y1 "200.85522", x2 "290.75465", y2 "201.0373", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
, g [ id "Line_37" ] [ line [ x1 "266.07286", y1 "164.80058", x2 "278.3874", y2 "164.94049", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|> Element.html
|
||||
|> Element.el []
|
54
examples/simple/src/Dotted.elm
Normal file
54
examples/simple/src/Dotted.elm
Normal file
@ -0,0 +1,54 @@
|
||||
module Dotted exposing (lines)
|
||||
|
||||
import Element
|
||||
import Svg
|
||||
import Svg.Attributes as Attr
|
||||
|
||||
|
||||
|
||||
{-
|
||||
.css-m2heu9 {
|
||||
stroke: #8a4baf;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 0.5 10;
|
||||
-webkit-animation: animation-yweh2o 400ms linear infinite;
|
||||
animation: animation-yweh2o 400ms linear infinite;
|
||||
}
|
||||
-}
|
||||
{-
|
||||
<svg width="20" height="30" viewBox="0 0 20 30" class="css-p2euw5">
|
||||
<path d="M10 40 L10 -10" class="css-m2heu9"></path>
|
||||
</svg>
|
||||
-}
|
||||
|
||||
|
||||
lines =
|
||||
Svg.svg
|
||||
[ Attr.width "20"
|
||||
, Attr.height "30"
|
||||
, Attr.viewBox "0 0 20 30"
|
||||
]
|
||||
[ Svg.path
|
||||
[ Attr.stroke "#2a75ff"
|
||||
, Attr.strokeWidth "4"
|
||||
, Attr.strokeLinecap "round"
|
||||
, Attr.strokeDasharray "0.5 10"
|
||||
, Attr.d "M10 40 L10 -10"
|
||||
, Attr.class "dotted-line"
|
||||
]
|
||||
[]
|
||||
]
|
||||
|> Element.html
|
||||
|> Element.el
|
||||
[ Element.centerX
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- rgb(0, 36, 71)
|
||||
-- #002447
|
||||
{-
|
||||
|
||||
.css-m2heu9{stroke:#8a4baf;stroke-width:3;stroke-linecap:round;stroke-dasharray:0.5 10;-webkit-animation:animation-yweh2o 400ms linear infinite;animation:animation-yweh2o 400ms linear infinite;}@-webkit-keyframes animation-yweh2o{to{stroke-dashoffset:10;}}@keyframes animation-yweh2o{to{stroke-dashoffset:10;}}
|
||||
-}
|
20
examples/simple/src/Ellie.elm
Normal file
20
examples/simple/src/Ellie.elm
Normal file
@ -0,0 +1,20 @@
|
||||
module Ellie exposing (outputTab)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Html
|
||||
import Html.Attributes as Attr
|
||||
|
||||
|
||||
outputTab : String -> Element msg
|
||||
outputTab ellieId =
|
||||
Html.iframe
|
||||
[ Attr.src <| "https://ellie-app.com/embed/" ++ ellieId ++ "?panel=output"
|
||||
, Attr.style "width" "100%"
|
||||
, Attr.style "height" "400px"
|
||||
, Attr.style "border" "0"
|
||||
, Attr.style "overflow" "hidden"
|
||||
, Attr.attribute "sandbox" "allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
|
||||
]
|
||||
[]
|
||||
|> Element.html
|
||||
|> Element.el [ Element.width Element.fill ]
|
76
examples/simple/src/Feed.elm
Normal file
76
examples/simple/src/Feed.elm
Normal file
@ -0,0 +1,76 @@
|
||||
module Feed exposing (fileToGenerate)
|
||||
|
||||
import Metadata exposing (Metadata(..))
|
||||
import Pages
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Rss
|
||||
|
||||
|
||||
fileToGenerate :
|
||||
{ siteTagline : String
|
||||
, siteUrl : String
|
||||
}
|
||||
->
|
||||
List
|
||||
{ path : PagePath 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
|
18
examples/simple/src/FontAwesome.elm
Normal file
18
examples/simple/src/FontAwesome.elm
Normal file
@ -0,0 +1,18 @@
|
||||
module FontAwesome exposing (icon, styledIcon)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Html
|
||||
import Html.Attributes
|
||||
|
||||
|
||||
styledIcon : String -> List (Element.Attribute msg) -> Element msg
|
||||
styledIcon classString styles =
|
||||
Html.i [ Html.Attributes.class classString ] []
|
||||
|> Element.html
|
||||
|> Element.el styles
|
||||
|
||||
|
||||
icon : String -> Element msg
|
||||
icon classString =
|
||||
Html.i [ Html.Attributes.class classString ] []
|
||||
|> Element.html
|
116
examples/simple/src/Index.elm
Normal file
116
examples/simple/src/Index.elm
Normal 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
|
||||
]
|
||||
]
|
286
examples/simple/src/Main.elm
Normal file
286
examples/simple/src/Main.elm
Normal 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
|
||||
}
|
||||
)
|
||||
)
|
||||
]
|
317
examples/simple/src/MarkdownRenderer.elm
Normal file
317
examples/simple/src/MarkdownRenderer.elm
Normal 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"
|
51
examples/simple/src/Metadata.elm
Normal file
51
examples/simple/src/Metadata.elm
Normal 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
|
34
examples/simple/src/MySitemap.elm
Normal file
34
examples/simple/src/MySitemap.elm
Normal 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)
|
||||
}
|
||||
]
|
||||
)
|
44
examples/simple/src/Palette.elm
Normal file
44
examples/simple/src/Palette.elm
Normal file
@ -0,0 +1,44 @@
|
||||
module Palette exposing (blogHeading, color, heading)
|
||||
|
||||
import Element exposing (Element)
|
||||
import Element.Font as Font
|
||||
import Element.Region
|
||||
|
||||
|
||||
color =
|
||||
{ primary = Element.rgb255 0 6 255
|
||||
, secondary = Element.rgb255 0 242 96
|
||||
}
|
||||
|
||||
|
||||
heading : Int -> List (Element msg) -> Element msg
|
||||
heading level content =
|
||||
Element.paragraph
|
||||
([ Font.bold
|
||||
, Font.family [ Font.typeface "Montserrat" ]
|
||||
, Element.Region.heading level
|
||||
]
|
||||
++ (case level of
|
||||
1 ->
|
||||
[ Font.size 36 ]
|
||||
|
||||
2 ->
|
||||
[ Font.size 24 ]
|
||||
|
||||
_ ->
|
||||
[ Font.size 20 ]
|
||||
)
|
||||
)
|
||||
content
|
||||
|
||||
|
||||
blogHeading : String -> Element msg
|
||||
blogHeading title =
|
||||
Element.paragraph
|
||||
[ Font.bold
|
||||
, Font.family [ Font.typeface "Montserrat" ]
|
||||
, Element.Region.heading 1
|
||||
, Font.size 36
|
||||
, Font.center
|
||||
]
|
||||
[ Element.text title ]
|
56
examples/simple/src/RssPlugin.elm
Normal file
56
examples/simple/src/RssPlugin.elm
Normal 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 "/") ]
|
161
examples/simple/src/Showcase.elm
Normal file
161
examples/simple/src/Showcase.elm
Normal 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"
|
||||
]
|
46
examples/simple/static/admin/config.yml
Normal file
46
examples/simple/static/admin/config.yml
Normal file
@ -0,0 +1,46 @@
|
||||
backend:
|
||||
name: git-gateway
|
||||
|
||||
media_folder: "examples/docs/images" # Folder where user uploaded files should go
|
||||
public_folder: "examples/docs/images"
|
||||
publish_mode: "editorial_workflow" # see https://www.netlifycms.org/docs/open-authoring/
|
||||
|
||||
collections: # A list of collections the CMS should be able to edit
|
||||
- name: "post" # Used in routes, ie.: /admin/collections/:slug/edit
|
||||
label: "Post" # Used in the UI, ie.: "New Post"
|
||||
folder: "examples/docs/content/blog" # The path to the folder where the documents are stored
|
||||
filter: {field: "type", value: "blog"}
|
||||
create: true # Allow users to create new documents in this collection
|
||||
fields: # The fields each document in this collection have
|
||||
- { label: "Title", name: "title", widget: "string" }
|
||||
- { label: "Publish Date", name: "published", widget: "date" }
|
||||
- { label: "Intro Blurb", name: "description", widget: "text" }
|
||||
- { label: "Image", name: "image", widget: "image", required: true }
|
||||
- label: "Author"
|
||||
name: "author"
|
||||
widget: "select"
|
||||
options: ["Dillon Kearns"]
|
||||
default: "Dillon Kearns"
|
||||
- { label: "Body", name: "body", widget: "markdown" }
|
||||
- {
|
||||
label: "Type",
|
||||
name: "type",
|
||||
widget: "hidden",
|
||||
default: "blog",
|
||||
required: false,
|
||||
}
|
||||
- name: "docs" # Used in routes, ie.: /admin/collections/:slug/edit
|
||||
label: "Docs" # Used in the UI, ie.: "New Post"
|
||||
folder: "examples/docs/content/docs" # The path to the folder where the documents are stored
|
||||
filter: {field: "type", value: "doc"}
|
||||
create: true # Allow users to create new documents in this collection
|
||||
fields: # The fields each document in this collection have
|
||||
- { label: "Title", name: "title", widget: "string" }
|
||||
- { label: "Body", name: "body", widget: "markdown" }
|
||||
- {
|
||||
label: "Type",
|
||||
name: "type",
|
||||
widget: "hidden",
|
||||
default: "doc",
|
||||
required: false,
|
||||
}
|
13
examples/simple/static/admin/index.html
Normal file
13
examples/simple/static/admin/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Content Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://identity-js.netlify.com/v1/netlify-identity-widget.js"></script>
|
||||
<!-- Include the script that builds the page and powers Netlify CMS -->
|
||||
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
|
||||
</body>
|
||||
</html>
|
43
examples/simple/syntax.css
Normal file
43
examples/simple/syntax.css
Normal file
@ -0,0 +1,43 @@
|
||||
pre.elmsh {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
code.elmsh {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Roboto Mono' !important;
|
||||
font-size: 20px !important;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.elmsh-line:before {
|
||||
/* content: attr(data-elmsh-lc); */
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
width: 40px;
|
||||
padding: 0 20px 0 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.elmsh {
|
||||
color: #f8f8f2;
|
||||
background: #000;
|
||||
}
|
||||
.elmsh-hl {background: #343434;}
|
||||
.elmsh-add {background: #003800;}
|
||||
.elmsh-del {background: #380000;}
|
||||
.elmsh-comm {color: #75715e;}
|
||||
.elmsh1 {color: #ae81ff;}
|
||||
.elmsh2 {color: #e6db74;}
|
||||
.elmsh3 {color: #66d9ef;}
|
||||
.elmsh4 {color: #f92672;}
|
||||
.elmsh5 {color: #a6e22e;}
|
||||
.elmsh6 {color: #ae81ff;}
|
||||
.elmsh7 {color: #fd971f;}
|
||||
|
1691
examples/simple/vendor/elm-ui/Element.elm
vendored
Normal file
1691
examples/simple/vendor/elm-ui/Element.elm
vendored
Normal file
File diff suppressed because it is too large
Load Diff
226
examples/simple/vendor/elm-ui/Element/Background.elm
vendored
Normal file
226
examples/simple/vendor/elm-ui/Element/Background.elm
vendored
Normal 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"
|
281
examples/simple/vendor/elm-ui/Element/Border.elm
vendored
Normal file
281
examples/simple/vendor/elm-ui/Element/Border.elm
vendored
Normal 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
|
||||
-- }
|
265
examples/simple/vendor/elm-ui/Element/Events.elm
vendored
Normal file
265
examples/simple/vendor/elm-ui/Element/Events.elm
vendored
Normal 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
|
525
examples/simple/vendor/elm-ui/Element/Font.elm
vendored
Normal file
525
examples/simple/vendor/elm-ui/Element/Font.elm
vendored
Normal 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
|
2232
examples/simple/vendor/elm-ui/Element/Input.elm
vendored
Normal file
2232
examples/simple/vendor/elm-ui/Element/Input.elm
vendored
Normal file
File diff suppressed because it is too large
Load Diff
70
examples/simple/vendor/elm-ui/Element/Keyed.elm
vendored
Normal file
70
examples/simple/vendor/elm-ui/Element/Keyed.elm
vendored
Normal 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)
|
117
examples/simple/vendor/elm-ui/Element/Lazy.elm
vendored
Normal file
117
examples/simple/vendor/elm-ui/Element/Lazy.elm
vendored
Normal 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 "")
|
107
examples/simple/vendor/elm-ui/Element/Region.elm
vendored
Normal file
107
examples/simple/vendor/elm-ui/Element/Region.elm
vendored
Normal 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
|
325
examples/simple/vendor/elm-ui/Internal/Flag.elm
vendored
Normal file
325
examples/simple/vendor/elm-ui/Internal/Flag.elm
vendored
Normal 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
|
270
examples/simple/vendor/elm-ui/Internal/Grid.elm
vendored
Normal file
270
examples/simple/vendor/elm-ui/Internal/Grid.elm
vendored
Normal 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
|
3544
examples/simple/vendor/elm-ui/Internal/Model.elm
vendored
Normal file
3544
examples/simple/vendor/elm-ui/Internal/Model.elm
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1772
examples/simple/vendor/elm-ui/Internal/Style.elm
vendored
Normal file
1772
examples/simple/vendor/elm-ui/Internal/Style.elm
vendored
Normal file
File diff suppressed because it is too large
Load Diff
307
generator/src/cli.js
Normal file
307
generator/src/cli.js
Normal 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
75
generator/src/codegen.js
Normal 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 };
|
@ -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
48
generator/src/copy-dir.js
Normal 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 };
|
72
generator/src/dir-helpers.js
Normal file
72
generator/src/dir-helpers.js
Normal 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,
|
||||
};
|
21
generator/src/generate-manifest.js
Normal file
21
generator/src/generate-manifest.js
Normal 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;
|
165
generator/src/index-template.js
Normal file
165
generator/src/index-template.js
Normal 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,
|
||||
})
|
||||
);
|
||||
`;
|
60
generator/src/seo-renderer.js
Normal file
60
generator/src/seo-renderer.js
Normal 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>`;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "esnext",
|
||||
"noImplicitAny": true,
|
||||
"removeComments": true,
|
||||
"strictNullChecks": true,
|
||||
|
366
package-lock.json
generated
366
package-lock.json
generated
@ -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": {
|
||||
|
@ -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
75
plugins/Cloudinary.elm
Normal 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
0
src/EncodeHelpers.elm
Normal file
84
src/Head.elm
84
src/Head.elm
@ -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 =
|
||||
|
@ -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 )
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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 ->
|
||||
|
@ -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 :
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
{-| -}
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
|
@ -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
8
src/Path.elm
Normal 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
|
31
src/RequestsAndPending.elm
Normal file
31
src/RequestsAndPending.elm
Normal 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)
|
442
tests/BetaStaticHttpRequestsTests.elm
Normal file
442
tests/BetaStaticHttpRequestsTests.elm
Normal 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
|
||||
}
|
@ -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"
|
||||
|
@ -21,7 +21,7 @@ requestsDict requestMap =
|
||||
|> List.map
|
||||
(\( request, response ) ->
|
||||
( request |> Request.hash
|
||||
, response
|
||||
, Just response
|
||||
)
|
||||
)
|
||||
|> Dict.fromList
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user