├── .babelrc ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── demo ├── .gitignore ├── Create-Elm-App-README.md ├── build.sh ├── elm.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo.svg │ └── manifest.json ├── src │ ├── Controls.elm │ ├── Editor.elm │ ├── ExtraMarks.elm │ ├── Links.elm │ ├── Main.elm │ ├── Page.elm │ ├── Page │ │ ├── Basic.elm │ │ ├── Examples.elm │ │ ├── Home.elm │ │ ├── Markdown.elm │ │ ├── SpecExtension.elm │ │ └── SpecFromScratch.elm │ ├── Route.elm │ ├── Session.elm │ ├── css │ │ ├── demo.css │ │ ├── main.css │ │ ├── modal.css │ │ ├── reset.css │ │ └── rte.css │ └── index.js └── tests │ ├── Commands │ ├── TestBackspaceBlock.elm │ ├── TestBackspaceInlineElement.elm │ ├── TestBackspaceText.elm │ ├── TestBackspaceWord.elm │ ├── TestDeleteBlock.elm │ ├── TestDeleteInlineElement.elm │ ├── TestDeleteText.elm │ ├── TestDeleteWord.elm │ ├── TestInsertAfterBlockLeaf.elm │ ├── TestInsertBlock.elm │ ├── TestInsertInline.elm │ ├── TestInsertLineBreak.elm │ ├── TestInsertText.elm │ ├── TestJoinBackward.elm │ ├── TestJoinForward.elm │ ├── TestLift.elm │ ├── TestLiftEmpty.elm │ ├── TestRemoveRange.elm │ ├── TestRemoveRangeAndInsert.elm │ ├── TestRemoveSelectedLeafElement.elm │ ├── TestSelectAll.elm │ ├── TestSplitBlock.elm │ ├── TestToggleBlock.elm │ ├── TestToggleMark.elm │ └── TestWrap.elm │ ├── List │ ├── README.md │ ├── TestJoinBackward.elm │ ├── TestJoinForward.elm │ ├── TestLift.elm │ ├── TestLiftEmpty.elm │ ├── TestSplit.elm │ └── TestWrap.elm │ ├── Model │ ├── TestMark.elm │ └── TestNode.elm │ ├── README.md │ ├── SimpleSpec.elm │ ├── TestAnnotation.elm │ ├── TestDomNode.elm │ ├── TestHtml.elm │ ├── TestNode.elm │ ├── TestPath.elm │ ├── TestSpec.elm │ └── TestState.elm ├── elm.json ├── js └── elmEditor.js ├── package-lock.json ├── package.json ├── publish.sh └── src └── RichText ├── Annotation.elm ├── Commands.elm ├── Config ├── Command.elm ├── Decorations.elm ├── ElementDefinition.elm ├── Keys.elm ├── MarkDefinition.elm └── Spec.elm ├── Definitions.elm ├── Editor.elm ├── Html.elm ├── Internal ├── BeforeInput.elm ├── Constants.elm ├── Definitions.elm ├── DeleteWord.elm ├── DomNode.elm ├── Editor.elm ├── Event.elm ├── History.elm ├── HtmlNode.elm ├── KeyDown.elm ├── Paste.elm ├── Path.elm ├── Selection.elm └── Spec.elm ├── List.elm ├── Model ├── Attribute.elm ├── Element.elm ├── History.elm ├── HtmlNode.elm ├── InlineElement.elm ├── Mark.elm ├── Node.elm ├── Selection.elm ├── State.elm └── Text.elm ├── Node.elm └── State.elm /.babelrc: -------------------------------------------------------------------------------- 1 | // .babelrc 2 | { 3 | "presets": ["env"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution 2 | build/ 3 | 4 | # elm-package generated files 5 | elm-stuff 6 | 7 | # elm-repl generated files 8 | repl-temp-* 9 | 10 | # Dependency directories 11 | node_modules 12 | 13 | # Desktop Services Store on macOS 14 | .DS_Store 15 | 16 | # Ignore IDE files 17 | .idea 18 | 19 | js-dist 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | In the interest of fostering an open and welcoming environment, we as 7 | contributors and maintainers pledge to make participation in our project and 8 | our community a harassment-free experience for everyone, regardless of age, body 9 | size, disability, ethnicity, sex characteristics, gender identity and expression, 10 | level of experience, education, socio-economic status, nationality, personal 11 | appearance, race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies within all project spaces, and it also applies when 50 | an individual is representing the project or its community in public spaces. 51 | Examples of representing a project or community include using an official 52 | project e-mail address, posting via an official social media account, or acting 53 | as an appointed representative at an online or offline event. Representation of 54 | a project may be further defined and clarified by project maintainers. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported by contacting me at michael.josh.weiss@gmail.com . All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Michael Weiss 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rich Text Editor Toolkit 2 | Create rich text editors in Elm. 3 | 4 | Rich Text Editor Toolkit is an open source project to make cross platform editors on the web. This package treats contenteditable as an I/O device, and uses browser events and mutation observers to detect changes and update itself. The editor's model is defined and validated by a programmable specification that allows you to create a custom tailored editor that fits your needs. 5 | 6 | This package was heavily inspired by other rich text editor frameworks like ProseMirror, Trix, and DraftJS. 7 | 8 | ## Resources 9 | - Elm Package: https://package.elm-lang.org/packages/mweiss/elm-rte-toolkit/latest/ 10 | - Demo page: https://mweiss.github.io/elm-rte-toolkit (source code is in the [demo](demo) directory) 11 | - Wiki: https://github.com/mweiss/elm-rte-toolkit/wiki 12 | 13 | ## Getting started 14 | 15 | This package requires some webcomponents to get started. 16 | 17 | If you can support ES6, you can include [js/elmEditor.js](js/elmEditor.js) in your project and import it 18 | along with your favorite webcomponent polyfill. 19 | 20 | ```js 21 | import '@webcomponents/webcomponentsjs/webcomponents-bundle.js' 22 | import 'elmEditor.js' 23 | ``` 24 | 25 | The demo in this repository does it that way. 26 | 27 | However, if you want to use a bundler and polyfill, you can import your favorite polyfill and 28 | import the npm package that has this repository's js compiled to es5 with npm, e.g: 29 | 30 | ```bash 31 | npm install --save @webcomponents/webcomponentsjs 32 | npm install --save elm-rte-toolkit 33 | ``` 34 | 35 | And in your javascript, you can import it like so: 36 | 37 | ```js 38 | import '@webcomponents/webcomponentsjs/webcomponents-bundle.js' 39 | import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js' 40 | import 'elm-rte-toolkit'; 41 | ``` 42 | 43 | ### Starting CSS 44 | 45 | You can use whatever styles you want for the editor, but you may want to use the following as 46 | a jumping off point. Most importantly, you'll probably want `white-space: pre-wrap;` to distinguish 47 | between multiple spaces: 48 | 49 | ```css 50 | .rte-main { 51 | text-align: left; 52 | outline: none; 53 | user-select: text; 54 | -webkit-user-select: text; 55 | white-space: pre-wrap; 56 | word-wrap: break-word; 57 | } 58 | 59 | .rte-hide-caret { 60 | caret-color: transparent; 61 | } 62 | 63 | ``` 64 | 65 | ## Contributing 66 | 67 | This package is open-source software, freely distributable under the terms of an [BSD-3-Clause license](LICENSE). The [source code is hosted on GitHub](https://github.com/mweiss/elm-rte-toolkit). 68 | 69 | Contributions in the form of bug reports, pull requests, or thoughtful discussions in the [GitHub issue tracker](https://github.com/mweiss/elm-rte-toolkit/issues) are welcome. Please see the [Code of Conduct](CODE_OF_CONDUCT.md) for our pledge to contributors. 70 | 71 | ### Running the demo 72 | 73 | The demo was bootstrapped with [create-elm-app](https://github.com/halfzebra/create-elm-app). See that repository for instructions of how to install the `elm-app` command. 74 | 75 | To debug the demo locally, run the following from the repository's root directory: 76 | ```bash 77 | cd demo 78 | elm-app start 79 | ``` 80 | 81 | To build the demo, run the following from the repository's root directory: 82 | ```bash 83 | cd demo 84 | ./build.sh 85 | ``` 86 | 87 | The demo is hosted with gh-pages, so to update the demo, please update the gh-pages branch with the latest 88 | build. 89 | 90 | ### Running tests 91 | 92 | For now, because of mysterious package issues with elm-test I don't want to debug, 93 | tests for the package are in the demo app folder. To run tests: 94 | 95 | ```bash 96 | cd demo 97 | elm-test 98 | ``` 99 | 100 | 101 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution 2 | build/ 3 | 4 | # elm-package generated files 5 | elm-stuff 6 | 7 | # elm-repl generated files 8 | repl-temp-* 9 | 10 | # Dependency directories 11 | node_modules 12 | 13 | # Desktop Services Store on macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /demo/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | elm-app build 4 | sed -i '.bak' 's+/static+static+g' build/index.html 5 | -------------------------------------------------------------------------------- /demo/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "../src" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/browser": "1.0.1", 11 | "elm/core": "1.0.2", 12 | "elm/html": "1.0.0", 13 | "elm/json": "1.1.2", 14 | "elm/regex": "1.0.0", 15 | "elm/url": "1.0.0", 16 | "elm-community/array-extra": "2.1.0", 17 | "elm-community/json-extra": "4.2.0", 18 | "elm-community/list-extra": "8.2.2", 19 | "folkertdev/elm-deque": "3.0.1", 20 | "hecrj/html-parser": "2.3.4", 21 | "lattyware/elm-fontawesome": "4.0.0", 22 | "pablohirafuji/elm-markdown": "2.0.5" 23 | }, 24 | "indirect": { 25 | "elm/parser": "1.1.0", 26 | "elm/svg": "1.0.1", 27 | "elm/time": "1.0.0", 28 | "elm/virtual-dom": "1.0.2", 29 | "rtfeldman/elm-hex": "1.0.0", 30 | "rtfeldman/elm-iso8601-date-strings": "1.1.3" 31 | } 32 | }, 33 | "test-dependencies": { 34 | "direct": { 35 | "elm-explorations/test": "1.0.0" 36 | }, 37 | "indirect": { 38 | "elm/random": "1.0.0" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@webcomponents/webcomponentsjs": { 8 | "version": "2.4.3", 9 | "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.3.tgz", 10 | "integrity": "sha512-cV4+sAmshf8ysU2USutrSRYQkJzEYKHsRCGa0CkMElGpG5747VHtkfsW3NdVIBV/m2MDKXTDydT4lkrysH7IFA==", 11 | "dev": true 12 | }, 13 | "elm-rte-toolkit": { 14 | "version": "1.0.1", 15 | "resolved": "https://registry.npmjs.org/elm-rte-toolkit/-/elm-rte-toolkit-1.0.1.tgz", 16 | "integrity": "sha512-ctoXiE40xeRTRw5xty4WXaITXcvPAE4El2okngvqIQa23k/Ijn1cIPsMk7UNEBKYqrlWXoaqHHyn7VUvQpLNcQ==" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "Elm rte demo", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "dependencies": { 10 | "elm-rte-toolkit": "^1.0.1" 11 | }, 12 | "devDependencies": { 13 | "@webcomponents/webcomponentsjs": "^2.4.3" 14 | }, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mweiss/elm-rte-toolkit/416950f8faa771b6587bb33cb8f96b1ff09a844c/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | Rich Text Editor Toolkit 17 | 18 | 19 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 14 | 15 | 22 | 23 | 26 | 27 | 30 | 31 | 34 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Elm App", 3 | "name": "Create Elm App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/ExtraMarks.elm: -------------------------------------------------------------------------------- 1 | module ExtraMarks exposing (..) 2 | 3 | import RichText.Config.MarkDefinition 4 | exposing 5 | ( HtmlToMark 6 | , MarkDefinition 7 | , MarkToHtml 8 | , defaultHtmlToMark 9 | , markDefinition 10 | ) 11 | import RichText.Model.HtmlNode exposing (HtmlNode(..)) 12 | 13 | 14 | underline : MarkDefinition 15 | underline = 16 | markDefinition 17 | { name = "underline" 18 | , toHtmlNode = underlineToHtmlNode 19 | , fromHtmlNode = htmlNodeToUnderline 20 | } 21 | 22 | 23 | underlineToHtmlNode : MarkToHtml 24 | underlineToHtmlNode _ children = 25 | ElementNode "u" [] children 26 | 27 | 28 | htmlNodeToUnderline : HtmlToMark 29 | htmlNodeToUnderline = 30 | defaultHtmlToMark "u" 31 | 32 | 33 | strikethrough : MarkDefinition 34 | strikethrough = 35 | markDefinition 36 | { name = "strikethrough" 37 | , toHtmlNode = strikethroughToHtmlNode 38 | , fromHtmlNode = htmlNodeToStrikethrough 39 | } 40 | 41 | 42 | strikethroughToHtmlNode : MarkToHtml 43 | strikethroughToHtmlNode _ children = 44 | ElementNode "s" [] children 45 | 46 | 47 | htmlNodeToStrikethrough : HtmlToMark 48 | htmlNodeToStrikethrough = 49 | defaultHtmlToMark "s" 50 | -------------------------------------------------------------------------------- /demo/src/Links.elm: -------------------------------------------------------------------------------- 1 | module Links exposing (..) 2 | 3 | 4 | rteToolkit : String 5 | rteToolkit = 6 | "https://github.com/mweiss/elm-rte-toolkit" 7 | -------------------------------------------------------------------------------- /demo/src/Page.elm: -------------------------------------------------------------------------------- 1 | module Page exposing (Page(..), view) 2 | 3 | import Browser exposing (Document) 4 | import FontAwesome.Styles 5 | import Html exposing (Html, a, article, button, div, footer, header, i, img, li, nav, p, span, text, ul) 6 | import Html.Attributes exposing (class, classList, href, style) 7 | import Html.Events exposing (onClick) 8 | import Route exposing (Route) 9 | 10 | 11 | {-| Determines which navbar link (if any) will be rendered as active. 12 | Note that we don't enumerate every page here, because the navbar doesn't 13 | have links for every page. Anything that's not part of the navbar falls 14 | under Other. 15 | -} 16 | type Page 17 | = Basic 18 | | Markdown 19 | | SpecExtension 20 | | SpecFromScratch 21 | | Home 22 | | Examples 23 | 24 | 25 | {-| Take a page's Html and frames it with a header and footer. 26 | The caller provides the current user, so we can display in either 27 | "signed in" (rendering username) or "signed out" mode. 28 | isLoading is for determining whether we should show a loading spinner 29 | in the header. (This comes up during slow page transitions.) 30 | -} 31 | view : Page -> { title : String, content : List (Html msg) } -> Document msg 32 | view page { title, content } = 33 | { title = title 34 | , body = fontAwesomeStyle :: viewHeader page :: viewContent content :: [ viewFooter ] 35 | } 36 | 37 | 38 | fontAwesomeStyle : Html msg 39 | fontAwesomeStyle = 40 | -- Fix to Issue #20: 41 | -- Wrap font awesome styles in a div because of dangerous extensions: https://discourse.elm-lang.org/t/runtime-errors-caused-by-chrome-extensions/ 42 | Html.div [] 43 | [ FontAwesome.Styles.css ] 44 | 45 | 46 | viewContent : List (Html msg) -> Html msg 47 | viewContent content = 48 | article [] content 49 | 50 | 51 | viewHeader : Page -> Html msg 52 | viewHeader page = 53 | header [] 54 | [ nav [] 55 | [ a [ class "logo", Route.href Route.Home ] 56 | [ text "Rich Text Editor Toolkit" ] 57 | , div [ class "nav-links" ] <| 58 | navbarLink page Route.Home [ text "Home" ] 59 | :: viewMenu page 60 | ] 61 | ] 62 | 63 | 64 | viewMenu : Page -> List (Html msg) 65 | viewMenu page = 66 | let 67 | linkTo = 68 | navbarLink page 69 | in 70 | [ linkTo Route.Examples [ text "Examples" ] 71 | , navbarExternalLink "https://github.com/mweiss/elm-rte-toolkit" [ text "Github" ] 72 | ] 73 | 74 | 75 | viewFooter : Html msg 76 | viewFooter = 77 | footer [] 78 | [ div [ class "container" ] 79 | [ span [ class "attribution" ] 80 | [ text "This is a demo for the " 81 | , a [ href "https://github.com/mweiss/elm-rte-toolkit" ] [ text " Elm Rich Text Editor Toolkit" ] 82 | , text ". Code & design licensed under BSD-3-Clause License." 83 | ] 84 | ] 85 | ] 86 | 87 | 88 | navbarLink : Page -> Route -> List (Html msg) -> Html msg 89 | navbarLink page route linkContent = 90 | a [ classList [ ( "nav-link", True ), ( "active", isActive page route ) ], Route.href route ] linkContent 91 | 92 | 93 | navbarExternalLink : String -> List (Html msg) -> Html msg 94 | navbarExternalLink href linkContent = 95 | a [ class "nav-link", Html.Attributes.href href ] linkContent 96 | 97 | 98 | isActive : Page -> Route -> Bool 99 | isActive page route = 100 | case ( page, route ) of 101 | ( Home, Route.Home ) -> 102 | True 103 | 104 | ( Basic, Route.Basic ) -> 105 | True 106 | 107 | ( Markdown, Route.Markdown ) -> 108 | True 109 | 110 | _ -> 111 | False 112 | -------------------------------------------------------------------------------- /demo/src/Page/Basic.elm: -------------------------------------------------------------------------------- 1 | module Page.Basic exposing (..) 2 | 3 | import Controls exposing (EditorMsg(..)) 4 | import Editor 5 | import Html exposing (Html, a, h1, p, text) 6 | import Html.Attributes exposing (href, title) 7 | import Links exposing (rteToolkit) 8 | import RichText.Definitions as Specs 9 | import RichText.Editor as RTE 10 | import Session exposing (Session) 11 | 12 | 13 | type alias Model = 14 | { session : Session 15 | , editor : Editor.Model 16 | } 17 | 18 | 19 | type Msg 20 | = EditorMsg Editor.EditorMsg 21 | | GotSession Session 22 | 23 | 24 | config = 25 | RTE.config 26 | { decorations = Editor.decorations 27 | , commandMap = Editor.commandBindings Specs.markdown 28 | , spec = Specs.markdown 29 | , toMsg = InternalMsg 30 | } 31 | 32 | 33 | view : Model -> { title : String, content : List (Html Msg) } 34 | view model = 35 | { title = "Basic" 36 | , content = 37 | [ h1 [] [ text "Basic example" ] 38 | , p [] 39 | [ text """You can use this package to create all sorts of editors. Trying to write 40 | one from scratch can be a little overwhelming though, so the package provides a 41 | default spec and default commands as a jumping off point for your own editor. 42 | In this example, we use the default spec to create an editor which supports 43 | things like headers, lists, as well as links and images.""" 44 | ] 45 | , p [] 46 | [ text "You can see the code for this example in the " 47 | , a 48 | [ title "git repo" 49 | , href (rteToolkit ++ "/tree/master/demo/src/Page/Basic.elm") 50 | ] 51 | [ text "git repo." ] 52 | ] 53 | , Html.map EditorMsg (Editor.view config model.editor) 54 | ] 55 | } 56 | 57 | 58 | init : Session -> ( Model, Cmd Msg ) 59 | init session = 60 | ( { session = session 61 | , editor = Editor.init Editor.initialState 62 | } 63 | , Cmd.none 64 | ) 65 | 66 | 67 | update : Msg -> Model -> ( Model, Cmd Msg ) 68 | update msg model = 69 | case msg of 70 | EditorMsg editorMsg -> 71 | let 72 | ( e, _ ) = 73 | Editor.update config editorMsg model.editor 74 | in 75 | ( { model | editor = e }, Cmd.none ) 76 | 77 | _ -> 78 | ( model, Cmd.none ) 79 | 80 | 81 | toSession : Model -> Session 82 | toSession model = 83 | model.session 84 | 85 | 86 | subscriptions : Model -> Sub Msg 87 | subscriptions model = 88 | Session.changes GotSession (Session.navKey model.session) 89 | -------------------------------------------------------------------------------- /demo/src/Page/Examples.elm: -------------------------------------------------------------------------------- 1 | module Page.Examples exposing (..) 2 | 3 | import Html exposing (Html, a, h1, h2, li, p, text, ul) 4 | import Html.Attributes exposing (class) 5 | import Route exposing (Route) 6 | import Session exposing (Session) 7 | 8 | 9 | type alias Model = 10 | { session : Session } 11 | 12 | 13 | type Msg 14 | = GotSession Session 15 | 16 | 17 | values : List { title : String, route : Route, text : String } 18 | values = 19 | [ { title = "Basics" 20 | , text = 21 | "This example shows how to set up a minimal " 22 | ++ "rich text editor with the default configuration." 23 | , route = Route.Basic 24 | } 25 | , { title = "Markdown" 26 | , text = 27 | "This example shows how you can switch between a " 28 | ++ "plain markdown editor and a fancier rich text editor." 29 | , route = Route.Markdown 30 | } 31 | , { title = "Extend a specification" 32 | , text = 33 | "This example shows how you can extend the default specification " 34 | ++ "with your own mark and element definitions." 35 | , route = Route.SpecExtension 36 | } 37 | , { title = "New specification" 38 | , text = "This example shows how you can create a new document specification from scratch." 39 | , route = Route.SpecFromScratch 40 | } 41 | ] 42 | 43 | 44 | view : Model -> { title : String, content : List (Html Msg) } 45 | view _ = 46 | { title = "Examples" 47 | , content = 48 | [ h1 [] [ text "Examples of some of the things this toolkit can do" ] 49 | , ul [ class "grid-list" ] <| 50 | List.map 51 | (\v -> 52 | li [] 53 | [ a [ class "blocklink", Route.href v.route ] 54 | [ h2 [] [ text v.title ] 55 | , p [] 56 | [ text <| 57 | v.text 58 | ] 59 | ] 60 | ] 61 | ) 62 | values 63 | ] 64 | } 65 | 66 | 67 | init : Session -> ( Model, Cmd Msg ) 68 | init session = 69 | ( { session = session }, Cmd.none ) 70 | 71 | 72 | update : Msg -> Model -> ( Model, Cmd Msg ) 73 | update _ model = 74 | ( model, Cmd.none ) 75 | 76 | 77 | toSession : Model -> Session 78 | toSession model = 79 | model.session 80 | 81 | 82 | subscriptions : Model -> Sub Msg 83 | subscriptions model = 84 | Session.changes GotSession (Session.navKey model.session) 85 | -------------------------------------------------------------------------------- /demo/src/Route.elm: -------------------------------------------------------------------------------- 1 | module Route exposing (Route(..), fromUrl, href, replaceUrl) 2 | 3 | import Browser.Navigation as Nav 4 | import Html exposing (Attribute) 5 | import Html.Attributes as Attr 6 | import Url exposing (Url) 7 | import Url.Parser as Parser exposing ((), Parser, oneOf, s) 8 | 9 | 10 | 11 | -- ROUTING 12 | 13 | 14 | type Route 15 | = Basic 16 | | Markdown 17 | | SpecExtension 18 | | SpecFromScratch 19 | | Home 20 | | Examples 21 | 22 | 23 | parser : Parser (Route -> a) a 24 | parser = 25 | oneOf 26 | [ Parser.map Home Parser.top 27 | , Parser.map Examples (s "examples") 28 | , Parser.map Basic (s "examples" s "basic") 29 | , Parser.map Markdown (s "examples" s "markdown") 30 | , Parser.map SpecExtension (s "examples" s "spec-extension") 31 | , Parser.map SpecFromScratch (s "examples" s "spec-from-scratch") 32 | ] 33 | 34 | 35 | 36 | -- PUBLIC HELPERS 37 | 38 | 39 | href : Route -> Attribute msg 40 | href targetRoute = 41 | Attr.href (routeToString targetRoute) 42 | 43 | 44 | replaceUrl : Nav.Key -> Route -> Cmd msg 45 | replaceUrl key route = 46 | Nav.replaceUrl key (routeToString route) 47 | 48 | 49 | fromUrl : Url -> Maybe Route 50 | fromUrl url = 51 | -- The RealWorld spec treats the fragment like a path. 52 | -- This makes it *literally* the path, so we can proceed 53 | -- with parsing as if it had been a normal path all along. 54 | { url | path = Maybe.withDefault "" url.fragment, fragment = Nothing } 55 | |> Parser.parse parser 56 | 57 | 58 | 59 | -- INTERNAL 60 | 61 | 62 | routeToString : Route -> String 63 | routeToString page = 64 | "#/" ++ String.join "/" (routeToPieces page) 65 | 66 | 67 | routeToPieces : Route -> List String 68 | routeToPieces page = 69 | case page of 70 | Home -> 71 | [] 72 | 73 | Basic -> 74 | [ "examples", "basic" ] 75 | 76 | Markdown -> 77 | [ "examples", "markdown" ] 78 | 79 | SpecExtension -> 80 | [ "examples", "spec-extension" ] 81 | 82 | SpecFromScratch -> 83 | [ "examples", "spec-from-scratch" ] 84 | 85 | Examples -> 86 | [ "examples" ] 87 | -------------------------------------------------------------------------------- /demo/src/Session.elm: -------------------------------------------------------------------------------- 1 | port module Session exposing (..) 2 | 3 | import Browser.Navigation as Nav 4 | import Json.Encode exposing (Value) 5 | 6 | 7 | type Session 8 | = Session Nav.Key 9 | 10 | 11 | navKey : Session -> Nav.Key 12 | navKey session = 13 | case session of 14 | Session key -> 15 | key 16 | 17 | 18 | changes : (Session -> msg) -> Nav.Key -> Sub msg 19 | changes toMsg key = 20 | onStoreChange (\_ -> toMsg (Session key)) 21 | 22 | 23 | port onStoreChange : (Value -> msg) -> Sub msg 24 | -------------------------------------------------------------------------------- /demo/src/css/demo.css: -------------------------------------------------------------------------------- 1 | header { 2 | position: relative; 3 | padding: 62px 6px 0; 4 | } 5 | 6 | nav { 7 | display: flex; 8 | justify-content: space-between; 9 | flex-wrap: wrap; 10 | font-weight: 600; 11 | } 12 | 13 | header, article, footer nav { 14 | max-width: 720px; 15 | margin: 0 auto; 16 | } 17 | 18 | body { 19 | padding: 8px; 20 | } 21 | .nav-links { 22 | flex-grow: 1; 23 | display: flex; 24 | flex-wrap: wrap; 25 | justify-content: flex-end; 26 | align-items: flex-end; 27 | } 28 | 29 | .nav-links > a { 30 | margin-left: 17px; 31 | padding-bottom: 3px; 32 | } 33 | 34 | a.logo { 35 | padding-left: 38px; 36 | letter-spacing: -1px; 37 | background-size: 28px 28px; 38 | background-position: top left; 39 | background-repeat: no-repeat; 40 | font-size: 24px; 41 | line-height: 30px; 42 | } 43 | 44 | footer { 45 | border-top: 1px solid #f2f2f2; 46 | } 47 | 48 | article { 49 | margin-bottom: 64px; 50 | } 51 | 52 | article .main-header { 53 | text-align: center; 54 | } 55 | 56 | article p a { 57 | text-decoration: underline; 58 | } 59 | 60 | footer .container { 61 | margin-top: 16px; 62 | padding-bottom: 64px; 63 | } 64 | 65 | /* 66 | Examples page styles 67 | ---- 68 | */ 69 | 70 | ul.grid-list { 71 | list-style: none; 72 | padding: 0; 73 | margin: 36px 0 -36px; 74 | display: flex; 75 | justify-content: space-between; 76 | flex-wrap: wrap; 77 | position: relative; 78 | } 79 | 80 | ul.grid-list > li { 81 | display: block; 82 | max-width: 340px; 83 | min-width: 40%; 84 | width: 100%; 85 | margin-bottom: 36px; 86 | font-size: 14px; 87 | line-height: 20px; 88 | margin-left: 8px; 89 | margin-right: 8px; 90 | } 91 | 92 | ul.grid-list h2 { 93 | margin: 0 0 9px; 94 | line-height: 1; 95 | font-size: 20px; 96 | } 97 | 98 | ul.grid-list { 99 | text-align: left; 100 | } 101 | 102 | a:visited { 103 | color: inherit; 104 | } 105 | 106 | footer a { 107 | text-decoration: underline; 108 | } 109 | 110 | a { 111 | color: #333333; 112 | } 113 | 114 | a:hover { 115 | color: #5890ff; 116 | } 117 | 118 | a { 119 | text-decoration: none; 120 | } 121 | 122 | /* 123 | * Markdown example 124 | */ 125 | .markdown-textarea { 126 | width: 100%; 127 | min-height: 150px; 128 | } 129 | 130 | 131 | /* 132 | * Extension example 133 | */ 134 | .rte-main figure { 135 | margin-top: 16px; 136 | margin-bottom: 8px; 137 | width: 100%; 138 | text-align: center; 139 | } 140 | 141 | .rte-main figure.rte-selected { 142 | outline: transparent; 143 | } 144 | 145 | .rte-main figure.rte-selected img { 146 | outline: 2px solid #8cf; 147 | } 148 | 149 | figure input.caption { 150 | color: #555; 151 | width: 100%; 152 | border: none; 153 | height: 24px; 154 | resize: none; 155 | caret-color: #333333; 156 | text-align: center; 157 | outline: none; 158 | } 159 | 160 | /* 161 | * create spec example 162 | */ 163 | li.todo-list-item { 164 | margin: 5px; 165 | list-style-type: none; 166 | display: flex; 167 | border: 1px solid #ccc; 168 | border-radius: 8px; 169 | } 170 | 171 | li.todo-list-item input { 172 | margin-right: 8px; 173 | } 174 | 175 | .todo-list-item-contents { 176 | width: 100%; 177 | margin-top: 5px; 178 | } 179 | 180 | 181 | div.checkbox { 182 | position: relative; 183 | display: inline-block; 184 | margin-top: 9px; 185 | margin-left: 10px; 186 | margin-right: 10px; 187 | width: 16px; 188 | height: 16px; 189 | border: 1px solid #ccc; 190 | border-radius: 5px; 191 | } 192 | 193 | 194 | div.checkbox::before { 195 | position: absolute; 196 | left: 0; 197 | top: 50%; 198 | height: 50%; 199 | width: 3px; 200 | background-color: #336699; 201 | content: ""; 202 | transform: translateX(5px) rotate(-45deg); 203 | transform-origin: left bottom; 204 | } 205 | 206 | div.checkbox::after { 207 | position: absolute; 208 | left: 0; 209 | bottom: 0; 210 | height: 3px; 211 | width: 100%; 212 | background-color: #336699; 213 | content: ""; 214 | transform: translateX(5px) rotate(-45deg); 215 | transform-origin: left bottom; 216 | } 217 | 218 | 219 | div.checkbox.not-checked::before { 220 | background-color: transparent; 221 | } 222 | 223 | div.checkbox.not-checked::after { 224 | background-color: transparent; 225 | } 226 | 227 | 228 | .not-found { 229 | padding: 16px; 230 | text-align: center; 231 | } -------------------------------------------------------------------------------- /demo/src/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | elm-hot creates an additional div wrapper around the app to make HMR possible. 3 | This could break styling in development mode if you are using Elm UI. 4 | 5 | More context in the issue: 6 | https://github.com/halfzebra/create-elm-app/issues/320 7 | */ 8 | [data-elm-hot="true"] { 9 | height: inherit; 10 | } 11 | 12 | body { 13 | font-family: 'Source Sans Pro', 'Trebuchet MS', 'Lucida Grande', 'Bitstream Vera Sans', 'Helvetica Neue', sans-serif; 14 | margin: 0; 15 | text-align: center; 16 | color: #293c4b; 17 | } 18 | 19 | article h1, article h2, article h3, article p { 20 | text-align: left; 21 | } 22 | 23 | article > h1 { 24 | font-size: 30px; 25 | margin: 24px 8px; 26 | } 27 | 28 | 29 | td, th { 30 | border: 1px solid #ccc; 31 | } 32 | 33 | 34 | .page-main { 35 | background: 36 | rgb(255, 255, 255) none repeat scroll 0% 0%; 37 | max-width: 42em; 38 | margin: 20px auto; 39 | padding: 20px; 40 | } 41 | -------------------------------------------------------------------------------- /demo/src/css/modal.css: -------------------------------------------------------------------------------- 1 | .modal-container { 2 | border: 1px solid #333; 3 | padding: 10px; 4 | background-color: white; 5 | border-radius: 0 0 0 0; 6 | position: fixed; 7 | overflow: hidden; 8 | transition: transform 0.2s ease-out; 9 | z-index: 200; 10 | } 11 | 12 | .modal-container h3 { 13 | text-align: center; 14 | } 15 | 16 | .modal--top .modal-container { 17 | top: 0; 18 | left: 0; 19 | width: 100%; 20 | border-width: 0 0 1px 0; 21 | transform: translate(0%, -100%); 22 | } 23 | .modal--top.modal-isOpen .modal-container { 24 | transform: translate(0%, 0%); 25 | } 26 | 27 | .modal--right .modal-container { 28 | top: 0; 29 | right: 0; 30 | height: 100%; 31 | border-width: 0 0 0 1px; 32 | transform: translate(100%, 0%); 33 | } 34 | .modal--right.modal-isOpen .modal-container { 35 | transform: translate(0%, 0%); 36 | } 37 | 38 | .modal--bottom .modal-container { 39 | bottom: 0; 40 | left: 0; 41 | width: 100%; 42 | border-width: 1px 0 0 0; 43 | transform: translate(0%, 100%); 44 | } 45 | .modal--bottom.modal-isOpen .modal-container { 46 | transform: translate(0%, 0%); 47 | } 48 | 49 | .modal--left .modal-container { 50 | top: 0; 51 | left: 0; 52 | height: 100%; 53 | border-width: 0 1px 0 0; 54 | transform: translate(-100%, 0%); 55 | } 56 | .modal--left.modal-isOpen .modal-container { 57 | transform: translate(0%, 0%); 58 | } 59 | 60 | .modal-backdrop { 61 | position: fixed; 62 | height: 100%; 63 | top: 0; 64 | left: 0; 65 | width: 100%; 66 | background: rgba(0,0,0,0); 67 | z-index: 100; 68 | display: none; 69 | } 70 | .modal-isOpen .modal-backdrop { 71 | background: rgba(0,0,0,0.4); 72 | display: block; 73 | } 74 | -------------------------------------------------------------------------------- /demo/src/css/reset.css: -------------------------------------------------------------------------------- 1 | /* Box sizing rules */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | /* Remove default padding */ 9 | ul[class], 10 | ol[class] { 11 | padding: 0; 12 | } 13 | 14 | /* Remove default margin */ 15 | body, 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | p, 21 | ul[class], 22 | ol[class], 23 | li, 24 | figure, 25 | figcaption, 26 | blockquote, 27 | dl, 28 | dd { 29 | margin: 0; 30 | } 31 | 32 | /* Set core body defaults */ 33 | body { 34 | min-height: 100vh; 35 | scroll-behavior: smooth; 36 | text-rendering: optimizeSpeed; 37 | line-height: 1.5; 38 | } 39 | 40 | /* Remove list styles on ul, ol elements with a class attribute */ 41 | ul[class], 42 | ol[class] { 43 | list-style: none; 44 | } 45 | 46 | /* A elements that don't have a class get default styles */ 47 | a:not([class]) { 48 | text-decoration-skip-ink: auto; 49 | } 50 | 51 | /* Make images easier to work with */ 52 | img { 53 | max-width: 100%; 54 | } 55 | 56 | /* Natural flow and rhythm in articles by default */ 57 | article > * + * { 58 | margin-top: 1em; 59 | } 60 | 61 | /* Inherit fonts for inputs and buttons */ 62 | input, 63 | button, 64 | textarea, 65 | select { 66 | font: inherit; 67 | } 68 | 69 | /* Remove all animations and transitions for people that prefer not to see them */ 70 | @media (prefers-reduced-motion: reduce) { 71 | * { 72 | animation-duration: 0.01ms !important; 73 | animation-iteration-count: 1 !important; 74 | transition-duration: 0.01ms !important; 75 | scroll-behavior: auto !important; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /demo/src/css/rte.css: -------------------------------------------------------------------------------- 1 | 2 | .rte-main { 3 | margin-top: 16px; 4 | min-height: 100px; 5 | text-align: left; 6 | outline: none; 7 | user-select: text; 8 | -webkit-user-select: text; 9 | white-space: pre-wrap; 10 | word-wrap: break-word; 11 | } 12 | 13 | .rte-main img { 14 | min-width: 20px; 15 | min-height: 20px; 16 | max-width: 200px; 17 | border: 1px solid #f2f2f2; 18 | } 19 | 20 | .rte-hide-caret { 21 | caret-color: transparent; 22 | } 23 | 24 | /* 25 | * Workaround for https://github.com/mweiss/elm-rte-toolkit/issues/16, iOS has issues 26 | * changing caret color on elements that are already selected. 27 | */ 28 | @supports (-webkit-touch-callout: none) { 29 | .rte-hide-caret { 30 | caret-color: auto; 31 | } 32 | } 33 | 34 | .rte-button { 35 | color: #999; 36 | cursor: pointer; 37 | margin-right: 16px; 38 | padding: 2px 0; 39 | display: inline-block; 40 | } 41 | 42 | .rte-button.rte-active { 43 | color: #5890ff; 44 | } 45 | 46 | .rte-button.rte-disabled { 47 | color: #ccc; 48 | pointer-events: none; 49 | } 50 | 51 | .rte-controls { 52 | text-align: left; 53 | font-size: 14px; 54 | padding-bottom: 8px; 55 | -webkit-user-select: none; 56 | -moz-user-select: none; 57 | -ms-user-select: none; 58 | user-select: none; 59 | display: inline-block; 60 | margin-left: 16px; 61 | } 62 | 63 | .rte-controls-container { 64 | border-bottom: 1px solid #f2f2f2; 65 | text-align: left; 66 | } 67 | 68 | .editor-container { 69 | padding: 12px 16px 16px 16px; 70 | border: 1px solid #f2f2f2; 71 | } 72 | 73 | .rte-selected { 74 | outline: 2px solid #8cf; 75 | } 76 | 77 | 78 | .rte-main a { 79 | color: blue; 80 | } 81 | 82 | .rte-main a:hover { 83 | color: darkblue; 84 | } 85 | 86 | .rte-main a:visited { 87 | color: blueviolet; 88 | } 89 | 90 | .rte-main blockquote { 91 | padding-left: 16px; 92 | border-left: 2px solid #ccc 93 | } 94 | 95 | .rte-main pre { 96 | white-space: pre-wrap; 97 | } 98 | 99 | .rte-main p { 100 | margin-bottom: 1em; 101 | margin-top: 1em; 102 | } -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import './css/reset.css'; 2 | import './css/main.css'; 3 | import './css/demo.css'; 4 | import './css/rte.css'; 5 | import './css/modal.css'; 6 | 7 | import '@webcomponents/webcomponentsjs/webcomponents-bundle.js' 8 | import { Elm } from './Main.elm'; 9 | import '../../js/elmEditor'; 10 | 11 | Elm.Main.init({ 12 | node: document.getElementById('root') 13 | }); 14 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestBackspaceBlock.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestBackspaceBlock exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (backspaceBlock) 6 | import RichText.Definitions exposing (doc, horizontalRule, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret, singleNodeRange) 19 | import RichText.Model.State exposing (State, state, withSelection) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block 31 | (Element.element paragraph []) 32 | (inlineChildren <| Array.fromList [ plainText "p1" ]) 33 | , block 34 | (Element.element horizontalRule []) 35 | Leaf 36 | , block 37 | (Element.element paragraph []) 38 | (inlineChildren <| Array.fromList [ plainText "p2" ]) 39 | ] 40 | ) 41 | ) 42 | (Just <| caret [ 2, 0 ] 0) 43 | 44 | 45 | expectedExample : State 46 | expectedExample = 47 | state 48 | (block 49 | (Element.element doc []) 50 | (blockChildren <| 51 | Array.fromList 52 | [ block 53 | (Element.element paragraph []) 54 | (inlineChildren <| Array.fromList [ plainText "p1" ]) 55 | , block 56 | (Element.element paragraph []) 57 | (inlineChildren <| Array.fromList [ plainText "p2" ]) 58 | ] 59 | ) 60 | ) 61 | (Just <| caret [ 1, 0 ] 0) 62 | 63 | 64 | testBackspaceBlock : Test 65 | testBackspaceBlock = 66 | describe "Tests the backspaceBlock transform" 67 | [ test "Tests that the example case works as expected" <| 68 | \_ -> Expect.equal (Ok expectedExample) (backspaceBlock example) 69 | , test "it should give an error if the selection is not at the beginning of a text block" <| 70 | \_ -> 71 | Expect.equal (Err "Cannot backspace a block element if we're not at the beginning of a text block") 72 | (backspaceBlock (example |> withSelection (Just <| caret [ 1, 0 ] 1))) 73 | , test "it should give an error if the selection is a range" <| 74 | \_ -> 75 | Expect.equal (Err "Cannot backspace a block element if we're not at the beginning of a text block") 76 | (backspaceBlock (example |> withSelection (Just <| singleNodeRange [ 1, 0 ] 0 1))) 77 | ] 78 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestBackspaceInlineElement.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestBackspaceInlineElement exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (backspaceInlineElement) 6 | import RichText.Definitions exposing (doc, image, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node exposing (Block, Children(..), Inline, block, blockChildren, inlineChildren, inlineElement, plainText) 9 | import RichText.Model.Selection exposing (caret) 10 | import RichText.Model.State exposing (State, state, withSelection) 11 | import Test exposing (Test, describe, test) 12 | 13 | 14 | example : State 15 | example = 16 | state 17 | (block 18 | (Element.element doc []) 19 | (blockChildren <| 20 | Array.fromList 21 | [ block 22 | (Element.element paragraph []) 23 | (inlineChildren <| 24 | Array.fromList 25 | [ plainText "text" 26 | , inlineElement (Element.element image []) [] 27 | , plainText "text2" 28 | ] 29 | ) 30 | ] 31 | ) 32 | ) 33 | (Just <| caret [ 0, 2 ] 0) 34 | 35 | 36 | expectedExample : State 37 | expectedExample = 38 | state 39 | (block 40 | (Element.element doc []) 41 | (blockChildren <| 42 | Array.fromList 43 | [ block 44 | (Element.element paragraph []) 45 | (inlineChildren <| 46 | Array.fromList 47 | [ plainText "text" 48 | , plainText "text2" 49 | ] 50 | ) 51 | ] 52 | ) 53 | ) 54 | (Just <| caret [ 0, 1 ] 0) 55 | 56 | 57 | testBackspaceInlineElement : Test 58 | testBackspaceInlineElement = 59 | describe "Tests the backspaceInlineElement transform" 60 | [ test "Tests that the example case works as expected" <| 61 | \_ -> Expect.equal (Ok expectedExample) (backspaceInlineElement example) 62 | , test "Tests that we can only backspace an inline element when the offset is 0" <| 63 | \_ -> 64 | Expect.equal (Err "I can only backspace an inline element if the offset is 0") 65 | (backspaceInlineElement (example |> withSelection (Just <| caret [ 0, 2 ] 1))) 66 | ] 67 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestBackspaceText.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestBackspaceText exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (backspaceText) 6 | import RichText.Definitions exposing (bold, doc, horizontalRule, image, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Mark exposing (mark) 9 | import RichText.Model.Node 10 | exposing 11 | ( Block 12 | , Children(..) 13 | , Inline 14 | , block 15 | , blockChildren 16 | , inlineChildren 17 | , inlineElement 18 | , markedText 19 | , plainText 20 | ) 21 | import RichText.Model.Selection exposing (caret, singleNodeRange) 22 | import RichText.Model.State exposing (State, state, withSelection) 23 | import Test exposing (Test, describe, test) 24 | 25 | 26 | example : State 27 | example = 28 | state 29 | (block 30 | (Element.element doc []) 31 | (blockChildren <| 32 | Array.fromList 33 | [ block 34 | (Element.element paragraph []) 35 | (inlineChildren <| 36 | Array.fromList 37 | [ plainText "text" 38 | , markedText "text2" [ mark bold [] ] 39 | ] 40 | ) 41 | ] 42 | ) 43 | ) 44 | (Just <| caret [ 0, 1 ] 0) 45 | 46 | 47 | expectedExample : State 48 | expectedExample = 49 | state 50 | (block 51 | (Element.element doc []) 52 | (blockChildren <| 53 | Array.fromList 54 | [ block 55 | (Element.element paragraph []) 56 | (inlineChildren <| 57 | Array.fromList 58 | [ plainText "tex" 59 | , markedText "text2" [ mark bold [] ] 60 | ] 61 | ) 62 | ] 63 | ) 64 | ) 65 | (Just <| caret [ 0, 0 ] 3) 66 | 67 | 68 | expectedExampleOffsetOne : State 69 | expectedExampleOffsetOne = 70 | state 71 | (block 72 | (Element.element doc []) 73 | (blockChildren <| 74 | Array.fromList 75 | [ block 76 | (Element.element paragraph []) 77 | (inlineChildren <| 78 | Array.fromList 79 | [ plainText "text" 80 | , markedText "ext2" [ mark bold [] ] 81 | ] 82 | ) 83 | ] 84 | ) 85 | ) 86 | (Just <| caret [ 0, 1 ] 0) 87 | 88 | 89 | inlineElementState : State 90 | inlineElementState = 91 | state 92 | (block 93 | (Element.element doc []) 94 | (blockChildren <| 95 | Array.fromList 96 | [ block 97 | (Element.element paragraph []) 98 | (inlineChildren <| 99 | Array.fromList 100 | [ plainText "text" 101 | , inlineElement (Element.element image []) [] 102 | , markedText "text2" [ mark bold [] ] 103 | ] 104 | ) 105 | ] 106 | ) 107 | ) 108 | (Just <| caret [ 0, 2 ] 0) 109 | 110 | 111 | testBackspaceText : Test 112 | testBackspaceText = 113 | describe "Tests the backspaceText transform" 114 | [ test "Tests that the example case works as expected" <| 115 | \_ -> Expect.equal (Ok expectedExample) (backspaceText example) 116 | , test "Tests that the example case works as expected when the offset is 1" <| 117 | \_ -> 118 | Expect.equal (Ok expectedExampleOffsetOne) 119 | (backspaceText (example |> withSelection (Just <| caret [ 0, 1 ] 1))) 120 | , test "it should return an error if the offset is > 1" <| 121 | \_ -> 122 | Expect.equal (Err "I use native behavior when doing backspace when the anchor offset could not result in a node change") 123 | (backspaceText (example |> withSelection (Just <| caret [ 0, 1 ] 2))) 124 | , test "it should return an error if it's at the beginning of the document" <| 125 | \_ -> 126 | Expect.equal (Err "Cannot backspace if the previous node is a block") 127 | (backspaceText (example |> withSelection (Just <| caret [ 0, 0 ] 0))) 128 | , test "it should return an error if the previous node is an inline element" <| 129 | \_ -> 130 | Expect.equal (Err "Cannot backspace if the previous node is an inline leaf") 131 | (backspaceText inlineElementState) 132 | ] 133 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestDeleteBlock.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestDeleteBlock exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (deleteBlock) 6 | import RichText.Definitions exposing (doc, horizontalRule, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret, singleNodeRange) 19 | import RichText.Model.State exposing (State, state, withSelection) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block 31 | (Element.element paragraph []) 32 | (inlineChildren <| Array.fromList [ plainText "p1" ]) 33 | , block 34 | (Element.element horizontalRule []) 35 | Leaf 36 | , block 37 | (Element.element paragraph []) 38 | (inlineChildren <| Array.fromList [ plainText "p2" ]) 39 | ] 40 | ) 41 | ) 42 | (Just <| caret [ 0, 0 ] 2) 43 | 44 | 45 | expectedExample : State 46 | expectedExample = 47 | state 48 | (block 49 | (Element.element doc []) 50 | (blockChildren <| 51 | Array.fromList 52 | [ block 53 | (Element.element paragraph []) 54 | (inlineChildren <| Array.fromList [ plainText "p1" ]) 55 | , block 56 | (Element.element paragraph []) 57 | (inlineChildren <| Array.fromList [ plainText "p2" ]) 58 | ] 59 | ) 60 | ) 61 | (Just <| caret [ 0, 0 ] 2) 62 | 63 | 64 | testDeleteBlock : Test 65 | testDeleteBlock = 66 | describe "Tests the deleteBlock transform" 67 | [ test "Tests that the example case works as expected" <| 68 | \_ -> Expect.equal (Ok expectedExample) (deleteBlock example) 69 | , test "it should give an error if the selection is not at the beginning of a text block" <| 70 | \_ -> 71 | Expect.equal (Err "Cannot delete a block element if we're not at the end of a text block") 72 | (deleteBlock (example |> withSelection (Just <| caret [ 1, 0 ] 1))) 73 | , test "it should give an error if the selection is a range" <| 74 | \_ -> 75 | Expect.equal (Err "Cannot delete a block element if we're not at the end of a text block") 76 | (deleteBlock (example |> withSelection (Just <| singleNodeRange [ 1, 0 ] 0 1))) 77 | ] 78 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestDeleteInlineElement.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestDeleteInlineElement exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (deleteInlineElement) 6 | import RichText.Definitions exposing (doc, image, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node exposing (Block, Children(..), Inline, block, blockChildren, inlineChildren, inlineElement, plainText) 9 | import RichText.Model.Selection exposing (caret) 10 | import RichText.Model.State exposing (State, state, withSelection) 11 | import Test exposing (Test, describe, test) 12 | 13 | 14 | example : State 15 | example = 16 | state 17 | (block 18 | (Element.element doc []) 19 | (blockChildren <| 20 | Array.fromList 21 | [ block 22 | (Element.element paragraph []) 23 | (inlineChildren <| 24 | Array.fromList 25 | [ plainText "text" 26 | , inlineElement (Element.element image []) [] 27 | , plainText "text2" 28 | ] 29 | ) 30 | ] 31 | ) 32 | ) 33 | (Just <| caret [ 0, 0 ] 4) 34 | 35 | 36 | expectedExample : State 37 | expectedExample = 38 | state 39 | (block 40 | (Element.element doc []) 41 | (blockChildren <| 42 | Array.fromList 43 | [ block 44 | (Element.element paragraph []) 45 | (inlineChildren <| 46 | Array.fromList 47 | [ plainText "text" 48 | , plainText "text2" 49 | ] 50 | ) 51 | ] 52 | ) 53 | ) 54 | (Just <| caret [ 0, 0 ] 4) 55 | 56 | 57 | testDeleteInlineElement : Test 58 | testDeleteInlineElement = 59 | describe "Tests the deleteInlineElement transform" 60 | [ test "Tests that the example case works as expected" <| 61 | \_ -> Expect.equal (Ok expectedExample) (deleteInlineElement example) 62 | , test "Tests that we can only delete an inline element when the offset is not at the end of a text node" <| 63 | \_ -> 64 | Expect.equal (Err "I cannot delete an inline element if the cursor is not at the end of an inline node") 65 | (deleteInlineElement (example |> withSelection (Just <| caret [ 0, 0 ] 1))) 66 | ] 67 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestDeleteText.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestDeleteText exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (deleteText) 6 | import RichText.Definitions exposing (bold, doc, image, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Mark exposing (mark) 9 | import RichText.Model.Node 10 | exposing 11 | ( Block 12 | , Children(..) 13 | , Inline 14 | , block 15 | , blockChildren 16 | , inlineChildren 17 | , inlineElement 18 | , markedText 19 | , plainText 20 | ) 21 | import RichText.Model.Selection exposing (caret) 22 | import RichText.Model.State exposing (State, state, withSelection) 23 | import Test exposing (Test, describe, test) 24 | 25 | 26 | example : State 27 | example = 28 | state 29 | (block 30 | (Element.element doc []) 31 | (blockChildren <| 32 | Array.fromList 33 | [ block 34 | (Element.element paragraph []) 35 | (inlineChildren <| 36 | Array.fromList 37 | [ plainText "text" 38 | , markedText "text2" [ mark bold [] ] 39 | ] 40 | ) 41 | ] 42 | ) 43 | ) 44 | (Just <| caret [ 0, 0 ] 4) 45 | 46 | 47 | expectedExample : State 48 | expectedExample = 49 | state 50 | (block 51 | (Element.element doc []) 52 | (blockChildren <| 53 | Array.fromList 54 | [ block 55 | (Element.element paragraph []) 56 | (inlineChildren <| 57 | Array.fromList 58 | [ plainText "text" 59 | , markedText "ext2" [ mark bold [] ] 60 | ] 61 | ) 62 | ] 63 | ) 64 | ) 65 | (Just <| caret [ 0, 1 ] 0) 66 | 67 | 68 | expectedExampleOffsetOne : State 69 | expectedExampleOffsetOne = 70 | state 71 | (block 72 | (Element.element doc []) 73 | (blockChildren <| 74 | Array.fromList 75 | [ block 76 | (Element.element paragraph []) 77 | (inlineChildren <| 78 | Array.fromList 79 | [ plainText "tex" 80 | , markedText "text2" [ mark bold [] ] 81 | ] 82 | ) 83 | ] 84 | ) 85 | ) 86 | (Just <| caret [ 0, 0 ] 3) 87 | 88 | 89 | inlineElementState : State 90 | inlineElementState = 91 | state 92 | (block 93 | (Element.element doc []) 94 | (blockChildren <| 95 | Array.fromList 96 | [ block 97 | (Element.element paragraph []) 98 | (inlineChildren <| 99 | Array.fromList 100 | [ plainText "text" 101 | , inlineElement (Element.element image []) [] 102 | , markedText "text2" [ mark bold [] ] 103 | ] 104 | ) 105 | ] 106 | ) 107 | ) 108 | (Just <| caret [ 0, 0 ] 4) 109 | 110 | 111 | testDeleteText : Test 112 | testDeleteText = 113 | describe "Tests the deleteText transform" 114 | [ test "Tests that the example case works as expected" <| 115 | \_ -> Expect.equal (Ok expectedExample) (deleteText example) 116 | , test "Tests that the example case works as expected when the offset is 1 off from the end" <| 117 | \_ -> 118 | Expect.equal (Ok expectedExampleOffsetOne) 119 | (deleteText (example |> withSelection (Just <| caret [ 0, 0 ] 3))) 120 | , test "it should return an error if the offset is > 1 off from the end" <| 121 | \_ -> 122 | Expect.equal (Err "I use the default behavior when deleting text when the anchor offset is not at the end of a text node") 123 | (deleteText (example |> withSelection (Just <| caret [ 0, 1 ] 2))) 124 | , test "it should return an error if it's at the end of the document" <| 125 | \_ -> 126 | Expect.equal (Err "I cannot do delete because there is no neighboring text node") 127 | (deleteText (example |> withSelection (Just <| caret [ 0, 1 ] 5))) 128 | , test "it should return an error if the previous node is an inline element" <| 129 | \_ -> 130 | Expect.equal (Err "Cannot delete if the previous node is an inline leaf") 131 | (deleteText inlineElementState) 132 | ] 133 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestInsertAfterBlockLeaf.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestInsertAfterBlockLeaf exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (insertAfterBlockLeaf) 6 | import RichText.Definitions exposing (doc, horizontalRule, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret) 19 | import RichText.Model.State exposing (State, state) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | emptyParagraph : Block 24 | emptyParagraph = 25 | block 26 | (Element.element paragraph []) 27 | (inlineChildren <| Array.fromList [ plainText "" ]) 28 | 29 | 30 | example : State 31 | example = 32 | state 33 | (block 34 | (Element.element doc []) 35 | (blockChildren <| 36 | Array.fromList 37 | [ block 38 | (Element.element paragraph []) 39 | (inlineChildren <| Array.fromList [ plainText "test" ]) 40 | , block 41 | (Element.element horizontalRule []) 42 | Leaf 43 | ] 44 | ) 45 | ) 46 | (Just <| caret [ 1 ] 0) 47 | 48 | 49 | expectedExample : State 50 | expectedExample = 51 | state 52 | (block 53 | (Element.element doc []) 54 | (blockChildren <| 55 | Array.fromList 56 | [ block 57 | (Element.element paragraph []) 58 | (inlineChildren <| Array.fromList [ plainText "test" ]) 59 | , block 60 | (Element.element horizontalRule []) 61 | Leaf 62 | , emptyParagraph 63 | ] 64 | ) 65 | ) 66 | (Just <| caret [ 2, 0 ] 0) 67 | 68 | 69 | testInsertBlock : Test 70 | testInsertBlock = 71 | describe "Tests the insertBlock transform" 72 | [ test "the example case works as expected" <| 73 | \_ -> 74 | Expect.equal 75 | (Ok expectedExample) 76 | (insertAfterBlockLeaf emptyParagraph example) 77 | ] 78 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestInsertLineBreak.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestInsertLineBreak exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (insertLineBreak) 6 | import RichText.Definitions exposing (doc, hardBreak, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , inlineElement 17 | , plainText 18 | ) 19 | import RichText.Model.Selection exposing (caret) 20 | import RichText.Model.State exposing (State, state) 21 | import Test exposing (Test, describe, test) 22 | 23 | 24 | example : State 25 | example = 26 | state 27 | (block 28 | (Element.element doc []) 29 | (blockChildren <| 30 | Array.fromList 31 | [ block 32 | (Element.element paragraph []) 33 | (inlineChildren <| 34 | Array.fromList 35 | [ plainText "text" 36 | ] 37 | ) 38 | ] 39 | ) 40 | ) 41 | (Just <| caret [ 0, 0 ] 2) 42 | 43 | 44 | expectedExample : State 45 | expectedExample = 46 | state 47 | (block 48 | (Element.element doc []) 49 | (blockChildren <| 50 | Array.fromList 51 | [ block 52 | (Element.element paragraph []) 53 | (inlineChildren <| 54 | Array.fromList 55 | [ plainText "te" 56 | , inlineElement (Element.element hardBreak []) [] 57 | , plainText "xt" 58 | ] 59 | ) 60 | ] 61 | ) 62 | ) 63 | (Just <| caret [ 0, 2 ] 0) 64 | 65 | 66 | testInsertLineBreak : Test 67 | testInsertLineBreak = 68 | describe "Tests the insertLineBreak transform" 69 | [ test "Tests that the example case works as expected" <| 70 | \_ -> Expect.equal (Ok expectedExample) (insertLineBreak example) 71 | ] 72 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestInsertText.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestInsertText exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (insertText) 6 | import RichText.Definitions exposing (doc, horizontalRule, image, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , inlineElement 17 | , plainText 18 | ) 19 | import RichText.Model.Selection exposing (caret, singleNodeRange) 20 | import RichText.Model.State exposing (State, state, withSelection) 21 | import Test exposing (Test, describe, test) 22 | 23 | 24 | example : State 25 | example = 26 | state 27 | (block 28 | (Element.element doc []) 29 | (blockChildren <| 30 | Array.fromList 31 | [ block 32 | (Element.element paragraph []) 33 | (inlineChildren <| 34 | Array.fromList 35 | [ plainText "text" 36 | ] 37 | ) 38 | ] 39 | ) 40 | ) 41 | (Just <| caret [ 0, 0 ] 2) 42 | 43 | 44 | expectedExample : State 45 | expectedExample = 46 | state 47 | (block 48 | (Element.element doc []) 49 | (blockChildren <| 50 | Array.fromList 51 | [ block 52 | (Element.element paragraph []) 53 | (inlineChildren <| 54 | Array.fromList 55 | [ plainText "teinsertxt" ] 56 | ) 57 | ] 58 | ) 59 | ) 60 | (Just <| caret [ 0, 0 ] 8) 61 | 62 | 63 | expectedRange : State 64 | expectedRange = 65 | state 66 | (block 67 | (Element.element doc []) 68 | (blockChildren <| 69 | Array.fromList 70 | [ block 71 | (Element.element paragraph []) 72 | (inlineChildren <| 73 | Array.fromList 74 | [ plainText "insertxt" ] 75 | ) 76 | ] 77 | ) 78 | ) 79 | (Just <| caret [ 0, 0 ] 6) 80 | 81 | 82 | blockSelected : State 83 | blockSelected = 84 | state 85 | (block 86 | (Element.element doc []) 87 | (blockChildren <| 88 | Array.fromList 89 | [ block 90 | (Element.element paragraph []) 91 | (inlineChildren <| 92 | Array.fromList 93 | [ plainText "insertxt" ] 94 | ) 95 | , block 96 | (Element.element horizontalRule []) 97 | Leaf 98 | ] 99 | ) 100 | ) 101 | (Just <| caret [ 1 ] 0) 102 | 103 | 104 | inlineSelected : State 105 | inlineSelected = 106 | state 107 | (block 108 | (Element.element doc []) 109 | (blockChildren <| 110 | Array.fromList 111 | [ block 112 | (Element.element paragraph []) 113 | (inlineChildren <| 114 | Array.fromList 115 | [ inlineElement (Element.element image []) [] ] 116 | ) 117 | ] 118 | ) 119 | ) 120 | (Just <| caret [ 0, 0 ] 0) 121 | 122 | 123 | testInsertText : Test 124 | testInsertText = 125 | describe "Tests the insertText transform" 126 | [ test "Tests that the example case works as expected" <| 127 | \_ -> Expect.equal (Ok expectedExample) (insertText "insert" example) 128 | , test "it should insert into a range" <| 129 | \_ -> 130 | Expect.equal 131 | (Ok expectedRange) 132 | (insertText "insert" 133 | (example 134 | |> withSelection (Just <| singleNodeRange [ 0, 0 ] 0 2) 135 | ) 136 | ) 137 | , test "it should fail if a block leaf is selected" <| 138 | \_ -> 139 | Expect.equal 140 | (Err "I was expecting a text leaf, but instead I found a block node") 141 | (insertText "insert" blockSelected) 142 | , test "it should fail if an inline leaf is selected" <| 143 | \_ -> 144 | Expect.equal 145 | (Err "I was expecting a text leaf, but instead found an inline element") 146 | (insertText "insert" inlineSelected) 147 | ] 148 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestJoinBackward.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestJoinBackward exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (joinBackward) 6 | import RichText.Definitions exposing (doc, horizontalRule, image, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , inlineElement 17 | , plainText 18 | ) 19 | import RichText.Model.Selection exposing (caret) 20 | import RichText.Model.State exposing (State, state) 21 | import Test exposing (Test, describe, test) 22 | 23 | 24 | example : State 25 | example = 26 | state 27 | (block 28 | (Element.element doc []) 29 | (blockChildren <| 30 | Array.fromList 31 | [ block 32 | (Element.element paragraph []) 33 | (inlineChildren <| 34 | Array.fromList 35 | [ plainText "text" 36 | ] 37 | ) 38 | , block 39 | (Element.element paragraph []) 40 | (inlineChildren <| 41 | Array.fromList 42 | [ plainText "text2" 43 | ] 44 | ) 45 | ] 46 | ) 47 | ) 48 | (Just <| caret [ 1, 0 ] 0) 49 | 50 | 51 | expectedExample : State 52 | expectedExample = 53 | state 54 | (block 55 | (Element.element doc []) 56 | (blockChildren <| 57 | Array.fromList 58 | [ block 59 | (Element.element paragraph []) 60 | (inlineChildren <| 61 | Array.fromList 62 | [ plainText "text" 63 | , plainText "text2" 64 | ] 65 | ) 66 | ] 67 | ) 68 | ) 69 | (Just <| caret [ 0, 0 ] 4) 70 | 71 | 72 | inlineExample : State 73 | inlineExample = 74 | state 75 | (block 76 | (Element.element doc []) 77 | (blockChildren <| 78 | Array.fromList 79 | [ block 80 | (Element.element paragraph []) 81 | (inlineChildren <| 82 | Array.fromList 83 | [ plainText "text" 84 | ] 85 | ) 86 | , block 87 | (Element.element paragraph []) 88 | (inlineChildren <| 89 | Array.fromList 90 | [ inlineElement (Element.element image []) [] 91 | ] 92 | ) 93 | ] 94 | ) 95 | ) 96 | (Just <| caret [ 1, 0 ] 0) 97 | 98 | 99 | expectedInlineExample : State 100 | expectedInlineExample = 101 | state 102 | (block 103 | (Element.element doc []) 104 | (blockChildren <| 105 | Array.fromList 106 | [ block 107 | (Element.element paragraph []) 108 | (inlineChildren <| 109 | Array.fromList 110 | [ plainText "text" 111 | , inlineElement (Element.element image []) [] 112 | ] 113 | ) 114 | ] 115 | ) 116 | ) 117 | (Just <| caret [ 0, 0 ] 4) 118 | 119 | 120 | blockExample : State 121 | blockExample = 122 | state 123 | (block 124 | (Element.element doc []) 125 | (blockChildren <| 126 | Array.fromList 127 | [ block 128 | (Element.element paragraph []) 129 | (inlineChildren <| 130 | Array.fromList 131 | [ plainText "text" 132 | ] 133 | ) 134 | , block 135 | (Element.element horizontalRule []) 136 | Leaf 137 | ] 138 | ) 139 | ) 140 | (Just <| caret [ 1 ] 0) 141 | 142 | 143 | testJoinBackward : Test 144 | testJoinBackward = 145 | describe "Tests the joinBackward transform" 146 | [ test "Tests that the example case works as expected" <| 147 | \_ -> 148 | Expect.equal (Ok expectedExample) (joinBackward example) 149 | , test "we can join an inline node backward" <| 150 | \_ -> Expect.equal (Ok expectedInlineExample) (joinBackward inlineExample) 151 | , test "we cannot join a block leaf with a text block" <| 152 | \_ -> 153 | Expect.equal 154 | (Err "I can only join backward if the selection is at beginning of a text block") 155 | (joinBackward blockExample) 156 | ] 157 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestJoinForward.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestJoinForward exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (joinForward) 6 | import RichText.Definitions exposing (doc, horizontalRule, image, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , inlineElement 17 | , plainText 18 | ) 19 | import RichText.Model.Selection exposing (caret) 20 | import RichText.Model.State exposing (State, state) 21 | import Test exposing (Test, describe, test) 22 | 23 | 24 | example : State 25 | example = 26 | state 27 | (block 28 | (Element.element doc []) 29 | (blockChildren <| 30 | Array.fromList 31 | [ block 32 | (Element.element paragraph []) 33 | (inlineChildren <| Array.fromList [ plainText "text" ]) 34 | , block 35 | (Element.element paragraph []) 36 | (inlineChildren <| Array.fromList [ plainText "text2" ]) 37 | ] 38 | ) 39 | ) 40 | (Just <| caret [ 0, 0 ] 4) 41 | 42 | 43 | expectedExample : State 44 | expectedExample = 45 | state 46 | (block 47 | (Element.element doc []) 48 | (blockChildren <| 49 | Array.fromList 50 | [ block 51 | (Element.element paragraph []) 52 | (inlineChildren <| 53 | Array.fromList 54 | [ plainText "text" 55 | , plainText "text2" 56 | ] 57 | ) 58 | ] 59 | ) 60 | ) 61 | (Just <| caret [ 0, 0 ] 4) 62 | 63 | 64 | inlineExample : State 65 | inlineExample = 66 | state 67 | (block 68 | (Element.element doc []) 69 | (blockChildren <| 70 | Array.fromList 71 | [ block 72 | (Element.element paragraph []) 73 | (inlineChildren <| 74 | Array.fromList 75 | [ plainText "text" 76 | ] 77 | ) 78 | , block 79 | (Element.element paragraph []) 80 | (inlineChildren <| 81 | Array.fromList 82 | [ inlineElement (Element.element image []) [] 83 | ] 84 | ) 85 | ] 86 | ) 87 | ) 88 | (Just <| caret [ 0, 0 ] 4) 89 | 90 | 91 | expectedInlineExample : State 92 | expectedInlineExample = 93 | state 94 | (block 95 | (Element.element doc []) 96 | (blockChildren <| 97 | Array.fromList 98 | [ block 99 | (Element.element paragraph []) 100 | (inlineChildren <| 101 | Array.fromList 102 | [ plainText "text" 103 | , inlineElement (Element.element image []) [] 104 | ] 105 | ) 106 | ] 107 | ) 108 | ) 109 | (Just <| caret [ 0, 0 ] 4) 110 | 111 | 112 | blockExample : State 113 | blockExample = 114 | state 115 | (block 116 | (Element.element doc []) 117 | (blockChildren <| 118 | Array.fromList 119 | [ block 120 | (Element.element paragraph []) 121 | (inlineChildren <| 122 | Array.fromList 123 | [ plainText "text" 124 | ] 125 | ) 126 | , block 127 | (Element.element horizontalRule []) 128 | Leaf 129 | ] 130 | ) 131 | ) 132 | (Just <| caret [ 1 ] 0) 133 | 134 | 135 | testJoinForward : Test 136 | testJoinForward = 137 | describe "Tests the joinForward transform" 138 | [ test "Tests that the example case works as expected" <| 139 | \_ -> 140 | Expect.equal (Ok expectedExample) (joinForward example) 141 | , test "we can join an inline node backward" <| 142 | \_ -> Expect.equal (Ok expectedInlineExample) (joinForward inlineExample) 143 | , test "we cannot join a block leaf with a text block" <| 144 | \_ -> 145 | Expect.equal 146 | (Err "I can only join forward if the selection is at end of a text block") 147 | (joinForward blockExample) 148 | ] 149 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestLift.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestLift exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (lift) 6 | import RichText.Definitions exposing (blockquote, doc, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , inlineElement 17 | , plainText 18 | ) 19 | import RichText.Model.Selection exposing (caret, range) 20 | import RichText.Model.State exposing (State, state, withSelection) 21 | import Test exposing (Test, describe, test) 22 | 23 | 24 | example : State 25 | example = 26 | state 27 | (block 28 | (Element.element doc []) 29 | (blockChildren <| 30 | Array.fromList 31 | [ block 32 | (Element.element blockquote []) 33 | (blockChildren <| 34 | Array.fromList 35 | [ block (Element.element paragraph []) 36 | (inlineChildren <| Array.fromList [ plainText "text" ]) 37 | ] 38 | ) 39 | ] 40 | ) 41 | ) 42 | (Just <| caret [ 0, 0, 0 ] 0) 43 | 44 | 45 | expectedExample : State 46 | expectedExample = 47 | state 48 | (block 49 | (Element.element doc []) 50 | (blockChildren <| 51 | Array.fromList 52 | [ block 53 | (Element.element paragraph []) 54 | (inlineChildren <| Array.fromList [ plainText "text" ]) 55 | ] 56 | ) 57 | ) 58 | (Just <| caret [ 0, 0 ] 0) 59 | 60 | 61 | rangeExample : State 62 | rangeExample = 63 | state 64 | (block 65 | (Element.element doc []) 66 | (blockChildren <| 67 | Array.fromList 68 | [ block 69 | (Element.element blockquote []) 70 | (blockChildren <| 71 | Array.fromList 72 | [ block (Element.element paragraph []) 73 | (inlineChildren <| Array.fromList [ plainText "text" ]) 74 | ] 75 | ) 76 | , block 77 | (Element.element blockquote []) 78 | (blockChildren <| 79 | Array.fromList 80 | [ block (Element.element paragraph []) 81 | (inlineChildren <| Array.fromList [ plainText "text" ]) 82 | ] 83 | ) 84 | ] 85 | ) 86 | ) 87 | (Just <| range [ 0, 0, 0 ] 0 [ 1, 0, 0 ] 0) 88 | 89 | 90 | expectedRangeExample : State 91 | expectedRangeExample = 92 | state 93 | (block 94 | (Element.element doc []) 95 | (blockChildren <| 96 | Array.fromList 97 | [ block (Element.element paragraph []) 98 | (inlineChildren <| Array.fromList [ plainText "text" ]) 99 | , block (Element.element paragraph []) 100 | (inlineChildren <| Array.fromList [ plainText "text" ]) 101 | ] 102 | ) 103 | ) 104 | (Just <| range [ 0, 0 ] 0 [ 1, 0 ] 0) 105 | 106 | 107 | testLift : Test 108 | testLift = 109 | describe "Tests the lift transform" 110 | [ test "the example case works as expected" <| 111 | \_ -> 112 | Expect.equal (Ok expectedExample) (lift example) 113 | , test "the example case works with non-zero offset" <| 114 | \_ -> 115 | Expect.equal (Ok (expectedExample |> withSelection (Just <| caret [ 0, 0 ] 2))) 116 | (lift (example |> withSelection (Just <| caret [ 0, 0, 0 ] 2))) 117 | , test "range selection lifts all elements in range" <| 118 | \_ -> 119 | Expect.equal (Ok expectedRangeExample) (lift rangeExample) 120 | ] 121 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestLiftEmpty.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestLiftEmpty exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (liftEmpty) 6 | import RichText.Definitions exposing (blockquote, doc, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , inlineElement 17 | , plainText 18 | ) 19 | import RichText.Model.Selection exposing (caret, range) 20 | import RichText.Model.State exposing (State, state, withSelection) 21 | import Test exposing (Test, describe, test) 22 | 23 | 24 | example : State 25 | example = 26 | state 27 | (block 28 | (Element.element doc []) 29 | (blockChildren <| 30 | Array.fromList 31 | [ block 32 | (Element.element blockquote []) 33 | (blockChildren <| 34 | Array.fromList 35 | [ block (Element.element paragraph []) 36 | (inlineChildren <| Array.fromList [ plainText "" ]) 37 | ] 38 | ) 39 | ] 40 | ) 41 | ) 42 | (Just <| caret [ 0, 0, 0 ] 0) 43 | 44 | 45 | expectedExample : State 46 | expectedExample = 47 | state 48 | (block 49 | (Element.element doc []) 50 | (blockChildren <| 51 | Array.fromList 52 | [ block 53 | (Element.element paragraph []) 54 | (inlineChildren <| Array.fromList [ plainText "" ]) 55 | ] 56 | ) 57 | ) 58 | (Just <| caret [ 0, 0 ] 0) 59 | 60 | 61 | nonEmptyExample : State 62 | nonEmptyExample = 63 | state 64 | (block 65 | (Element.element doc []) 66 | (blockChildren <| 67 | Array.fromList 68 | [ block 69 | (Element.element blockquote []) 70 | (blockChildren <| 71 | Array.fromList 72 | [ block (Element.element paragraph []) 73 | (inlineChildren <| Array.fromList [ plainText "test" ]) 74 | ] 75 | ) 76 | ] 77 | ) 78 | ) 79 | (Just <| caret [ 0, 0, 0 ] 0) 80 | 81 | 82 | testLiftEmpty : Test 83 | testLiftEmpty = 84 | describe "Tests the liftEmpty transform" 85 | [ test "the example case works as expected" <| 86 | \_ -> 87 | Expect.equal (Ok expectedExample) (liftEmpty example) 88 | , test "it fails if it's not empty" <| 89 | \_ -> 90 | Expect.equal (Err "I can only lift an empty text block") (liftEmpty nonEmptyExample) 91 | ] 92 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestRemoveSelectedLeafElement.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestRemoveSelectedLeafElement exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (removeSelectedLeafElement) 6 | import RichText.Definitions exposing (doc, horizontalRule, image, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , inlineElement 17 | , plainText 18 | ) 19 | import RichText.Model.Selection exposing (caret, range) 20 | import RichText.Model.State exposing (State, state, withSelection) 21 | import Test exposing (Test, describe, test) 22 | 23 | 24 | example : State 25 | example = 26 | state 27 | (block 28 | (Element.element doc []) 29 | (blockChildren <| 30 | Array.fromList 31 | [ block 32 | (Element.element paragraph []) 33 | (inlineChildren <| 34 | Array.fromList 35 | [ plainText "hello" 36 | , inlineElement (Element.element image []) [] 37 | , plainText "world" 38 | ] 39 | ) 40 | ] 41 | ) 42 | ) 43 | (Just <| caret [ 0, 1 ] 0) 44 | 45 | 46 | expectedExample : State 47 | expectedExample = 48 | state 49 | (block 50 | (Element.element doc []) 51 | (blockChildren <| 52 | Array.fromList 53 | [ block 54 | (Element.element paragraph []) 55 | (inlineChildren <| 56 | Array.fromList 57 | [ plainText "hello" 58 | , plainText "world" 59 | ] 60 | ) 61 | ] 62 | ) 63 | ) 64 | (Just <| caret [ 0, 0 ] 5) 65 | 66 | 67 | blockLeaf : State 68 | blockLeaf = 69 | state 70 | (block 71 | (Element.element doc []) 72 | (blockChildren <| 73 | Array.fromList 74 | [ block 75 | (Element.element paragraph []) 76 | (inlineChildren <| 77 | Array.fromList 78 | [ plainText "test" 79 | ] 80 | ) 81 | , block 82 | (Element.element horizontalRule []) 83 | Leaf 84 | ] 85 | ) 86 | ) 87 | (Just <| caret [ 1 ] 0) 88 | 89 | 90 | expectedRemoveBlockLeaf : State 91 | expectedRemoveBlockLeaf = 92 | state 93 | (block 94 | (Element.element doc []) 95 | (blockChildren <| 96 | Array.fromList 97 | [ block 98 | (Element.element paragraph []) 99 | (inlineChildren <| 100 | Array.fromList 101 | [ plainText "test" 102 | ] 103 | ) 104 | ] 105 | ) 106 | ) 107 | (Just <| caret [ 0, 0 ] 4) 108 | 109 | 110 | testRemoveSelectedLeafElement : Test 111 | testRemoveSelectedLeafElement = 112 | describe "Tests the removeSelectedLeafElement transform" 113 | [ test "Tests that the example case works as expected" <| 114 | \_ -> Expect.equal (Ok expectedExample) (removeSelectedLeafElement example) 115 | , test "Tests that removing a block leaf works as expected" <| 116 | \_ -> Expect.equal (Ok expectedRemoveBlockLeaf) (removeSelectedLeafElement blockLeaf) 117 | , test "Test that it fails if a leaf is not selected" <| 118 | \_ -> 119 | Expect.equal (Err "There's no leaf node at the given selection") 120 | (removeSelectedLeafElement 121 | (blockLeaf |> withSelection (Just <| caret [ 0 ] 0)) 122 | ) 123 | , test "Test that it fails if a range is selected" <| 124 | \_ -> 125 | Expect.equal (Err "I cannot remove a leaf element if it is not collapsed") 126 | (removeSelectedLeafElement 127 | (blockLeaf |> withSelection (Just <| range [ 0, 0 ] 0 [ 1 ] 1)) 128 | ) 129 | ] 130 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestSelectAll.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestSelectAll exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (selectAll) 6 | import RichText.Definitions exposing (doc, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node exposing (Block, Children(..), Inline, block, blockChildren, inlineChildren, markedText, plainText) 9 | import RichText.Model.Selection exposing (caret, singleNodeRange) 10 | import RichText.Model.State exposing (State, state) 11 | import Test exposing (Test, describe, test) 12 | 13 | 14 | example : State 15 | example = 16 | state 17 | (block 18 | (Element.element doc []) 19 | (blockChildren <| 20 | Array.fromList 21 | [ block (Element.element paragraph []) 22 | (inlineChildren <| Array.fromList [ plainText "text" ]) 23 | ] 24 | ) 25 | ) 26 | (Just <| caret [ 0, 0 ] 0) 27 | 28 | 29 | expectedExample : State 30 | expectedExample = 31 | state 32 | (block 33 | (Element.element doc []) 34 | (blockChildren <| 35 | Array.fromList 36 | [ block 37 | (Element.element paragraph []) 38 | (inlineChildren <| Array.fromList [ plainText "text" ]) 39 | ] 40 | ) 41 | ) 42 | (Just <| singleNodeRange [ 0, 0 ] 0 4) 43 | 44 | 45 | testToggleMark : Test 46 | testToggleMark = 47 | describe "Tests the toggleBlock transform" 48 | [ test "the example case works as expected" <| 49 | \_ -> 50 | Expect.equal (Ok expectedExample) (selectAll example) 51 | ] 52 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestToggleBlock.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestToggleBlock exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (toggleTextBlock) 6 | import RichText.Definitions exposing (doc, heading, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret) 19 | import RichText.Model.State exposing (State, state) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block (Element.element paragraph []) 31 | (inlineChildren <| Array.fromList [ plainText "text" ]) 32 | ] 33 | ) 34 | ) 35 | (Just <| caret [ 0, 0 ] 0) 36 | 37 | 38 | expectedExample : State 39 | expectedExample = 40 | state 41 | (block 42 | (Element.element doc []) 43 | (blockChildren <| 44 | Array.fromList 45 | [ block 46 | (Element.element heading []) 47 | (inlineChildren <| Array.fromList [ plainText "text" ]) 48 | ] 49 | ) 50 | ) 51 | (Just <| caret [ 0, 0 ] 0) 52 | 53 | 54 | testToggleBlock : Test 55 | testToggleBlock = 56 | describe "Tests the toggleBlock transform" 57 | [ test "the example case works as expected" <| 58 | \_ -> 59 | Expect.equal (Ok expectedExample) 60 | (toggleTextBlock 61 | (Element.element heading []) 62 | (Element.element paragraph []) 63 | False 64 | example 65 | ) 66 | , test "the reverse case works as expected" <| 67 | \_ -> 68 | Expect.equal (Ok example) 69 | (toggleTextBlock 70 | (Element.element heading []) 71 | (Element.element paragraph []) 72 | False 73 | expectedExample 74 | ) 75 | ] 76 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestToggleMark.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestToggleMark exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (toggleMark) 6 | import RichText.Definitions exposing (bold, doc, heading, markdown, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Mark exposing (Mark, MarkOrder, ToggleAction(..), mark, markOrderFromSpec) 9 | import RichText.Model.Node exposing (Block, Children(..), Inline, block, blockChildren, inlineChildren, markedText, plainText) 10 | import RichText.Model.Selection exposing (caret) 11 | import RichText.Model.State exposing (State, state) 12 | import Test exposing (Test, describe, test) 13 | 14 | 15 | boldMark : Mark 16 | boldMark = 17 | mark bold [] 18 | 19 | 20 | markdownMarkOrder : MarkOrder 21 | markdownMarkOrder = 22 | markOrderFromSpec markdown 23 | 24 | 25 | example : State 26 | example = 27 | state 28 | (block 29 | (Element.element doc []) 30 | (blockChildren <| 31 | Array.fromList 32 | [ block (Element.element paragraph []) 33 | (inlineChildren <| Array.fromList [ plainText "text" ]) 34 | ] 35 | ) 36 | ) 37 | (Just <| caret [ 0, 0 ] 0) 38 | 39 | 40 | expectedExample : State 41 | expectedExample = 42 | state 43 | (block 44 | (Element.element doc []) 45 | (blockChildren <| 46 | Array.fromList 47 | [ block 48 | (Element.element paragraph []) 49 | (inlineChildren <| Array.fromList [ markedText "" [ boldMark ], plainText "text" ]) 50 | ] 51 | ) 52 | ) 53 | (Just <| caret [ 0, 0 ] 0) 54 | 55 | 56 | testToggleMark : Test 57 | testToggleMark = 58 | describe "Tests the toggleBlock transform" 59 | [ test "the example case works as expected" <| 60 | \_ -> 61 | Expect.equal (Ok expectedExample) 62 | (toggleMark 63 | markdownMarkOrder 64 | boldMark 65 | Add 66 | example 67 | ) 68 | ] 69 | -------------------------------------------------------------------------------- /demo/tests/Commands/TestWrap.elm: -------------------------------------------------------------------------------- 1 | module Commands.TestWrap exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Commands exposing (wrap) 6 | import RichText.Definitions exposing (blockquote, doc, paragraph) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , markedText 17 | , plainText 18 | ) 19 | import RichText.Model.Selection exposing (caret, singleNodeRange) 20 | import RichText.Model.State exposing (State, state) 21 | import Test exposing (Test, describe, test) 22 | 23 | 24 | example : State 25 | example = 26 | state 27 | (block 28 | (Element.element doc []) 29 | (blockChildren <| 30 | Array.fromList 31 | [ block (Element.element paragraph []) 32 | (inlineChildren <| Array.fromList [ plainText "text" ]) 33 | ] 34 | ) 35 | ) 36 | (Just <| caret [ 0, 0 ] 0) 37 | 38 | 39 | expectedExample : State 40 | expectedExample = 41 | state 42 | (block 43 | (Element.element doc []) 44 | (blockChildren <| 45 | Array.fromList 46 | [ block 47 | (Element.element blockquote []) 48 | (blockChildren <| 49 | Array.fromList 50 | [ block 51 | (Element.element paragraph []) 52 | (inlineChildren <| Array.fromList [ plainText "text" ]) 53 | ] 54 | ) 55 | ] 56 | ) 57 | ) 58 | (Just <| caret [ 0, 0, 0 ] 0) 59 | 60 | 61 | testWrap : Test 62 | testWrap = 63 | describe "Tests the toggleBlock transform" 64 | [ test "the example case works as expected" <| 65 | \_ -> 66 | Expect.equal (Ok expectedExample) (wrap identity (Element.element blockquote []) example) 67 | ] 68 | -------------------------------------------------------------------------------- /demo/tests/List/README.md: -------------------------------------------------------------------------------- 1 | TODO: These are just barebone tests. More tests should be done for these commands. -------------------------------------------------------------------------------- /demo/tests/List/TestJoinBackward.elm: -------------------------------------------------------------------------------- 1 | module List.TestJoinBackward exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Definitions exposing (doc, listItem, orderedList, paragraph) 6 | import RichText.List exposing (defaultListDefinition, joinBackward) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret) 19 | import RichText.Model.State exposing (State, state) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block (Element.element orderedList []) 31 | (blockChildren <| 32 | Array.fromList <| 33 | [ block 34 | (Element.element listItem []) 35 | (blockChildren <| 36 | Array.fromList 37 | [ block 38 | (Element.element paragraph []) 39 | (inlineChildren <| 40 | Array.fromList 41 | [ plainText "text" 42 | ] 43 | ) 44 | ] 45 | ) 46 | , block 47 | (Element.element listItem []) 48 | (blockChildren <| 49 | Array.fromList 50 | [ block 51 | (Element.element paragraph []) 52 | (inlineChildren <| 53 | Array.fromList 54 | [ plainText "text2" 55 | ] 56 | ) 57 | ] 58 | ) 59 | ] 60 | ) 61 | ] 62 | ) 63 | ) 64 | (Just <| caret [ 0, 1, 0, 0 ] 0) 65 | 66 | 67 | expectedExample : State 68 | expectedExample = 69 | state 70 | (block 71 | (Element.element doc []) 72 | (blockChildren <| 73 | Array.fromList 74 | [ block (Element.element orderedList []) 75 | (blockChildren <| 76 | Array.fromList <| 77 | [ block 78 | (Element.element listItem []) 79 | (blockChildren <| 80 | Array.fromList 81 | [ block 82 | (Element.element paragraph []) 83 | (inlineChildren <| 84 | Array.fromList 85 | [ plainText "text" 86 | ] 87 | ) 88 | , block 89 | (Element.element paragraph []) 90 | (inlineChildren <| 91 | Array.fromList 92 | [ plainText "text2" 93 | ] 94 | ) 95 | ] 96 | ) 97 | ] 98 | ) 99 | ] 100 | ) 101 | ) 102 | (Just <| caret [ 0, 0, 1, 0 ] 0) 103 | 104 | 105 | testJoinBackward : Test 106 | testJoinBackward = 107 | describe "Tests the joinBackward transform" 108 | [ test "Tests that the example case works as expected" <| 109 | \_ -> 110 | Expect.equal (Ok expectedExample) (joinBackward defaultListDefinition example) 111 | ] 112 | -------------------------------------------------------------------------------- /demo/tests/List/TestJoinForward.elm: -------------------------------------------------------------------------------- 1 | module List.TestJoinForward exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Definitions exposing (doc, listItem, orderedList, paragraph) 6 | import RichText.List exposing (defaultListDefinition, joinForward) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret) 19 | import RichText.Model.State exposing (State, state) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block (Element.element orderedList []) 31 | (blockChildren <| 32 | Array.fromList <| 33 | [ block 34 | (Element.element listItem []) 35 | (blockChildren <| 36 | Array.fromList 37 | [ block 38 | (Element.element paragraph []) 39 | (inlineChildren <| 40 | Array.fromList 41 | [ plainText "text" 42 | ] 43 | ) 44 | ] 45 | ) 46 | , block 47 | (Element.element listItem []) 48 | (blockChildren <| 49 | Array.fromList 50 | [ block 51 | (Element.element paragraph []) 52 | (inlineChildren <| 53 | Array.fromList 54 | [ plainText "text2" 55 | ] 56 | ) 57 | ] 58 | ) 59 | ] 60 | ) 61 | ] 62 | ) 63 | ) 64 | (Just <| caret [ 0, 0, 0, 0 ] 4) 65 | 66 | 67 | expectedExample : State 68 | expectedExample = 69 | state 70 | (block 71 | (Element.element doc []) 72 | (blockChildren <| 73 | Array.fromList 74 | [ block (Element.element orderedList []) 75 | (blockChildren <| 76 | Array.fromList <| 77 | [ block 78 | (Element.element listItem []) 79 | (blockChildren <| 80 | Array.fromList 81 | [ block 82 | (Element.element paragraph []) 83 | (inlineChildren <| 84 | Array.fromList 85 | [ plainText "text" 86 | ] 87 | ) 88 | , block 89 | (Element.element paragraph []) 90 | (inlineChildren <| 91 | Array.fromList 92 | [ plainText "text2" 93 | ] 94 | ) 95 | ] 96 | ) 97 | ] 98 | ) 99 | ] 100 | ) 101 | ) 102 | (Just <| caret [ 0, 0, 0, 0 ] 4) 103 | 104 | 105 | testJoinForward : Test 106 | testJoinForward = 107 | describe "Tests the joinForward transform" 108 | [ test "Tests that the example case works as expected" <| 109 | \_ -> 110 | Expect.equal (Ok expectedExample) (joinForward defaultListDefinition example) 111 | ] 112 | -------------------------------------------------------------------------------- /demo/tests/List/TestLift.elm: -------------------------------------------------------------------------------- 1 | module List.TestLift exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Definitions exposing (doc, listItem, orderedList, paragraph) 6 | import RichText.List exposing (defaultListDefinition, lift) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret) 19 | import RichText.Model.State exposing (State, state) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block (Element.element orderedList []) 31 | (blockChildren <| 32 | Array.fromList <| 33 | [ block 34 | (Element.element listItem []) 35 | (blockChildren <| 36 | Array.fromList 37 | [ block 38 | (Element.element paragraph []) 39 | (inlineChildren <| 40 | Array.fromList 41 | [ plainText "text" 42 | ] 43 | ) 44 | ] 45 | ) 46 | , block 47 | (Element.element listItem []) 48 | (blockChildren <| 49 | Array.fromList 50 | [ block 51 | (Element.element paragraph []) 52 | (inlineChildren <| 53 | Array.fromList 54 | [ plainText "text2" 55 | ] 56 | ) 57 | ] 58 | ) 59 | ] 60 | ) 61 | ] 62 | ) 63 | ) 64 | (Just <| caret [ 0, 0, 0, 0 ] 4) 65 | 66 | 67 | expectedExample : State 68 | expectedExample = 69 | state 70 | (block 71 | (Element.element doc []) 72 | (blockChildren <| 73 | Array.fromList 74 | [ block 75 | (Element.element paragraph []) 76 | (inlineChildren <| 77 | Array.fromList 78 | [ plainText "text" 79 | ] 80 | ) 81 | , block (Element.element orderedList []) 82 | (blockChildren <| 83 | Array.fromList <| 84 | [ block 85 | (Element.element listItem []) 86 | (blockChildren <| 87 | Array.fromList 88 | [ block 89 | (Element.element paragraph []) 90 | (inlineChildren <| 91 | Array.fromList 92 | [ plainText "text2" 93 | ] 94 | ) 95 | ] 96 | ) 97 | ] 98 | ) 99 | ] 100 | ) 101 | ) 102 | (Just <| caret [ 0, 0 ] 4) 103 | 104 | 105 | testLift : Test 106 | testLift = 107 | describe "Tests the lift transform" 108 | [ test "Tests that the example case works as expected" <| 109 | \_ -> 110 | Expect.equal (Ok expectedExample) (lift defaultListDefinition example) 111 | ] 112 | -------------------------------------------------------------------------------- /demo/tests/List/TestLiftEmpty.elm: -------------------------------------------------------------------------------- 1 | module List.TestLiftEmpty exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Definitions exposing (doc, listItem, orderedList, paragraph) 6 | import RichText.List exposing (defaultListDefinition, liftEmpty) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret) 19 | import RichText.Model.State exposing (State, state, withSelection) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block (Element.element orderedList []) 31 | (blockChildren <| 32 | Array.fromList <| 33 | [ block 34 | (Element.element listItem []) 35 | (blockChildren <| 36 | Array.fromList 37 | [ block 38 | (Element.element paragraph []) 39 | (inlineChildren <| 40 | Array.fromList 41 | [ plainText "" 42 | ] 43 | ) 44 | ] 45 | ) 46 | , block 47 | (Element.element listItem []) 48 | (blockChildren <| 49 | Array.fromList 50 | [ block 51 | (Element.element paragraph []) 52 | (inlineChildren <| 53 | Array.fromList 54 | [ plainText "text2" 55 | ] 56 | ) 57 | ] 58 | ) 59 | ] 60 | ) 61 | ] 62 | ) 63 | ) 64 | (Just <| caret [ 0, 0, 0, 0 ] 0) 65 | 66 | 67 | expectedExample : State 68 | expectedExample = 69 | state 70 | (block 71 | (Element.element doc []) 72 | (blockChildren <| 73 | Array.fromList 74 | [ block 75 | (Element.element paragraph []) 76 | (inlineChildren <| 77 | Array.fromList 78 | [ plainText "" 79 | ] 80 | ) 81 | , block (Element.element orderedList []) 82 | (blockChildren <| 83 | Array.fromList <| 84 | [ block 85 | (Element.element listItem []) 86 | (blockChildren <| 87 | Array.fromList 88 | [ block 89 | (Element.element paragraph []) 90 | (inlineChildren <| 91 | Array.fromList 92 | [ plainText "text2" 93 | ] 94 | ) 95 | ] 96 | ) 97 | ] 98 | ) 99 | ] 100 | ) 101 | ) 102 | (Just <| caret [ 0, 0 ] 0) 103 | 104 | 105 | testLift : Test 106 | testLift = 107 | describe "Tests the liftEmpty transform" 108 | [ test "Tests that the example case works as expected" <| 109 | \_ -> 110 | Expect.equal (Ok expectedExample) (liftEmpty defaultListDefinition example) 111 | , test "Tests that a non-empty list node cannot be lifted" <| 112 | \_ -> 113 | Expect.equal 114 | (Err "I cannot lift a node that is not an empty text block") 115 | (liftEmpty defaultListDefinition (example |> withSelection (Just <| caret [ 0, 1, 0, 0 ] 0))) 116 | ] 117 | -------------------------------------------------------------------------------- /demo/tests/List/TestSplit.elm: -------------------------------------------------------------------------------- 1 | module List.TestSplit exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Definitions exposing (doc, listItem, orderedList, paragraph) 6 | import RichText.List exposing (defaultListDefinition, split) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret) 19 | import RichText.Model.State exposing (State, state) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block (Element.element orderedList []) 31 | (blockChildren <| 32 | Array.fromList <| 33 | [ block 34 | (Element.element listItem []) 35 | (blockChildren <| 36 | Array.fromList 37 | [ block 38 | (Element.element paragraph []) 39 | (inlineChildren <| 40 | Array.fromList 41 | [ plainText "text" 42 | ] 43 | ) 44 | ] 45 | ) 46 | ] 47 | ) 48 | ] 49 | ) 50 | ) 51 | (Just <| caret [ 0, 0, 0, 0 ] 2) 52 | 53 | 54 | expectedExample : State 55 | expectedExample = 56 | state 57 | (block 58 | (Element.element doc []) 59 | (blockChildren <| 60 | Array.fromList 61 | [ block (Element.element orderedList []) 62 | (blockChildren <| 63 | Array.fromList <| 64 | [ block 65 | (Element.element listItem []) 66 | (blockChildren <| 67 | Array.fromList 68 | [ block 69 | (Element.element paragraph []) 70 | (inlineChildren <| 71 | Array.fromList 72 | [ plainText "te" 73 | ] 74 | ) 75 | ] 76 | ) 77 | , block 78 | (Element.element listItem []) 79 | (blockChildren <| 80 | Array.fromList 81 | [ block 82 | (Element.element paragraph []) 83 | (inlineChildren <| 84 | Array.fromList 85 | [ plainText "xt" 86 | ] 87 | ) 88 | ] 89 | ) 90 | ] 91 | ) 92 | ] 93 | ) 94 | ) 95 | (Just <| caret [ 0, 1, 0, 0 ] 0) 96 | 97 | 98 | testSplit : Test 99 | testSplit = 100 | describe "Tests the split transform" 101 | [ test "Tests that the example case works as expected" <| 102 | \_ -> 103 | Expect.equal (Ok expectedExample) (split defaultListDefinition example) 104 | ] 105 | -------------------------------------------------------------------------------- /demo/tests/List/TestWrap.elm: -------------------------------------------------------------------------------- 1 | module List.TestWrap exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Definitions exposing (doc, listItem, orderedList, paragraph) 6 | import RichText.List exposing (ListType(..), defaultListDefinition, wrap) 7 | import RichText.Model.Element as Element 8 | import RichText.Model.Node 9 | exposing 10 | ( Block 11 | , Children(..) 12 | , Inline 13 | , block 14 | , blockChildren 15 | , inlineChildren 16 | , plainText 17 | ) 18 | import RichText.Model.Selection exposing (caret, range) 19 | import RichText.Model.State exposing (State, state) 20 | import Test exposing (Test, describe, test) 21 | 22 | 23 | example : State 24 | example = 25 | state 26 | (block 27 | (Element.element doc []) 28 | (blockChildren <| 29 | Array.fromList 30 | [ block 31 | (Element.element paragraph []) 32 | (inlineChildren <| 33 | Array.fromList 34 | [ plainText "text" 35 | ] 36 | ) 37 | , block 38 | (Element.element paragraph []) 39 | (inlineChildren <| 40 | Array.fromList 41 | [ plainText "text2" 42 | ] 43 | ) 44 | ] 45 | ) 46 | ) 47 | (Just <| range [ 0, 0 ] 0 [ 1, 0 ] 0) 48 | 49 | 50 | expectedExample : State 51 | expectedExample = 52 | state 53 | (block 54 | (Element.element doc []) 55 | (blockChildren <| 56 | Array.fromList 57 | [ block (Element.element orderedList []) 58 | (blockChildren <| 59 | Array.fromList <| 60 | [ block 61 | (Element.element listItem []) 62 | (blockChildren <| 63 | Array.fromList 64 | [ block 65 | (Element.element paragraph []) 66 | (inlineChildren <| 67 | Array.fromList 68 | [ plainText "text" 69 | ] 70 | ) 71 | ] 72 | ) 73 | , block 74 | (Element.element listItem []) 75 | (blockChildren <| 76 | Array.fromList 77 | [ block 78 | (Element.element paragraph []) 79 | (inlineChildren <| 80 | Array.fromList 81 | [ plainText "text2" 82 | ] 83 | ) 84 | ] 85 | ) 86 | ] 87 | ) 88 | ] 89 | ) 90 | ) 91 | (Just <| range [ 0, 0, 0, 0 ] 0 [ 0, 1, 0, 0 ] 0) 92 | 93 | 94 | testWrap : Test 95 | testWrap = 96 | describe "Tests the wrap transform" 97 | [ test "Tests that the example case works as expected" <| 98 | \_ -> 99 | Expect.equal (Ok expectedExample) (wrap defaultListDefinition Ordered example) 100 | ] 101 | -------------------------------------------------------------------------------- /demo/tests/Model/TestMark.elm: -------------------------------------------------------------------------------- 1 | module Model.TestMark exposing (..) 2 | 3 | import Expect 4 | import RichText.Config.MarkDefinition exposing (MarkDefinition, defaultMarkDefinition) 5 | import RichText.Definitions exposing (bold, code, italic, link, markdown) 6 | import RichText.Model.Attribute exposing (Attribute(..)) 7 | import RichText.Model.Mark exposing (Mark, MarkOrder, ToggleAction(..), mark, markOrderFromSpec, sort, toggle) 8 | import Test exposing (Test, describe, test) 9 | 10 | 11 | markdownMarkOrder : MarkOrder 12 | markdownMarkOrder = 13 | markOrderFromSpec markdown 14 | 15 | 16 | marksToSort : List Mark 17 | marksToSort = 18 | [ mark bold [], mark italic [], mark code [], mark link [] ] 19 | 20 | 21 | expectedMarksAfterSort : List Mark 22 | expectedMarksAfterSort = 23 | [ mark link [], mark bold [], mark italic [], mark code [] ] 24 | 25 | 26 | dummy1 : MarkDefinition 27 | dummy1 = 28 | defaultMarkDefinition "dummy1" 29 | 30 | 31 | dummy2 : MarkDefinition 32 | dummy2 = 33 | defaultMarkDefinition "dummy2" 34 | 35 | 36 | marksOutsideOfSpec : List Mark 37 | marksOutsideOfSpec = 38 | [ mark bold [], mark dummy2 [], mark italic [], mark dummy1 [], mark code [], mark link [] ] 39 | 40 | 41 | expectedMarksOutsideOfSpec : List Mark 42 | expectedMarksOutsideOfSpec = 43 | [ mark dummy1 [], mark dummy2 [], mark link [], mark bold [], mark italic [], mark code [] ] 44 | 45 | 46 | testSort : Test 47 | testSort = 48 | describe "Tests that sort works correctly" 49 | [ test "marks are sorted in the right order" <| 50 | \_ -> 51 | Expect.equal expectedMarksAfterSort (sort markdownMarkOrder marksToSort) 52 | , test "marks not in the spec are sorted by alphabetical order in the beginning of the list" <| 53 | \_ -> 54 | Expect.equal expectedMarksOutsideOfSpec (sort markdownMarkOrder marksOutsideOfSpec) 55 | ] 56 | 57 | 58 | beforeAddNewMark : List Mark 59 | beforeAddNewMark = 60 | [ mark bold [] ] 61 | 62 | 63 | afterAddNewMark : List Mark 64 | afterAddNewMark = 65 | [ mark bold [], mark italic [] ] 66 | 67 | 68 | beforeAddExistingMark : List Mark 69 | beforeAddExistingMark = 70 | [ mark link [ StringAttribute "href" "google.com" ] ] 71 | 72 | 73 | afterAddExistingMark : List Mark 74 | afterAddExistingMark = 75 | [ mark link [ StringAttribute "href" "yahoo.com" ] ] 76 | 77 | 78 | testToggle : Test 79 | testToggle = 80 | describe "Tests that toggle works correctly" 81 | [ test "adding a new mark works as expected" <| 82 | \_ -> 83 | Expect.equal 84 | afterAddNewMark 85 | (toggle Add markdownMarkOrder (mark italic []) beforeAddNewMark) 86 | , test "adding a mark with the same name replaces the current mark" <| 87 | \_ -> 88 | Expect.equal 89 | afterAddExistingMark 90 | (toggle Add markdownMarkOrder (mark link [ StringAttribute "href" "yahoo.com" ]) beforeAddExistingMark) 91 | , test "removing a mark works as expected" <| 92 | \_ -> 93 | Expect.equal 94 | [ mark bold [], mark italic [] ] 95 | (toggle Remove markdownMarkOrder (mark code []) [ mark code [], mark bold [], mark italic [] ]) 96 | , test "flipping a mark that's not in the list should add it" <| 97 | \_ -> 98 | Expect.equal 99 | [ mark bold [], mark italic [], mark code [] ] 100 | (toggle Flip markdownMarkOrder (mark code []) [ mark bold [], mark italic [] ]) 101 | , test "flipping a mark that's in the list should remove it" <| 102 | \_ -> 103 | Expect.equal 104 | [ mark bold [], mark italic [] ] 105 | (toggle Flip markdownMarkOrder (mark code []) [ mark bold [], mark italic [], mark code [] ]) 106 | ] 107 | -------------------------------------------------------------------------------- /demo/tests/Model/TestNode.elm: -------------------------------------------------------------------------------- 1 | module Model.TestNode exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Model.Mark exposing (Mark, mark) 6 | import RichText.Model.Node 7 | exposing 8 | ( InlineTree(..) 9 | , commonAncestor 10 | , decrement 11 | , increment 12 | , marksToMarkNodeList 13 | , parent 14 | , toString 15 | ) 16 | import SimpleSpec exposing (bold, italic, strikethrough) 17 | import Test exposing (Test, describe, test) 18 | 19 | 20 | mark1 = 21 | mark bold [] 22 | 23 | 24 | mark2 = 25 | mark italic [] 26 | 27 | 28 | mark3 = 29 | mark strikethrough [] 30 | 31 | 32 | testMarkLists = 33 | [ [ mark1, mark2 ], [ mark1, mark2, mark3 ], [ mark2 ], [] ] 34 | 35 | 36 | expectedMarkNodes = 37 | Array.fromList <| 38 | [ MarkNode 39 | { mark = mark1 40 | , children = 41 | Array.fromList <| 42 | [ MarkNode 43 | { mark = mark2 44 | , children = 45 | Array.fromList <| 46 | [ LeafNode 0 47 | , MarkNode 48 | { mark = mark3 49 | , children = 50 | Array.fromList <| 51 | [ LeafNode 1 ] 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | , MarkNode { mark = mark2, children = Array.fromList <| [ LeafNode 2 ] } 58 | , LeafNode 3 59 | ] 60 | 61 | 62 | testNoMarksList = 63 | [ [], [], [], [] ] 64 | 65 | 66 | expectedNoMarksList = 67 | Array.fromList <| [ LeafNode 0, LeafNode 1, LeafNode 2, LeafNode 3 ] 68 | 69 | 70 | testMarksToMarkNodeList : Test 71 | testMarksToMarkNodeList = 72 | describe "Tests that marksToMarkNodeList works as expected" 73 | [ test "Tests that the output of marksToMarkNodeList is correct" <| 74 | \_ -> 75 | Expect.equal expectedMarkNodes (marksToMarkNodeList testMarkLists) 76 | , test "Tests that the output of no marks is correct" <| 77 | \_ -> 78 | Expect.equal expectedNoMarksList (marksToMarkNodeList testNoMarksList) 79 | ] 80 | 81 | 82 | testCommonAncestor : Test 83 | testCommonAncestor = 84 | describe "Tests that commonAncestor works as expected" 85 | [ test "Test that the we can find the common ancestor of root and another path" <| 86 | \_ -> Expect.equal [] (commonAncestor [] [ 0, 1 ]) 87 | , test "Test that the we can find the common ancestor of parent and another path" <| 88 | \_ -> Expect.equal [ 0 ] (commonAncestor [ 0 ] [ 0, 1 ]) 89 | , test "Test that the we can find the common ancestor of two siblings" <| 90 | \_ -> Expect.equal [ 0 ] (commonAncestor [ 0, 2 ] [ 0, 1 ]) 91 | , test "Test that the we can find the common of two long paths" <| 92 | \_ -> Expect.equal [ 0, 1 ] (commonAncestor [ 0, 1, 2, 3, 4 ] [ 0, 1, 3, 2, 4 ]) 93 | ] 94 | 95 | 96 | testDecrement : Test 97 | testDecrement = 98 | describe "Tests that decrement works as expected" 99 | [ test "Decrementing the root path gives the root path" <| 100 | \_ -> Expect.equal [] (decrement []) 101 | , test "Decrementing a path should work as expected" <| 102 | \_ -> Expect.equal [ 0, 1 ] (decrement [ 0, 2 ]) 103 | , test "Decrementing can go negative" <| 104 | \_ -> Expect.equal [ 0, -1 ] (decrement [ 0, 0 ]) 105 | ] 106 | 107 | 108 | testIncrement : Test 109 | testIncrement = 110 | describe "Tests that increment works as expected" 111 | [ test "Incrementing the root path gives the root path" <| 112 | \_ -> Expect.equal [] (increment []) 113 | , test "Incrementing a path should work as expected" <| 114 | \_ -> Expect.equal [ 0, 2 ] (increment [ 0, 1 ]) 115 | ] 116 | 117 | 118 | testParent : Test 119 | testParent = 120 | describe "Tests that parent works as expected" 121 | [ test "The parent of the root path is the root path" <| 122 | \_ -> Expect.equal [] (parent []) 123 | , test "The parent should be the same list with the last element removed" <| 124 | \_ -> Expect.equal [ 1, 2, 3 ] (parent [ 1, 2, 3, 4 ]) 125 | ] 126 | 127 | 128 | testToString : Test 129 | testToString = 130 | describe "Tests that toString works as expected" 131 | [ test "The root path toString is the empty string" <| 132 | \_ -> Expect.equal "" (toString []) 133 | , test "The path toString should work for a path of length 1" <| 134 | \_ -> Expect.equal "1" (toString [ 1 ]) 135 | , test "The path toString should work for a path of length > 1" <| 136 | \_ -> Expect.equal "1:3:0" (toString [ 1, 3, 0 ]) 137 | ] 138 | -------------------------------------------------------------------------------- /demo/tests/README.md: -------------------------------------------------------------------------------- 1 | ## TODO: 2 | These tests were moved temporarily to the demo because elm-test was giving mysterious 3 | dependency errors in the package. Once the error is resolved, they should 4 | be move back into the package. -------------------------------------------------------------------------------- /demo/tests/SimpleSpec.elm: -------------------------------------------------------------------------------- 1 | module SimpleSpec exposing (..) 2 | 3 | import Array exposing (Array) 4 | import RichText.Config.ElementDefinition 5 | exposing 6 | ( ElementDefinition 7 | , blockNode 8 | , defaultElementToHtml 9 | , defaultHtmlToElement 10 | , elementDefinition 11 | , inlineLeaf 12 | , textBlock 13 | ) 14 | import RichText.Config.MarkDefinition exposing (defaultHtmlToMark, markDefinition) 15 | import RichText.Config.Spec 16 | exposing 17 | ( Spec 18 | , emptySpec 19 | , withElementDefinitions 20 | , withMarkDefinitions 21 | ) 22 | import RichText.Model.Element exposing (Element, element) 23 | import RichText.Model.HtmlNode exposing (HtmlNode(..)) 24 | import RichText.Model.Mark exposing (Mark) 25 | 26 | 27 | codeBlockToHtmlNode : Element -> Array HtmlNode -> HtmlNode 28 | codeBlockToHtmlNode _ children = 29 | ElementNode "pre" 30 | [] 31 | (Array.fromList [ ElementNode "code" [] children ]) 32 | 33 | 34 | crazyBlockToHtmlNode : Element -> Array HtmlNode -> HtmlNode 35 | crazyBlockToHtmlNode _ children = 36 | ElementNode "div" 37 | [] 38 | <| 39 | Array.fromList 40 | [ ElementNode "img" [] Array.empty 41 | , ElementNode "div" [] (Array.fromList [ ElementNode "hr" [] Array.empty ]) 42 | , ElementNode "div" [] children 43 | ] 44 | 45 | 46 | htmlNodeToCrazyBlock : ElementDefinition -> HtmlNode -> Maybe ( Element, Array HtmlNode ) 47 | htmlNodeToCrazyBlock def node = 48 | case node of 49 | ElementNode name _ children -> 50 | if name == "div" && Array.length children /= 3 then 51 | Nothing 52 | 53 | else 54 | case Array.get 2 children of 55 | Nothing -> 56 | Nothing 57 | 58 | Just n -> 59 | case n of 60 | ElementNode _ _ c -> 61 | Just ( element def [], c ) 62 | 63 | _ -> 64 | Nothing 65 | 66 | TextNode _ -> 67 | Nothing 68 | 69 | 70 | htmlNodeToCodeBlock : ElementDefinition -> HtmlNode -> Maybe ( Element, Array HtmlNode ) 71 | htmlNodeToCodeBlock def node = 72 | case node of 73 | ElementNode name _ children -> 74 | if name == "pre" && Array.length children == 1 then 75 | case Array.get 0 children of 76 | Nothing -> 77 | Nothing 78 | 79 | Just n -> 80 | case n of 81 | ElementNode _ _ childChildren -> 82 | Just ( element def [], childChildren ) 83 | 84 | _ -> 85 | Nothing 86 | 87 | else 88 | Nothing 89 | 90 | _ -> 91 | Nothing 92 | 93 | 94 | boldToHtmlNode : Mark -> Array HtmlNode -> HtmlNode 95 | boldToHtmlNode _ children = 96 | ElementNode "b" [] children 97 | 98 | 99 | italicToHtmlNode : Mark -> Array HtmlNode -> HtmlNode 100 | italicToHtmlNode _ children = 101 | ElementNode "i" [] children 102 | 103 | 104 | codeBlock = 105 | elementDefinition 106 | { name = "code_block" 107 | , group = "block" 108 | , contentType = blockNode [] 109 | , toHtmlNode = codeBlockToHtmlNode 110 | , fromHtmlNode = htmlNodeToCodeBlock 111 | , selectable = False 112 | } 113 | 114 | 115 | crazyBlock = 116 | elementDefinition 117 | { name = "crazy_block" 118 | , group = "block" 119 | , contentType = blockNode [] 120 | , toHtmlNode = crazyBlockToHtmlNode 121 | , fromHtmlNode = htmlNodeToCrazyBlock 122 | , selectable = False 123 | } 124 | 125 | 126 | paragraph = 127 | elementDefinition 128 | { name = "paragraph" 129 | , group = "block" 130 | , contentType = textBlock { allowedGroups = [ "inline" ], allowedMarks = [] } 131 | , toHtmlNode = defaultElementToHtml "p" 132 | , fromHtmlNode = defaultHtmlToElement "p" 133 | , selectable = False 134 | } 135 | 136 | 137 | image = 138 | elementDefinition 139 | { name = "image" 140 | , group = "inline" 141 | , contentType = inlineLeaf 142 | , toHtmlNode = defaultElementToHtml "img" 143 | , fromHtmlNode = defaultHtmlToElement "img" 144 | , selectable = False 145 | } 146 | 147 | 148 | bold = 149 | markDefinition 150 | { name = "bold" 151 | , toHtmlNode = boldToHtmlNode 152 | , fromHtmlNode = defaultHtmlToMark "b" 153 | } 154 | 155 | 156 | italic = 157 | markDefinition 158 | { name = "italic" 159 | , toHtmlNode = italicToHtmlNode 160 | , fromHtmlNode = defaultHtmlToMark "i" 161 | } 162 | 163 | 164 | strikethrough = 165 | markDefinition 166 | { name = "strikethrough" 167 | , toHtmlNode = italicToHtmlNode 168 | , fromHtmlNode = defaultHtmlToMark "s" 169 | } 170 | 171 | 172 | simpleSpec : Spec 173 | simpleSpec = 174 | emptySpec 175 | |> withElementDefinitions 176 | [ codeBlock 177 | , crazyBlock 178 | , paragraph 179 | , image 180 | ] 181 | |> withMarkDefinitions 182 | [ bold 183 | , italic 184 | ] 185 | -------------------------------------------------------------------------------- /demo/tests/TestAnnotation.elm: -------------------------------------------------------------------------------- 1 | module TestAnnotation exposing (..) 2 | 3 | import Expect 4 | import RichText.Annotation exposing (isSelectable, selectable) 5 | import RichText.Definitions exposing (doc) 6 | import RichText.Model.Element as Element 7 | import RichText.Model.Node exposing (Block, Children(..), Inline(..), Path, block, plainText) 8 | import RichText.Node 9 | exposing 10 | ( Fragment(..) 11 | , Node(..) 12 | ) 13 | import Set 14 | import Test exposing (Test, describe, test) 15 | 16 | 17 | testIsSelectable : Test 18 | testIsSelectable = 19 | describe "Tests that a node is selectable" 20 | [ test "Test that a text node is selectable" <| 21 | \_ -> Expect.equal True <| isSelectable (Inline (plainText "")) 22 | , test "Test that a element node with a selectable mark is selectable" <| 23 | \_ -> 24 | Expect.equal True <| 25 | isSelectable 26 | (Block 27 | (block (Element.element doc [] |> Element.withAnnotations (Set.fromList [ selectable ])) Leaf) 28 | ) 29 | , test "Test that a element node without a selectable mark is not selectable" <| 30 | \_ -> 31 | Expect.equal False <| 32 | isSelectable 33 | (Block 34 | (block (Element.element doc []) Leaf) 35 | ) 36 | ] 37 | -------------------------------------------------------------------------------- /demo/tests/TestDomNode.elm: -------------------------------------------------------------------------------- 1 | module TestDomNode exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Internal.DomNode exposing (DomNode(..), domElementNodeType, domTextNodeType, findTextChanges) 6 | import RichText.Model.HtmlNode exposing (HtmlNode(..)) 7 | import Test exposing (Test, describe, test) 8 | 9 | 10 | pHtmlNode = 11 | ElementNode "p" [] <| Array.fromList [ TextNode "sample" ] 12 | 13 | 14 | pHtmlNodeDifferentText = 15 | ElementNode "p" [] <| Array.fromList [ TextNode "sample2" ] 16 | 17 | 18 | pWithImgHtmlNode = 19 | ElementNode "p" [] <| Array.fromList [ ElementNode "img" [] Array.empty, TextNode "sample" ] 20 | 21 | 22 | divHtmlNode = 23 | ElementNode "div" [] <| Array.fromList [ TextNode "sample" ] 24 | 25 | 26 | pWithImgDomNode = 27 | DomNode 28 | { nodeValue = Nothing 29 | , nodeType = domElementNodeType 30 | , childNodes = 31 | Just <| 32 | Array.fromList 33 | [ DomNode { nodeValue = Nothing, nodeType = domElementNodeType, childNodes = Just Array.empty, tagName = Just "IMG" } 34 | , DomNode { nodeValue = Just "sample", nodeType = domTextNodeType, childNodes = Nothing, tagName = Nothing } 35 | ] 36 | , tagName = Just "P" 37 | } 38 | 39 | 40 | pDomNode = 41 | DomNode 42 | { nodeValue = Nothing 43 | , nodeType = domElementNodeType 44 | , childNodes = 45 | Just <| 46 | Array.fromList 47 | [ DomNode { nodeValue = Just "sample", nodeType = domTextNodeType, childNodes = Nothing, tagName = Nothing } 48 | ] 49 | , tagName = Just "P" 50 | } 51 | 52 | 53 | testFindTextChanges : Test 54 | testFindTextChanges = 55 | describe "Tests the function which finds any text changes between the HtmlNode representation and the actual DOM representation" 56 | [ test "Test the same structure returns the no text change" <| 57 | \_ -> 58 | Expect.equal (Ok []) (findTextChanges pHtmlNode pDomNode) 59 | , test "Different type of node results in Error" <| 60 | \_ -> 61 | Expect.equal (Err "Dom node's tag was P, but I was expecting div") (findTextChanges divHtmlNode pDomNode) 62 | , test "Extra html node results in Error" <| 63 | \_ -> 64 | Expect.equal (Err "Dom node's children length was 1, but I was expecting 2") (findTextChanges pWithImgHtmlNode pDomNode) 65 | , test "Extra dom node results in Error" <| 66 | \_ -> 67 | Expect.equal (Err "Dom node's children length was 2, but I was expecting 1") (findTextChanges pHtmlNode pWithImgDomNode) 68 | , test "Finds text changes" <| 69 | \_ -> 70 | Expect.equal (Ok [ ( [ 0 ], "sample" ) ]) (findTextChanges pHtmlNodeDifferentText pDomNode) 71 | ] 72 | -------------------------------------------------------------------------------- /demo/tests/TestHtml.elm: -------------------------------------------------------------------------------- 1 | module TestHtml exposing (..) 2 | 3 | {-| TODO: add a lot more tests, this right now only covers the documentation example. 4 | -} 5 | 6 | import Array 7 | import Expect 8 | import RichText.Definitions exposing (doc, image, markdown, paragraph) 9 | import RichText.Html exposing (blockFromHtml, fromHtml, toHtml, toHtmlNode) 10 | import RichText.Model.Attribute exposing (Attribute(..)) 11 | import RichText.Model.Element as Element 12 | import RichText.Model.HtmlNode exposing (HtmlNode(..)) 13 | import RichText.Model.Node exposing (Block, Children(..), Inline, block, blockChildren, inlineChildren, inlineElement, plainText) 14 | import RichText.Node exposing (Fragment(..)) 15 | import Test exposing (..) 16 | 17 | 18 | exampleBlock : Block 19 | exampleBlock = 20 | block 21 | (Element.element doc []) 22 | (blockChildren <| 23 | Array.fromList 24 | [ block 25 | (Element.element paragraph []) 26 | (inlineChildren <| 27 | Array.fromList 28 | [ plainText "text" 29 | , inlineElement (Element.element image [ StringAttribute "src" "logo.svg" ]) [] 30 | , plainText "text2" 31 | ] 32 | ) 33 | ] 34 | ) 35 | 36 | 37 | exampleFragment : Fragment 38 | exampleFragment = 39 | BlockFragment <| Array.fromList [ exampleBlock ] 40 | 41 | 42 | exampleHtml : String 43 | exampleHtml = 44 | "

texttext2

" 45 | 46 | 47 | exampleHtmlNode : HtmlNode 48 | exampleHtmlNode = 49 | ElementNode "div" [ ( "data-rte-doc", "true" ) ] <| 50 | Array.fromList 51 | [ ElementNode "p" [] <| 52 | Array.fromList 53 | [ TextNode "text" 54 | , ElementNode "img" [ ( "src", "logo.svg" ) ] Array.empty 55 | , TextNode "text2" 56 | ] 57 | ] 58 | 59 | 60 | testToHtml : Test 61 | testToHtml = 62 | describe "Tests the toHtml function" 63 | [ test "Make sure the example works as expected" <| 64 | \_ -> 65 | Expect.equal exampleHtml (toHtml markdown exampleBlock) 66 | ] 67 | 68 | 69 | testToHtmlNode : Test 70 | testToHtmlNode = 71 | describe "Tests the toHtmlNode function" 72 | [ test "Make sure the example works as expected" <| 73 | \_ -> 74 | Expect.equal exampleHtmlNode (toHtmlNode markdown exampleBlock) 75 | ] 76 | 77 | 78 | testFromHtml : Test 79 | testFromHtml = 80 | describe "Tests the fromHtml function" 81 | [ test "Make sure the example works as expected" <| 82 | \_ -> 83 | Expect.equal (Ok (Array.fromList [ exampleFragment ])) (fromHtml markdown exampleHtml) 84 | ] 85 | 86 | 87 | testBlockFromHtml : Test 88 | testBlockFromHtml = 89 | describe "Tests the blockFromHtml function" 90 | [ test "Make sure the example works as expected" <| 91 | \_ -> 92 | Expect.equal (Ok exampleBlock) (blockFromHtml markdown exampleHtml) 93 | ] 94 | -------------------------------------------------------------------------------- /demo/tests/TestPath.elm: -------------------------------------------------------------------------------- 1 | module TestPath exposing (..) 2 | 3 | import Array exposing (Array) 4 | import Expect 5 | import RichText.Internal.Path 6 | exposing 7 | ( domToEditor 8 | , editorToDom 9 | ) 10 | import RichText.Model.Element exposing (element) 11 | import RichText.Model.Mark exposing (mark) 12 | import RichText.Model.Node exposing (Inline(..), block, inlineChildren, plainText) 13 | import RichText.Model.Text as Text exposing (withText) 14 | import SimpleSpec exposing (bold, codeBlock, crazyBlock, paragraph, simpleSpec) 15 | import Test exposing (..) 16 | 17 | 18 | paragraphParams = 19 | element paragraph [] 20 | 21 | 22 | codeBlockParams = 23 | element codeBlock [] 24 | 25 | 26 | crazyBlockParams = 27 | element crazyBlock [] 28 | 29 | 30 | boldMark = 31 | mark bold [] 32 | 33 | 34 | paragraphNode = 35 | block 36 | paragraphParams 37 | (inlineChildren <| Array.fromList [ plainText "sample" ]) 38 | 39 | 40 | boldParagraphNode = 41 | block 42 | paragraphParams 43 | (inlineChildren <| 44 | Array.fromList 45 | [ Text 46 | (Text.empty 47 | |> withText "sample" 48 | |> Text.withMarks [ boldMark ] 49 | ) 50 | ] 51 | ) 52 | 53 | 54 | crazyBlockNode = 55 | block 56 | crazyBlockParams 57 | (inlineChildren <| 58 | Array.fromList 59 | [ plainText "sample" ] 60 | ) 61 | 62 | 63 | codeBlockNode = 64 | block 65 | codeBlockParams 66 | (inlineChildren <| Array.fromList [ plainText "sample" ]) 67 | 68 | 69 | testDomToEditor : Test 70 | testDomToEditor = 71 | describe "Tests the transformation function from a dom node path to an editor node path" 72 | [ test "Test that an empty spec returns the same path" <| 73 | \_ -> 74 | Expect.equal (Just [ 0 ]) (domToEditor simpleSpec paragraphNode [ 0 ]) 75 | , test "Test the empty path" <| 76 | \_ -> 77 | Expect.equal (Just []) (domToEditor simpleSpec paragraphNode []) 78 | , test "Test invalid path" <| 79 | \_ -> 80 | Expect.equal Nothing (domToEditor simpleSpec paragraphNode [ 1 ]) 81 | , test "Test node spec with custom toHtmlNode" <| 82 | \_ -> 83 | Expect.equal (Just [ 0 ]) (domToEditor simpleSpec codeBlockNode [ 0, 0 ]) 84 | , test "Test invalid node with custom toHtmlNode" <| 85 | \_ -> 86 | Expect.equal Nothing (domToEditor simpleSpec codeBlockNode [ 1, 0 ]) 87 | , test "Test bold spec with custom toHtmlNode" <| 88 | \_ -> 89 | Expect.equal (Just [ 0 ]) (domToEditor simpleSpec boldParagraphNode [ 0, 0 ]) 90 | , test "Test more complicated custom toHtmlNode" <| 91 | \_ -> 92 | Expect.equal (Just [ 0 ]) (domToEditor simpleSpec crazyBlockNode [ 2, 0 ]) 93 | , test "Test more complicated custom toHtmlNode but select the parent in the dom tree" <| 94 | \_ -> 95 | Expect.equal (Just []) (domToEditor simpleSpec crazyBlockNode [ 2 ]) 96 | , test "Test more complicated custom toHtmlNode but select a sibling node in the dom tree" <| 97 | \_ -> 98 | Expect.equal (Just []) (domToEditor simpleSpec crazyBlockNode [ 1, 0 ]) 99 | ] 100 | 101 | 102 | testEditorToDom : Test 103 | testEditorToDom = 104 | describe "Tests the transformation function from an editor node path to a dom node path" 105 | [ test "Test that an empty spec returns the same path" <| 106 | \_ -> 107 | Expect.equal (Just [ 0 ]) (editorToDom simpleSpec paragraphNode [ 0 ]) 108 | , test "Test the empty path" <| 109 | \_ -> 110 | Expect.equal (Just []) (editorToDom simpleSpec paragraphNode []) 111 | , test "Test invalid path" <| 112 | \_ -> 113 | Expect.equal Nothing (editorToDom simpleSpec paragraphNode [ 1 ]) 114 | , test "Test node spec with custom toHtmlNode" <| 115 | \_ -> 116 | Expect.equal (Just [ 0, 0 ]) (editorToDom simpleSpec codeBlockNode [ 0 ]) 117 | , test "Test invalid node with custom toHtmlNode" <| 118 | \_ -> 119 | Expect.equal Nothing (editorToDom simpleSpec codeBlockNode [ 1 ]) 120 | , test "Test bold spec with custom toHtmlNode" <| 121 | \_ -> 122 | Expect.equal (Just [ 0, 0 ]) (editorToDom simpleSpec boldParagraphNode [ 0 ]) 123 | , test "Test more complicated custom toHtmlNode" <| 124 | \_ -> 125 | Expect.equal (Just [ 2, 0 ]) (editorToDom simpleSpec crazyBlockNode [ 0 ]) 126 | ] 127 | -------------------------------------------------------------------------------- /demo/tests/TestSpec.elm: -------------------------------------------------------------------------------- 1 | module TestSpec exposing (..) 2 | 3 | import Array exposing (Array) 4 | import Expect 5 | import RichText.Definitions exposing (blockquote, bold, italic, markdown, paragraph) 6 | import RichText.Internal.Spec exposing (htmlToElementArray) 7 | import RichText.Model.Element exposing (element) 8 | import RichText.Model.Mark exposing (mark) 9 | import RichText.Model.Node 10 | exposing 11 | ( Inline(..) 12 | , block 13 | , blockChildren 14 | , inlineChildren 15 | , plainText 16 | ) 17 | import RichText.Model.Text as Text 18 | import RichText.Node exposing (Fragment(..)) 19 | import Test exposing (Test, describe, test) 20 | 21 | 22 | oneParagraph = 23 | "

test

" 24 | 25 | 26 | expectedOneParagraph = 27 | Array.fromList <| 28 | [ BlockFragment <| 29 | Array.fromList 30 | [ block 31 | (element paragraph []) 32 | (inlineChildren 33 | (Array.fromList 34 | [ plainText "test" 35 | ] 36 | ) 37 | ) 38 | ] 39 | ] 40 | 41 | 42 | twoParagraphs = 43 | "

test1

test2

" 44 | 45 | 46 | twoParagraphsBlockFragment = 47 | Array.fromList 48 | [ block 49 | (element paragraph []) 50 | (inlineChildren 51 | (Array.fromList 52 | [ plainText "test1" 53 | ] 54 | ) 55 | ) 56 | , block 57 | (element paragraph []) 58 | (inlineChildren 59 | (Array.fromList 60 | [ plainText "test2" 61 | ] 62 | ) 63 | ) 64 | ] 65 | 66 | 67 | expectedTwoParagraphs = 68 | Array.fromList <| 69 | [ BlockFragment <| twoParagraphsBlockFragment ] 70 | 71 | 72 | justText = 73 | "test" 74 | 75 | 76 | justTextInlineFragment = 77 | Array.fromList 78 | [ plainText "test" 79 | ] 80 | 81 | 82 | expectedJustText = 83 | Array.fromList 84 | [ InlineFragment <| justTextInlineFragment ] 85 | 86 | 87 | blockquoteAndParagraphs = 88 | "

test1

test2

" 89 | 90 | 91 | expectedBlockquoteAndParagraphs = 92 | Array.fromList 93 | [ BlockFragment <| 94 | Array.fromList 95 | [ block 96 | (element blockquote []) 97 | (blockChildren twoParagraphsBlockFragment) 98 | ] 99 | ] 100 | 101 | 102 | oneParagraphWithBold = 103 | "

test

" 104 | 105 | 106 | boldMark = 107 | mark bold [] 108 | 109 | 110 | italicMark = 111 | mark italic [] 112 | 113 | 114 | expectedOneParagraphWithBold = 115 | Array.fromList <| 116 | [ BlockFragment <| 117 | Array.fromList 118 | [ block 119 | (element paragraph []) 120 | (inlineChildren 121 | (Array.fromList 122 | [ Text (Text.empty |> Text.withText "test" |> Text.withMarks [ boldMark ]) 123 | ] 124 | ) 125 | ) 126 | ] 127 | ] 128 | 129 | 130 | oneParagraphWithBoldAndItalic = 131 | "

test1

" 132 | 133 | 134 | expectedOneParagraphWithBoldAndItalic = 135 | Array.fromList <| 136 | [ BlockFragment <| 137 | Array.fromList 138 | [ block 139 | (element paragraph []) 140 | (inlineChildren 141 | (Array.fromList 142 | [ Text (Text.empty |> Text.withText "tes" |> Text.withMarks [ boldMark ]) 143 | , Text (Text.empty |> Text.withText "t" |> Text.withMarks [ boldMark, italicMark ]) 144 | , Text (Text.empty |> Text.withText "1" |> Text.withMarks [ italicMark ]) 145 | ] 146 | ) 147 | ) 148 | ] 149 | ] 150 | 151 | 152 | testHtmlToElementArray : Test 153 | testHtmlToElementArray = 154 | describe "Tests that htmlToElementArray works as expected" 155 | [ test "Tests that a basic paragraph can be parsed" <| 156 | \_ -> Expect.equal (Ok expectedOneParagraph) (htmlToElementArray markdown oneParagraph) 157 | , test "Tests that multiple paragraphs can be parsed" <| 158 | \_ -> Expect.equal (Ok expectedTwoParagraphs) (htmlToElementArray markdown twoParagraphs) 159 | , test "Tests that simple text content can be parsed" <| 160 | \_ -> Expect.equal (Ok expectedJustText) (htmlToElementArray markdown justText) 161 | , test "Tests that paragraphs wrapped in a code block can be parsed" <| 162 | \_ -> Expect.equal (Ok expectedBlockquoteAndParagraphs) (htmlToElementArray markdown blockquoteAndParagraphs) 163 | , test "Tests that a paragraph with bold text works as expected" <| 164 | \_ -> Expect.equal (Ok expectedOneParagraphWithBold) (htmlToElementArray markdown oneParagraphWithBold) 165 | , test "Tests that a paragraph with bold and italic text works as expected" <| 166 | \_ -> Expect.equal (Ok expectedOneParagraphWithBoldAndItalic) (htmlToElementArray markdown oneParagraphWithBoldAndItalic) 167 | ] 168 | -------------------------------------------------------------------------------- /demo/tests/TestState.elm: -------------------------------------------------------------------------------- 1 | module TestState exposing (..) 2 | 3 | import Array 4 | import Expect 5 | import RichText.Definitions exposing (blockquote, bold, doc, listItem, markdown, paragraph) 6 | import RichText.Model.Element as Element 7 | import RichText.Model.Mark exposing (mark) 8 | import RichText.Model.Node exposing (Block, Children(..), Inline, block, blockChildren, inlineChildren, markedText, plainText) 9 | import RichText.Model.Selection exposing (caret) 10 | import RichText.Model.State exposing (State, state) 11 | import RichText.State exposing (reduce, validate) 12 | import Test exposing (Test, describe, test) 13 | 14 | 15 | reduceExample : State 16 | reduceExample = 17 | state 18 | (block 19 | (Element.element doc []) 20 | (blockChildren <| 21 | Array.fromList 22 | [ block 23 | (Element.element paragraph []) 24 | (inlineChildren <| Array.fromList [ plainText "te", plainText "xt" ]) 25 | ] 26 | ) 27 | ) 28 | (Just <| caret [ 0, 1 ] 2) 29 | 30 | 31 | expectedReduceExample : State 32 | expectedReduceExample = 33 | state 34 | (block 35 | (Element.element doc []) 36 | (blockChildren <| 37 | Array.fromList 38 | [ block 39 | (Element.element paragraph []) 40 | (inlineChildren <| Array.fromList [ plainText "text" ]) 41 | ] 42 | ) 43 | ) 44 | (Just <| caret [ 0, 0 ] 4) 45 | 46 | 47 | reduceEmptyExample : State 48 | reduceEmptyExample = 49 | state 50 | (block 51 | (Element.element doc []) 52 | (blockChildren <| 53 | Array.fromList 54 | [ block 55 | (Element.element paragraph []) 56 | (inlineChildren <| 57 | Array.fromList 58 | [ markedText "" [ mark bold [] ] 59 | , plainText "text" 60 | ] 61 | ) 62 | ] 63 | ) 64 | ) 65 | (Just <| caret [ 0, 1 ] 2) 66 | 67 | 68 | expectedReduceEmptyExample : State 69 | expectedReduceEmptyExample = 70 | state 71 | (block 72 | (Element.element doc []) 73 | (blockChildren <| 74 | Array.fromList 75 | [ block 76 | (Element.element paragraph []) 77 | (inlineChildren <| 78 | Array.fromList 79 | [ plainText "text" ] 80 | ) 81 | ] 82 | ) 83 | ) 84 | (Just <| caret [ 0, 0 ] 2) 85 | 86 | 87 | validateExample : State 88 | validateExample = 89 | state 90 | (block 91 | (Element.element doc []) 92 | (blockChildren <| 93 | Array.fromList 94 | [ block 95 | (Element.element paragraph []) 96 | (inlineChildren <| Array.fromList [ plainText "text" ]) 97 | ] 98 | ) 99 | ) 100 | (Just <| caret [ 0, 0 ] 2) 101 | 102 | 103 | invalidGroupExample : State 104 | invalidGroupExample = 105 | state 106 | (block 107 | (Element.element doc []) 108 | (blockChildren <| 109 | Array.fromList 110 | [ block 111 | (Element.element listItem []) 112 | (inlineChildren <| Array.fromList [ plainText "text" ]) 113 | ] 114 | ) 115 | ) 116 | (Just <| caret [ 0, 0 ] 2) 117 | 118 | 119 | invalidChildrenExample : State 120 | invalidChildrenExample = 121 | state 122 | (block 123 | (Element.element doc []) 124 | (blockChildren <| 125 | Array.fromList 126 | [ block 127 | (Element.element blockquote []) 128 | (inlineChildren <| Array.fromList [ plainText "text" ]) 129 | ] 130 | ) 131 | ) 132 | (Just <| caret [ 0, 0 ] 2) 133 | 134 | 135 | testReduce : Test 136 | testReduce = 137 | describe "Tests the reduce function" 138 | [ test "Tests that the example case works as expected" <| 139 | \_ -> 140 | Expect.equal expectedReduceExample (reduce reduceExample) 141 | , test "Tests that reducing an empty text block works as expected" <| 142 | \_ -> 143 | Expect.equal expectedReduceEmptyExample (reduce reduceEmptyExample) 144 | ] 145 | 146 | 147 | testValidate : Test 148 | testValidate = 149 | describe "Tests the validate function" 150 | [ test "Tests that the example case works as expected" <| 151 | \_ -> 152 | Expect.equal (Ok validateExample) (validate markdown validateExample) 153 | , test "groups are validated" <| 154 | \_ -> 155 | Expect.equal 156 | (Err "Group list_item is not in allowed groups [block]") 157 | (validate markdown invalidGroupExample) 158 | , test "children are validated" <| 159 | \_ -> 160 | Expect.equal 161 | (Err "I was expecting textblock content type, but instead I got BlockNodeType") 162 | (validate markdown invalidChildrenExample) 163 | ] 164 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "mweiss/elm-rte-toolkit", 4 | "summary": "Build rich text editors in Elm", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.4", 7 | "exposed-modules": [ 8 | "RichText.Annotation", 9 | "RichText.Commands", 10 | "RichText.Config.Command", 11 | "RichText.Config.Decorations", 12 | "RichText.Config.ElementDefinition", 13 | "RichText.Config.Keys", 14 | "RichText.Config.MarkDefinition", 15 | "RichText.Config.Spec", 16 | "RichText.Definitions", 17 | "RichText.Editor", 18 | "RichText.Html", 19 | "RichText.List", 20 | "RichText.Model.Attribute", 21 | "RichText.Model.Element", 22 | "RichText.Model.History", 23 | "RichText.Model.HtmlNode", 24 | "RichText.Model.InlineElement", 25 | "RichText.Model.Mark", 26 | "RichText.Model.Node", 27 | "RichText.Model.Selection", 28 | "RichText.Model.State", 29 | "RichText.Model.Text", 30 | "RichText.Node", 31 | "RichText.State" 32 | ], 33 | "elm-version": "0.19.1 <= v < 0.20.0", 34 | "dependencies": { 35 | "elm/browser": "1.0.0 <= v < 2.0.0", 36 | "elm/core": "1.0.0 <= v < 2.0.0", 37 | "elm/html": "1.0.0 <= v < 2.0.0", 38 | "elm/json": "1.0.0 <= v < 2.0.0", 39 | "elm/parser": "1.0.0 <= v < 2.0.0", 40 | "elm/regex": "1.0.0 <= v < 2.0.0", 41 | "elm/time": "1.0.0 <= v < 2.0.0", 42 | "elm/url": "1.0.0 <= v < 2.0.0", 43 | "elm/virtual-dom": "1.0.0 <= v < 2.0.0", 44 | "elm-community/array-extra": "2.1.0 <= v < 3.0.0", 45 | "elm-community/json-extra": "4.0.0 <= v < 5.0.0", 46 | "elm-community/list-extra": "8.0.0 <= v < 9.0.0", 47 | "folkertdev/elm-deque": "3.0.1 <= v < 4.0.0", 48 | "hecrj/html-parser": "2.3.4 <= v < 3.0.0", 49 | "rtfeldman/elm-hex": "1.0.0 <= v < 2.0.0", 50 | "rtfeldman/elm-iso8601-date-strings": "1.0.0 <= v < 2.0.0" 51 | }, 52 | "test-dependencies": { 53 | "elm/random": "1.0.0 <= v < 2.0.0", 54 | "elm-explorations/test": "1.0.0 <= v < 2.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-rte-toolkit", 3 | "version": "1.0.5", 4 | "description": "Elm toolkit for rich text editors", 5 | "main": "elmEditor.js", 6 | "scripts": { 7 | "build": "babel js -d js-dist", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/mweiss/elm-rte-toolkit.git" 13 | }, 14 | "author": "Michael Weiss", 15 | "license": "BSD-3-Clause", 16 | "bugs": { 17 | "url": "https://github.com/mweiss/elm-rte-toolkit/issues" 18 | }, 19 | "homepage": "https://github.com/mweiss/elm-rte-toolkit#readme", 20 | "devDependencies": { 21 | "babel-cli": "^6.26.0", 22 | "babel-preset-env": "^1.7.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | npm run build 4 | cd js-dist 5 | cp ../package.json . 6 | npm publish 7 | -------------------------------------------------------------------------------- /src/RichText/Config/Keys.elm: -------------------------------------------------------------------------------- 1 | module RichText.Config.Keys exposing (short, alt, meta, ctrl, shift, return, enter, backspace, delete) 2 | 3 | {-| This module contains String constants related to defining keyboard commands. 4 | 5 | @docs short, alt, meta, ctrl, shift, return, enter, backspace, delete 6 | 7 | -} 8 | 9 | 10 | {-| Platform specific modifier that is initialized on InternalEditorMsg.Init. When resolving a 11 | command binding, it resolves to `"Meta"` on mac/iOS and `"Control"` on other platforms. 12 | -} 13 | short : String 14 | short = 15 | "__Short__" 16 | 17 | 18 | {-| Alt key 19 | -} 20 | alt : String 21 | alt = 22 | "Alt" 23 | 24 | 25 | {-| Meta key 26 | -} 27 | meta : String 28 | meta = 29 | "Meta" 30 | 31 | 32 | {-| Control key 33 | -} 34 | ctrl : String 35 | ctrl = 36 | "Control" 37 | 38 | 39 | {-| Shift key 40 | -} 41 | shift : String 42 | shift = 43 | "Shift" 44 | 45 | 46 | {-| Return key 47 | -} 48 | return : String 49 | return = 50 | "Return" 51 | 52 | 53 | {-| Enter key 54 | -} 55 | enter : String 56 | enter = 57 | "Enter" 58 | 59 | 60 | {-| Backspace key 61 | -} 62 | backspace : String 63 | backspace = 64 | "Backspace" 65 | 66 | 67 | {-| Delete key 68 | -} 69 | delete : String 70 | delete = 71 | "Delete" 72 | -------------------------------------------------------------------------------- /src/RichText/Config/MarkDefinition.elm: -------------------------------------------------------------------------------- 1 | module RichText.Config.MarkDefinition exposing 2 | ( MarkDefinition, markDefinition, MarkToHtml, HtmlToMark, name, toHtmlNode, fromHtmlNode 3 | , defaultMarkDefinition, defaultMarkToHtml, defaultHtmlToMark 4 | ) 5 | 6 | {-| A mark definition describes how to encode and decode a mark. 7 | 8 | 9 | # Mark 10 | 11 | @docs MarkDefinition, markDefinition, MarkToHtml, HtmlToMark, name, toHtmlNode, fromHtmlNode 12 | 13 | 14 | # Struts 15 | 16 | @docs defaultMarkDefinition, defaultMarkToHtml, defaultHtmlToMark 17 | 18 | -} 19 | 20 | import Array exposing (Array) 21 | import RichText.Internal.Definitions as Internal 22 | import RichText.Model.Attribute exposing (Attribute(..)) 23 | import RichText.Model.HtmlNode exposing (HtmlNode(..)) 24 | import RichText.Model.Mark exposing (Mark) 25 | 26 | 27 | {-| A mark definition defines how a mark is encoded an decoded. 28 | -} 29 | type alias MarkDefinition = 30 | Internal.MarkDefinition 31 | 32 | 33 | {-| Type alias for a mark encoding function 34 | 35 | codeToHtmlNode : MarkToHtml 36 | codeToHtmlNode _ children = 37 | ElementNode "code" [] children 38 | 39 | -} 40 | type alias MarkToHtml = 41 | Mark -> Array HtmlNode -> HtmlNode 42 | 43 | 44 | {-| Type alias for a mark decoding function 45 | 46 | htmlNodeToCode : HtmlToMark 47 | htmlNodeToCode definition node = 48 | case node of 49 | ElementNode name _ children -> 50 | if name == 'code' then 51 | Just ( mark def [], children ) 52 | 53 | else 54 | Nothing 55 | 56 | _ -> 57 | Nothing 58 | 59 | -} 60 | type alias HtmlToMark = 61 | MarkDefinition -> HtmlNode -> Maybe ( Mark, Array HtmlNode ) 62 | 63 | 64 | {-| Defines a mark. The arguments are as follows: 65 | 66 | - `name` - The unique name for this mark. This should be something like 'bold' or 'link'. 67 | 68 | - `toHtmlNode` - The function that converts the mark to html. This is used in rendering, 69 | DOM validation, and path translation. 70 | 71 | - `fromHtmlNode` - The function that converts html to marks. This is used in things 72 | like paste to determine the editor nodes from html. 73 | 74 | ``` 75 | code : MarkDefinition 76 | code = 77 | markDefinition {name="code", toHtmlNode=codeToHtmlNode, fromHtmlNode=htmlNodeToCode} 78 | ``` 79 | 80 | -} 81 | markDefinition : 82 | { name : String 83 | , toHtmlNode : MarkToHtml 84 | , fromHtmlNode : HtmlToMark 85 | } 86 | -> MarkDefinition 87 | markDefinition contents = 88 | Internal.MarkDefinition 89 | contents 90 | 91 | 92 | {-| Name of the mark this mark definition defines. 93 | 94 | name code 95 | --> "code" 96 | 97 | -} 98 | name : MarkDefinition -> String 99 | name definition_ = 100 | case definition_ of 101 | Internal.MarkDefinition c -> 102 | c.name 103 | 104 | 105 | {-| Function which encodes a mark to Html 106 | -} 107 | toHtmlNode : MarkDefinition -> MarkToHtml 108 | toHtmlNode definition_ = 109 | case definition_ of 110 | Internal.MarkDefinition c -> 111 | c.toHtmlNode 112 | 113 | 114 | {-| Function which decodes a mark from Html 115 | -} 116 | fromHtmlNode : MarkDefinition -> HtmlToMark 117 | fromHtmlNode definition_ = 118 | case definition_ of 119 | Internal.MarkDefinition c -> 120 | c.fromHtmlNode 121 | 122 | 123 | {-| Creates a mark definition which assumes the name of the mark is the same as the name of the 124 | html node. 125 | 126 | defaultMarkDefinition "b" 127 | --> definition which encodes to ... and decodes from "..." 128 | 129 | -} 130 | defaultMarkDefinition : String -> MarkDefinition 131 | defaultMarkDefinition name_ = 132 | markDefinition 133 | { name = name_ 134 | , toHtmlNode = defaultMarkToHtml name_ 135 | , fromHtmlNode = defaultHtmlToMark name_ 136 | } 137 | 138 | 139 | {-| Creates an `MarkToHtml` function that will encode a mark to html with the same name as the mark. 140 | 141 | defaultMarkToHtml "b" 142 | --> returns a function which encodes to "..." 143 | 144 | -} 145 | defaultMarkToHtml : String -> MarkToHtml 146 | defaultMarkToHtml tag mark_ children = 147 | ElementNode tag 148 | (List.filterMap 149 | (\attr -> 150 | case attr of 151 | StringAttribute k v -> 152 | Just ( k, v ) 153 | 154 | _ -> 155 | Nothing 156 | ) 157 | (Internal.attributesFromMark mark_) 158 | ) 159 | children 160 | 161 | 162 | {-| Creates an `HtmlToMark` function that will decode a mark from the tag name specified. 163 | 164 | defaultHtmlToMark "b" 165 | --> returns a function which decodes from "..." 166 | 167 | -} 168 | defaultHtmlToMark : String -> HtmlToMark 169 | defaultHtmlToMark htmlTag def node = 170 | case node of 171 | ElementNode name_ _ children -> 172 | if name_ == htmlTag then 173 | Just ( Internal.mark def [], children ) 174 | 175 | else 176 | Nothing 177 | 178 | _ -> 179 | Nothing 180 | -------------------------------------------------------------------------------- /src/RichText/Config/Spec.elm: -------------------------------------------------------------------------------- 1 | module RichText.Config.Spec exposing (Spec, emptySpec, markDefinitions, markDefinition, elementDefinitions, elementDefinition, withMarkDefinitions, withElementDefinitions) 2 | 3 | {-| A spec describes what nodes and marks can be in an editor. 4 | 5 | @docs Spec, emptySpec, markDefinitions, markDefinition, elementDefinitions, elementDefinition, withMarkDefinitions, withElementDefinitions 6 | 7 | -} 8 | 9 | import Dict exposing (Dict) 10 | import RichText.Internal.Definitions 11 | exposing 12 | ( ContentType(..) 13 | , Element 14 | , ElementDefinition(..) 15 | , ElementToHtml 16 | , HtmlToElement 17 | , HtmlToMark 18 | , Mark 19 | , MarkDefinition(..) 20 | , MarkToHtml 21 | ) 22 | 23 | 24 | {-| A spec describes what nodes and marks can be in an editor. It's used internally to encode an 25 | editor to html, and to transform html to editor nodes. Note for the latter, the order of nodes and 26 | marks is significant, because that is the order in which each node and mark's decoder method is 27 | applied. 28 | 29 | simpleSpec : Spec 30 | simpleSpec = 31 | emptySpec 32 | |> withElementDefinitions 33 | [ codeBlock 34 | , crazyBlock 35 | , paragraph 36 | , image 37 | ] 38 | |> withMarkDefinitions 39 | [ bold 40 | , italic 41 | ] 42 | 43 | -} 44 | type Spec 45 | = Spec SpecContents 46 | 47 | 48 | {-| An empty spec 49 | -} 50 | emptySpec : Spec 51 | emptySpec = 52 | Spec { marks = [], nameToMark = Dict.empty, elements = [], nameToElement = Dict.empty } 53 | 54 | 55 | {-| list of `MarkDefinition` from a spec 56 | -} 57 | markDefinitions : Spec -> List MarkDefinition 58 | markDefinitions spec = 59 | case spec of 60 | Spec c -> 61 | c.marks 62 | 63 | 64 | {-| list of `ElementDefinition` from a spec 65 | -} 66 | elementDefinitions : Spec -> List ElementDefinition 67 | elementDefinitions spec = 68 | case spec of 69 | Spec c -> 70 | c.elements 71 | 72 | 73 | {-| a spec with the given mark definitions 74 | -} 75 | withMarkDefinitions : List MarkDefinition -> Spec -> Spec 76 | withMarkDefinitions marks spec = 77 | case spec of 78 | Spec c -> 79 | Spec 80 | { c 81 | | marks = marks 82 | , nameToMark = 83 | Dict.fromList <| 84 | List.map 85 | (\x -> 86 | case x of 87 | MarkDefinition m -> 88 | ( m.name, x ) 89 | ) 90 | marks 91 | } 92 | 93 | 94 | {-| a spec with the given element definitions 95 | -} 96 | withElementDefinitions : List ElementDefinition -> Spec -> Spec 97 | withElementDefinitions nodes spec = 98 | case spec of 99 | Spec c -> 100 | Spec 101 | { c 102 | | elements = nodes 103 | , nameToElement = 104 | Dict.fromList <| 105 | List.map 106 | (\x -> 107 | case x of 108 | ElementDefinition m -> 109 | ( m.name, x ) 110 | ) 111 | nodes 112 | } 113 | 114 | 115 | {-| Returns the mark definition with the given name from a spec. 116 | 117 | markDefinition "bold" markdown 118 | --> Just (bold mark definition) 119 | 120 | -} 121 | markDefinition : String -> Spec -> Maybe MarkDefinition 122 | markDefinition name spec = 123 | case spec of 124 | Spec c -> 125 | Dict.get name c.nameToMark 126 | 127 | 128 | {-| Returns the element definition with the given name from a spec. 129 | 130 | elementDefinition "paragraph" markdown 131 | --> Just (paragraph element definition) 132 | 133 | -} 134 | elementDefinition : String -> Spec -> Maybe ElementDefinition 135 | elementDefinition name spec = 136 | case spec of 137 | Spec c -> 138 | Dict.get name c.nameToElement 139 | 140 | 141 | type alias SpecContents = 142 | { marks : List MarkDefinition 143 | , nameToMark : Dict String MarkDefinition 144 | , elements : List ElementDefinition 145 | , nameToElement : Dict String ElementDefinition 146 | } 147 | -------------------------------------------------------------------------------- /src/RichText/Internal/BeforeInput.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.BeforeInput exposing (..) 2 | 3 | import Json.Decode as D 4 | import RichText.Config.Command exposing (CommandMap, namedCommandListFromInputEvent) 5 | import RichText.Config.Spec exposing (Spec) 6 | import RichText.Internal.Editor exposing (Editor, Message(..), Tagger, applyNamedCommandList, forceRerender, isComposing) 7 | import RichText.Internal.Event exposing (InputEvent) 8 | 9 | 10 | preventDefaultOn : CommandMap -> Spec -> Editor -> Message -> ( Message, Bool ) 11 | preventDefaultOn commandMap spec editor msg = 12 | case msg of 13 | BeforeInputEvent inputEvent -> 14 | if inputEvent.isComposing || isComposing editor then 15 | ( msg, False ) 16 | 17 | else 18 | ( msg, shouldPreventDefault commandMap spec editor inputEvent ) 19 | 20 | _ -> 21 | ( msg, False ) 22 | 23 | 24 | shouldPreventDefault : CommandMap -> Spec -> Editor -> InputEvent -> Bool 25 | shouldPreventDefault commandMap spec editor inputEvent = 26 | case handleInputEvent commandMap spec editor inputEvent of 27 | Err _ -> 28 | False 29 | 30 | Ok _ -> 31 | True 32 | 33 | 34 | preventDefaultOnBeforeInputDecoder : Tagger msg -> CommandMap -> Spec -> Editor -> D.Decoder ( msg, Bool ) 35 | preventDefaultOnBeforeInputDecoder tagger commandMap spec editor = 36 | D.map (\( i, b ) -> ( tagger i, b )) (D.map (preventDefaultOn commandMap spec editor) beforeInputDecoder) 37 | 38 | 39 | beforeInputDecoder : D.Decoder Message 40 | beforeInputDecoder = 41 | D.map BeforeInputEvent 42 | (D.map3 InputEvent 43 | (D.maybe (D.field "data" D.string)) 44 | (D.oneOf [ D.field "isComposing" D.bool, D.succeed False ]) 45 | (D.field "inputType" D.string) 46 | ) 47 | 48 | 49 | handleInputEvent : CommandMap -> Spec -> Editor -> InputEvent -> Result String Editor 50 | handleInputEvent commandMap spec editor inputEvent = 51 | let 52 | namedCommandList = 53 | namedCommandListFromInputEvent inputEvent commandMap 54 | in 55 | applyNamedCommandList namedCommandList spec editor 56 | 57 | 58 | handleBeforeInput : InputEvent -> CommandMap -> Spec -> Editor -> Editor 59 | handleBeforeInput inputEvent commandMap spec editor = 60 | if inputEvent.isComposing || isComposing editor then 61 | editor 62 | 63 | else 64 | case handleInputEvent commandMap spec editor inputEvent of 65 | Err _ -> 66 | editor 67 | 68 | Ok newEditor -> 69 | -- HACK: Android has very strange behavior with regards to before input events, e.g. 70 | -- prevent default doesn't actually stop the DOM from being modified, so 71 | -- we're forcing a rerender if we update the editor state on a command 72 | forceRerender newEditor 73 | -------------------------------------------------------------------------------- /src/RichText/Internal/Constants.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.Constants exposing (zeroWidthSpace, selection, selectable, lift) 2 | 3 | {-| Miscellaneous constants used throughout the code 4 | 5 | @docs zeroWidthSpace, selection, selectable, lift 6 | 7 | -} 8 | 9 | 10 | {-| A string representing the unicode character for a zero width space. Browsers remove empty 11 | text nodes, so in order to keep the expected DOM structure and the real DOM structure consistent, 12 | we use zero width spaces for empty html text nodes. 13 | -} 14 | zeroWidthSpace : String 15 | zeroWidthSpace = 16 | "\u{200B}" 17 | 18 | 19 | {-| Represents that a node is currently selected. This annotation is transient, e.g. it 20 | should be cleared before a transform or command is complete. This annotation is also used when 21 | rendering to annotate a selected node for decorators. 22 | -} 23 | selection : String 24 | selection = 25 | "__selection__" 26 | 27 | 28 | {-| Represents that a node can be selected. This annotation is not transient. 29 | -} 30 | selectable : String 31 | selectable = 32 | "__selectable__" 33 | 34 | 35 | {-| Represents that a node should be lifted. This annotation is transient, e.g. it should be 36 | cleared before a transform or command is complete. 37 | -} 38 | lift : String 39 | lift = 40 | "__lift__" 41 | -------------------------------------------------------------------------------- /src/RichText/Internal/Definitions.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.Definitions exposing (..) 2 | 3 | {-| Internal module for various entities related to definition, elements and nodes to keep records 4 | private and avoid dependency loops. 5 | -} 6 | 7 | import Array exposing (Array) 8 | import RichText.Internal.Constants exposing (selectable) 9 | import RichText.Model.Attribute exposing (Attribute) 10 | import RichText.Model.HtmlNode exposing (HtmlNode) 11 | import Set exposing (Set) 12 | 13 | 14 | {-| Implementation note: 15 | 16 | We only store the name in element parameters and marks because we want to follow the Elm 17 | architecture and not store functions in the model. 18 | 19 | The benefits of this is that it avoids serialization issues with the debugger and potential 20 | runtime errors on (==). The tradeoff to not storing the serialization functions directly in the 21 | model is an extra dictionary lookup for each node everytime we want to get the definition, as 22 | well as a somewhat more annoying API where functions need an extra spec argument so we can lookup 23 | the definition. 24 | 25 | -} 26 | type ContentType 27 | = BlockNodeType (Maybe (Set String)) 28 | | TextBlockNodeType { allowedGroups : Maybe (Set String), allowedMarks : Maybe (Set String) } 29 | | BlockLeafNodeType 30 | | InlineLeafNodeType 31 | 32 | 33 | type alias ElementParametersContents = 34 | { name : String 35 | , attributes : List Attribute 36 | , annotations : Set String 37 | } 38 | 39 | 40 | type Element 41 | = ElementParameters ElementParametersContents 42 | 43 | 44 | element : ElementDefinition -> List Attribute -> Element 45 | element def attrs = 46 | case def of 47 | ElementDefinition d -> 48 | ElementParameters 49 | { name = d.name 50 | , attributes = attrs 51 | , annotations = 52 | if d.selectable then 53 | Set.singleton selectable 54 | 55 | else 56 | Set.empty 57 | } 58 | 59 | 60 | nameFromElement : Element -> String 61 | nameFromElement parameters = 62 | case parameters of 63 | ElementParameters c -> 64 | c.name 65 | 66 | 67 | attributesFromElement : Element -> List Attribute 68 | attributesFromElement parameters = 69 | case parameters of 70 | ElementParameters c -> 71 | c.attributes 72 | 73 | 74 | annotationsFromElement : Element -> Set String 75 | annotationsFromElement parameters = 76 | case parameters of 77 | ElementParameters c -> 78 | c.annotations 79 | 80 | 81 | elementWithAnnotations : Set String -> Element -> Element 82 | elementWithAnnotations annotations parameters = 83 | case parameters of 84 | ElementParameters c -> 85 | ElementParameters <| { c | annotations = annotations } 86 | 87 | 88 | elementWithAttributes : List Attribute -> Element -> Element 89 | elementWithAttributes attrs parameters = 90 | case parameters of 91 | ElementParameters c -> 92 | ElementParameters <| { c | attributes = attrs } 93 | 94 | 95 | type Mark 96 | = Mark Contents 97 | 98 | 99 | type alias Contents = 100 | { name : String, attributes : List Attribute } 101 | 102 | 103 | mark : MarkDefinition -> List Attribute -> Mark 104 | mark n a = 105 | case n of 106 | MarkDefinition nn -> 107 | Mark { name = nn.name, attributes = a } 108 | 109 | 110 | nameFromMark : Mark -> String 111 | nameFromMark m = 112 | case m of 113 | Mark c -> 114 | c.name 115 | 116 | 117 | attributesFromMark : Mark -> List Attribute 118 | attributesFromMark m = 119 | case m of 120 | Mark c -> 121 | c.attributes 122 | 123 | 124 | markWithAttributes : List Attribute -> Mark -> Mark 125 | markWithAttributes attributes_ m = 126 | case m of 127 | Mark c -> 128 | Mark { c | attributes = attributes_ } 129 | 130 | 131 | type MarkDefinition 132 | = MarkDefinition MarkDefinitionContents 133 | 134 | 135 | type alias MarkDefinitionContents = 136 | { name : String 137 | , toHtmlNode : MarkToHtml 138 | , fromHtmlNode : HtmlToMark 139 | } 140 | 141 | 142 | type alias MarkToHtml = 143 | Mark -> Array HtmlNode -> HtmlNode 144 | 145 | 146 | type alias HtmlToMark = 147 | MarkDefinition -> HtmlNode -> Maybe ( Mark, Array HtmlNode ) 148 | 149 | 150 | type alias ElementToHtml = 151 | Element -> Array HtmlNode -> HtmlNode 152 | 153 | 154 | type alias HtmlToElement = 155 | ElementDefinition -> HtmlNode -> Maybe ( Element, Array HtmlNode ) 156 | 157 | 158 | type ElementDefinition 159 | = ElementDefinition ElementDefinitionContents 160 | 161 | 162 | type alias ElementDefinitionContents = 163 | { name : String 164 | , toHtmlNode : ElementToHtml 165 | , group : String 166 | , contentType : ContentType 167 | , fromHtmlNode : HtmlToElement 168 | , selectable : Bool 169 | } 170 | 171 | 172 | toStringContentType : ContentType -> String 173 | toStringContentType contentType = 174 | case contentType of 175 | TextBlockNodeType _ -> 176 | "TextBlockNodeType" 177 | 178 | InlineLeafNodeType -> 179 | "InlineLeafNodeType" 180 | 181 | BlockNodeType _ -> 182 | "BlockNodeType" 183 | 184 | BlockLeafNodeType -> 185 | "BlockLeafNodeType" 186 | -------------------------------------------------------------------------------- /src/RichText/Internal/DeleteWord.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.DeleteWord exposing (..) 2 | 3 | {- 4 | This is a helper module derived from the DraftJS logic for determining how to delete a word. 5 | -} 6 | 7 | import Regex 8 | 9 | 10 | punctuationRegexString : String 11 | punctuationRegexString = 12 | "[.,+*?$|#{}()'\\^\\-\\[\\]\\\\\\/!@%\"~=<>_:;" 13 | ++ "・、。〈-】〔-〟:-?!-/" 14 | ++ "[-`{-・⸮؟٪-٬؛،؍" 15 | ++ "﴾﴿᠁।၊။‐-‧‰-⁞]" 16 | 17 | 18 | chameleonCharactersRegexString : String 19 | chameleonCharactersRegexString = 20 | "['‘’]" 21 | 22 | 23 | whitespaceAndPunctuationRegexString : String 24 | whitespaceAndPunctuationRegexString = 25 | "\\s|(?![_])" ++ punctuationRegexString 26 | 27 | 28 | deleteWordRegexString : String 29 | deleteWordRegexString = 30 | "^" ++ "(?:" ++ whitespaceAndPunctuationRegexString ++ ")*" ++ "(?:" ++ chameleonCharactersRegexString ++ "|(?!" ++ whitespaceAndPunctuationRegexString ++ ").)*" ++ "(?:(?!" ++ whitespaceAndPunctuationRegexString ++ ").)" 31 | 32 | 33 | backspaceWordRegexString : String 34 | backspaceWordRegexString = 35 | "(?:(?!" ++ whitespaceAndPunctuationRegexString ++ ").)" ++ "(?:" ++ chameleonCharactersRegexString ++ "|(?!" ++ whitespaceAndPunctuationRegexString ++ ").)*" ++ "(?:" ++ whitespaceAndPunctuationRegexString ++ ")*" ++ "$" 36 | 37 | 38 | deleteWordRegex : Regex.Regex 39 | deleteWordRegex = 40 | Maybe.withDefault Regex.never (Regex.fromString deleteWordRegexString) 41 | 42 | 43 | backspaceWordRegex : Regex.Regex 44 | backspaceWordRegex = 45 | Maybe.withDefault Regex.never (Regex.fromString backspaceWordRegexString) 46 | -------------------------------------------------------------------------------- /src/RichText/Internal/Event.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.Event exposing (EditorChange, InitEvent, InputEvent, KeyboardEvent, PasteEvent, TextChange) 2 | 3 | {-| This module holds the records used for decoded events like input, keyboard, as well 4 | as a few custom events. 5 | -} 6 | 7 | import Json.Encode as E 8 | import RichText.Model.Node exposing (Path) 9 | import RichText.Model.Selection exposing (Selection) 10 | 11 | 12 | {-| Whenever the elm-editor MutationObserver detects a change, it triggers an editor change event 13 | that the editor has to respond to. Note that it's important for the editor to respond to every 14 | change event so that the VirtualDOM doesn't try to render when the DOM is not in the state that 15 | it's expecting. 16 | -} 17 | type alias EditorChange = 18 | { root : E.Value 19 | , selection : Maybe Selection 20 | , characterDataMutations : Maybe (List TextChange) 21 | , timestamp : Int 22 | , isComposing : Bool 23 | } 24 | 25 | 26 | {-| The attributes parsed from an input event. 27 | -} 28 | type alias InputEvent = 29 | { data : Maybe String, isComposing : Bool, inputType : String } 30 | 31 | 32 | {-| The attributes parsed from a keyboard event. 33 | -} 34 | type alias KeyboardEvent = 35 | { keyCode : Int 36 | , key : String 37 | , altKey : Bool 38 | , metaKey : Bool 39 | , ctrlKey : Bool 40 | , shiftKey : Bool 41 | , isComposing : Bool 42 | } 43 | 44 | 45 | {-| The attributes parsed from a `pastewithdata` event. 46 | -} 47 | type alias PasteEvent = 48 | { text : String 49 | , html : String 50 | } 51 | 52 | 53 | {-| The attributes parsed from an `editorinit` event. 54 | -} 55 | type alias InitEvent = 56 | { shortKey : String 57 | } 58 | 59 | 60 | {-| A represents a text change at the given path in a editor node or DOM tree. The string provided 61 | is the new text at that path. 62 | -} 63 | type alias TextChange = 64 | ( Path, String ) 65 | -------------------------------------------------------------------------------- /src/RichText/Internal/History.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.History exposing (..) 2 | 3 | {-| This module contains the internal data structure for undo/redo history. 4 | -} 5 | 6 | import BoundedDeque exposing (BoundedDeque) 7 | import RichText.Model.State exposing (State) 8 | 9 | 10 | {-| `History` contains the undo deque and redo stack related to undo history. 11 | -} 12 | type History 13 | = History Contents 14 | 15 | 16 | {-| The contents used to initialize history, namely: 17 | 18 | - `undoDeque` is a deque of (action-name, previousStack) 19 | - `redoStack` is a list of states that have just been undone 20 | - `groupDelayMilliseconds` is the delay which the editor will group text changes into one entry 21 | - `lastTextChangeTimestamp` is the last text change timestamp in milliseconds 22 | 23 | -} 24 | type alias Contents = 25 | { undoDeque : BoundedDeque ( String, State ) 26 | , redoStack : List State 27 | , groupDelayMilliseconds : Int 28 | , lastTextChangeTimestamp : Int 29 | } 30 | 31 | 32 | {-| Retrieves the contents of `History` 33 | -} 34 | contents : History -> Contents 35 | contents history = 36 | case history of 37 | History c -> 38 | c 39 | 40 | 41 | peek : History -> Maybe ( String, State ) 42 | peek history = 43 | case history of 44 | History c -> 45 | BoundedDeque.first c.undoDeque 46 | 47 | 48 | undoList : History -> List ( String, State ) 49 | undoList history = 50 | case history of 51 | History c -> 52 | BoundedDeque.toList c.undoDeque 53 | 54 | 55 | redoList : History -> List State 56 | redoList history = 57 | case history of 58 | History c -> 59 | c.redoStack 60 | 61 | 62 | {-| Initializes history from `Contents` 63 | -} 64 | fromContents : Contents -> History 65 | fromContents c = 66 | History c 67 | 68 | 69 | {-| Initializes an `History` with an empty Deque and initial size 70 | 71 | - `groupDelayMilliseconds` is the delay which the editor will group text changes into one entry 72 | - `size` is the number of states stored in the history 73 | 74 | -} 75 | empty : { groupDelayMilliseconds : Int, size : Int } -> History 76 | empty config = 77 | History 78 | { undoDeque = BoundedDeque.empty config.size 79 | , redoStack = [] 80 | , groupDelayMilliseconds = config.groupDelayMilliseconds 81 | , lastTextChangeTimestamp = 0 82 | } 83 | -------------------------------------------------------------------------------- /src/RichText/Internal/HtmlNode.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.HtmlNode exposing (..) 2 | 3 | import Array exposing (Array) 4 | import RichText.Config.ElementDefinition as ElementDefinition 5 | import RichText.Config.MarkDefinition as MarkDefinition 6 | import RichText.Config.Spec exposing (Spec) 7 | import RichText.Internal.Spec exposing (elementDefinitionWithDefault, markDefinitionWithDefault) 8 | import RichText.Model.Element exposing (Element) 9 | import RichText.Model.HtmlNode exposing (HtmlNode(..)) 10 | import RichText.Model.InlineElement as InlineElement 11 | import RichText.Model.Mark exposing (Mark) 12 | import RichText.Model.Node as Node 13 | exposing 14 | ( Block 15 | , Children(..) 16 | , Inline(..) 17 | , InlineTree(..) 18 | , childNodes 19 | , toBlockArray 20 | , toInlineArray 21 | , toInlineTree 22 | ) 23 | import RichText.Model.Text exposing (text) 24 | 25 | 26 | childNodesPlaceholder = 27 | Array.fromList 28 | [ ElementNode "__child_node_marker__" [] Array.empty ] 29 | 30 | 31 | {-| Renders marks to their HtmlNode representation. 32 | -} 33 | markToHtmlNode : Spec -> Mark -> Array HtmlNode -> HtmlNode 34 | markToHtmlNode spec mark children = 35 | let 36 | markDefinition = 37 | markDefinitionWithDefault mark spec 38 | in 39 | MarkDefinition.toHtmlNode markDefinition mark children 40 | 41 | 42 | {-| Renders element parameters to their HtmlNode representation. 43 | -} 44 | elementToHtmlNode : Spec -> Element -> Array HtmlNode -> HtmlNode 45 | elementToHtmlNode spec parameters children = 46 | let 47 | elementDefinition = 48 | elementDefinitionWithDefault parameters spec 49 | in 50 | ElementDefinition.toHtmlNode elementDefinition parameters children 51 | 52 | 53 | {-| Renders element block nodes to their HtmlNode representation. 54 | -} 55 | editorBlockNodeToHtmlNode : Spec -> Block -> HtmlNode 56 | editorBlockNodeToHtmlNode spec node = 57 | elementToHtmlNode spec (Node.element node) (childNodesToHtmlNode spec (childNodes node)) 58 | 59 | 60 | {-| Renders child nodes to their HtmlNode representation. 61 | -} 62 | childNodesToHtmlNode : Spec -> Children -> Array HtmlNode 63 | childNodesToHtmlNode spec childNodes = 64 | case childNodes of 65 | BlockChildren blockArray -> 66 | Array.map (editorBlockNodeToHtmlNode spec) (toBlockArray blockArray) 67 | 68 | InlineChildren inlineLeafArray -> 69 | Array.map (editorInlineLeafTreeToHtmlNode spec (toInlineArray inlineLeafArray)) (toInlineTree inlineLeafArray) 70 | 71 | Leaf -> 72 | Array.empty 73 | 74 | 75 | {-| Renders text nodes to their HtmlNode representation. 76 | -} 77 | textToHtmlNode : String -> HtmlNode 78 | textToHtmlNode text = 79 | TextNode text 80 | 81 | 82 | errorNode : HtmlNode 83 | errorNode = 84 | ElementNode "div" [ ( "class", "rte-error" ) ] Array.empty 85 | 86 | 87 | editorInlineLeafTreeToHtmlNode : Spec -> Array Inline -> InlineTree -> HtmlNode 88 | editorInlineLeafTreeToHtmlNode spec array tree = 89 | case tree of 90 | LeafNode i -> 91 | case Array.get i array of 92 | Nothing -> 93 | errorNode 94 | 95 | Just l -> 96 | editorInlineLeafToHtmlNode spec l 97 | 98 | MarkNode n -> 99 | markToHtmlNode spec n.mark (Array.map (editorInlineLeafTreeToHtmlNode spec array) n.children) 100 | 101 | 102 | {-| Renders inline leaf nodes to their HtmlNode representation. 103 | -} 104 | editorInlineLeafToHtmlNode : Spec -> Inline -> HtmlNode 105 | editorInlineLeafToHtmlNode spec node = 106 | case node of 107 | Text contents -> 108 | textToHtmlNode (text contents) 109 | 110 | InlineElement l -> 111 | elementToHtmlNode spec (InlineElement.element l) Array.empty 112 | -------------------------------------------------------------------------------- /src/RichText/Internal/KeyDown.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.KeyDown exposing (..) 2 | 3 | import Json.Decode as D 4 | import RichText.Config.Command exposing (CommandMap, namedCommandListFromKeyboardEvent) 5 | import RichText.Config.Spec exposing (Spec) 6 | import RichText.Internal.Editor exposing (Editor, Message(..), Tagger, applyNamedCommandList, isComposing, shortKey) 7 | import RichText.Internal.Event exposing (KeyboardEvent) 8 | 9 | 10 | preventDefaultOn : CommandMap -> Spec -> Editor -> Message -> ( Message, Bool ) 11 | preventDefaultOn commandMap spec editor msg = 12 | case msg of 13 | KeyDownEvent key -> 14 | if key.isComposing || isComposing editor then 15 | ( msg, False ) 16 | 17 | else 18 | ( msg, shouldPreventDefault commandMap spec editor key ) 19 | 20 | _ -> 21 | ( msg, False ) 22 | 23 | 24 | shouldPreventDefault : CommandMap -> Spec -> Editor -> KeyboardEvent -> Bool 25 | shouldPreventDefault comamndMap spec editor keyboardEvent = 26 | case handleKeyDownEvent comamndMap spec editor keyboardEvent of 27 | Err _ -> 28 | False 29 | 30 | Ok _ -> 31 | True 32 | 33 | 34 | preventDefaultOnKeyDownDecoder : Tagger msg -> CommandMap -> Spec -> Editor -> D.Decoder ( msg, Bool ) 35 | preventDefaultOnKeyDownDecoder tagger commandMap spec editor = 36 | D.map (\( i, b ) -> ( tagger i, b )) (D.map (preventDefaultOn commandMap spec editor) keyDownDecoder) 37 | 38 | 39 | keyDownDecoder : D.Decoder Message 40 | keyDownDecoder = 41 | D.map KeyDownEvent <| 42 | D.map7 KeyboardEvent 43 | (D.field "keyCode" D.int) 44 | (D.field "key" D.string) 45 | (D.field "altKey" D.bool) 46 | (D.field "metaKey" D.bool) 47 | (D.field "ctrlKey" D.bool) 48 | (D.field "shiftKey" D.bool) 49 | (D.oneOf [ D.field "isComposing" D.bool, D.succeed False ]) 50 | 51 | 52 | handleKeyDownEvent : CommandMap -> Spec -> Editor -> KeyboardEvent -> Result String Editor 53 | handleKeyDownEvent commandMap spec editor event = 54 | let 55 | namedCommandList = 56 | namedCommandListFromKeyboardEvent (shortKey editor) event commandMap 57 | in 58 | applyNamedCommandList namedCommandList spec editor 59 | 60 | 61 | handleKeyDown : KeyboardEvent -> CommandMap -> Spec -> Editor -> Editor 62 | handleKeyDown keyboardEvent commandMap spec editor = 63 | if keyboardEvent.isComposing || isComposing editor then 64 | editor 65 | 66 | else 67 | Result.withDefault editor <| handleKeyDownEvent commandMap spec editor keyboardEvent 68 | -------------------------------------------------------------------------------- /src/RichText/Internal/Selection.elm: -------------------------------------------------------------------------------- 1 | module RichText.Internal.Selection exposing 2 | ( domToEditor 3 | , editorToDom 4 | ) 5 | 6 | import RichText.Config.Spec exposing (Spec) 7 | import RichText.Internal.Path as Path 8 | import RichText.Model.Node exposing (Block, Path) 9 | import RichText.Model.Selection 10 | exposing 11 | ( Selection 12 | , anchorNode 13 | , anchorOffset 14 | , focusNode 15 | , focusOffset 16 | , range 17 | ) 18 | 19 | 20 | domToEditor : Spec -> Block -> Selection -> Maybe Selection 21 | domToEditor spec = 22 | transformSelection (Path.domToEditor spec) 23 | 24 | 25 | editorToDom : Spec -> Block -> Selection -> Maybe Selection 26 | editorToDom spec = 27 | transformSelection (Path.editorToDom spec) 28 | 29 | 30 | transformSelection : (Block -> Path -> Maybe Path) -> Block -> Selection -> Maybe Selection 31 | transformSelection transformation node selection = 32 | case transformation node (anchorNode selection) of 33 | Nothing -> 34 | Nothing 35 | 36 | Just an -> 37 | case transformation node (focusNode selection) of 38 | Nothing -> 39 | Nothing 40 | 41 | Just fn -> 42 | Just <| range an (anchorOffset selection) fn (focusOffset selection) 43 | -------------------------------------------------------------------------------- /src/RichText/Model/Element.elm: -------------------------------------------------------------------------------- 1 | module RichText.Model.Element exposing (Element, element, annotations, attributes, name, withAnnotations, withAttributes) 2 | 3 | {-| An element represents the parameters of any non-text node. 4 | 5 | @docs Element, element, annotations, attributes, name, withAnnotations, withAttributes 6 | 7 | -} 8 | 9 | import RichText.Internal.Definitions as Internal exposing (ElementDefinition) 10 | import RichText.Model.Attribute exposing (Attribute) 11 | import Set exposing (Set) 12 | 13 | 14 | {-| An `Element` represents the parameters of non-text nodes. It consists of an element name, 15 | a list of attributes, and a set of annotations. 16 | -} 17 | type alias Element = 18 | Internal.Element 19 | 20 | 21 | {-| Creates an element. The arguments are as follows: 22 | 23 | - The first argument is the `ElementDefinition` that defines this element. Note that even though 24 | elements require an element definition, it's still safe to use `(==)` because the function arguments 25 | are not stored on the resulting `Element`. 26 | 27 | - The second argument is element's list of attributes. 28 | 29 | ``` 30 | element header [IntegerAttribute "level" 1] 31 | --> creates a header (h1) element 32 | ``` 33 | 34 | -} 35 | element : ElementDefinition -> List Attribute -> Element 36 | element = 37 | Internal.element 38 | 39 | 40 | {-| Annotations from an element 41 | 42 | annotations (element horizontal_rule [] (Set.singleton selectable)) 43 | --> Set [ selectable ] 44 | 45 | -} 46 | annotations : Element -> Set String 47 | annotations = 48 | Internal.annotationsFromElement 49 | 50 | 51 | {-| Attributes from an element 52 | 53 | attributes (element image [StringAttribute "src" "logo.svg"]) 54 | --> [StringAttribute "src" "logo.svg"] 55 | 56 | -} 57 | attributes : Element -> List Attribute 58 | attributes = 59 | Internal.attributesFromElement 60 | 61 | 62 | {-| Name from an element 63 | 64 | name (element image [StringAttribute "src" "logo.svg"]) 65 | --> "image" 66 | 67 | -} 68 | name : Element -> String 69 | name = 70 | Internal.nameFromElement 71 | 72 | 73 | {-| An element with the annotations changed to the given set 74 | 75 | element <| withAnnotations (Set.singleton selectable) 76 | --> an element with the annotations changed to the singleton selectable set 77 | 78 | -} 79 | withAnnotations : Set String -> Element -> Element 80 | withAnnotations = 81 | Internal.elementWithAnnotations 82 | 83 | 84 | {-| An element with the attributes changed to the given list 85 | 86 | element <| withAnnotations [StringAttribute "src" "logo.svg"] 87 | --> an element with the attributes changed to the list provided 88 | 89 | -} 90 | withAttributes : List Attribute -> Element -> Element 91 | withAttributes = 92 | Internal.elementWithAttributes 93 | -------------------------------------------------------------------------------- /src/RichText/Model/History.elm: -------------------------------------------------------------------------------- 1 | module RichText.Model.History exposing (History, empty, peek, undoList, redoList) 2 | 3 | {-| This module contains the type used to store undo/redo history. 4 | 5 | @docs History, empty, peek, undoList, redoList 6 | 7 | -} 8 | 9 | import RichText.Internal.History as Internal 10 | import RichText.Model.State exposing (State) 11 | 12 | 13 | {-| `History` contains the undo deque and redo stack related to undo history. 14 | -} 15 | type alias History = 16 | Internal.History 17 | 18 | 19 | {-| Provides an empty `History` with the given config. The config values are as follows: 20 | 21 | - `groupDelayMilliseconds` is the interval which the editor will ignore adding multiple text changes onto the undo stack. This is 22 | so the history doesn't get overwhelmed by single character changes. 23 | - `size` is the number of states stored in the history 24 | 25 | -} 26 | empty : { groupDelayMilliseconds : Int, size : Int } -> History 27 | empty = 28 | Internal.empty 29 | 30 | 31 | {-| Returns the last executed action and previous state on the undo stack. 32 | -} 33 | peek : History -> Maybe ( String, State ) 34 | peek = 35 | Internal.peek 36 | 37 | 38 | {-| Returns the entire undo stack. 39 | -} 40 | undoList : History -> List ( String, State ) 41 | undoList = 42 | Internal.undoList 43 | 44 | 45 | {-| Returns the entire redo stack. 46 | -} 47 | redoList : History -> List State 48 | redoList = 49 | Internal.redoList 50 | -------------------------------------------------------------------------------- /src/RichText/Model/HtmlNode.elm: -------------------------------------------------------------------------------- 1 | module RichText.Model.HtmlNode exposing (HtmlNode(..), HtmlAttribute) 2 | 3 | {-| `HtmlNode` is used to determine how to render the editor. We don't use the built in Html library 4 | because we can't inspect a node after it has been created. 5 | 6 | @docs HtmlNode, HtmlAttribute 7 | 8 | -} 9 | 10 | import Array exposing (Array) 11 | 12 | 13 | {-| An HTML node. It can be either an `ElementNode` or `TextNode` 14 | 15 | ElementNode "p" [ ( "class", "my-paragraph" ) ] (Array.fromList [ Text "sample" ]) 16 | 17 | -} 18 | type HtmlNode 19 | = ElementNode String (List HtmlAttribute) (Array HtmlNode) 20 | | TextNode String 21 | 22 | 23 | {-| An HTML attribute: 24 | 25 | ( "src", "logo.svg" ) 26 | 27 | -} 28 | type alias HtmlAttribute = 29 | ( String, String ) 30 | -------------------------------------------------------------------------------- /src/RichText/Model/InlineElement.elm: -------------------------------------------------------------------------------- 1 | module RichText.Model.InlineElement exposing (InlineElement, inlineElement, element, marks, withElement, withMarks) 2 | 3 | {-| An inline element is an element with marks. It represents the contents of an inline node that is 4 | not a text node. 5 | 6 | @docs InlineElement, inlineElement, element, marks, withElement, withMarks 7 | 8 | -} 9 | 10 | import RichText.Model.Element exposing (Element) 11 | import RichText.Model.Mark exposing (Mark) 12 | 13 | 14 | {-| `InlineElement` is an element with marks. It represents the contents of an inline node that is 15 | not a text node. 16 | -} 17 | type InlineElement 18 | = InlineElement InlineElementContents 19 | 20 | 21 | type alias InlineElementContents = 22 | { marks : List Mark 23 | , element : Element 24 | } 25 | 26 | 27 | {-| Marks from an inline element 28 | -} 29 | marks : InlineElement -> List Mark 30 | marks parameters = 31 | case parameters of 32 | InlineElement c -> 33 | c.marks 34 | 35 | 36 | {-| `Element` from an inline element 37 | -} 38 | element : InlineElement -> Element 39 | element parameters = 40 | case parameters of 41 | InlineElement c -> 42 | c.element 43 | 44 | 45 | {-| Creates an inline element from an element and a list of marks 46 | -} 47 | inlineElement : Element -> List Mark -> InlineElement 48 | inlineElement parameters m = 49 | InlineElement { element = parameters, marks = m } 50 | 51 | 52 | {-| Creates an inline element with the new `Element` 53 | -} 54 | withElement : Element -> InlineElement -> InlineElement 55 | withElement eparams iparams = 56 | case iparams of 57 | InlineElement c -> 58 | InlineElement { c | element = eparams } 59 | 60 | 61 | {-| Creates an inline element with the new marks 62 | -} 63 | withMarks : List Mark -> InlineElement -> InlineElement 64 | withMarks m iparams = 65 | case iparams of 66 | InlineElement c -> 67 | InlineElement { c | marks = m } 68 | -------------------------------------------------------------------------------- /src/RichText/Model/Selection.elm: -------------------------------------------------------------------------------- 1 | module RichText.Model.Selection exposing 2 | ( Selection, anchorNode, anchorOffset, focusNode, focusOffset 3 | , caret, singleNodeRange, range 4 | , isCollapsed, normalize 5 | ) 6 | 7 | {-| A `Selection` represents the information received and translated from the selection web API. Note that 8 | the `anchorNode` and `focusNode` are translations of the node paths relative to the editor. 9 | 10 | 11 | # Selection 12 | 13 | @docs Selection, anchorNode, anchorOffset, focusNode, focusOffset 14 | 15 | 16 | # Initialization 17 | 18 | @docs caret, singleNodeRange, range 19 | 20 | 21 | # Helpers 22 | 23 | @docs isCollapsed, normalize 24 | 25 | -} 26 | 27 | import RichText.Model.Node exposing (Path) 28 | 29 | 30 | {-| A `Selection` represents the information received and translated from the selection API. Note that 31 | the `anchorNode` and `focusNode` are translations of the node paths relative to the editor. 32 | -} 33 | type Selection 34 | = Selection Contents 35 | 36 | 37 | type alias Contents = 38 | { anchorOffset : Int 39 | , anchorNode : Path 40 | , focusOffset : Int 41 | , focusNode : Path 42 | } 43 | 44 | 45 | {-| The path to the selection anchor node 46 | -} 47 | anchorNode : Selection -> Path 48 | anchorNode selection = 49 | case selection of 50 | Selection c -> 51 | c.anchorNode 52 | 53 | 54 | {-| The selection anchor offset 55 | -} 56 | anchorOffset : Selection -> Int 57 | anchorOffset selection = 58 | case selection of 59 | Selection c -> 60 | c.anchorOffset 61 | 62 | 63 | {-| The path to the selection focus node 64 | -} 65 | focusNode : Selection -> Path 66 | focusNode selection = 67 | case selection of 68 | Selection c -> 69 | c.focusNode 70 | 71 | 72 | {-| The selection focus offset 73 | -} 74 | focusOffset : Selection -> Int 75 | focusOffset selection = 76 | case selection of 77 | Selection c -> 78 | c.focusOffset 79 | 80 | 81 | {-| This is a helper method for constructing a caret selection. 82 | 83 | caret [0, 1] 0 84 | --> Creates a selection with { anchorNode=[0,1], anchorOffset=0, focusNode=[0,1], focusOffset=0 } 85 | 86 | -} 87 | caret : Path -> Int -> Selection 88 | caret nodePath offset = 89 | singleNodeRange nodePath offset offset 90 | 91 | 92 | {-| This is a helper method for creating a range selection 93 | 94 | range [0, 1] 0 [1, 1] 1 95 | --> Creates a selection with { anchorNode=[0,1], anchorOffset=0, focusNode=[1,1], focusOffset=1 } 96 | 97 | -} 98 | range : Path -> Int -> Path -> Int -> Selection 99 | range aNode aOffset fNode fOffset = 100 | Selection 101 | { anchorOffset = aOffset 102 | , anchorNode = aNode 103 | , focusOffset = fOffset 104 | , focusNode = fNode 105 | } 106 | 107 | 108 | {-| This is a helper method for creating a selection over a single node 109 | 110 | singleNodeRange [0, 1] 0 1 111 | --> Creates a selection with { anchorNode=[0,1], anchorOffset=0, focusNode=[0,1], focusOffset=1 } 112 | 113 | -} 114 | singleNodeRange : Path -> Int -> Int -> Selection 115 | singleNodeRange node aOffset fOffset = 116 | range node aOffset node fOffset 117 | 118 | 119 | {-| This is a helper method for determining if a selection is collapsed. 120 | 121 | isCollapsed <| singleNodeRange [0, 1] 0 1 122 | --> False 123 | 124 | isCollapsed <| caret [0, 1] 0 125 | --> True 126 | 127 | -} 128 | isCollapsed : Selection -> Bool 129 | isCollapsed selection = 130 | case selection of 131 | Selection c -> 132 | c.anchorOffset == c.focusOffset && c.anchorNode == c.focusNode 133 | 134 | 135 | {-| Sorts the selection's anchor to be before the focus. This method is helpful because in the selection 136 | API, a selection's anchor node is not always before a selection's focus node, but when reasoning about editor 137 | operations, we want the anchor to be before the focus. 138 | 139 | normalize <| range [ 1, 1 ] 0 [ 0, 1 ] 1 140 | --> { anchorNode=[0,1], anchorOffset=1, focusNode=[1,1], focusOffset=0 } 141 | 142 | normalize <| singleNodeRange [0, 1] 1 0 143 | --> { anchorNode=[0,1], anchorOffset=0, focusNode=[0,1], focusOffset=1 } 144 | 145 | -} 146 | normalize : Selection -> Selection 147 | normalize selection = 148 | case selection of 149 | Selection c -> 150 | Selection <| 151 | case compare c.anchorNode c.focusNode of 152 | EQ -> 153 | { c | anchorOffset = min c.focusOffset c.anchorOffset, focusOffset = max c.focusOffset c.anchorOffset } 154 | 155 | LT -> 156 | c 157 | 158 | GT -> 159 | { c | focusNode = c.anchorNode, focusOffset = c.anchorOffset, anchorNode = c.focusNode, anchorOffset = c.focusOffset } 160 | -------------------------------------------------------------------------------- /src/RichText/Model/State.elm: -------------------------------------------------------------------------------- 1 | module RichText.Model.State exposing (State, state, root, selection, withRoot, withSelection) 2 | 3 | {-| A `State` consists of a root block and a selection. `State` allows you to keep 4 | track of and manipulate the contents of the editor. 5 | 6 | @docs State, state, root, selection, withRoot, withSelection 7 | 8 | -} 9 | 10 | import RichText.Model.Node exposing (Block) 11 | import RichText.Model.Selection exposing (Selection) 12 | 13 | 14 | {-| A `State` consists of a root block and a selection. `State` allows you to keep 15 | track of and manipulate the contents of the editor. 16 | -} 17 | type State 18 | = State Contents 19 | 20 | 21 | type alias Contents = 22 | { root : Block 23 | , selection : Maybe Selection 24 | } 25 | 26 | 27 | {-| Creates a `State`. The arguments are as follows: 28 | 29 | - `root` is a block node that represents the root of the editor. 30 | 31 | - `selection` is a `Maybe Selection` that is the selected part of the editor 32 | 33 | ``` 34 | root : Block 35 | root = 36 | block 37 | (Element.element doc []) 38 | (blockChildren <| 39 | Array.fromList 40 | [ block 41 | (Element.element paragraph []) 42 | (inlineChildren <| Array.fromList [ plainText "" ]) 43 | ] 44 | ) 45 | 46 | state root Nothing 47 | --> an empty editor state with no selection 48 | ``` 49 | 50 | -} 51 | state : Block -> Maybe Selection -> State 52 | state root_ sel_ = 53 | State { root = root_, selection = sel_ } 54 | 55 | 56 | {-| the selection from the state 57 | -} 58 | selection : State -> Maybe Selection 59 | selection st = 60 | case st of 61 | State s -> 62 | s.selection 63 | 64 | 65 | {-| the root node from the state 66 | -} 67 | root : State -> Block 68 | root st = 69 | case st of 70 | State s -> 71 | s.root 72 | 73 | 74 | {-| a state with the given selection 75 | -} 76 | withSelection : Maybe Selection -> State -> State 77 | withSelection sel st = 78 | case st of 79 | State s -> 80 | State { s | selection = sel } 81 | 82 | 83 | {-| a state with the given root 84 | -} 85 | withRoot : Block -> State -> State 86 | withRoot node st = 87 | case st of 88 | State s -> 89 | State { s | root = node } 90 | -------------------------------------------------------------------------------- /src/RichText/Model/Text.elm: -------------------------------------------------------------------------------- 1 | module RichText.Model.Text exposing (Text, empty, text, marks, annotations, withText, withMarks, withAnnotations) 2 | 3 | {-| `Text` represents an editor text node and associated mark and annotation metadata. 4 | 5 | @docs Text, empty, text, marks, annotations, withText, withMarks, withAnnotations 6 | 7 | -} 8 | 9 | import RichText.Model.Mark exposing (Mark) 10 | import Set exposing (Set) 11 | 12 | 13 | {-| `Text` represents an editor text node and associated mark and annotation metadata. 14 | -} 15 | type Text 16 | = Text TextContents 17 | 18 | 19 | type alias TextContents = 20 | { marks : List Mark 21 | , annotations : Set String 22 | , text : String 23 | } 24 | 25 | 26 | {-| empty `Text` 27 | -} 28 | empty : Text 29 | empty = 30 | Text { text = "", marks = [], annotations = Set.empty } 31 | 32 | 33 | {-| marks from `Text` 34 | -} 35 | marks : Text -> List Mark 36 | marks parameters = 37 | case parameters of 38 | Text c -> 39 | c.marks 40 | 41 | 42 | {-| annotations from `Text` 43 | -} 44 | annotations : Text -> Set String 45 | annotations parameters = 46 | case parameters of 47 | Text c -> 48 | c.annotations 49 | 50 | 51 | {-| text from `Text` 52 | -} 53 | text : Text -> String 54 | text parameters = 55 | case parameters of 56 | Text c -> 57 | c.text 58 | 59 | 60 | {-| `Text` with the given text 61 | -} 62 | withText : String -> Text -> Text 63 | withText s parameters = 64 | case parameters of 65 | Text c -> 66 | Text { c | text = s } 67 | 68 | 69 | {-| `Text` with the given annotations 70 | -} 71 | withAnnotations : Set String -> Text -> Text 72 | withAnnotations ann parameters = 73 | case parameters of 74 | Text c -> 75 | Text { c | annotations = ann } 76 | 77 | 78 | {-| `Text` with the given marks 79 | -} 80 | withMarks : List Mark -> Text -> Text 81 | withMarks m parameters = 82 | case parameters of 83 | Text c -> 84 | Text { c | marks = m } 85 | --------------------------------------------------------------------------------