├── res ├── scaffold │ ├── blog │ │ ├── src │ │ │ ├── assets │ │ │ │ ├── app.js │ │ │ │ ├── images │ │ │ │ │ ├── logo.png │ │ │ │ │ ├── header_01.jpg │ │ │ │ │ ├── header_02.jpg │ │ │ │ │ └── github.svg │ │ │ │ ├── markdown.css │ │ │ │ └── counter.css │ │ │ ├── pages │ │ │ │ ├── external.yaml │ │ │ │ ├── index.md │ │ │ │ ├── articles │ │ │ │ │ ├── 001.md │ │ │ │ │ ├── 003.md │ │ │ │ │ └── 002.md │ │ │ │ ├── about.md │ │ │ │ └── installation.md │ │ │ ├── Siteelm │ │ │ │ ├── Html │ │ │ │ │ └── Attributes.elm │ │ │ │ ├── Html.elm │ │ │ │ └── Page.elm │ │ │ ├── Dynamic │ │ │ │ └── Counter.elm │ │ │ └── Static │ │ │ │ ├── Page.elm │ │ │ │ ├── Article.elm │ │ │ │ ├── View.elm │ │ │ │ └── Home.elm │ │ ├── siteelm.yaml │ │ └── elm.json │ ├── .gitignore │ └── basic │ │ ├── src │ │ ├── pages │ │ │ ├── other.yaml │ │ │ ├── sub │ │ │ │ ├── a.md │ │ │ │ └── b.md │ │ │ ├── articles │ │ │ │ ├── 01.md │ │ │ │ └── 02.md │ │ │ └── index.md │ │ ├── Siteelm │ │ │ ├── Html │ │ │ │ └── Attributes.elm │ │ │ ├── Html.elm │ │ │ └── Page.elm │ │ ├── assets │ │ │ ├── images │ │ │ │ ├── github.svg │ │ │ │ ├── header.svg │ │ │ │ └── logo.svg │ │ │ └── style.css │ │ ├── Static │ │ │ ├── View.elm │ │ │ ├── Sub.elm │ │ │ └── Basic.elm │ │ └── Dynamic │ │ │ └── Counter.elm │ │ ├── siteelm.yaml │ │ └── elm.json └── img │ ├── siteelm.svg │ └── about.svg ├── main.js ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── Makefile ├── tsconfig.json ├── src ├── server.ts ├── generator │ ├── snippet.ts │ ├── elmtojs.ts │ └── jstohtml.ts ├── initializer.ts ├── watcher.ts ├── index.ts ├── config.ts └── generator.ts ├── README.md ├── package.json └── LICENSE /res/scaffold/blog/src/assets/app.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./lib/index.js') 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .DS_Store 4 | Thumbs.db 5 | lib 6 | -------------------------------------------------------------------------------- /res/scaffold/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /*/**/dist 3 | /*/**/elm-stuff 4 | !Lib 5 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikueater/siteelm/HEAD/res/scaffold/blog/src/assets/images/logo.png -------------------------------------------------------------------------------- /res/scaffold/basic/src/pages/other.yaml: -------------------------------------------------------------------------------- 1 | - name: Unagi 2 | price: 2000 3 | - name: Hamo 4 | price: 1000 5 | - name: Hamachi 6 | price: 500 7 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/pages/sub/a.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Sub 3 | title: A 4 | --- 5 | 6 | ### about 7 | 8 | this is a **sub** page _"A"_ 9 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/pages/sub/b.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Sub 3 | title: B 4 | --- 5 | 6 | ### about 7 | 8 | this is a **sub** page _"B"_ 9 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/assets/images/header_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikueater/siteelm/HEAD/res/scaffold/blog/src/assets/images/header_01.jpg -------------------------------------------------------------------------------- /res/scaffold/blog/src/assets/images/header_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikueater/siteelm/HEAD/res/scaffold/blog/src/assets/images/header_02.jpg -------------------------------------------------------------------------------- /res/scaffold/blog/src/pages/external.yaml: -------------------------------------------------------------------------------- 1 | - name: Unagi 2 | price: 2000 3 | - name: Hamo 4 | price: 1000 5 | - name: Hamachi 6 | price: 500 7 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/pages/articles/01.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Sub 3 | date: 2020-01-02 4 | title: Article(2020-01-02) 5 | --- 6 | 7 | ### Sample Article 8 | 9 | 2020/01/02 10 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/pages/articles/02.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Sub 3 | date: 2020-01-04 4 | title: Article(2020-01-04) 5 | --- 6 | 7 | ### Sample Article 8 | 9 | 2020/01/04 10 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/assets/markdown.css: -------------------------------------------------------------------------------- 1 | .md { 2 | margin: 0; 3 | padding: 1rem 0; 4 | } 5 | 6 | .md * { 7 | margin: 0; 8 | } 9 | 10 | .md h1 { 11 | padding: 0; 12 | font-size: 2rem; 13 | } 14 | 15 | .md p { 16 | padding: 0.5rem 0; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## overview 11 | 12 | ## goal 13 | 14 | - [ ] things satisfied after this 15 | 16 | ## idea 17 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Home 3 | title: siteelm introduction 4 | articles: 5 | preamblesIn: ./articles 6 | products: 7 | external: ./external.yaml 8 | --- 9 | 10 | # Welcome! 11 | 12 | Siteelm is a simple static site generator for Elm. 13 | And this is its blog template. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: library 3 | 4 | 5 | library: 6 | npm run build 7 | 8 | demo: 9 | npm install -g elm siteelm 10 | npm install 11 | npm run build 12 | cd res/scaffold/blog; siteelm make -o 13 | 14 | demo-basic: 15 | cd res/scaffold/basic; siteelm server 16 | 17 | demo-blog: 18 | cd res/scaffold/blog; siteelm server 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "lib", 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "declaration": false, 10 | "esModuleInterop": true, 11 | "sourceMap": false 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["test/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /res/scaffold/basic/siteelm.yaml: -------------------------------------------------------------------------------- 1 | build: 2 | dist_dir: ./dist 3 | contents: 4 | src_dir: ./src/pages 5 | index: ./src/pages/index.md 6 | exclude: 7 | - ./**/*.yaml 8 | assets: 9 | src_dir: ./src/assets 10 | static_elm: 11 | src_dir: ./src/Static 12 | exclude: 13 | - ./**/View.elm 14 | dynamic_elm: 15 | src_dir: ./src/Dynamic 16 | 17 | -------------------------------------------------------------------------------- /res/scaffold/blog/siteelm.yaml: -------------------------------------------------------------------------------- 1 | build: 2 | dist_dir: ./dist 3 | contents: 4 | src_dir: ./src/pages 5 | index: ./src/pages/index.md 6 | exclude: 7 | - ./**/*.yaml 8 | assets: 9 | src_dir: ./src/assets 10 | static_elm: 11 | src_dir: ./src/Static 12 | exclude: 13 | - ./**/View.elm 14 | dynamic_elm: 15 | src_dir: ./src/Dynamic 16 | 17 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/pages/articles/001.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Article 3 | date: 2020-05-01 4 | title: The concept of Siteelm 5 | --- 6 | 7 | Though I said "Siteelm is a simple static site generator," because I develop this for my work, there are some unique features. 8 | 9 | It's good to consider Siteelm is not for only blogs. In other words, in some respects this isn't optimized for blogs. 10 | 11 | The reason I made this was "I want to write my corporate site with Elm." 12 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/pages/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Page 3 | title: About this blog 4 | image: /images/header_02.jpg 5 | --- 6 | 7 | ## outline 8 | 9 | This is _the Siteelm blog template_. 10 | While the title contains the word _blog_, it's just a demonstration. 11 | 12 | ## rights 13 | 14 | You can change everything in this template. 15 | Of cource you can remove the footer. 16 | And you can also use assets like image resources if you want to. 17 | 18 | However the logo of Siteelm, you cannot modify it. 19 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/pages/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Page 3 | title: Installation 4 | image: /images/header_02.jpg 5 | --- 6 | 7 | ## initialize a project 8 | 9 | ```shell 10 | % npm init -y 11 | % npm install -D elm siteelm 12 | % npx siteelm init 13 | ``` 14 | 15 | Then you'll see a scaffold in the directory. 16 | 17 | ## develop 18 | 19 | ```shell 20 | % npx siteelm server -d 21 | ``` 22 | 23 | Then access "http://localhost:3000/" 24 | The server supports file watching and auto reloading. If you don't need the server, use _siteelm watch_ instead. 25 | 26 | 27 | ## build 28 | 29 | ```shell 30 | % npx siteelm make -o 31 | ``` 32 | -------------------------------------------------------------------------------- /res/scaffold/basic/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.4", 11 | "elm/html": "1.0.0", 12 | "elm/json": "1.1.3", 13 | "elm-explorations/markdown": "1.0.0" 14 | }, 15 | "indirect": { 16 | "elm/time": "1.0.0", 17 | "elm/url": "1.0.0", 18 | "elm/virtual-dom": "1.0.2" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": {}, 23 | "indirect": {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/Siteelm/Html/Attributes.elm: -------------------------------------------------------------------------------- 1 | module Siteelm.Html.Attributes exposing (charset, content, data, rel, role) 2 | 3 | import Html exposing (Attribute) 4 | import Html.Attributes exposing (attribute) 5 | 6 | 7 | content : String -> Attribute msg 8 | content = 9 | attribute "content" 10 | 11 | 12 | charset : String -> Attribute msg 13 | charset = 14 | attribute "charset" 15 | 16 | 17 | rel : String -> Attribute msg 18 | rel = 19 | attribute "rel" 20 | 21 | 22 | role : String -> Attribute msg 23 | role = 24 | attribute "role" 25 | 26 | 27 | data : String -> String -> Attribute msg 28 | data name value = 29 | attribute (String.join "-" [ "data", name ]) value 30 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/Siteelm/Html/Attributes.elm: -------------------------------------------------------------------------------- 1 | module Siteelm.Html.Attributes exposing (charset, content, data, rel, role) 2 | 3 | import Html exposing (Attribute) 4 | import Html.Attributes exposing (attribute) 5 | 6 | 7 | content : String -> Attribute msg 8 | content = 9 | attribute "content" 10 | 11 | 12 | charset : String -> Attribute msg 13 | charset = 14 | attribute "charset" 15 | 16 | 17 | rel : String -> Attribute msg 18 | rel = 19 | attribute "rel" 20 | 21 | 22 | role : String -> Attribute msg 23 | role = 24 | attribute "role" 25 | 26 | 27 | data : String -> String -> Attribute msg 28 | data name value = 29 | attribute (String.join "-" [ "data", name ]) value 30 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | github -------------------------------------------------------------------------------- /res/scaffold/blog/src/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | github 2 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/Static/View.elm: -------------------------------------------------------------------------------- 1 | module Static.View exposing (header) 2 | 3 | import Html exposing (Html, a, div, img) 4 | import Html.Attributes exposing (alt, class, height, href, src, width) 5 | import Siteelm.Html.Attributes exposing (role) 6 | 7 | 8 | header : Html Never 9 | header = 10 | div [ class "header" ] 11 | [ div [ class "stripe" ] 12 | [ a [ href "https://github.com/nikueater/siteelm" ] 13 | [ img [ src "/images/github.svg", width 48, height 48, alt "git hub" ] [] 14 | ] 15 | ] 16 | , a [ href "/", role "banner" ] 17 | [ img [ class "logo", src "/images/logo.svg", width 320, height 160, alt "siteelm" ] [] 18 | ] 19 | ] 20 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Basic 3 | title: HOME 4 | products: 5 | - name: Apple 6 | price: 100 7 | - name: Banana 8 | price: 150 9 | recommends: 10 | external: ./other.yaml 11 | articles: 12 | preamblesIn: ./articles 13 | --- 14 | ### Markdown 15 | - [sub page (A)](/sub/a) 16 | - [sub page (B)](/sub/b) 17 | 18 | 19 | ```elm 20 | type alias Preamble = 21 | { "title": "String" 22 | , "name": "String" 23 | } 24 | 25 | decoder : Decoder Preamble 26 | decoder = 27 | D.map2 Preamble 28 | (field "title" string) 29 | (field "name" string) 30 | 31 | main : Page Preamble 32 | main = 33 | page 34 | { decoder = decoder 35 | , head = head 36 | , body = body 37 | } 38 | . 39 | . 40 | . 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/pages/articles/003.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Article 3 | date: 2020-05-03 4 | title: Loading external YAML files 5 | --- 6 | 7 | 8 | The "external" property enables you to load YAML files in the preamble section. 9 | It's useful when you want to share specific data among articles, for example, a list of pinned articles. 10 | 11 | 12 | ### page/recommendations.yaml 13 | 14 | ```yaml 15 | - name: eel 16 | url: /sushi/eel 17 | price: 150 18 | - name: salmon 19 | url: /sushi/salmon 20 | price: 100 21 | - name: mackerel 22 | url: /sushi/mackerel 23 | price: 120 24 | ``` 25 | 26 | ### page/20200501.md 27 | 28 | ```markdown 29 | --- 30 | module: Static.Dialy 31 | specialties: 32 | external: ./recommendations.yaml 33 | 34 | --- 35 | 36 | Today we bought ingredients below... 37 | 38 | ``` 39 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/assets/counter.css: -------------------------------------------------------------------------------- 1 | .counter * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .counter { 6 | width: 320px; 7 | height: 96px; 8 | display: flex; 9 | align-items: stretch; 10 | } 11 | 12 | .counter > * { 13 | flex: 1; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | font-weight: bold; 18 | font-size: 20px; 19 | color: #ffffff; 20 | } 21 | 22 | .counter > div { 23 | background-color: #58637a; 24 | font-family: Questrial, sans-serif; 25 | } 26 | 27 | .counter > button { 28 | border: none; 29 | background-color: #3db7Cf; 30 | font-family: Questrial, sans-serif; 31 | padding: 0; 32 | } 33 | 34 | .counter > *:first-child { 35 | border-radius: 80px 0 0 80px; 36 | } 37 | 38 | .counter > *:last-child { 39 | border-radius: 0 80px 80px 0; 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import expressWs from 'express-ws' 3 | import {Config} from './config' 4 | import watchAll from './watcher' 5 | import generateAll from './generator' 6 | 7 | const server = (config: Config) => { 8 | generateAll(config, {isServer: true}) 9 | // 10 | const ews = expressWs(express()) 11 | const app = ews.app 12 | app.set('port', 3000) 13 | app.use(express.static(config.build.dist_dir)) 14 | app.ws('/', () => { 15 | console.log('auto reloader connected') 16 | }) 17 | // start a server 18 | app.listen(app.get('port'), () => { 19 | console.log(`running server localhost:${app.get('port')}`) 20 | 21 | // start watching 22 | const wss = ews.getWss() 23 | watchAll(config, () => { 24 | wss.clients.forEach((c)=>{ 25 | c.send(JSON.stringify({reload: true})) 26 | }) 27 | }) 28 | }) 29 | } 30 | 31 | export default server; 32 | -------------------------------------------------------------------------------- /res/scaffold/blog/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.0", 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.5", 12 | "elm/html": "1.0.0", 13 | "elm/json": "1.1.3", 14 | "elm/time": "1.0.0", 15 | "elm-explorations/markdown": "1.0.0", 16 | "mdgriffith/elm-ui": "1.1.5", 17 | "rtfeldman/elm-iso8601-date-strings": "1.1.3" 18 | }, 19 | "indirect": { 20 | "avh4/elm-color": "1.0.0", 21 | "elm/parser": "1.1.0", 22 | "elm/url": "1.0.0", 23 | "elm/virtual-dom": "1.0.2", 24 | "ianmackenzie/elm-units": "2.5.0" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": {}, 29 | "indirect": {} 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/assets/images/header.svg: -------------------------------------------------------------------------------- 1 | header -------------------------------------------------------------------------------- /src/generator/snippet.ts: -------------------------------------------------------------------------------- 1 | 2 | export const staticElmInitCode = (moduleName: string, flags: object): string => { 3 | return `var app = Elm.${moduleName}.init({flags:${JSON.stringify(flags)}}); 4 | ` 5 | } 6 | 7 | export const dynamicElmInitCode = (moduleName: string, flags: string, key: string): string => { 8 | const objName = moduleName.replace('.', '') 9 | return ` 10 | window.app = window.app || {} 11 | var ns = document.querySelectorAll('div[data-elm-module="${moduleName}"][data-unique-key="${key}"]') 12 | for(var i=0;i 0 ? ('_' + i) : '') 14 | window.app[name] = Elm.${moduleName}.init({node:ns[i],flags:${flags}}) 15 | } 16 | ` 17 | } 18 | 19 | export const autoReloaderCode = ():string => { 20 | return `(function(){ 21 | var host = (window.location.origin).replace(/^http/, 'ws'); 22 | var ws = new WebSocket(host); 23 | ws.onmessage = function(e) { 24 | if(JSON.parse(e.data).reload) { 25 | window.location.reload() 26 | } 27 | } 28 | }())` 29 | } 30 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/Dynamic/Counter.elm: -------------------------------------------------------------------------------- 1 | module Dynamic.Counter exposing (main) 2 | 3 | import Browser 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | type alias Model = 10 | { value : Int 11 | } 12 | 13 | 14 | type Msg 15 | = Inc 16 | | Dec 17 | 18 | 19 | main : Program Model Model Msg 20 | main = 21 | Browser.element 22 | { init = \flags -> ( flags, Cmd.none ) 23 | , update = update 24 | , view = view 25 | , subscriptions = always Sub.none 26 | } 27 | 28 | 29 | update : Msg -> Model -> ( Model, Cmd Msg ) 30 | update msg model = 31 | case msg of 32 | Inc -> 33 | ( { model | value = model.value + 1 }, Cmd.none ) 34 | 35 | Dec -> 36 | ( { model | value = model.value - 1 }, Cmd.none ) 37 | 38 | 39 | view : Model -> Html Msg 40 | view model = 41 | div [ class "counter" ] 42 | [ button [ onClick Dec ] [ text "-1" ] 43 | , div [ class "value" ] [ text <| String.fromInt model.value ] 44 | , button [ onClick Inc ] [ text "+1" ] 45 | ] 46 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/Dynamic/Counter.elm: -------------------------------------------------------------------------------- 1 | module Dynamic.Counter exposing (main) 2 | 3 | import Browser 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | type alias Model = 10 | { value : Int 11 | } 12 | 13 | 14 | type Msg 15 | = Inc 16 | | Dec 17 | 18 | 19 | main : Program Model Model Msg 20 | main = 21 | Browser.element 22 | { init = \flags -> ( flags, Cmd.none ) 23 | , update = update 24 | , view = view 25 | , subscriptions = always Sub.none 26 | } 27 | 28 | 29 | update : Msg -> Model -> ( Model, Cmd Msg ) 30 | update msg model = 31 | case msg of 32 | Inc -> 33 | ( { model | value = model.value + 1 }, Cmd.none ) 34 | 35 | Dec -> 36 | ( { model | value = model.value - 1 }, Cmd.none ) 37 | 38 | 39 | view : Model -> Html Msg 40 | view model = 41 | div [ class "counter" ] 42 | [ button [ onClick Dec ] [ text "-1" ] 43 | , div [ class "value" ] [ text <| String.fromInt model.value ] 44 | , button [ onClick Inc ] [ text "+1" ] 45 | ] 46 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/pages/articles/002.md: -------------------------------------------------------------------------------- 1 | --- 2 | module: Static.Article 3 | date: 2020-05-02 4 | title: Dynamic Element 5 | --- 6 | 7 | _"Siteelm.Html.dynamic"_ is one of the ways to embed dynamic components in the static layout. 8 | For example, when you want to put something interactive, you can use it. 9 | 10 | (Currently you cannot use elm-ui for dynamic elements when you use it in static layouts because class names in both elements are duplicated.) 11 | 12 | ### Dynamic/Counter.elm 13 | 14 | ```elm 15 | module Dynamic.Counter exposing (main) 16 | 17 | type alias Model = 18 | { value : Int 19 | } 20 | 21 | main : Program Model Model Msg 22 | main = 23 | Browser.element 24 | { init = \flags -> ( flags, Cmd.none ) 25 | , update = update 26 | , view = view 27 | , subscriptions = \_ -> Sub.none 28 | } 29 | ... 30 | ``` 31 | 32 | ### siteelm.yaml 33 | 34 | ```yaml 35 | build: 36 | dynamic_elm: 37 | src_dir: ./src/Dynamic 38 | ``` 39 | 40 | ### Static/Layout.elm 41 | 42 | ```elm 43 | import Siteelm.Html as Html 44 | 45 | counter : Html Never 46 | counter = 47 | Html.dynamic 48 | { moduleName = "Dynamic.Counter" 49 | , flags = "{value: 100}" 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/Siteelm/Html.elm: -------------------------------------------------------------------------------- 1 | module Siteelm.Html exposing (body, dynamic, head, html, link, meta, script, title) 2 | 3 | import Html exposing (Attribute, Html, node, text) 4 | import Html.Attributes exposing (attribute) 5 | import Siteelm.Html.Attributes exposing (data) 6 | 7 | 8 | meta : List (Attribute msg) -> Html msg 9 | meta attrs = 10 | node "meta" attrs [] 11 | 12 | 13 | link : List (Attribute msg) -> Html msg 14 | link attrs = 15 | node "link" attrs [] 16 | 17 | 18 | script : String -> String -> Html msg 19 | script src code = 20 | node "siteelm-custom" [ data "tag" "script", attribute "src" src ] [ text code ] 21 | 22 | 23 | title : List (Attribute msg) -> String -> Html msg 24 | title attrs s = 25 | node "title" attrs [ Html.text s ] 26 | 27 | 28 | head : List (Attribute msg) -> List (Html msg) -> Html msg 29 | head attrs es = 30 | node "head" attrs es 31 | 32 | 33 | html : List (Attribute msg) -> List (Html msg) -> Html msg 34 | html attrs es = 35 | node "html" attrs es 36 | 37 | 38 | body : List (Attribute msg) -> List (Html msg) -> Html msg 39 | body attrs es = 40 | node "body" attrs es 41 | 42 | 43 | dynamic : { moduleName : String, flags : String } -> Html msg 44 | dynamic { moduleName, flags } = 45 | Html.div 46 | [ data "elm-module" moduleName 47 | , data "flags" flags 48 | ] 49 | [] 50 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/Siteelm/Html.elm: -------------------------------------------------------------------------------- 1 | module Siteelm.Html exposing (body, dynamic, head, html, link, meta, script, title) 2 | 3 | import Html exposing (Attribute, Html, node, text) 4 | import Html.Attributes exposing (attribute) 5 | import Siteelm.Html.Attributes exposing (data) 6 | 7 | 8 | meta : List (Attribute msg) -> Html msg 9 | meta attrs = 10 | node "meta" attrs [] 11 | 12 | 13 | link : List (Attribute msg) -> Html msg 14 | link attrs = 15 | node "link" attrs [] 16 | 17 | 18 | script : String -> String -> Html msg 19 | script src code = 20 | node "siteelm-custom" [ data "tag" "script", attribute "src" src ] [ text code ] 21 | 22 | 23 | title : List (Attribute msg) -> String -> Html msg 24 | title attrs s = 25 | node "title" attrs [ Html.text s ] 26 | 27 | 28 | head : List (Attribute msg) -> List (Html msg) -> Html msg 29 | head attrs es = 30 | node "head" attrs es 31 | 32 | 33 | html : List (Attribute msg) -> List (Html msg) -> Html msg 34 | html attrs es = 35 | node "html" attrs es 36 | 37 | 38 | body : List (Attribute msg) -> List (Html msg) -> Html msg 39 | body attrs es = 40 | node "body" attrs es 41 | 42 | 43 | dynamic : { moduleName : String, flags : String } -> Html msg 44 | dynamic { moduleName, flags } = 45 | Html.div 46 | [ data "elm-module" moduleName 47 | , data "flags" flags 48 | ] 49 | [] 50 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/Static/Page.elm: -------------------------------------------------------------------------------- 1 | module Static.Page exposing (main) 2 | 3 | import Element exposing (..) 4 | import Html exposing (Html) 5 | import Html.Attributes exposing (class) 6 | import Json.Decode as Decode exposing (Decoder, string) 7 | import Json.Decode.Pipeline exposing (required) 8 | import Markdown 9 | import Siteelm.Page exposing (Page, page) 10 | import Static.View exposing (head, headerImage, pageLayout) 11 | 12 | 13 | main : Page Preamble 14 | main = 15 | page 16 | { decoder = preambleDecoder 17 | , head = head 18 | , body = body 19 | } 20 | 21 | 22 | type alias Preamble = 23 | { title : String 24 | , image : String 25 | } 26 | 27 | 28 | preambleDecoder : Decoder Preamble 29 | preambleDecoder = 30 | Decode.succeed Preamble 31 | |> required "title" string 32 | |> required "image" string 33 | 34 | 35 | body : Preamble -> String -> List (Html Never) 36 | body preamble markdown = 37 | pageLayout <| 38 | column 39 | [ width fill 40 | , spacing 8 41 | , clipX 42 | ] 43 | [ headerImage 44 | { src = preamble.image 45 | , title = preamble.title 46 | } 47 | , paragraph 48 | [ width fill 49 | , paddingXY 0 20 50 | ] 51 | [ html (Markdown.toHtml [ class "md" ] markdown) ] 52 | ] 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | siteelm 2 | 3 | 4 | demo: https://siteelm.netlify.com/ ([source](/res/scaffold/basic)) 5 | 6 | ## about 7 | It's just another static site generator for Elm, but has some features. 8 | 9 | - you can write YAML in preamble sections 10 | - in a preamble, you can load external YAML files 11 | - it's easy to mix dynamic Elm components 12 | - simple rules to use 13 | 14 | ## concept 15 | to make a template, what you need to do is writing preamble models, their decoder, and two functions return Html Never (for HEAD and BODY) 16 | 17 | siteelm 18 | 19 | 20 | ## usage 21 | ### install 22 | ```sh 23 | % npm install -g elm 24 | % npm install -g siteelm 25 | ``` 26 | or if you'd like to do everything locally, 27 | ```sh 28 | % npm install -D elm siteelm 29 | ``` 30 | in this case, type _"% npx siteelm 〜"_ instead of _"% siteelm 〜"_ 31 | 32 | ### initialize project 33 | ```sh 34 | % mkdir mysite 35 | % cd mysite 36 | % siteelm init 37 | ``` 38 | Then you'll see a scaffold in the directory. 39 | 40 | 41 | ### developing 42 | ```sh 43 | % cd mysite 44 | % siteelm server -d 45 | ``` 46 | Then access "http://localhost:3000/" 47 | The server supports file watching and auto reloading. 48 | If you don't need the server, use `siteelm watch` instead. 49 | 50 | ### building 51 | ```sh 52 | % cd mysite 53 | % siteelm make -o 54 | ``` 55 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/Static/Sub.elm: -------------------------------------------------------------------------------- 1 | module Static.Sub exposing (main) 2 | 3 | import Html exposing (Html, a, div, h2, nav, text) 4 | import Html.Attributes exposing (class, href) 5 | import Json.Decode as D exposing (Decoder) 6 | import Markdown 7 | import Siteelm.Html as Html 8 | import Siteelm.Html.Attributes exposing (charset, rel) 9 | import Siteelm.Page exposing (Page, page) 10 | import Static.View as View 11 | 12 | 13 | main : Page Preamble 14 | main = 15 | page 16 | { decoder = preambleDecoder 17 | , head = viewHead 18 | , body = viewBody 19 | } 20 | 21 | 22 | type alias Preamble = 23 | { title : String 24 | } 25 | 26 | 27 | preambleDecoder : Decoder Preamble 28 | preambleDecoder = 29 | D.map Preamble 30 | (D.field "title" D.string) 31 | 32 | 33 | viewHead : Preamble -> String -> List (Html Never) 34 | viewHead preamble _ = 35 | [ Html.meta [ charset "utf-8" ] 36 | , Html.title [] (preamble.title ++ " | sample site") 37 | , Html.link [ rel "stylesheet", href "/style.css" ] 38 | , Html.link [ rel "stylesheet", href "https://fonts.googleapis.com/css?family=Questrial&display=swap" ] 39 | ] 40 | 41 | 42 | viewBody : Preamble -> String -> List (Html Never) 43 | viewBody preamble body = 44 | [ View.header 45 | , div [ class "main" ] 46 | [ nav [] 47 | [ a [ href "/", class "prev" ] [ text "home" ] 48 | ] 49 | , h2 [] [ text preamble.title ] 50 | , div [ class "inner" ] 51 | [ Markdown.toHtml [] body 52 | ] 53 | ] 54 | ] 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siteelm", 3 | "version": "0.3.2", 4 | "description": "a static site generator for Elm", 5 | "bin": { 6 | "siteelm": "main.js" 7 | }, 8 | "engines": { 9 | "node": ">=12.2" 10 | }, 11 | "files": [ 12 | "README.md", 13 | "LICENSE", 14 | "main.js", 15 | "package.json", 16 | "lib", 17 | "res" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/nikueater/siteelm.git" 22 | }, 23 | "homepage": "https://github.com/nikueater/siteelm", 24 | "scripts": { 25 | "build": "npx tsc", 26 | "test": "echo \"Error: no test specified\" && exit 1" 27 | }, 28 | "keywords": [ 29 | "elm", 30 | "static site generator" 31 | ], 32 | "author": "nikueater", 33 | "license": "Apache-2.0", 34 | "dependencies": { 35 | "commander": "^3.0.2", 36 | "express": "^4.17.1", 37 | "express-ws": "^4.0.0", 38 | "fs-extra": "^8.1.0", 39 | "glob": "^7.1.6", 40 | "html-minifier": "^4.0.0", 41 | "js-yaml": "^3.13.1", 42 | "jsdom": "^16.2.2", 43 | "parsimmon": "^1.13.0", 44 | "prompts": "^2.3.2", 45 | "tmp": "^0.1.0", 46 | "traverse": "^0.6.6", 47 | "watch": "^1.0.2" 48 | }, 49 | "devDependencies": { 50 | "@types/express": "^4.17.6", 51 | "@types/express-ws": "^3.0.0", 52 | "@types/fs-extra": "^8.1.0", 53 | "@types/glob": "^7.1.1", 54 | "@types/html-minifier": "^3.5.3", 55 | "@types/js-yaml": "^3.12.3", 56 | "@types/jsdom": "^12.2.4", 57 | "@types/node": "^12.12.37", 58 | "@types/parsimmon": "^1.10.1", 59 | "@types/prompts": "^2.0.7", 60 | "@types/tmp": "^0.1.0", 61 | "@types/traverse": "^0.6.32", 62 | "@types/watch": "^1.0.1", 63 | "typescript": "^3.8.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/initializer.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import glob from 'glob' 4 | import prompts from 'prompts' 5 | 6 | const scaffoldDir = path.join(__dirname, '../res/scaffold') 7 | let scaffoldName = 'basic' 8 | 9 | /** 10 | * just copy a scaffold 11 | */ 12 | const initialize = async () => { 13 | const whitelist = [ 14 | './package.json', 15 | './package-lock.json', 16 | './node_modules/**/*', 17 | ] 18 | const files = glob.sync('./**/*',{ignore: whitelist, dot: false, nodir: true}) 19 | console.log(files) 20 | 21 | if(files.length) { 22 | console.error('this directory is not empty!') 23 | console.error('(if you need, you can put only "package.json", "package-lock.json", and "node_modules")') 24 | return 25 | } 26 | 27 | console.log(`choose the template`) 28 | console.log(`[0] basic (default)`) 29 | console.log(`[1] blog`) 30 | let template = -1 31 | while(![0, 1].includes(template)) { 32 | const res = await prompts({ 33 | 'type': 'number', 34 | 'name': 'template', 35 | 'message': 'template:' 36 | }) 37 | template = res.template ?? 0 38 | } 39 | switch(template) { 40 | case 0: 41 | scaffoldName = 'basic' 42 | break 43 | case 1: 44 | scaffoldName = 'blog' 45 | break 46 | } 47 | 48 | console.log(`copying the scaffold "${scaffoldName}..."`) 49 | const scaffold = path.join(scaffoldDir, scaffoldName) 50 | fs.copySync(scaffold, '.') 51 | console.log('done!') 52 | console.log('------------') 53 | console.log(' siteelm server: run a server for developing') 54 | console.log(' siteelm make: generate a site') 55 | console.log('------------') 56 | } 57 | 58 | export default initialize 59 | -------------------------------------------------------------------------------- /src/watcher.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import watch, {FileOrFiles} from 'watch' 3 | import {Config} from './config' 4 | import fs, {Stats} from 'fs' 5 | import generateAll, {convertOnlyContentFiles, copyAssets} from './generator' 6 | 7 | const watchAll = (config: Config, onChange: () => void) => { 8 | var elmdirs : string[] = [] 9 | if(fs.existsSync('./elm.json')) { 10 | const json = JSON.parse(fs.readFileSync('./elm.json', 'utf-8')) as any 11 | elmdirs = (json['source-directories'] || []) as (string[]) 12 | } 13 | // watch directories(generate all) 14 | const dirs = [ 15 | elmdirs ? '' : (config.build.static_elm.src_dir || ''), 16 | elmdirs ? '' : (config.build.dynamic_elm.src_dir || ''), 17 | ].concat(elmdirs).filter(x => x !== '') 18 | 19 | 20 | 21 | // start watching 22 | dirs.forEach(x => { 23 | console.log(`WATCH: ${x}`) 24 | watch.watchTree(x, {interval: 1.5} , (files: FileOrFiles) => { 25 | if(typeof files === "string" && path.extname(files) === '.elm') { 26 | generateAll(config, {isServer: true}) 27 | if(onChange) { 28 | onChange() 29 | } 30 | } 31 | }) 32 | }) 33 | 34 | // watch assets 35 | watch.watchTree(config.build.assets.src_dir, () => { 36 | console.log("ASSETS CHANGED") 37 | copyAssets(config) 38 | }) 39 | 40 | // watch the content dir 41 | var ignoreOnce = true 42 | watch.watchTree(config.build.contents.src_dir, {interval: 1.5}, () =>{ 43 | if(!ignoreOnce) { 44 | convertOnlyContentFiles(config) 45 | if(onChange) { 46 | onChange() 47 | } 48 | } else { 49 | ignoreOnce = false 50 | } 51 | }) 52 | } 53 | 54 | export default watchAll; 55 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/Siteelm/Page.elm: -------------------------------------------------------------------------------- 1 | module Siteelm.Page exposing (Page, page) 2 | 3 | import Browser 4 | import Html exposing (Html) 5 | import Json.Decode exposing (Decoder, decodeString) 6 | import Siteelm.Html as Html 7 | 8 | 9 | {-| Generate a Program for static page. You need to give a decoder for your 10 | preamble model and a view function which takes the preamble and 11 | plain text body (e.g. markdown text). 12 | -} 13 | page : 14 | { decoder : Decoder a 15 | , head : a -> String -> List (Html Never) 16 | , body : a -> String -> List (Html Never) 17 | } 18 | -> Page a 19 | page { decoder, head, body } = 20 | Browser.document 21 | { init = \f -> ( decode decoder f, Cmd.none ) 22 | , update = \_ m -> ( m, Cmd.none ) 23 | , view = \m -> { title = "", body = [ renderPage head body m ] } 24 | , subscriptions = always Sub.none 25 | } 26 | 27 | 28 | type alias Page a = 29 | Program Flags (Model a) Never 30 | 31 | 32 | type alias Model a = 33 | { preamble : Maybe a 34 | , body : String 35 | } 36 | 37 | 38 | type alias Flags = 39 | { preamble : String 40 | , body : String 41 | } 42 | 43 | 44 | decode : Decoder a -> Flags -> Model a 45 | decode decoder flags = 46 | let 47 | preamble = 48 | flags.preamble 49 | |> decodeString decoder 50 | |> Result.toMaybe 51 | in 52 | { preamble = preamble 53 | , body = flags.body 54 | } 55 | 56 | 57 | renderPage : (a -> String -> List (Html Never)) -> (a -> String -> List (Html Never)) -> Model a -> Html Never 58 | renderPage head body model = 59 | case model.preamble of 60 | Just p -> 61 | Html.html [] 62 | [ Html.head [] <| head p model.body 63 | , Html.body [] <| body p model.body 64 | ] 65 | 66 | Nothing -> 67 | Html.text "" 68 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/Siteelm/Page.elm: -------------------------------------------------------------------------------- 1 | module Siteelm.Page exposing (Page, page) 2 | 3 | import Browser 4 | import Html exposing (Html) 5 | import Json.Decode exposing (Decoder, decodeString) 6 | import Siteelm.Html as Html 7 | 8 | 9 | {-| Generate a Program for static page. You need to give a decoder for your 10 | preamble model and a view function which takes the preamble and 11 | plain text body (e.g. markdown text). 12 | -} 13 | page : 14 | { decoder : Decoder a 15 | , head : a -> String -> List (Html Never) 16 | , body : a -> String -> List (Html Never) 17 | } 18 | -> Page a 19 | page { decoder, head, body } = 20 | Browser.document 21 | { init = \f -> ( decode decoder f, Cmd.none ) 22 | , update = \_ m -> ( m, Cmd.none ) 23 | , view = \m -> { title = "", body = [ renderPage head body m ] } 24 | , subscriptions = always Sub.none 25 | } 26 | 27 | 28 | type alias Page a = 29 | Program Flags (Model a) Never 30 | 31 | 32 | type alias Model a = 33 | { preamble : Maybe a 34 | , body : String 35 | } 36 | 37 | 38 | type alias Flags = 39 | { preamble : String 40 | , body : String 41 | } 42 | 43 | 44 | decode : Decoder a -> Flags -> Model a 45 | decode decoder flags = 46 | let 47 | preamble = 48 | flags.preamble 49 | |> decodeString decoder 50 | |> Result.toMaybe 51 | in 52 | { preamble = preamble 53 | , body = flags.body 54 | } 55 | 56 | 57 | renderPage : (a -> String -> List (Html Never)) -> (a -> String -> List (Html Never)) -> Model a -> Html Never 58 | renderPage head body model = 59 | case model.preamble of 60 | Just p -> 61 | Html.html [] 62 | [ Html.head [] <| head p model.body 63 | , Html.body [] <| body p model.body 64 | ] 65 | 66 | Nothing -> 67 | Html.text "" 68 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import program from 'commander' 2 | import readConfigFrom from './config' 3 | import server from './server' 4 | import generateAll, {copyAssets} from './generator' 5 | import initialize from './initializer' 6 | import watchAll from './watcher' 7 | 8 | const version = '0.3.2' 9 | 10 | program 11 | .version(version, '-v, --version') 12 | .option('-o, --optimize', 'use optimization') 13 | .option('-d, --draft', 'not to ignore drafts') 14 | 15 | 16 | // "siteelm make" 17 | program 18 | .command('make') 19 | .action(async () => { 20 | const option = { 21 | optimize: program.optimize, 22 | withDraft: program.draft 23 | } 24 | const config = readConfigFrom('./siteelm.yaml', option) 25 | const result = await generateAll(config) 26 | if(!result) { 27 | process.exitCode = 1 28 | } 29 | copyAssets(config) 30 | }) 31 | 32 | // "siteelm server" 33 | program 34 | .command('server') 35 | .action(() => { 36 | const option = { 37 | optimize: program.optimize, 38 | withDraft: program.draft 39 | } 40 | const config = readConfigFrom('./siteelm.yaml', option) 41 | server(config) 42 | }) 43 | 44 | // "siteelm watch" 45 | program 46 | .command('watch') 47 | .action(async () => { 48 | const option = { 49 | optimize: program.optimize, 50 | withDraft: program.draft 51 | } 52 | const config = readConfigFrom('./siteelm.yaml', option) 53 | const result = await generateAll(config) 54 | if(!result) { 55 | process.exitCode = 1 56 | } 57 | watchAll(config, () => {}) 58 | }) 59 | 60 | // "siteelm init" 61 | program 62 | .command('init') 63 | .action(async () => { 64 | await initialize() 65 | }) 66 | 67 | program.parse(process.argv) 68 | 69 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/Static/Article.elm: -------------------------------------------------------------------------------- 1 | module Static.Article exposing (main) 2 | 3 | import Element exposing (..) 4 | import Element.Font as Font 5 | import Html exposing (Html) 6 | import Html.Attributes exposing (class) 7 | import Iso8601 8 | import Json.Decode as Decode exposing (Decoder, string) 9 | import Json.Decode.Pipeline exposing (required) 10 | import Markdown 11 | import Siteelm.Page exposing (Page, page) 12 | import Static.View exposing (head, pageLayout) 13 | import Time exposing (Posix) 14 | 15 | 16 | main : Page Preamble 17 | main = 18 | page 19 | { decoder = preambleDecoder 20 | , head = head 21 | , body = body 22 | } 23 | 24 | 25 | type alias Preamble = 26 | { title : String 27 | , date : Posix 28 | } 29 | 30 | 31 | preambleDecoder : Decoder Preamble 32 | preambleDecoder = 33 | Decode.succeed Preamble 34 | |> required "title" string 35 | |> required "date" Iso8601.decoder 36 | 37 | 38 | body : Preamble -> String -> List (Html Never) 39 | body preamble markdown = 40 | let 41 | date = 42 | preamble.date 43 | |> Iso8601.fromTime 44 | |> String.split "T" 45 | |> List.head 46 | |> Maybe.withDefault "" 47 | in 48 | pageLayout <| 49 | column 50 | [ width (maximum 720 fill) 51 | , centerX 52 | , paddingXY 20 0 53 | , spacing 8 54 | ] 55 | [ paragraph 56 | [ Font.size 36 57 | , Font.bold 58 | , centerX 59 | , clipX 60 | , Font.center 61 | ] 62 | [ text preamble.title 63 | ] 64 | , el 65 | [ Font.size 14 66 | , Font.bold 67 | , centerX 68 | ] 69 | (text date) 70 | , paragraph 71 | [ width fill 72 | , paddingXY 0 20 73 | ] 74 | [ html (Markdown.toHtml [ class "md" ] markdown) ] 75 | ] 76 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import yaml from 'js-yaml' 3 | 4 | export interface Config { 5 | build: { 6 | dist_dir: string 7 | contents: { 8 | src_dir: string 9 | index: string 10 | exclude?: string[] 11 | draft?: boolean 12 | } 13 | assets: { 14 | src_dir: string 15 | } 16 | elm: { 17 | command?: string 18 | optimize?: boolean 19 | } 20 | static_elm: { 21 | src_dir: string 22 | exclude?: string[] 23 | } 24 | dynamic_elm: { 25 | src_dir: string 26 | exclude?: string[] 27 | } 28 | } 29 | } 30 | 31 | 32 | /** 33 | * @param file config file 34 | * @param option additional options 35 | * @returns object 36 | */ 37 | const readConfigFrom = (file: string, option?: {optimize: boolean, withDraft: boolean}): Config => { 38 | const yml = fs.readFileSync(file, 'utf-8') 39 | var conf = yaml.safeLoad(yml) 40 | const opt = option || {optimize: false, withDraft: false} 41 | if(!conf.build.elm) { 42 | conf.build.elm = {} 43 | } 44 | if(fs.existsSync('./package.json') && !conf.build.elm.command) { 45 | conf.build.elm.command = 'npx elm' 46 | } 47 | if(typeof conf.build.elm.optimize !== 'boolean') { 48 | conf.build.elm.optimize = opt.optimize 49 | } 50 | if(typeof conf.build.dist_dir !== 'string') { 51 | conf.build.dist_dir = './dist' 52 | } 53 | if(typeof conf.build.assets.src_dir !== 'string') { 54 | conf.build.assets.src_dir = './assets' 55 | } 56 | if(typeof conf.build.contents.draft !== 'boolean') { 57 | conf.build.contents.draft = opt.withDraft 58 | } 59 | if(!conf.build.static_elm.src_dir || !conf.build.dynamic_elm.src_dir) { 60 | const elm_json = JSON.parse(fs.readFileSync('./elm.json', 'utf-8')) 61 | if(!conf.build.static_elm.src_dir) { 62 | conf.build.static_elm.src_dir = elm_json['source-directories'] 63 | } 64 | if(!conf.build.dynamic_elm.src_dir) { 65 | conf.build.dynamic_elm.src_dir = elm_json['source-directories'] 66 | } 67 | } 68 | return conf 69 | } 70 | 71 | export default readConfigFrom; 72 | -------------------------------------------------------------------------------- /res/img/siteelm.svg: -------------------------------------------------------------------------------- 1 | siteelm -------------------------------------------------------------------------------- /res/img/about.svg: -------------------------------------------------------------------------------- 1 | aboutmodule: Articletitle: productsitems: - name: banana price: 100## products of the week!here are some products we highly reccomend!PreambleBody{ title: String, items: List Fruit} StringHEADBODYHTMLmain : Page Preamblemain = page { decoder = decoder , head = head , body = body }decoder : Decoder Preamblehead : Preamble -> String -> List (Html Never)body : Preamble -> String -> List (Html Never) -------------------------------------------------------------------------------- /res/scaffold/basic/src/assets/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html{ 6 | width: 100%; 7 | background-color: #ffffff; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | body { 13 | font-family: Questrial, sans-serif; 14 | font-size: 20px; 15 | color: #242424; 16 | margin: 0; 17 | padding: 0; 18 | text-decoration-skip-ink: none; 19 | } 20 | 21 | .main { 22 | max-width: 720px; 23 | margin: auto; 24 | padding-bottom: 96px; 25 | } 26 | 27 | h2 { 28 | background-color: #58637a; 29 | color: #ffffff; 30 | margin: 0; 31 | height: 3rem; 32 | line-height: 3rem; 33 | padding: 0 1rem; 34 | border-radius: 1.5rem; 35 | } 36 | 37 | img { 38 | display: block; 39 | } 40 | 41 | li { 42 | margin-bottom: 0.5rem; 43 | } 44 | 45 | a.prev { 46 | background-color: #faab00; 47 | padding: 0.25rem 1rem; 48 | margin: 0.25rem 1rem; 49 | display: inline-block; 50 | font-size: 1rem; 51 | font-weight: bold; 52 | height: 24px; 53 | width: 72px; 54 | color: #ffffff; 55 | position: relative; 56 | border-radius: 0 12px 12px 0; 57 | } 58 | 59 | a.prev:before { 60 | content: ''; 61 | border: 12px solid transparent; 62 | border-right: 12px solid #faab00; 63 | position: absolute; 64 | top: 50%; 65 | left: -24px; 66 | margin-top: -12px; 67 | } 68 | .header { 69 | text-align: center; 70 | } 71 | 72 | .header img.logo { 73 | margin: 12px auto 28px auto; 74 | } 75 | 76 | .stripe { 77 | width: 100%; 78 | background-color: #58637a; 79 | height: 48px; 80 | margin-bottom: 32px; 81 | text-align: left; 82 | padding: 0 1rem; 83 | } 84 | 85 | .inner { 86 | padding: 0.5rem 1.5rem; 87 | } 88 | 89 | ul.products { 90 | padding: 0; 91 | margin: 1rem 0; 92 | list-style: none; 93 | display: flex; 94 | } 95 | 96 | ul.products li { 97 | margin: 0.2rem; 98 | width: 162px; 99 | height: 100px; 100 | background-color: #3db7Cf; 101 | padding: 10px; 102 | display: flex; 103 | } 104 | 105 | ul.products li > div { 106 | display: flex; 107 | align-items: center; 108 | justify-content: center; 109 | } 110 | 111 | ul.products li > div:first-child { 112 | flex: 1.618; 113 | background-color: #ffffff; 114 | border-radius: 2px; 115 | color: #58637a; 116 | font-size: 1rem; 117 | } 118 | 119 | ul.products li > div:nth-child(2) { 120 | flex: 1; 121 | color: #ffffff; 122 | margin-left: 10px; 123 | font-size: 1.0rem; 124 | } 125 | 126 | .counter { 127 | width: 320px; 128 | height: 160px; 129 | padding: 2em; 130 | margin: auto; 131 | display: flex; 132 | align-items: stretch; 133 | } 134 | 135 | .counter > * { 136 | flex: 1; 137 | display: flex; 138 | justify-content: center; 139 | align-items: center; 140 | font-weight: bold; 141 | font-size: 20px; 142 | color: #ffffff; 143 | } 144 | 145 | .counter > div { 146 | background-color: #58637a; 147 | font-family: Questrial, sans-serif; 148 | } 149 | 150 | .counter > button { 151 | border: none; 152 | background-color: #3db7Cf; 153 | font-family: Questrial, sans-serif; 154 | padding: 0; 155 | } 156 | 157 | .counter > *:first-child { 158 | border-radius: 80px 0 0 80px; 159 | } 160 | 161 | .counter > *:last-child { 162 | border-radius: 0 80px 80px 0; 163 | } 164 | 165 | code { 166 | font-size: 14px!important; 167 | } 168 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | logobasic template -------------------------------------------------------------------------------- /src/generator/elmtojs.ts: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | import fs from 'fs' 3 | import tmp from 'tmp' 4 | import {spawn} from 'child_process' 5 | import readline from 'readline' 6 | import path from 'path' 7 | import {Config} from '../config' 8 | 9 | /** 10 | * @param config Config 11 | * @returns a raw javascript code string 12 | */ 13 | export const compileStaticElmWith = async (config: Config): Promise => { 14 | const tmpFile = tmp.fileSync({postfix: '.js'}) 15 | const srcDir = config.build.static_elm.src_dir || '' 16 | const exclude = config.build.static_elm.exclude || [] 17 | const r = await compileElmWith(config, srcDir, exclude, tmpFile.name) 18 | const code = r ? fs.readFileSync(tmpFile.name, 'utf-8') : '' 19 | tmpFile.removeCallback() 20 | return code 21 | } 22 | 23 | /** 24 | * @param config Config 25 | * @returns output file path (absolute in the site) 26 | */ 27 | export const compileDynamicElmWith = async (config: Config): Promise => { 28 | const fName = 'dynamic.js' 29 | const outFile = path.join(config.build.dist_dir, fName) 30 | const srcDir = config.build.dynamic_elm.src_dir || '' 31 | const exclude = config.build.dynamic_elm.exclude || [] 32 | const r = await compileElmWith(config, srcDir, exclude, outFile) 33 | return r ? `/${path.relative(config.build.dist_dir, outFile)}` : '' 34 | } 35 | 36 | /** 37 | * @param config Config 38 | * @param srcDir source directory 39 | * @param exclude glob patterns to ignore 40 | * @param output a file name to output 41 | * @returns succeeded or not 42 | */ 43 | const compileElmWith = async (config: Config, srcDir: string, exclude: string[], output: string): Promise => { 44 | const globOption = {ignore: exclude} 45 | const elmFiles = 46 | glob.sync(`${srcDir}/**/*.elm`, globOption) 47 | if(elmFiles.length === 0) { 48 | return true 49 | } 50 | 51 | console.log(`Elm: ${srcDir} (${elmFiles.join(', ')})`) 52 | 53 | // considering "elm" and "npx elm" 54 | const command = (config.build.elm.command || 'elm').split(' ') 55 | const args = [ 56 | command.slice(1), 57 | ["make"], 58 | elmFiles, 59 | [config.build.elm.optimize ? "--optimize" : ""], 60 | [`--output=${output}`] 61 | ] 62 | .flat() 63 | .filter((x: string) => x.length > 0) 64 | const r = await promiseSpawn(command[0], args) 65 | return r === 0 66 | } 67 | 68 | const promiseSpawn = (command: string, args: string[]) => 69 | new Promise((resolve, reject) => { 70 | const sp = spawn(command, args, {stdio: 'pipe'}) 71 | var msgOk: string[] = [] 72 | var msgNg: string[] = [] 73 | readline 74 | .createInterface({input: sp.stdout, terminal: false}) 75 | .on('line', data => { 76 | msgOk.push(data) 77 | }) 78 | 79 | readline 80 | .createInterface({input: sp.stderr, terminal: false}) 81 | .on('line', data => { 82 | msgNg.push(data) 83 | }) 84 | 85 | sp.on('close', code => { 86 | switch (code) { 87 | case 0: 88 | formatPrintChildProcsssStdOut(msgOk) 89 | resolve(0) 90 | break 91 | default: 92 | formatPrintChildProcsssStdOut(msgNg) 93 | reject(code) 94 | break 95 | } 96 | }) 97 | }) 98 | 99 | const formatPrintChildProcsssStdOut = (stdout: string[]): void => { 100 | //var duplicate: string[] = [] 101 | stdout 102 | .filter(x => typeof x === 'string' && x.trim() !== '') 103 | .forEach(x => { 104 | console.log(` elm: ${x}`) 105 | }) 106 | } 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/generator.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import glob from 'glob' 4 | import jsToHtmlWith from './generator/jstohtml' 5 | import {Config} from './config' 6 | import {compileStaticElmWith, compileDynamicElmWith} from './generator/elmtojs' 7 | 8 | class Cache { 9 | staticCode: string = "" 10 | dynamicCode: string = "" 11 | autoReload: boolean = false 12 | option?: {isServer?: boolean} 13 | constructor(staticCode: string, dynamicCode: string, autoReload: boolean, option?: {isServer?: boolean} ) { 14 | this.staticCode = staticCode 15 | this.dynamicCode = dynamicCode 16 | this.autoReload = autoReload 17 | this.option = option 18 | } 19 | } 20 | var cache = new Cache("", "", false) 21 | 22 | 23 | /** 24 | * main function for generating the site 25 | * @param config 26 | * @param isServer 27 | */ 28 | const generateAll = async (config: Config, option?: {isServer?: boolean}): Promise => { 29 | console.log(`START: ${(new Date).toISOString()}`) 30 | // 1. generate static pages 31 | const elm = await compileStaticElmWith(config) 32 | const appjs = await compileDynamicElmWith(config) 33 | const contentFiles = 34 | glob.sync(`${config.build.contents.src_dir}/**/*`, {ignore: config.build.contents.exclude || [], nodir: true}) 35 | const autoReload = (option || {}).isServer || false 36 | var result: {ok: string[], ng: string[]} = {ok: [], ng: []} 37 | contentFiles.forEach(x => { 38 | const r = convertAndSave(x, config, elm, appjs, autoReload) 39 | if (r) { 40 | result.ok.push(x) 41 | } else { 42 | result.ng.push(x) 43 | } 44 | }) 45 | 46 | // 3. show result 47 | console.log('RESULT:') 48 | console.log(` OK: ${result.ok.length}`) 49 | result.ng.forEach((x, i) => { 50 | console.log(` NG(${i+1}): ${x}`) 51 | }) 52 | 53 | // cache 54 | cache = new Cache(elm, appjs, autoReload, option) 55 | 56 | return (result.ok.length > 0 && result.ng.length === 0) 57 | } 58 | 59 | /** 60 | * main function for generating the site 61 | * @param config 62 | */ 63 | const copyAssets = (config: Config) => { 64 | fs.copySync(config.build.assets.src_dir, config.build.dist_dir) 65 | } 66 | 67 | /** 68 | * using the cache, convert a content file to a static html and save it 69 | * @param file path of a content file 70 | * @param config 71 | */ 72 | const convertOnlyContentFiles = (config: Config): void => { 73 | if(!cache.staticCode) { 74 | console.log("Build All") 75 | generateAll(config, cache.option) 76 | } else { 77 | var result: {ok: string[], ng: string[]} = {ok: [], ng: []} 78 | const contentFiles = 79 | glob.sync(`${config.build.contents.src_dir}/**/*`, {ignore: config.build.contents.exclude || [], nodir: true}) 80 | contentFiles.forEach(x => { 81 | const r = convertAndSave(x, config, cache.staticCode, cache.dynamicCode, cache.autoReload) 82 | if (r) { 83 | result.ok.push(x) 84 | } else { 85 | result.ng.push(x) 86 | } 87 | }) 88 | // 3. show result 89 | console.log('RESULT:') 90 | console.log(` OK: ${result.ok.length}`) 91 | result.ng.forEach((x, i) => { 92 | console.log(` NG(${i+1}): ${x}`) 93 | }) 94 | } 95 | } 96 | 97 | 98 | /** 99 | * create a path for saving 100 | * @param file path of a content file 101 | * @param config 102 | */ 103 | const savePathFor = (file: string, config: Config): string => { 104 | if (file === config.build.contents.index) { 105 | return path.join(config.build.dist_dir, 'index.html') 106 | } else { 107 | const r = path.relative(config.build.contents.src_dir, file) 108 | const p = path.parse(r) 109 | return path.join(config.build.dist_dir, p.dir, p.name, 'index.html') 110 | } 111 | } 112 | 113 | /** 114 | * convert a content file to a static html and save it 115 | * @param file path of a content file 116 | * @param config 117 | * @param elmcode a raw javascript code string 118 | * @param appjs path for the dynamic elm code 119 | * @param autoReloader enable auto reloading 120 | */ 121 | const convertAndSave = (file: string, config: Config, elmcode: string, appjs: string, autoReloader: boolean): boolean => { 122 | console.log(`> ${file}`) 123 | const savePath = savePathFor(file, config) 124 | const draft = config.build.contents.draft || false 125 | const html = jsToHtmlWith(file, config.build.contents.src_dir, elmcode, appjs, draft, autoReloader, config.build.contents.exclude || []) 126 | if(html !== '') { 127 | // console.log(`< ${savePath}`) 128 | fs.ensureFileSync(savePath) 129 | fs.writeFileSync(savePath, html) 130 | return true 131 | } else { 132 | console.log('error: check if the preamble is wrong form or head and body output nothing.') 133 | console.log('ERROR: Failed to convert!') 134 | return false 135 | } 136 | } 137 | 138 | export {generateAll as default, convertOnlyContentFiles, copyAssets} 139 | -------------------------------------------------------------------------------- /res/scaffold/basic/src/Static/Basic.elm: -------------------------------------------------------------------------------- 1 | module Static.Basic exposing (main) 2 | 3 | import Html exposing (Html, a, div, h2, img, li, text, ul) 4 | import Html.Attributes exposing (alt, class, href, name, src) 5 | import Json.Decode as D exposing (Decoder) 6 | import Markdown 7 | import Siteelm.Html as Html 8 | import Siteelm.Html.Attributes exposing (charset, content, rel) 9 | import Siteelm.Page exposing (Page, page) 10 | import Static.View as View 11 | 12 | 13 | main : Page Preamble 14 | main = 15 | page 16 | { decoder = preambleDecoder 17 | , head = viewHead 18 | , body = viewBody 19 | } 20 | 21 | 22 | {-| Preamble is what you write on the head of the content files. 23 | -} 24 | type alias Preamble = 25 | { title : String 26 | , products : List Product 27 | , recommends : List Product 28 | , articles : List Article 29 | } 30 | 31 | 32 | type alias Product = 33 | { name : String 34 | , price : Int 35 | } 36 | 37 | 38 | type alias Article = 39 | { url : String 40 | , title : String 41 | , date : String 42 | } 43 | 44 | 45 | {-| Preamble is passed as a JSON string. So it requires a decoder. 46 | -} 47 | preambleDecoder : Decoder Preamble 48 | preambleDecoder = 49 | D.map4 Preamble 50 | (D.field "title" D.string) 51 | (D.field "products" (D.list productDecoder)) 52 | (D.field "recommends" (D.list productDecoder)) 53 | (D.field "articles" (D.list articleDecoder)) 54 | 55 | 56 | productDecoder : Decoder Product 57 | productDecoder = 58 | D.map2 Product 59 | (D.field "name" D.string) 60 | (D.field "price" D.int) 61 | 62 | 63 | articleDecoder : Decoder Article 64 | articleDecoder = 65 | D.map3 Article 66 | (D.field "url" D.string) 67 | (D.field "title" D.string) 68 | (D.field "date" D.string) 69 | 70 | 71 | {-| Make contents inside the _head_ tag. 72 | -} 73 | viewHead : Preamble -> String -> List (Html Never) 74 | viewHead preamble _ = 75 | [ Html.meta [ charset "utf-8" ] 76 | , Html.title [] (preamble.title ++ " | siteelm") 77 | , Html.meta [ name "description", content "this is a simple static site generator for elm" ] 78 | , Html.link [ rel "stylesheet", href "/style.css" ] 79 | , Html.link [ rel "stylesheet", href "https://fonts.googleapis.com/css?family=Questrial&display=swap" ] 80 | , Html.link [ rel "stylesheet", href "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/default.min.css" ] 81 | , Html.script "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js" "" 82 | , Html.script "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/languages/elm.min.js" "" 83 | , Html.script "" "hljs.initHighlightingOnLoad();" 84 | ] 85 | 86 | 87 | {-| Make contents inside the _body_ tag. The parameter "body" is usually something like markdown. 88 | -} 89 | viewBody : Preamble -> String -> List (Html Never) 90 | viewBody preamble body = 91 | [ View.header 92 | , div 93 | [ class "main" ] 94 | [ img [ src "/images/header.svg", alt "" ] [] 95 | , div [] 96 | [ h2 [] [ text "body text" ] 97 | , div [ class "inner" ] 98 | [ text "since the body text is just a string, give it to a markdown parser" 99 | , Markdown.toHtml [] body 100 | ] 101 | ] 102 | , div [] 103 | [ h2 [] [ text "dynamic components" ] 104 | , div [ class "inner" ] 105 | [ text "with Browser.element, dynamic contents can be embedded" 106 | , Html.dynamic 107 | { moduleName = "Dynamic.Counter" 108 | , flags = "{value: 100}" 109 | } 110 | ] 111 | ] 112 | , div [] 113 | [ h2 [] [ text "preamble" ] 114 | , div [ class "inner" ] 115 | [ text "you can write YAML in the preamble section" 116 | , viewProducts preamble.products 117 | , text "external YAML files can be imported" 118 | , viewProducts preamble.recommends 119 | ] 120 | ] 121 | , div [] 122 | [ h2 [] [ text "get preambles in a directory" ] 123 | , div [ class "inner" ] 124 | [ div [] 125 | [ text "Use \"preamblesIn\" parameter to get preambles of files in a specified directory." 126 | ] 127 | , div [] 128 | [ text "Be aware that there are some parameters which Siteelm automatically sets in an preamble. At the moment, a property \"url\" is that." 129 | ] 130 | , ul [] 131 | (List.map 132 | viewArticle 133 | preamble.articles 134 | ) 135 | ] 136 | ] 137 | ] 138 | ] 139 | 140 | 141 | viewArticle : Article -> Html Never 142 | viewArticle article = 143 | li [] 144 | [ a [ href article.url ] 145 | [ text article.title 146 | ] 147 | ] 148 | 149 | 150 | viewProducts : List Product -> Html Never 151 | viewProducts = 152 | ul [ class "products" ] 153 | << List.map 154 | (\x -> 155 | li [] 156 | [ div [] [ text x.name ] 157 | , div [] [ text ("¥" ++ String.fromInt x.price) ] 158 | ] 159 | ) 160 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/Static/View.elm: -------------------------------------------------------------------------------- 1 | module Static.View exposing (head, headerImage, pageLayout) 2 | 3 | import Element exposing (..) 4 | import Element.Background as Bg 5 | import Element.Font as Font 6 | import Html exposing (Html) 7 | import Html.Attributes exposing (href, name) 8 | import Siteelm.Html as Html 9 | import Siteelm.Html.Attributes exposing (charset, content, rel) 10 | 11 | 12 | maxWidth : Int 13 | maxWidth = 14 | 900 15 | 16 | 17 | bgColor : Color 18 | bgColor = 19 | rgb255 232 217 188 20 | 21 | 22 | head : { a | title : String } -> String -> List (Html Never) 23 | head preamble _ = 24 | [ Html.meta [ charset "utf-8" ] 25 | , Html.title [] (preamble.title ++ " | siteelm blog") 26 | , Html.meta [ name "description", content "siteelm blog template" ] 27 | , Html.meta [ name "viewport", content "width=device-width,initial-scale=1" ] 28 | , Html.link [ rel "stylesheet", href "/markdown.css" ] 29 | , Html.link [ rel "stylesheet", href "/counter.css" ] 30 | , Html.link [ rel "stylesheet", href "https://fonts.googleapis.com/css?family=Questrial&display=swap" ] 31 | , Html.link [ rel "stylesheet", href "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/default.min.css" ] 32 | , Html.script "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js" "" 33 | , Html.script "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/languages/elm.min.js" "" 34 | , Html.script "" "hljs.initHighlightingOnLoad();" 35 | ] 36 | 37 | 38 | pageLayout : Element Never -> List (Html Never) 39 | pageLayout child = 40 | List.singleton <| 41 | layout 42 | [ width fill 43 | , height fill 44 | , Font.color (rgb255 66 66 66) 45 | ] 46 | (column 47 | [ width fill 48 | , height fill 49 | ] 50 | [ header 51 | , el 52 | [ width fill 53 | , height fill 54 | , Bg.color bgColor 55 | , paddingXY 0 20 56 | ] 57 | (el 58 | [ width (maximum maxWidth fill) 59 | , centerX 60 | ] 61 | child 62 | ) 63 | , footer 64 | ] 65 | ) 66 | 67 | 68 | header : Element Never 69 | header = 70 | let 71 | content ( url, name ) = 72 | link 73 | [ alignRight 74 | , paddingXY 8 0 75 | ] 76 | { url = url 77 | , label = text name 78 | } 79 | in 80 | column 81 | [ width (maximum maxWidth fill) 82 | , centerX 83 | ] 84 | [ row 85 | [ width fill ] 86 | [ link [] 87 | { url = "/" 88 | , label = 89 | image 90 | [ width (px 128) 91 | , height (px 96) 92 | ] 93 | { src = "/images/logo.png" 94 | , description = "siteelm" 95 | } 96 | } 97 | , wrappedRow 98 | [ Font.bold 99 | , Font.size 14 100 | , padding 0 101 | , width fill 102 | ] 103 | (List.map content 104 | [ ( "/about", "ABOUT" ) 105 | , ( "/installation", "INSTALLATION" ) 106 | ] 107 | ++ [ link [] 108 | { url = "https://github.com/nikueater/siteelm" 109 | , label = 110 | image 111 | [ width (px 48) 112 | , height (px 48) 113 | ] 114 | { src = "/images/github.svg" 115 | , description = "github" 116 | } 117 | } 118 | ] 119 | ) 120 | ] 121 | ] 122 | 123 | 124 | footer : Element Never 125 | footer = 126 | el 127 | [ width fill 128 | , Bg.color bgColor 129 | , paddingXY 0 8 130 | ] 131 | (row 132 | [ centerX 133 | , Font.size 12 134 | ] 135 | [ text "powered by " 136 | , link [ Font.color (rgb255 80 80 250) ] 137 | { url = "https://github.com/nikueater/siteelm" 138 | , label = text "siteelm" 139 | } 140 | ] 141 | ) 142 | 143 | 144 | headerImage : { title : String, src : String } -> Element Never 145 | headerImage { title, src } = 146 | el 147 | [ width (maximum maxWidth fill) 148 | , height (px 300) 149 | , clipX 150 | , clipY 151 | ] 152 | <| 153 | el 154 | [ width fill 155 | , height fill 156 | , padding 16 157 | , clipX 158 | , behindContent 159 | (image 160 | [ height (maximum 300 fill) 161 | , centerX 162 | , clipX 163 | ] 164 | { src = src 165 | , description = "" 166 | } 167 | ) 168 | , Font.color (rgb255 250 250 250) 169 | , Font.size 32 170 | , Font.bold 171 | ] 172 | (el [ centerY ] (text title)) 173 | -------------------------------------------------------------------------------- /res/scaffold/blog/src/Static/Home.elm: -------------------------------------------------------------------------------- 1 | module Static.Home exposing (main) 2 | 3 | import Element exposing (..) 4 | import Element.Background as Bg 5 | import Element.Border as Border 6 | import Element.Font as Font 7 | import Html exposing (Html) 8 | import Html.Attributes exposing (class) 9 | import Iso8601 10 | import Json.Decode as Decode exposing (Decoder, int, list, string) 11 | import Json.Decode.Pipeline exposing (optional, required) 12 | import Markdown 13 | import Siteelm.Html as Html 14 | import Siteelm.Page exposing (Page, page) 15 | import Static.View exposing (head, headerImage, pageLayout) 16 | import Time exposing (Posix) 17 | 18 | 19 | main : Page Preamble 20 | main = 21 | page 22 | { decoder = preambleDecoder 23 | , head = head 24 | , body = body 25 | } 26 | 27 | 28 | {-| Preambles is json decodable data in the head part of your md files. 29 | When you pass a directory path to "preamblesIn," preambles sections of all files in the directory will be passed. 30 | Also you can give a path to external yaml files with an "external" property. 31 | -} 32 | type alias Preamble = 33 | { title : String 34 | , articles : List Article 35 | , products : List Product 36 | } 37 | 38 | 39 | {-| The field "url" is automatically given by Siteelm even though you don't write it. 40 | -} 41 | type alias Article = 42 | { url : String 43 | , title : String 44 | , date : Posix 45 | } 46 | 47 | 48 | type alias Product = 49 | { name : String 50 | , price : Int 51 | } 52 | 53 | 54 | preambleDecoder : Decoder Preamble 55 | preambleDecoder = 56 | Decode.succeed Preamble 57 | |> required "title" string 58 | |> optional "articles" (list articleDecoder) [] 59 | |> optional "products" (list productDecoder) [] 60 | 61 | 62 | articleDecoder : Decoder Article 63 | articleDecoder = 64 | Decode.succeed Article 65 | |> required "url" string 66 | |> required "title" string 67 | |> required "date" Iso8601.decoder 68 | 69 | 70 | productDecoder : Decoder Product 71 | productDecoder = 72 | Decode.succeed Product 73 | |> required "name" string 74 | |> required "price" int 75 | 76 | 77 | body : Preamble -> String -> List (Html Never) 78 | body preamble markdown = 79 | pageLayout <| 80 | column 81 | [ width fill, clipX ] 82 | [ headerImage 83 | { src = "images/header_01.jpg" 84 | , title = preamble.title 85 | } 86 | , paragraph [] 87 | [ html (Markdown.toHtml [ class "md" ] markdown) 88 | ] 89 | , articles preamble.articles 90 | , demo preamble 91 | ] 92 | 93 | 94 | articles : List Article -> Element Never 95 | articles items = 96 | column 97 | [ width fill 98 | , height fill 99 | ] 100 | (items 101 | |> List.sortWith 102 | (\a b -> 103 | case compare (Time.posixToMillis a.date) (Time.posixToMillis b.date) of 104 | LT -> 105 | GT 106 | 107 | _ -> 108 | LT 109 | ) 110 | |> List.map article 111 | ) 112 | 113 | 114 | article : Article -> Element Never 115 | article item = 116 | let 117 | date = 118 | item.date 119 | |> Iso8601.fromTime 120 | |> String.split "T" 121 | |> List.head 122 | |> Maybe.withDefault "" 123 | in 124 | link 125 | [ width fill 126 | , Border.widthEach 127 | { top = 0 128 | , left = 0 129 | , right = 0 130 | , bottom = 1 131 | } 132 | , Border.dashed 133 | ] 134 | { url = item.url 135 | , label = 136 | row 137 | [ spacing 8 138 | , paddingEach 139 | { top = 10 140 | , left = 0 141 | , right = 0 142 | , bottom = 0 143 | } 144 | ] 145 | [ el 146 | [ Font.size 16 147 | , alignBottom 148 | ] 149 | (text date) 150 | , el 151 | [ Font.size 20 152 | , alignBottom 153 | , Font.bold 154 | , Font.color (rgb255 80 80 250) 155 | ] 156 | (text item.title) 157 | ] 158 | } 159 | 160 | 161 | demo : Preamble -> Element Never 162 | demo preamble = 163 | let 164 | caption title = 165 | el 166 | [ Font.size 24 167 | , Font.bold 168 | ] 169 | (text title) 170 | 171 | product p = 172 | row 173 | [ width (px 200) 174 | , height (px 100) 175 | , Bg.color (rgb255 61 183 207) 176 | , padding 6 177 | ] 178 | [ el 179 | [ width fill 180 | , height fill 181 | , centerX 182 | , Font.color 183 | (rgb 1 1 1) 184 | , Bg.color (rgb255 255 255 255) 185 | , Border.rounded 8 186 | ] 187 | (el 188 | [ Font.color (rgb255 61 187 207) 189 | , centerX 190 | , centerY 191 | ] 192 | (text p.name) 193 | ) 194 | , el 195 | [ width fill 196 | , height fill 197 | ] 198 | (el 199 | [ Font.color (rgb255 255 255 255) 200 | , centerX 201 | , centerY 202 | ] 203 | (text ("¥" ++ String.fromInt p.price)) 204 | ) 205 | ] 206 | in 207 | column 208 | [ paddingXY 0 20 209 | , width fill 210 | , spacing 8 211 | , Font.size 16 212 | ] 213 | [ el 214 | [ Font.size 32 215 | , Font.bold 216 | ] 217 | (text "Demo") 218 | , caption "preamble" 219 | , paragraph 220 | [ width fill ] 221 | [ text "You can write YAML in the preamble section. And external YAML files can be imported." 222 | , wrappedRow 223 | [ spacing 8 224 | , centerX 225 | ] 226 | (List.map product preamble.products) 227 | ] 228 | , caption "dynamic element" 229 | , paragraph [ width fill ] 230 | [ text "With Browser.element, dynamic contents can be embedded." 231 | , html <| 232 | Html.dynamic 233 | { moduleName = "Dynamic.Counter" 234 | , flags = "{value: 100}" 235 | } 236 | ] 237 | ] 238 | -------------------------------------------------------------------------------- /src/generator/jstohtml.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import glob from 'glob' 3 | import {JSDOM, VirtualConsole} from 'jsdom' 4 | import P from 'parsimmon' 5 | import yaml from 'js-yaml' 6 | import path from 'path' 7 | import {minify} from 'html-minifier' 8 | import traverse from 'traverse' 9 | import {staticElmInitCode, dynamicElmInitCode, autoReloaderCode} from './snippet' 10 | 11 | interface Preamble { 12 | module: string 13 | url: string 14 | draft?: boolean 15 | } 16 | 17 | class ConvertError implements Error { 18 | public name = 'ConvertError' 19 | constructor(public message: string) {} 20 | toString():string { 21 | return `${this.name}: ${this.message}` 22 | } 23 | } 24 | class InvalidPreambleError extends ConvertError { name = 'Preamble' } 25 | 26 | /** 27 | * @param source a file name for creating flags 28 | * @param elmcode a raw javascript code string 29 | * @param appjs the path for the dynamic elm code 30 | * @param root root directory of pages 31 | * @param withDraft flag for not ignoring drafts 32 | * @param autoReloader 33 | * @param excludes which are excluded by indexing 34 | * @returns void 35 | */ 36 | const jsToHtmlWith = (sourcePath: string, srcDir: string, elmcode: string, appjsPath: string, withDraft: boolean, autoReloader: boolean, excludes: string[]): string => { 37 | try { 38 | // create flags 39 | const document = parseDocument(fs.readFileSync(sourcePath, 'utf-8')) 40 | const p = parsePreamble(document[0], sourcePath, `${srcDir}/*`, excludes) 41 | const flags = { 42 | preamble: JSON.stringify(p), 43 | body: document[1] 44 | } 45 | if(p.draft == true && !withDraft) { 46 | return '' 47 | } 48 | // generate a DOM 49 | const vc = new VirtualConsole() 50 | vc.on('info', (x: string) => { 51 | console.log(`info: ${x}`) 52 | }) 53 | vc.on('warn', (x: string) => { 54 | if(x.startsWith('Compiled in DEV mode.')) { return } 55 | console.log(`warn: ${x}`) 56 | }) 57 | vc.on('error', (x: string) => { 58 | console.log(`error: ${x}`) 59 | }) 60 | const dom = new JSDOM('', {runScripts: 'outside-only', virtualConsole: vc}) 61 | dom.window.eval(elmcode) 62 | dom.window.eval(staticElmInitCode(p.module, flags)) 63 | const body = unescapeScripts(dom).window.document.body.innerHTML 64 | 65 | // formatting 66 | var ds = new JSDOM(body, {runScripts: 'outside-only'}) 67 | const head = ds.window.document.querySelector('head') 68 | if(ds.window.document.body.innerHTML === '') { 69 | return '' 70 | } 71 | if(head) { 72 | ds.window.document.querySelectorAll('style').forEach(x => { 73 | const styleParent = x.parentNode 74 | head.appendChild(x) 75 | if(styleParent?.parentNode && !styleParent.hasChildNodes()) { 76 | styleParent.parentNode.removeChild(styleParent) 77 | } 78 | }) 79 | // add dynamic elm elements 80 | if(appjsPath !== '') { 81 | ds = embedDynamicComponents(ds, appjsPath) 82 | } 83 | } 84 | 85 | // auto reloader 86 | if (autoReloader) { 87 | const s = ds.window.document.createElement('script') 88 | s.textContent = autoReloaderCode() 89 | ds.window.document.body.appendChild(s) 90 | } 91 | const html = `\n${ds.serialize()}` 92 | // turn the DOM into string and save it 93 | return minify(html, {minifyCSS: true, minifyJS: true}) 94 | } catch(e) { 95 | console.error('error:') 96 | console.error(e.toString()) 97 | } 98 | return '' 99 | } 100 | 101 | /** 102 | * @param source a string which has a preamble wrapped with "---" and a free text 103 | * @returns a preamble and a body 104 | */ 105 | const parseDocument = (source: string): string[] => { 106 | const delimiter = P.string("---").skip(P.optWhitespace) 107 | var ls = "" 108 | const mbody = P.takeWhile(c => { 109 | const result = ls !== "\n---" 110 | ls = (ls + c).slice(-4) 111 | return result 112 | }) 113 | const matter = delimiter.then(mbody).map(x => x.replace(/(---)$/, '')) 114 | const content = P.all.map(x => x.trim()) 115 | const doc = P.seq(matter, content).parse(source) 116 | return 'value' in doc ? doc.value : [] 117 | } 118 | 119 | /** 120 | * @param p yaml format string 121 | * @param source path of the current source file 122 | * @param root root directory of pages 123 | * @param excludes which are exclued by indexing 124 | * @param processed list of files have been parsed 125 | * @returns preamble interface data 126 | */ 127 | const parsePreamble = (p: string, source: string, root: string, excludes: string[], processed: string[] = []): Preamble => { 128 | const yml = yaml.safeLoad(p) 129 | const preamble = ((x: any): Preamble => x)(yml) 130 | if(typeof preamble.module !== 'string') { 131 | throw new InvalidPreambleError('no "module"') 132 | } 133 | if(typeof preamble.draft !== 'boolean') { 134 | preamble.draft = false 135 | } 136 | if(preamble.url) { 137 | throw new InvalidPreambleError("you can't use \"url\" at the top level") 138 | } 139 | let dir = path.dirname(root) 140 | let rel = path.relative(dir, source) 141 | let ext = path.extname(rel) 142 | let file = path.basename(rel, ext) 143 | let url = path.join(path.dirname(rel), path.basename(rel, ext)) 144 | if(file == 'index') { 145 | url = path.dirname(rel) 146 | } 147 | if(url == '.') { 148 | url = '' 149 | } 150 | preamble.url = `/${url}` 151 | return parseYaml(preamble, source, root, excludes, processed) 152 | } 153 | 154 | const parseYaml = (preamble: Preamble, source: string, root: string, excludes: string[], processed: string[]): Preamble => { 155 | // walk through all element to detect special values 156 | processed.push(path.normalize(source)) 157 | traverse(preamble).forEach(function(x) { 158 | switch(this.key) { 159 | case 'external': 160 | const dir = path.dirname(source) 161 | const file = x || '' 162 | const newSource = path.normalize(path.join(dir, file)) 163 | const y = fs.readFileSync(newSource, 'utf-8') 164 | const value = yaml.safeLoad(y) 165 | if(this.parent) { 166 | if(Object.keys(this.parent.node).length === 1) { 167 | this.parent.update(value) 168 | } else { 169 | throw new InvalidPreambleError('"external" cannot have siblings') 170 | } 171 | } 172 | preamble = parseYaml(preamble, newSource, root, excludes, processed) 173 | break 174 | case 'preamblesIn': 175 | const contentDir = path.normalize(path.join(path.dirname(source), x)) 176 | const contentFiles = 177 | glob.sync(path.join(contentDir, '*'), {ignore: excludes || [], nodir: true}) 178 | var ps : any[] = [] 179 | contentFiles 180 | .map(x => path.normalize(x)) 181 | .filter(x => !processed.includes(x)) 182 | .forEach(x => { 183 | const document = parseDocument(fs.readFileSync(x, 'utf-8')) 184 | ps.push(parsePreamble(document[0], x, root, excludes)) 185 | }) 186 | if(this.parent) { 187 | this.parent.update(ps) 188 | } 189 | break 190 | } 191 | }) 192 | return preamble 193 | } 194 | 195 | /** 196 | * @param dom JSDOM 197 | * @param appjs path for a js file from elm 198 | */ 199 | const embedDynamicComponents = (dom: JSDOM, appjs: string): JSDOM => { 200 | const script = dom.window.document.createElement('script') 201 | const head = dom.window.document.querySelector('head') 202 | if(!head) { 203 | return dom 204 | } 205 | script.src = appjs 206 | head.appendChild(script) 207 | var treateds: string[] = [] 208 | Array.from(dom.window.document.querySelectorAll('div[data-elm-module]')) 209 | .map(target => { 210 | const flags = target.getAttribute('data-flags') || '{}' 211 | const uniqueKey = Buffer.from(flags).toString('base64') 212 | target.setAttribute('data-unique-key', uniqueKey) 213 | return target 214 | }) 215 | .forEach(x => { 216 | const modName = x.getAttribute('data-elm-module') || '' 217 | const flags = x.getAttribute('data-flags') || '{}' 218 | const uniqueKey = x.getAttribute('data-unique-key') || '' 219 | const treatedKey = [modName, uniqueKey].join('-') 220 | if(treateds.includes(treatedKey)) { 221 | return 222 | } else { 223 | treateds.push(treatedKey) 224 | } 225 | const script = dom.window.document.createElement('script') 226 | script.textContent = dynamicElmInitCode(modName, flags, uniqueKey) 227 | dom.window.document.body.appendChild(script) 228 | }) 229 | return dom 230 | } 231 | 232 | /** 233 | * @param dom 234 | */ 235 | const unescapeScripts = (dom: JSDOM): JSDOM => { 236 | const customs = 237 | dom.window.document.querySelectorAll('siteelm-custom[data-tag="script"]') || [] 238 | 239 | customs.forEach((x) => { 240 | const parent = x.parentElement 241 | if(!parent) { 242 | return 243 | } 244 | const script = dom.window.document.createElement('script') 245 | const attrs = x.attributes 246 | script.textContent = x.textContent 247 | for(var i = 0; i < attrs.length; i++) { 248 | const attr = attrs.item(i) 249 | if(attr && attr.nodeValue && !attr.nodeName.startsWith('data-')) { 250 | script.setAttribute(attr.nodeName, attr.nodeValue) 251 | } 252 | } 253 | parent.insertBefore(script, x.nextSibling) 254 | parent.removeChild(x) 255 | }) 256 | return dom 257 | } 258 | 259 | 260 | export default jsToHtmlWith 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2019 nikueater 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | --------------------------------------------------------------------------------