From 142e0c9fac9896f8cb134fd1e1b010753402e7b8 Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 2 Feb 2019 17:14:33 +0100 Subject: Make it works and add “galette” recipe --- .gitignore | 13 ++--- .gitlab-ci.yml | 6 +- .tmuxinator.yml | 5 +- Makefile | 11 ++-- bower.json | 15 ----- default.nix | 17 ++++++ design/Main.hs | 24 +++++++- js/src/Dom.purs | 75 +++++++++++-------------- js/src/EditableNumber.purs | 47 ++++++++-------- js/src/Main.purs | 17 +++--- js/src/Parser.purs | 2 +- js/test/Main.purs | 6 +- nix/purescript.nix | 49 ++++++++++++++++ package.json | 2 +- psc-package.json | 18 ++++++ recettes/desserts/hyperglucidique/cheesecake.md | 6 +- recettes/plats/galettes.md | 30 ++++++++++ shell.nix | 18 ------ src/Main.hs | 14 ++--- stack.yaml | 6 +- 20 files changed, 236 insertions(+), 145 deletions(-) delete mode 100644 bower.json create mode 100644 default.nix create mode 100644 nix/purescript.nix create mode 100644 psc-package.json create mode 100644 recettes/plats/galettes.md delete mode 100644 shell.nix diff --git a/.gitignore b/.gitignore index 886423d..ceb09b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -_cache -public -.stack-work -output -node_modules -bower_components -yarn.lock +.psc-package/ +.stack-work/ +_cache/ +node_modules/ +output/ +public/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9677cd6..2016415 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,8 +11,8 @@ cache: - target - output - node_modules - - bower_components - yarn.lock + - .psc-package pages: script: @@ -20,11 +20,11 @@ pages: - apt-get install -y curl - curl -sL https://deb.nodesource.com/setup_8.x | bash - apt-get install -y xz-utils make nodejs - - npm install bower + - npm install psc-package - npm install purescript - npm install pulp - export PATH="$(pwd)/node_modules/.bin":$PATH - - bower install --allow-root + - psc-package install - make test - export STACK_ROOT="$(pwd)/.stack" - stack setup diff --git a/.tmuxinator.yml b/.tmuxinator.yml index c3a79fc..d9f8748 100644 --- a/.tmuxinator.yml +++ b/.tmuxinator.yml @@ -2,7 +2,4 @@ name: cooking windows: - main: - layout: fff4,119x58,0,0{94x58,0,0,0,24x58,95,0,1} - panes: - - # Empty - - make install watch + - make install watch diff --git a/Makefile b/Makefile index c823259..2ab6ba2 100644 --- a/Makefile +++ b/Makefile @@ -7,17 +7,17 @@ stop: @tmux kill-session -t cooking test: - @pulp test --src-path js + @pulp --psc-package test --src-path js clean: - @rm -rf bower_components @rm -rf node_modules + @rm -rf .psc-package @stack exec cooking clean > /dev/null 2>&1 || true @stack clean > /dev/null install: - @yarn install - @bower install + @npm install + @psc-package install @stack setup .PHONY: build @@ -28,5 +28,8 @@ build: watch: @nodemon --watch src -e hs --exec 'make watch-command --silent' +watch-js: + @pulp --psc-package --watch build --src-path js + watch-command: @(killall cooking || :) && sleep 1 && stack build && stack exec cooking watch diff --git a/bower.json b/bower.json deleted file mode 100644 index 8983d5a..0000000 --- a/bower.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "cooking", - "private": true, - "dependencies": { - "purescript-prelude": "*", - "purescript-console": "*", - "purescript-eff": "*", - "purescript-dom": "*", - "purescript-maybe": "*", - "purescript-parsing": "*", - "purescript-integers": "*", - "purescript-math": "*", - "purescript-spec": "*" - } -} diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..17084f8 --- /dev/null +++ b/default.nix @@ -0,0 +1,17 @@ +with import {}; { + env = stdenv.mkDerivation { + name = "env"; + buildInputs = with pkgs; [ + stack + psc-package + (callPackage ./nix/purescript.nix {}) + nodePackages.nodemon + tmux + tmuxinator + nodejs-8_x + ]; + shellHook = '' + export PATH=node_modules/.bin:$PATH; + ''; + }; +} diff --git a/design/Main.hs b/design/Main.hs index cd0218e..0af225d 100644 --- a/design/Main.hs +++ b/design/Main.hs @@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-} -import Clay -import Data.Monoid ((<>)) -import qualified Clay.Media as Media +import Clay +import qualified Clay.Media as Media +import Data.Monoid ((<>)) color1 = rgb 113 68 30 color2 = rgb 13 13 81 @@ -11,6 +11,8 @@ color3 = rgb 230 230 230 main :: IO () main = putCss $ do + appearKeyframes + body ? do maxWidth responsiveLimit sym2 margin (px 0) auto @@ -40,6 +42,12 @@ main = putCss $ do hover & textDecoration underline "#content" ? do + + animationName "appear" + animationDuration (sec 0.2) + animationTimingFunction easeIn + animationIterationCount (iterationCount 1.0) + mobile $ sym2 margin (px 0) (px 20) (h1 <> h2) ? color color1 @@ -106,3 +114,13 @@ desktop = query Media.screen [ Media.minWidth responsiveLimit ] responsiveLimit :: Size LengthUnit responsiveLimit = px 800 + +appearKeyframes :: Css +appearKeyframes = keyframes + "appear" + [ (0, do + "transform" -: "translateX(20px)" + opacity 0 + ) + , (100, "transform" -: "translateX(0px)") + ] diff --git a/js/src/Dom.purs b/js/src/Dom.purs index 426c390..167da38 100644 --- a/js/src/Dom.purs +++ b/js/src/Dom.purs @@ -9,54 +9,51 @@ module Dom , setValue ) where -import Control.Monad.Eff (Eff) import Control.Monad.Except (runExcept) as Except import Data.Array (range, catMaybes) as Array -import Data.Either (Either(Right)) -import Data.Foreign (toForeign) as Foreign import Data.Maybe (Maybe(Nothing, Just)) import Data.Traversable (sequence) as Traversable +import Effect (Effect) import Prelude -import DOM (DOM) -import DOM.HTML (window) as DOM -import DOM.HTML.HTMLInputElement (setValue) as HTMLInputElement -import DOM.HTML.Types (htmlDocumentToParentNode, readHTMLInputElement) as DOM -import DOM.HTML.Window (document) as DOM -import DOM.Node.Node (replaceChild, parentNode, appendChild) as DOM -import DOM.Node.NodeList (length, item) as DOM -import DOM.Node.ParentNode (QuerySelector) -import DOM.Node.ParentNode (querySelector, querySelectorAll) as DOM -import DOM.Node.Types (Element, Node, NodeList) -import DOM.Node.Types (elementToParentNode, readElement) as DOM +import Web.DOM.Element (toParentNode, fromNode) as Element +import Web.DOM.Internal.Types (Element, Node, NodeList) +import Web.DOM.Node (replaceChild, parentNode, appendChild) as Node +import Web.DOM.NodeList (length, item) as NodeList +import Web.DOM.ParentNode (QuerySelector) +import Web.DOM.ParentNode (querySelector, querySelectorAll) as DOM +import Web.HTML (window) as HTML +import Web.HTML.HTMLDocument as HTMLDocument +import Web.HTML.HTMLInputElement (setValue, fromElement) as HTMLInputElement +import Web.HTML.Window (document) as Window -foreign import onInput :: forall e. Element -> (String -> Eff (dom :: DOM | e) Unit) -> Eff (dom :: DOM | e) Unit +foreign import onInput :: Element -> (String -> Effect Unit) -> Effect Unit -selectElement :: forall e. QuerySelector -> Eff (dom :: DOM | e) (Maybe Element) +selectElement :: QuerySelector -> Effect (Maybe Element) selectElement query = do - document <- DOM.window >>= DOM.document - DOM.querySelector query (DOM.htmlDocumentToParentNode document) + document <- HTML.window >>= Window.document + DOM.querySelector query (HTMLDocument.toParentNode document) -selectElements :: forall e. QuerySelector -> Eff (dom :: DOM | e) (Array Element) +selectElements :: QuerySelector -> Effect (Array Element) selectElements query = do - document <- DOM.window >>= DOM.document - nodeList <- DOM.querySelectorAll query (DOM.htmlDocumentToParentNode document) + document <- HTML.window >>= Window.document + nodeList <- DOM.querySelectorAll query (HTMLDocument.toParentNode document) getNodes nodeList -selectElementFrom :: forall e. Element -> QuerySelector -> Eff (dom :: DOM | e) (Maybe Element) +selectElementFrom :: Element -> QuerySelector -> Effect (Maybe Element) selectElementFrom elem query = - DOM.querySelector query (DOM.elementToParentNode elem) + DOM.querySelector query (Element.toParentNode elem) -selectElementsFrom :: forall e. Element -> QuerySelector -> Eff (dom :: DOM | e) (Array Element) +selectElementsFrom :: Element -> QuerySelector -> Effect (Array Element) selectElementsFrom elem query = do - nodeList <- DOM.querySelectorAll query (DOM.elementToParentNode elem) + nodeList <- DOM.querySelectorAll query (Element.toParentNode elem) getNodes nodeList -getNodes :: forall e. NodeList -> Eff (dom :: DOM | e) (Array Element) +getNodes :: NodeList -> Effect (Array Element) getNodes nodeList = do - length <- DOM.length nodeList + length <- NodeList.length nodeList Array.range 0 length - # (map (\i -> (concatMaybe <<< map nodeToElement) <$> DOM.item i nodeList)) + # (map (\i -> (concatMaybe <<< map Element.fromNode) <$> NodeList.item i nodeList)) # Traversable.sequence # map Array.catMaybes @@ -66,33 +63,27 @@ concatMaybe mma = Just x -> x Nothing -> Nothing -nodeToElement :: Node -> Maybe Element -nodeToElement node = - case Except.runExcept $ DOM.readElement (Foreign.toForeign node) of - Right element -> Just element - _ -> Nothing - -replaceElement :: forall e. Node -> Node -> Eff (dom :: DOM | e) Unit +replaceElement :: Node -> Node -> Effect Unit replaceElement before after = do - parent <- DOM.parentNode before + parent <- Node.parentNode before case parent of Just n -> do - _ <- DOM.replaceChild after before n + _ <- Node.replaceChild after before n pure unit Nothing -> pure unit -appendNodes :: forall e. Node -> Array Node -> Eff (dom :: DOM | e) Unit +appendNodes :: Node -> Array Node -> Effect Unit appendNodes parent nodes = nodes - # map (\n -> DOM.appendChild n parent) + # map (\n -> Node.appendChild n parent) # Traversable.sequence # map (const unit) -setValue :: forall e. String -> Element -> Eff (dom :: DOM | e) Unit +setValue :: String -> Element -> Effect Unit setValue value elem = - case Except.runExcept $ DOM.readHTMLInputElement (Foreign.toForeign elem) of - Right inputElem -> do + case HTMLInputElement.fromElement elem of + Just inputElem -> do HTMLInputElement.setValue value inputElem _ -> pure unit diff --git a/js/src/EditableNumber.purs b/js/src/EditableNumber.purs index 6a6e3a8..02ffe58 100644 --- a/js/src/EditableNumber.purs +++ b/js/src/EditableNumber.purs @@ -3,18 +3,17 @@ module EditableNumber , set ) where -import Control.Monad.Eff (Eff) import Data.Maybe (Maybe(..)) -import DOM (DOM) -import DOM.HTML (window) as DOM -import DOM.HTML.Types (htmlDocumentToDocument) as DOM -import DOM.HTML.Window (document) as DOM -import DOM.Node.Document (createElement, createTextNode) as DOM -import DOM.Node.Element (setClassName, setAttribute) as DOM -import DOM.Node.Node (textContent) as DOM -import DOM.Node.Types (Element, Node) -import DOM.Node.Types (elementToNode, textToNode) as DOM +import Effect (Effect) import Prelude +import Web.DOM.Document (createElement, createTextNode) as Document +import Web.DOM.Element (setClassName, setAttribute, toNode) as Element +import Web.DOM.Internal.Types (Element, Node) +import Web.DOM.Node (textContent) as Node +import Web.DOM.Text (toNode) as Text +import Web.HTML (window) as HTML +import Web.HTML.HTMLDocument (toDocument) as HTMLDocument +import Web.HTML.Window (document) as Window import Dom (replaceElement, appendNodes) as Dom import Number (format) as Number @@ -26,31 +25,31 @@ type NumberElem = , number :: Number } -set :: forall e. { tag :: String, node :: Node } -> Eff (dom :: DOM | e) (Maybe NumberElem) +set :: { tag :: String, node :: Node } -> Effect (Maybe NumberElem) set { tag, node } = do - content <- DOM.textContent node + content <- Node.textContent node case Parser.textWithNumber content of Just twn -> do textWithNumber <- textWithNumberElem tag twn - Dom.replaceElement node (DOM.elementToNode textWithNumber) + Dom.replaceElement node (Element.toNode textWithNumber) pure (Just { elem: textWithNumber, number: twn.number }) Nothing -> pure Nothing -textWithNumberElem :: forall e. String -> TextWithNumber -> Eff (dom :: DOM | e) Element +textWithNumberElem :: String -> TextWithNumber -> Effect Element textWithNumberElem tag { begin, number, end } = do - document <- DOM.htmlDocumentToDocument <$> (DOM.window >>= DOM.document) - elem <- DOM.createElement tag document - beginNode <- DOM.textToNode <$> DOM.createTextNode begin document + document <- HTMLDocument.toDocument <$> (HTML.window >>= Window.document) + elem <- Document.createElement tag document + beginNode <- Text.toNode <$> Document.createTextNode begin document numberNode <- numberElem number - endNode <- DOM.textToNode <$> DOM.createTextNode end document - Dom.appendNodes (DOM.elementToNode elem) [ beginNode, DOM.elementToNode numberNode, endNode ] + endNode <- Text.toNode <$> Document.createTextNode end document + Dom.appendNodes (Element.toNode elem) [ beginNode, Element.toNode numberNode, endNode ] pure elem -numberElem :: forall e. Number -> Eff (dom :: DOM | e) Element +numberElem :: Number -> Effect Element numberElem number = do - document <- DOM.htmlDocumentToDocument <$> (DOM.window >>= DOM.document) - container <- DOM.createElement "input" document - DOM.setClassName "number" container - DOM.setAttribute "value" (Number.format number) container + document <- HTMLDocument.toDocument <$> (HTML.window >>= Window.document) + container <- Document.createElement "input" document + Element.setClassName "number" container + Element.setAttribute "value" (Number.format number) container pure container diff --git a/js/src/Main.purs b/js/src/Main.purs index a16b2cd..0ff5fd7 100644 --- a/js/src/Main.purs +++ b/js/src/Main.purs @@ -1,14 +1,13 @@ module Main (main) where -import Control.Monad.Eff (Eff) import Data.Array (catMaybes) as Array import Data.Maybe (Maybe(..)) import Data.Traversable (sequence, sequence_) as Traversable -import DOM (DOM) -import DOM.Node.ParentNode (QuerySelector(..)) -import DOM.Node.Types (elementToNode) as DOM -import DOM.Node.Types (Node) +import Effect (Effect) import Prelude +import Web.DOM.Element (toNode) as Element +import Web.DOM.Internal.Types (Node) +import Web.DOM.ParentNode (QuerySelector(..)) import Dom (selectElement, selectElements, onInput, setValue, selectElementFrom) as Dom import EditableNumber (NumberElem) @@ -16,20 +15,20 @@ import EditableNumber (set) as EditableNumber import Number (format) as Number import Parser (number) as Parser -main :: forall e. Eff (dom :: DOM | e) Unit +main :: Effect Unit main = do tagElems <- getNumberElements numberElems <- Array.catMaybes <$> (Traversable.sequence $ map EditableNumber.set tagElems) Traversable.sequence_ $ map (onInput numberElems) numberElems -getNumberElements :: forall e. Eff (dom :: DOM | e) (Array { tag :: String, node :: Node }) +getNumberElements :: Effect (Array { tag :: String, node :: Node }) getNumberElements = do - let fromElem tag elem = { tag: tag, node: DOM.elementToNode elem } + let fromElem tag elem = { tag: tag, node: Element.toNode elem } h1 <- map (fromElem "h1") <$> Dom.selectElement (QuerySelector "h1") lis <- map (fromElem "li") <$> Dom.selectElements (QuerySelector "ul > li") pure $ (maybeToArray h1 <> lis) -onInput :: forall e. Array NumberElem -> NumberElem -> Eff (dom :: DOM | e) Unit +onInput :: Array NumberElem -> NumberElem -> Effect Unit onInput numberElems { elem, number } = do Dom.onInput elem (\value -> do case Parser.number value of diff --git a/js/src/Parser.purs b/js/src/Parser.purs index 787c09e..b378e96 100644 --- a/js/src/Parser.purs +++ b/js/src/Parser.purs @@ -11,7 +11,7 @@ import Data.Either (Either(Right)) import Data.Int as Int import Data.Maybe (fromMaybe) as Maybe import Data.Maybe (Maybe(Just, Nothing)) -import Data.String as String +import Data.String.CodeUnits as String import Prelude import Text.Parsing.Parser (Parser) import Text.Parsing.Parser (runParser) as Parser diff --git a/js/test/Main.purs b/js/test/Main.purs index e23f9e2..3a4f03c 100644 --- a/js/test/Main.purs +++ b/js/test/Main.purs @@ -2,19 +2,19 @@ module Test.Main ( main ) where -import Control.Monad.Eff (Eff) import Data.Maybe (Maybe(Just, Nothing)) +import Effect (Effect) import Prelude import Test.Spec (describe, it) import Test.Spec.Assertions (shouldEqual) import Test.Spec.Reporter.Console (consoleReporter) -import Test.Spec.Runner (RunnerEffects, run) +import Test.Spec.Runner (run) import Number (roundAt, format) as Number import Parser (TextWithNumber) import Parser (textWithNumber, number) as Parser -main :: Eff (RunnerEffects ()) Unit +main :: Effect Unit main = run [consoleReporter] do describe "Number" do diff --git a/nix/purescript.nix b/nix/purescript.nix new file mode 100644 index 0000000..5b757b4 --- /dev/null +++ b/nix/purescript.nix @@ -0,0 +1,49 @@ +{ stdenv, fetchurl, makeWrapper, patchelf, gmpxx, ncurses5, zlib }: + +with stdenv; with lib; + +mkDerivation rec { + name = "purescript-binary-${version}"; + version = "0.12.0"; + platform = { + "x86_64-linux" = "linux64"; + }.${hostPlatform.system}; + src = fetchurl { + url = + "https://github.com/" + + "purescript/purescript/releases/download/" + + "v${version}/${platform}.tar.gz"; + sha256 = { + "x86_64-linux" = "1wf7n5y8qsa0s2p0nb5q81ck6ajfyp9ijr72bf6j6bhc6pcpgmyc"; + }.${hostPlatform.system}; + name = "purescript.tar.gz"; + }; + + buildInputs = [ makeWrapper ]; + unpackCmd = "tar -xzf $curSrc"; + + installPhase = '' + mkdir -p $out/bin $out/lib + cp purs $out/bin/ + runHook postInstall + ''; + + postInstall = let + libs = makeLibraryPath [ cc.cc gmpxx ncurses5 zlib ]; + in '' + interpreter="$(cat $NIX_CC/nix-support/dynamic-linker)" + ${patchelf}/bin/patchelf \ + --set-interpreter $interpreter \ + $out/bin/purs + wrapProgram $out/bin/purs \ + --prefix LD_LIBRARY_PATH : ${libs} + ''; + + meta = { + description = "A small strongly typed programming language with expressive + types that compiles to JavaScript, written in and inspired by Haskell."; + homepage = http://www.purescript.org/; + license = licenses.bsd3; + platforms = [ "x86_64-linux" ]; + }; +} diff --git a/package.json b/package.json index 7a740d4..7c10e20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, "devDependencies": { - "pulp": "^11.0.0" + "pulp": "^12.3.1" } } diff --git a/psc-package.json b/psc-package.json new file mode 100644 index 0000000..eedb386 --- /dev/null +++ b/psc-package.json @@ -0,0 +1,18 @@ +{ + "name": "cooking", + "set": "psc-0.12.2", + "source": "https://github.com/purescript/package-sets.git", + "depends": [ + "console", + "effect", + "integers", + "math", + "maybe", + "parsing", + "prelude", + "spec", + "strings", + "web-dom", + "web-html" + ] +} diff --git a/recettes/desserts/hyperglucidique/cheesecake.md b/recettes/desserts/hyperglucidique/cheesecake.md index 78951f7..74fe156 100644 --- a/recettes/desserts/hyperglucidique/cheesecake.md +++ b/recettes/desserts/hyperglucidique/cheesecake.md @@ -9,14 +9,14 @@ title: Cheesecake - 800 g de carré frais - 100 g de fromage blanc - 150 g de sucre -- 3 oeufs, -- citron, +- 3 œufs +- 1 citron - 250 g de mascarpone ## Pour le fond - 1 paquet de petit beurre mixé -- noix de pécan caramélisées +- des noix de pécan caramélisées # Recette diff --git a/recettes/plats/galettes.md b/recettes/plats/galettes.md new file mode 100644 index 0000000..8de44be --- /dev/null +++ b/recettes/plats/galettes.md @@ -0,0 +1,30 @@ +--- +title: Galettes +--- + +Ingrédients +=========== + +- 200 g de sarrasin +- 1 L d’eau +- 120 g de farine de froment T55 +- 120 g de farine de seigle T130 +- 60 g de farine aux 5 céréales +- 1 grand verre de lait entier +- 2 cc rases de sel +- 70 g de beurre +- 2 œufs +- 1 CS rase de miel + +Recette +======= + +#. Mélanger les farines. +#. Former un puit et y casser les œufs. +#. Mélanger avec une cuillère en bois ou un fouet. +#. Faire fondre le beurre à la casserole. +#. Ajouter dans la casserole : le lait, le sel et le miel. +#. Verser progressivement dans le puits en remuant. +#. Verser l’eau en remuant. +#. Laisser reposer 4 ou 5 heures. +#. Beurrer la poêle pour chaque galette. diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 7f7f3bd..0000000 --- a/shell.nix +++ /dev/null @@ -1,18 +0,0 @@ -with import {}; { - env = stdenv.mkDerivation { - name = "env"; - buildInputs = with pkgs; [ - stack - purescript - nodePackages.nodemon - nodePackages.bower - yarn - tmux - tmuxinator - nodejs-8_x - ]; - shellHook = '' - export PATH=node_modules/.bin:$PATH; - ''; - }; -} diff --git a/src/Main.hs b/src/Main.hs index 52d0cee..d0dd91f 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,12 +1,12 @@ {-# LANGUAGE OverloadedStrings #-} -import Data.List (sortBy) -import Data.Monoid (mappend) -import Data.Ord (comparing) +import Data.List (sortBy) +import Data.Monoid (mappend) +import Data.Ord (comparing) -import Hakyll -import Hakyll.Core.Item (Item(itemIdentifier)) -import System.Process (readProcess) +import Hakyll +import Hakyll.Core.Item (Item (itemIdentifier)) +import System.Process (readProcess) main :: IO () main = hakyllWith configuration $ do @@ -24,7 +24,7 @@ main = hakyllWith configuration $ do match "js/src/**" $ do route $ customRoute $ const "main.js" compile $ - unsafeCompiler (readProcess "pulp" [ "build", "--optimise", "--src-path", "js" ] "") + unsafeCompiler (readProcess "pulp" [ "--psc-package", "build", "--optimise", "--src-path", "js" ] "") >>= makeItem match "recettes/**" $ do diff --git a/stack.yaml b/stack.yaml index 018615a..cf3cbed 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1 +1,5 @@ -resolver: lts-8.3 +resolver: lts-13.5 + +extra-deps: + - hakyll-4.12.5.0@sha256:f55a2e99e1c2c780d74222c174bfbe77c90e6453c8ba2465a722ea995b69a4da + - lrucache-1.2.0.1@sha256:18fc3d7052012c7ab3cd395160f34b53c5e1ec5379cc45185baf35b90ffadc2e -- cgit v1.2.3