├── .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 |
20 | You need to enable JavaScript to run this app.
21 |
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 | "text text2
"
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 | "test 1
"
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 |
--------------------------------------------------------------------------------