├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── bower.json ├── example ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── package.json ├── spago.dhall ├── src │ ├── App.purs │ ├── Main.purs │ ├── index.html │ └── index.js └── webpack.config.js ├── jest.config.js ├── package.json ├── packages.dhall ├── spago.dhall ├── src └── Html │ ├── Parser.js │ ├── Parser.purs │ └── Renderer │ └── Halogen.purs └── test ├── Main.purs └── main.test.js /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Publish gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Setup node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 14 17 | - name: Build 18 | run: | 19 | cd example 20 | yarn add purescript@0.15 spago 21 | yarn build 22 | - name: Publish 23 | run: | 24 | cp docs/* . 25 | git config user.email "remotenonsense@gmail.com" 26 | git config user.name "GHActions" 27 | git add . && git commit -am 'build gh-pages' 28 | git push -f origin HEAD:gh-pages 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | yarn.lock 4 | 5 | .psc-ide-port 6 | .psci_modules 7 | .purs-repl 8 | .spago 9 | output 10 | docs 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Ping Chen 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-html-parser-halogen 2 | 3 | [![purescript-html-parser-halogen on Pursuit](https://pursuit.purescript.org/packages/purescript-html-parser-halogen/badge)](https://pursuit.purescript.org/packages/purescript-html-parser-halogen) 4 | 5 | A library to render raw HTML string into Halogen views. You might also be interested in [purescript-markdown-it-halogen](https://github.com/nonbili/purescript-markdown-it-halogen), a library to render Markdown into Halogen views. 6 | 7 | [Playground](https://rnons.github.io/purescript-html-parser-halogen/) 8 | 9 | ## How to use 10 | 11 | ```purescript 12 | import Html.Renderer.Halogen as RH 13 | 14 | rawHtml :: String 15 | rawHtml = """a link""" 16 | 17 | render = 18 | ... 19 | HH.div_ [ RH.render_ rawHtml ] 20 | ``` 21 | 22 | It's as simple as this, in most cases you only need the `render` function from `Html.Renderer.Halogen` module. 23 | 24 | ## Be cautious 25 | 26 | This library doesn't support malformed HTML, and is prone to XSS attack. Use it only when you trust the HTML string. 27 | 28 | You can balance and sanitize the HTML on the backend, e.g. `sanitizeBalance` from [xss-sanitize](http://hackage.haskell.org/package/xss-sanitize/docs/Text-HTML-SanitizeXSS.html#v:sanitizeBalance). 29 | 30 | ## How it works 31 | 32 | `Html.Parser` parses HTML `String` as `HtmlNode`. `Html.Renderer.Halogen` converts `HtmlNode` to halogen `HTML`. You can also write adapters to convert `HtmlNode` to the `HTML` type of other view libraries. 33 | 34 | If you want to `Html.Parser` with other view libraries, I can release it as a separate package, let me know if you are interested. 35 | 36 | ## Other approaches to render raw HTML into halogen views 37 | 38 | - https://github.com/slamdata/purescript-halogen/issues/324 39 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { presets: ["@babel/preset-env"] }; 2 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-html-parser-halogen", 3 | "description": "A library to render HTML string into halogen views", 4 | "keywords": [ 5 | "purescript", 6 | "halogen", 7 | "html" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/rnons/purescript-html-parser-halogen.git" 12 | }, 13 | "license": "BSD-3-Clause", 14 | "ignore": [ 15 | "**/.*", 16 | "bower_components", 17 | "docs", 18 | "example", 19 | "node_modules", 20 | "output", 21 | "yarn.lock" 22 | ], 23 | "dependencies": { 24 | "purescript-string-parsers": "^6.0.0", 25 | "purescript-halogen": "^6.1.0" 26 | }, 27 | "devDependencies": { 28 | "purescript-psci-support": "^5.0.0", 29 | "purescript-debug": "^5.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | -------------------------------------------------------------------------------- /example/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Ping Chen 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | yarn 3 | spago build -w 4 | yarn start 5 | ``` 6 | -------------------------------------------------------------------------------- /example/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "ignore": [ 5 | "**/.*", 6 | "node_modules", 7 | "bower_components", 8 | "output" 9 | ], 10 | "dependencies": { 11 | "purescript-string-parsers": "^6.0.0", 12 | "purescript-jest": "^0.5.0", 13 | "purescript-halogen": "^6.1.0" 14 | }, 15 | "devDependencies": { 16 | "purescript-psci-support": "^5.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "BSD-3-Clause", 4 | "scripts": { 5 | "build": "spago build -u '+RTS -N2 -RTS' && webpack --mode production --progress", 6 | "start": "webpack-dev-server --mode development --progress" 7 | }, 8 | "devDependencies": { 9 | "html-webpack-plugin": "^3.2.0", 10 | "webpack": "^4.30.0", 11 | "webpack-cli": "^3.3.1", 12 | "webpack-dev-server": "^3.3.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/spago.dhall: -------------------------------------------------------------------------------- 1 | { name = "example" 2 | , dependencies = 3 | [ "aff" 4 | , "arrays" 5 | , "const" 6 | , "control" 7 | , "dom-indexed" 8 | , "maybe" 9 | , "prelude" 10 | , "transformers" 11 | , "effect" 12 | , "halogen" 13 | ] 14 | , packages = ../packages.dhall 15 | , sources = [ "../src/**/*.purs", "src/**/*.purs" ] 16 | } 17 | -------------------------------------------------------------------------------- /example/src/App.purs: -------------------------------------------------------------------------------- 1 | module App where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.State (class MonadState) 6 | import Data.Const (Const) 7 | import Effect.Aff.Class (class MonadAff) 8 | import Halogen as H 9 | import Halogen.HTML as HH 10 | import Halogen.HTML.Events as HE 11 | import Halogen.HTML.Properties as HP 12 | import Html.Renderer.Halogen as PH 13 | 14 | type Query :: forall k. k -> Type 15 | type Query = Const Void 16 | 17 | data Action = OnValueChange String 18 | 19 | type State = { value:: String } 20 | 21 | 22 | initValue :: String 23 | initValue = """ 24 | 29 | 30 |
31 |

purescript-html-parser-halogen example

32 |
33 |

EDIT

34 | 35 |
36 |
37 |

PREVIEW

38 |
39 |
40 | 44 |
45 | """ 46 | 47 | initialState :: State 48 | initialState = { value: initValue } 49 | 50 | class_ :: forall r i. String -> HP.IProp ("class" :: String | r) i 51 | class_ = HP.class_ <<< HH.ClassName 52 | 53 | style :: forall r i. String -> HP.IProp ("style" :: String | r) i 54 | style = HP.attr (HH.AttrName "style") 55 | 56 | component :: forall query input output m. MonadAff m => H.Component query input output m 57 | component = H.mkComponent 58 | { initialState: const initialState 59 | , render 60 | , eval: H.mkEval $ H.defaultEval 61 | { handleAction = handleAction } 62 | } 63 | where 64 | 65 | render state = 66 | HH.div [ class_ "grid" ] 67 | [ HH.h2 [ class_ "header" ] 68 | [ HH.text "purescript-html-parser-halogen example" ] 69 | , HH.div [ class_ "col col-edit" ] 70 | [ HH.h4_ [ HH.text "EDIT" ] 71 | , HH.textarea 72 | [ class_ "edit" 73 | , HP.value state.value 74 | , HE.onValueInput \s -> OnValueChange s 75 | ] 76 | ] 77 | , HH.div [ class_ "col col-preview" ] 78 | [ HH.h4_ [ HH.text "PREVIEW" ] 79 | , HH.div [ class_ "preview" ] 80 | [ PH.render_ state.value ] 81 | ] 82 | , HH.div [ class_ "footer" ] 83 | [ HH.a 84 | [ HP.href demoSourceUrl] [HH.text "source code"] 85 | , HH.text " Powered by " 86 | , HH.img 87 | [ HP.src "https://upload.wikimedia.org/wikipedia/commons/6/64/PureScript_Logo.png" 88 | , style "width: 1rem; height: 1rem" 89 | ] 90 | ] 91 | ] 92 | where 93 | repoUrl = "https://github.com/rnons/purescript-html-parser-halogen" 94 | demoSourceUrl = repoUrl <> "/tree/master/example" 95 | 96 | handleAction :: forall m. 97 | MonadState State m => 98 | Action -> m Unit 99 | handleAction = case _ of 100 | OnValueChange value -> do 101 | H.modify_ $ _ { value = value } 102 | -------------------------------------------------------------------------------- /example/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | import Effect (Effect) 5 | import Halogen.Aff as HA 6 | import Halogen.VDom.Driver (runUI) 7 | import App (component) 8 | 9 | main :: Effect Unit 10 | main = HA.runHalogenAff do 11 | body <- HA.awaitBody 12 | runUI component unit body 13 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | purescript-html-parser-halogen example 6 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import { main } from "Main"; 2 | 3 | main(); 4 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: "./src/index.js", 6 | output: { 7 | path: __dirname + "/../docs", 8 | filename: "index.js" 9 | }, 10 | resolve: { 11 | modules: ["node_modules", "output"], 12 | extensions: [".js"] 13 | }, 14 | module: { 15 | rules: [] 16 | }, 17 | 18 | plugins: [ 19 | new HtmlWebpackPlugin({ 20 | template: "src/index.html", 21 | filename: "index.html", 22 | minify: { 23 | collapseWhitespace: true, 24 | minifyCSS: true 25 | } 26 | }) 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | testEnvironment: "jsdom", 6 | transform: { 7 | "^.+\\.(js|jsx)$": "babel-jest", 8 | }, 9 | 10 | // An array of directory names to be searched recursively up from the requiring module's location 11 | moduleDirectories: ["node_modules", "output"], 12 | 13 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 14 | testPathIgnorePatterns: ["/node_modules/", "/bower_components/", ".spago"] 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "spago build -u '+RTS -N2 -RTS'", 4 | "test": "spago build && jest" 5 | }, 6 | "devDependencies": { 7 | "@babel/preset-env": "^7.18.10", 8 | "babel-jest": "^28.1.3", 9 | "jest": "^28.1.0", 10 | "jest-environment-jsdom": "^28.1.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.4-20220808/packages.dhall 3 | sha256:60eee64b04ca0013fae3e02a69fc3b176105c6baa2f31865c67cd5f881a412fd 4 | 5 | let extra = 6 | { jest = 7 | { dependencies = [ "effect", "aff", "aff-promise" ] 8 | , repo = "https://github.com/nonbili/purescript-jest.git" 9 | , version = "v1.0.0" 10 | } 11 | } 12 | 13 | in upstream // extra 14 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | { name = "html-parser-halogen" 2 | , dependencies = 3 | [ "arrays" 4 | , "control" 5 | , "dom-indexed" 6 | , "foldable-traversable" 7 | , "effect" 8 | , "halogen" 9 | , "maybe" 10 | , "prelude" 11 | , "psci-support" 12 | , "jest" 13 | ] 14 | , packages = ./packages.dhall 15 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 16 | } 17 | -------------------------------------------------------------------------------- /src/Html/Parser.js: -------------------------------------------------------------------------------- 1 | function getAttributes(node) { 2 | var entries = []; 3 | for (var i = 0; i < node.attributes.length; i++) { 4 | let { name, value } = node.attributes.item(i); 5 | entries.push([name, value]); 6 | } 7 | return entries; 8 | } 9 | 10 | function walk(treeWalker) { 11 | var nodes = []; 12 | 13 | function handleNode(node) { 14 | if (["#comment", "#text"].includes(node.nodeName)) { 15 | var text = node.textContent; 16 | if (text) { 17 | nodes.push({ 18 | type: node.nodeName.slice(1), 19 | text 20 | }); 21 | } 22 | } else { 23 | var children = walk(treeWalker); 24 | treeWalker.currentNode = node; 25 | nodes.push({ 26 | type: "element", 27 | name: node.localName, 28 | attributes: getAttributes(node), 29 | children 30 | }); 31 | } 32 | } 33 | 34 | var currentNode = treeWalker.currentNode; 35 | var firstChild = treeWalker.firstChild(); 36 | if (firstChild) { 37 | handleNode(firstChild); 38 | } else { 39 | return nodes; 40 | } 41 | 42 | var nextSibling = treeWalker.nextSibling(); 43 | while (nextSibling) { 44 | handleNode(nextSibling); 45 | treeWalker.currentNode = nextSibling; 46 | nextSibling = treeWalker.nextSibling(); 47 | } 48 | 49 | return nodes; 50 | } 51 | 52 | export const parseFromString = elementCtor => attributeCtor => textCtor => commentCtor => input => { 53 | function mapNode(node) { 54 | if (node.type == "element") { 55 | return elementCtor({ 56 | name: node.name, 57 | attributes: node.attributes.map(([k, v]) => attributeCtor(k)(v)), 58 | children: node.children.map(mapNode) 59 | }); 60 | } else { 61 | var ctor = node.type == "text" ? textCtor : commentCtor; 62 | return ctor(node.text); 63 | } 64 | } 65 | 66 | var doc = new DOMParser().parseFromString(input, "text/html"); 67 | var headNodes = walk( 68 | doc.createTreeWalker(doc.documentElement.querySelector("head")) 69 | ); 70 | var bodyNodes = walk( 71 | doc.createTreeWalker(doc.documentElement.querySelector("body")) 72 | ); 73 | 74 | return [...headNodes, ...bodyNodes].map(node => { 75 | if (node.type == "element") { 76 | return mapNode(node); 77 | } else { 78 | var ctor = node.type == "text" ? textCtor : commentCtor; 79 | return ctor(node.text); 80 | } 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /src/Html/Parser.purs: -------------------------------------------------------------------------------- 1 | module Html.Parser 2 | ( HtmlNode(..) 3 | , Element(..) 4 | , HtmlAttribute(..) 5 | , parse 6 | ) where 7 | 8 | import Prelude 9 | 10 | data HtmlNode 11 | = HtmlElement Element 12 | | HtmlText String 13 | | HtmlComment String 14 | 15 | derive instance eqHtmlNode :: Eq HtmlNode 16 | 17 | type Element = 18 | { name :: String 19 | , attributes :: Array HtmlAttribute 20 | , children :: Array HtmlNode 21 | } 22 | 23 | data HtmlAttribute = HtmlAttribute String String 24 | 25 | derive instance eqHtmlAttribute :: Eq HtmlAttribute 26 | 27 | foreign import parseFromString 28 | :: (Element -> HtmlNode) 29 | -> (String -> String -> HtmlAttribute) 30 | -> (String -> HtmlNode) 31 | -> (String -> HtmlNode) 32 | -> String 33 | -> Array HtmlNode 34 | 35 | parse :: String -> Array HtmlNode 36 | parse input = 37 | parseFromString HtmlElement HtmlAttribute HtmlText HtmlComment input 38 | -------------------------------------------------------------------------------- /src/Html/Renderer/Halogen.purs: -------------------------------------------------------------------------------- 1 | module Html.Renderer.Halogen 2 | ( htmlAttributeToProp 3 | , elementToHtml 4 | , nodeToHtml 5 | , render_ 6 | , render 7 | , renderToArray 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Control.Alt ((<|>)) 13 | import DOM.HTML.Indexed (HTMLdiv) 14 | import Data.Array as Array 15 | import Data.Maybe (Maybe(..)) 16 | import Halogen.HTML as HH 17 | import Halogen.HTML.Properties as HP 18 | import Html.Parser (HtmlNode(..), HtmlAttribute(..), Element, parse) 19 | 20 | htmlAttributeToProp :: forall r i. HtmlAttribute -> HP.IProp r i 21 | htmlAttributeToProp (HtmlAttribute k v) = HP.attr (HH.AttrName k) v 22 | 23 | elementToHtml :: forall p i. Maybe HH.Namespace -> Element -> HH.HTML p i 24 | elementToHtml mParentNs ele = 25 | ctor 26 | (HH.ElemName ele.name) 27 | (Array.fromFoldable $ map htmlAttributeToProp ele.attributes) 28 | children 29 | where 30 | mCurNs = Array.find (\(HtmlAttribute k _) -> k == "xmlns") ele.attributes <#> 31 | \(HtmlAttribute _ v) -> HH.Namespace v 32 | mNs = mCurNs <|> mParentNs 33 | children = ele.children <#> nodeToHtml mNs 34 | ctor = case mNs of 35 | Just ns -> HH.elementNS ns 36 | Nothing -> HH.element 37 | 38 | nodeToHtml :: forall p i. Maybe HH.Namespace -> HtmlNode -> HH.HTML p i 39 | nodeToHtml mNs (HtmlElement ele) = elementToHtml mNs ele 40 | nodeToHtml _ (HtmlText str) = HH.text str 41 | nodeToHtml _ (HtmlComment _) = HH.text "" 42 | 43 | render_ :: forall p i. String -> HH.HTML p i 44 | render_ = render [] 45 | 46 | render :: forall p i. Array (HH.IProp HTMLdiv i) -> String -> HH.HTML p i 47 | render props = HH.div props <<< renderToArray 48 | 49 | renderToArray :: forall p i. String -> Array (HH.HTML p i) 50 | renderToArray raw = map (nodeToHtml Nothing) $ parse raw 51 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Array as Array 6 | import Data.Foldable (sequence_) 7 | import Effect (Effect) 8 | import Html.Parser (HtmlAttribute(..), HtmlNode(..), parse) 9 | import Jest (expectToBeTrue, expectToEqual, test) 10 | 11 | type Spec = 12 | { name :: String 13 | , raw :: String 14 | , expected :: Array HtmlNode 15 | } 16 | 17 | simpleATagExpected :: HtmlNode 18 | simpleATagExpected = HtmlElement 19 | { name: "a" 20 | , attributes: 21 | [ HtmlAttribute "href" "https://purescript.org" 22 | , HtmlAttribute "target" "blank_" 23 | ] 24 | , children: [HtmlText "purescript"] 25 | } 26 | 27 | specs :: Array Spec 28 | specs = 29 | [ { name: "plain text" 30 | , raw: "abc" 31 | , expected: [HtmlText "abc"] 32 | } 33 | , { name: "simple tag" 34 | , raw: "purescript" 35 | , expected: [simpleATagExpected] 36 | } 37 | , { name: "html entities" 38 | , raw: "
a&b
" 39 | , expected: 40 | [ HtmlElement 41 | { name: "div" 42 | , attributes: [] 43 | , children: [HtmlText "a&b"] 44 | } 45 | ] 46 | } 47 | , { name: "tag inside text" 48 | , raw: "a b c" 49 | , expected: 50 | [ HtmlText "a " 51 | , HtmlElement 52 | { name: "b" 53 | , attributes: [] 54 | , children: [HtmlText "b"] 55 | } 56 | , HtmlText " c" 57 | ] 58 | } 59 | , { name: "non breaking space" 60 | -- The first character is a non breaking space (code point 160). 61 | , raw: "  1234" 62 | , expected: [HtmlText "  1234"] 63 | } 64 | , { name: "" 66 | , expected: 67 | [ HtmlElement 68 | { name: "script" 69 | , attributes: [] 70 | , children: [HtmlText "ac"] 71 | } 72 | ] 73 | } 74 | ] 75 | 76 | rightHtml :: Array String 77 | rightHtml = 78 | [ "" 79 | , "" 80 | , "" 81 | , """""" 82 | , "" 83 | , """""" 84 | , "" 85 | ] 86 | 87 | main :: Effect Unit 88 | main = do 89 | sequence_ $ specs <#> \spec -> do 90 | test spec.name $ expectToEqual (parse spec.raw) spec.expected 91 | 92 | sequence_ $ rightHtml <#> \html -> do 93 | test ("should parse right: " <> html) $ 94 | expectToBeTrue $ Array.length (parse html) > 0 95 | -------------------------------------------------------------------------------- /test/main.test.js: -------------------------------------------------------------------------------- 1 | const Test = require("Test.Main"); 2 | 3 | Test.main(); 4 | --------------------------------------------------------------------------------