├── .babelrc ├── .circleci └── config.yml ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── draft-to-markdown.js ├── index.js └── markdown-to-draft.js └── test ├── draft-to-markdown.spec.js ├── idempotency.spec.js └── markdown-to-draft.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env" 4 | ], 5 | "env": { 6 | "esm": { 7 | "presets": [ 8 | ["@babel/env", { "modules": false }] 9 | ], 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | 4 | build: 5 | machine: 6 | image: circleci/classic:latest 7 | working_directory: ~/Rosey/markdown-draft-js 8 | parallelism: 1 9 | shell: /bin/bash --login 10 | environment: 11 | CIRCLE_ARTIFACTS: /tmp/circleci-artifacts 12 | CIRCLE_TEST_REPORTS: /tmp/circleci-test-results 13 | NODE_ENV: development 14 | DEBUG: kit:* 15 | steps: 16 | # Machine Setup 17 | # If you break your build into multiple jobs with workflows, you will probably want to do the parts of this that are relevant in each 18 | # The following `checkout` command checks out your code to your working directory. In 1.0 we did this implicitly. In 2.0 you can choose where in the course of a job your code should be checked out. 19 | - checkout 20 | # Prepare for artifact and test results collection equivalent to how it was done on 1.0. 21 | # In many cases you can simplify this from what is generated here. 22 | # 'See docs on artifact collection here https://circleci.com/docs/2.0/artifacts/' 23 | - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS 24 | # This is based on your 1.0 configuration file or project settings 25 | - run: 26 | working_directory: ~/Rosey/markdown-draft-js 27 | command: nvm install 10.13.0 && nvm alias default 10.13.0 28 | # The following line was run implicitly in your 1.0 builds based on what CircleCI inferred about the structure of your project. In 2.0 you need to be explicit about which commands should be run. In some cases you can discard inferred commands if they are not relevant to your project. 29 | - run: if [ -z "${NODE_ENV:-}" ]; then export NODE_ENV=test; fi 30 | - run: export PATH="~/Rosey/markdown-draft-js/node_modules/.bin:$PATH" 31 | - run: npm install 32 | # Test 33 | # This would typically be a build job when using workflows, possibly combined with build 34 | # This is based on your 1.0 configuration file or project settings 35 | - run: npm run test 36 | # This is based on your 1.0 configuration file or project settings 37 | - run: npm run lint 38 | - run: npm run lint:tests 39 | # Teardown 40 | # If you break your build into multiple jobs with workflows, you will probably want to do the parts of this that are relevant in each 41 | # Save test results 42 | - store_test_results: 43 | path: /tmp/circleci-test-results 44 | # Save artifacts 45 | - store_artifacts: 46 | path: /tmp/circleci-artifacts 47 | - store_artifacts: 48 | path: /tmp/circleci-test-results 49 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "env" : { 4 | "browser" : true 5 | }, 6 | "rules": { 7 | "comma-dangle": [1, "never"], 8 | "func-names": 0, 9 | "prefer-const": 0, 10 | "indent": [2, 2, {"SwitchCase": 1, "VariableDeclarator": 2}], 11 | "jsx-quotes": [2, "prefer-single"], 12 | "key-spacing": 0, 13 | "no-else-return": 0, 14 | "quotes": [2, "single"], 15 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 16 | "one-var": 0, 17 | "no-cond-assign": 0, 18 | "default-case": 0, 19 | "no-multiple-empty-lines": [2, {"max": 1}], 20 | "no-multi-spaces": [2, { exceptions: { "ImportDeclaration": true, "VariableDeclarator": true } }] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | esm 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## [2.4.0] - 2021-10-01 5 | 6 | - Include block as a param in entity open/close (https://github.com/Rosey/markdown-draft-js/pull/160) 7 | - Support for sub, sup, htmlblock (https://github.com/Rosey/markdown-draft-js/pull/163) 8 | 9 | ## [2.3.0] - 2021-07-16 10 | 11 | - Fix for overlapping inline styles (https://github.com/Rosey/markdown-draft-js/pull/130) 12 | - Bump lodash version to 4.17.19 (https://github.com/Rosey/markdown-draft-js/pull/132) 13 | - Fix ordered list numbering (https://github.com/Rosey/markdown-draft-js/pull/135) 14 | - Add strikethrough support (https://github.com/Rosey/markdown-draft-js/issues/156) 15 | - Fix issue with newlines after lists when `preserveNewLines` is `true` (https://github.com/Rosey/markdown-draft-js/pull/146) 16 | 17 | ## [2.2.1] - 2020-06-16 18 | 19 | - Update remarkable dependency (https://github.com/Rosey/markdown-draft-js/pull/126) 20 | - Reduce package size (https://github.com/Rosey/markdown-draft-js/pull/125) 21 | - Fix bug with some unicode surrogate pairs and entity items on draft-to-markdown (https://github.com/Rosey/markdown-draft-js/pull/123) 22 | 23 | ## [2.2.0] - 2020-01-30 24 | 25 | - Fixed issue with newlines not always matching correctly when `preserveNewlines: true` is set. Issue outlining the bug here: https://github.com/Rosey/markdown-draft-js/issues/111 26 | 27 | ## [2.1.1] - 2019-10-10 28 | ### Fixes 29 | 30 | - Fixed bug where inline styles like bold or italic would become malformed when converting from draft to markdown if they included a trailing `\n` character. 31 | 32 | ## [2.1.0] - 2019-10-09 33 | ### Fixes 34 | 35 | - Fixed issue when soft newlines were used in draft didn’t convert correctly to markdown. 36 | 37 | ### Documentation 38 | 39 | - Update README to be a bit more current w/r/t project status. 40 | 41 | ## [2.0.0] - 2019-08-07 42 | ### Changes 43 | - **Potentially breaking change:** Update remarkable.js dependency to version 2. Addresses security concerns and features a smaller package size. 44 | - Update devDependency packages to latest versions 45 | - Add husky pre-push linting hook 46 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at rose@r.osey.me. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 😷 **COVID update** I am now stuck at home full time with two very young children and get zero breaks all day from the moment I wake up until the moment I go to sleep. Anything that requires deep thought, like reviewing and writing code, as well as taking deep dives into open issues, is basically impossible for me right now! So sorry if things fall behind. 2 | 3 | ⭐️ **A quick maintenance note:** I ([Rosey](https://github.com/Rosey)) work full time and am the mom to a toddler, a soon-to-be newborn (coming Dec 2019), and a high–energy dog, and this is just a side–project 😇 Therefore, it’s much appreciated if people who encounter bugs and are able to do so, they open a PR with a proposed fix (vs opening an issue and waiting for me to fix it). OR if you happen to be using this library and see an open issue you’re able to fix, I would love it if you opened a PR with that fix! Of course, feel free to continue to open issues if you don’t have the time or knowledge to fix a bug you notice, I just want to set the expectation that response time will not be super speedy 🙃 I’ll do my best to review and merge any PRs that do get opened. Thank you! ❤️ And thank you to everyone who has helped out and contributed to this project, it has been a real delight 🥰 4 | 5 | # Markdown draft js 6 | 7 | A tool for converting [Draft.js](https://draftjs.org) [raw object](https://draftjs.org/docs/api-reference-data-conversion) to [markdown](https://daringfireball.net/projects/markdown/), and vice-versa. 8 | 9 | **Looking for an example?** [There is a running example here](https://rosey.github.io/markdown-draft-js/) 10 | 11 | ### Markdown draft js vs other similar projects - what’s the story? 12 | 13 | I started this project in 2016 because I was in need of a draft/markdown conversion tool that could handle custom entities, such as mentions, and the existing conversion tools out there didn’t support these slightly complex needs. I was also finding various bugs with the existing conversion tools and none of them seemed to be maintained, so I decided to write my own. 14 | 15 | It’s now 2019 and the landscape has potentially changed! I don’t spend a ton of time keeping tabs on other draftjs markdown conversion tools out there, but I believe there are a few that are actively maintained and significantly more popular than this one, such as [draft-js-export-markdown](https://github.com/sstur/draft-js-utils/tree/master/packages/draft-js-export-markdown). Before choosing this project, I encourage you to do your research! This may still be the best tool for what you need, but it’s always worth being critical and looking at all your options 😃 Stability wise, I use markdown-draft-js in a production environment with over 10k monthly active users and it has served very well so far. 16 | 17 | ## Basic Usage 18 | 19 | Please note: We recommend using a polyfill (like babel-polyfill) since we're using a bunch of modern array methods. 20 | 21 | `draftToMarkdown` expects a [RAW Draft.js JS object](https://draftjs.org/docs/api-reference-data-conversion). 22 | 23 | It returns a string of markdown. 24 | 25 | ```javascript 26 | // First, import `draftToMarkdown` 27 | import { draftToMarkdown } from 'markdown-draft-js'; 28 | 29 | var markdownString = draftToMarkdown(rawObject); 30 | ``` 31 | 32 | `markdownToDraft` expects a string containing markdown. 33 | 34 | It returns a [RAW Draft.js JS object](https://draftjs.org/docs/api-reference-data-conversion). 35 | 36 | ```javascript 37 | // First, import `draftToMarkdown` 38 | import { markdownToDraft } from 'markdown-draft-js'; 39 | 40 | var rawObject = markdownToDraft(markdownString); 41 | ``` 42 | 43 | ## Example 44 | 45 | ```javascript 46 | [---] 47 | 48 | import { draftToMarkdown, markdownToDraft } from 'markdown-draft-js'; 49 | import { EditorState, convertToRaw, convertFromRaw } from 'draft-js'; 50 | 51 | [---] 52 | 53 | constructor(props) { 54 | super(props); 55 | 56 | // Convert input from markdown to draftjs state 57 | const markdownString = this.props.markdownString; 58 | const rawData = markdownToDraft(markdownString); 59 | const contentState = convertFromRaw(rawData); 60 | const newEditorState = EditorState.createWithContent(contentState); 61 | this.state = { 62 | editorState: newEditorState, 63 | }; 64 | 65 | this.onChange = (editorState) => { 66 | this.setState({ editorState }); 67 | 68 | // Convert draftjs state to markdown 69 | const content = editorState.getCurrentContent(); 70 | const rawObject = convertToRaw(content); 71 | const markdownString = draftToMarkdown(rawObject); 72 | 73 | // Do something with the markdown 74 | }; 75 | } 76 | 77 | [---] 78 | ``` 79 | 80 | ## Custom Values 81 | 82 | In case you want to extend markdown’s functionality, you can. `draftToMarkdown` accepts an (optional) second `options` argument. 83 | 84 | It takes two values: `styleItems` and `entityItems`. This is because of a distinction in draftjs between styles and entities. You can read more about them on [Draft’s documentation](https://draftjs.org/docs/api-reference-character-metadata). 85 | 86 | Say I wanted to convert **red text** from my Draft.js editor to a span with a red colour style. Unless I write a custom method for it, the markdown parser will ignore this special style, since it’s not a normal, pre-defined style. (An example of this style item is defined in one of the Draft.js [custom colours](https://github.com/facebook/draft-js/tree/master/examples/color) examples.) 87 | 88 | However, I can pass in a custom renderer for the `red` style type, and then decide how I want it to be depicted in markdown. Since markdown parsers usually also accept HTML, in this example I’ll just have my custom renderer do a `span` with a red style. Here it is: 89 | 90 | ```javascript 91 | var markdownString = draftToMarkdown(rawObject, { 92 | styleItems: { 93 | red: { 94 | open: function () { 95 | return ''; 96 | }, 97 | 98 | close: function () { 99 | return ''; 100 | } 101 | } 102 | } 103 | }); 104 | ``` 105 | 106 | `red` is the value of the `style` key in the raw object. The `open` method is what precedes the actual text, and `close` is what succeeds it. 107 | 108 | Here’s another example, with a mention entity type - 109 | 110 | 111 | ```javascript 112 | var markdownString = draftToMarkdown(rawObject, { 113 | entityItems: { 114 | mention: { 115 | open: function (entity, block) { 116 | return ''; 117 | }, 118 | 119 | close: function (entity) { 120 | return ''; 121 | } 122 | } 123 | } 124 | }); 125 | ``` 126 | 127 | Since entities can also contain additional custom information - in this case, the user’s id, an `entity` object is passed to the open and close methods so that you can use that information in your open/close text if you need to. 128 | 129 | In case you need more information about the block the entity belongs to, it is available as the second parameter of the open/close methods. 130 | 131 | What if you wanted to go the opposite direction? markdownToDraft uses [Remarkable](https://github.com/jonschlinkert/remarkable) for defining custom markdown types. 132 | 133 | In this case, you need to write a [remarkable plugin](https://github.com/jonschlinkert/remarkable/blob/master/docs/plugins.md) first and pass it in to `markdownToDraft` - 134 | 135 | ```javascript 136 | var rawDraftJSObject = markdownToDraft(markdownString, { 137 | remarkablePlugins: [remarkableMentionPlugin], 138 | blockEntities: { 139 | mention_open: function (item) { 140 | return { 141 | type: "mention", 142 | mutability: "IMMUTABLE", 143 | data: { 144 | mention: { 145 | id: item.id, 146 | name: item.name 147 | } 148 | } 149 | }; 150 | } 151 | } 152 | }); 153 | ``` 154 | 155 | 156 | ## Additional options 157 | 158 | ### Remarkable options 159 | 160 | Since this module uses Remarkable under the hood, you can also pass down preset and options for the Remarkable parser. Simply add the `remarkablePreset` or `remarkableOptions` property (or both of them) to your options object. For example, let's say you wanted to use the `commonmark` preset and parse html as well: 161 | 162 | ```javascript 163 | var rawDraftJSObject = markdownToDraft(markdownString, { 164 | remarkablePreset: 'commonmark', 165 | remarkableOptions: { 166 | html: true 167 | } 168 | }); 169 | ``` 170 | 171 | #### Enabling / Disabling rules 172 | 173 | It's possible to enable or disable specific rules. Remarkable categorizes them into three groups, every file represents a possible rule: 174 | 175 | - [Inline](https://github.com/jonschlinkert/remarkable/tree/master/lib/rules_inline) (e.g. links, bold, italic) 176 | - [Block](https://github.com/jonschlinkert/remarkable/tree/master/lib/rules_block) (e.g. tables, headings) 177 | - [Core](https://github.com/jonschlinkert/remarkable/tree/master/lib/rules_core) (e.g. automatic link conversion or abbreviations) 178 | 179 | ```javascript 180 | var rawDraftJSObject = markdownToDraft(markdownString, { 181 | remarkablePreset: 'commonmark', 182 | remarkableOptions: { 183 | disable: { 184 | inline: ['links', 'emphasis'], 185 | block: ['heading'] 186 | }, 187 | enable: { 188 | block: 'table', 189 | core: ['abbr'] 190 | } 191 | } 192 | }); 193 | ``` 194 | 195 | The `table` rule is disabled by default but could be enabled like in the example above. 196 | 197 | ### More options 198 | 199 | `preserveNewlines` can be passed in to preserve multiple sequential empty lines. By default, markdown rules specify that blank whitespace is collapsed, the result being that more than one empty line will be reduced to a single empty line, but in the interest in maintaining 1:1 parity with draft appearance-wise, this option can be turned on if you like :) 200 | 201 | NOTE: If you plan on passing the markdown to a 3rd party markdown parser, markdown default behaviour IS to strip additional newlines, so the HTML it generates will likely strip those newlines at that point.... Which is why this is an option disabled by default. 202 | 203 | `escapeMarkdownCharacters` – by default this value is **true** and markdown characters (e.g. *~_`) typed directly into the editor by a user are escaped when converting from draft to markdown. 204 | 205 | Setting this option to **false** will prevent those special characters from being escaped, so the markdown string it generates will remain in its “raw” form. What this means is that when the markdown is later converted back to draftjs or parsed by a different markdown tool, any user-entered markdown will be rendered AS markdown, and will not "match" what the user initially entered. (So if the user explicitly typed in `**hello**`, converting to markdown and back to draft it will be restored as **hello** instead of the original `**hello**`) 206 | 207 | ### FAQ 208 | 209 | #### How do I get images to work? 210 | 211 | For now, check out [this pull request](https://github.com/Rosey/markdown-draft-js/pull/49) and the discussion below. [this comment](https://github.com/Rosey/markdown-draft-js/pull/49#issuecomment-369682808) outlines how to get images working by writing a basic plugin for markdown-draft-js. The reason it’s not built into the library itself is because draftjs doesn’t support images out of the box, so there’s no standardized way of supporting them in the library that will work for everyone. In the future I hope to publish a plugin for people to quickly add image support if they need to, but I haven’t quite gotten there yet 🙂 212 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['browserify', 'jasmine'], 5 | files: [ 6 | {pattern: 'test/**/*.spec.js', watched: true, included: true, served: true}, 7 | ], 8 | preprocessors: { 9 | 'src/**/*.js': ['browserify'], 10 | 'test/**/*.js': ['browserify'] 11 | }, 12 | browserify: { 13 | debug: true, 14 | transform: ['babelify'] 15 | }, 16 | exclude: [], 17 | reporters: ['progress'], 18 | port: 9876, 19 | colors: true, 20 | logLevel: config.LOG_INFO, 21 | autoWatch: true, 22 | browsers: ['Chrome'], 23 | singleRun: false 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-draft-js", 3 | "description": "Convert draftjs blocks to markdown using the marked library, and vice versa.", 4 | "author": "Rose Robertson", 5 | "version": "2.4.0", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Rosey/markdown-draft-js.git" 10 | }, 11 | "main": "lib/index.js", 12 | "module": "esm/index.js", 13 | "files": [ 14 | "lib", 15 | "esm" 16 | ], 17 | "scripts": { 18 | "build": "babel src --out-dir lib && NODE_ENV=esm babel src --out-dir esm", 19 | "prepublish": "npm run build", 20 | "test": "karma start --single-run", 21 | "lint": "eslint ./src", 22 | "lint:tests": "eslint ./test" 23 | }, 24 | "husky": { 25 | "hooks": { 26 | "pre-push": "npm run lint && npm run lint:tests" 27 | } 28 | }, 29 | "devDependencies": { 30 | "@babel/cli": "^7.5.5", 31 | "@babel/core": "^7.5.5", 32 | "@babel/preset-env": "^7.5.5", 33 | "babel-eslint": "^10.0.2", 34 | "babelify": "^10.0.0", 35 | "browserify": "^16.3.0", 36 | "eslint": "^6.1.0", 37 | "husky": "^3.0.2", 38 | "jasmine": "^3.4.0", 39 | "jasmine-core": "^3.4.0", 40 | "karma": "^4.2.0", 41 | "karma-browserify": "^6.1.0", 42 | "karma-chrome-launcher": "^3.0.0", 43 | "karma-jasmine": "^2.0.1", 44 | "watchify": "^3.11.1" 45 | }, 46 | "dependencies": { 47 | "remarkable": "^2.0.1" 48 | }, 49 | "engines": { 50 | "node": ">=10.13.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/draft-to-markdown.js: -------------------------------------------------------------------------------- 1 | const TRAILING_WHITESPACE = /[ \u0020\t\n]*$/; 2 | 3 | // This escapes some markdown but there's a few cases that are TODO - 4 | // - List items 5 | // - Back tics (see https://github.com/Rosey/markdown-draft-js/issues/52#issuecomment-388458017) 6 | // - Complex markdown, like links or images. Not sure it's even worth it, because if you're typing 7 | // that into draft chances are you know its markdown and maybe expect it convert? :/ 8 | const MARKDOWN_STYLE_CHARACTERS = ['*', '_', '~', '`']; 9 | const MARKDOWN_STYLE_CHARACTER_REGXP = /(\*|_|~|\\|`)/g; 10 | 11 | // I hate this a bit, being outside of the function’s scope 12 | // but can’t think of a better way to keep track of how many ordered list 13 | // items were are on, as draft doesn’t explicitly tell us in the raw object 😢. 14 | // This is a hash that will be assigned values based on depth, so like 15 | // orderedListNumber[0] = 1 would mean that ordered list at depth 0 is on number 1. 16 | // orderedListNumber[0] = 2 would mean that ordered list at depth 0 is on number 2. 17 | // This is so we have the right number of numbers when doing a list, eg 18 | // 1. Item One 19 | // 2. Item two 20 | // 3. Item three 21 | // And so on. 22 | var orderedListNumber = {}, 23 | previousOrderedListDepth = 0; 24 | 25 | // A map of draftjs block types -> markdown open and close characters 26 | // Both the open and close methods must exist, even if they simply return an empty string. 27 | // They should always return a string. 28 | const StyleItems = { 29 | // BLOCK LEVEL 30 | 'unordered-list-item': { 31 | open: function () { 32 | return '- '; 33 | }, 34 | 35 | close: function () { 36 | return ''; 37 | } 38 | }, 39 | 40 | 'ordered-list-item': { 41 | open: function (block, number = 1) { 42 | return `${number}. `; 43 | }, 44 | 45 | close: function () { 46 | return ''; 47 | } 48 | }, 49 | 50 | 'blockquote': { 51 | open: function () { 52 | return '> '; 53 | }, 54 | 55 | close: function () { 56 | return ''; 57 | } 58 | }, 59 | 60 | 'header-one': { 61 | open: function () { 62 | return '# '; 63 | }, 64 | 65 | close: function () { 66 | return ''; 67 | } 68 | }, 69 | 70 | 'header-two': { 71 | open: function () { 72 | return '## '; 73 | }, 74 | 75 | close: function () { 76 | return ''; 77 | } 78 | }, 79 | 80 | 'header-three': { 81 | open: function () { 82 | return '### '; 83 | }, 84 | 85 | close: function () { 86 | return ''; 87 | } 88 | }, 89 | 90 | 'header-four': { 91 | open: function () { 92 | return '#### '; 93 | }, 94 | 95 | close: function () { 96 | return ''; 97 | } 98 | }, 99 | 100 | 'header-five': { 101 | open: function () { 102 | return '##### '; 103 | }, 104 | 105 | close: function () { 106 | return ''; 107 | } 108 | }, 109 | 110 | 'header-six': { 111 | open: function () { 112 | return '###### '; 113 | }, 114 | 115 | close: function () { 116 | return ''; 117 | } 118 | }, 119 | 120 | 'code-block': { 121 | open: function (block) { 122 | return '```' + (block.data.language || '') + '\n'; 123 | }, 124 | 125 | close: function () { 126 | return '\n```'; 127 | } 128 | }, 129 | 130 | // INLINE LEVEL 131 | 'BOLD': { 132 | open: function () { 133 | return '**'; 134 | }, 135 | 136 | close: function () { 137 | return '**'; 138 | } 139 | }, 140 | 141 | 'ITALIC': { 142 | open: function () { 143 | return '_'; 144 | }, 145 | 146 | close: function () { 147 | return '_'; 148 | } 149 | }, 150 | 151 | 'STRIKETHROUGH': { 152 | open: function () { 153 | return '~~'; 154 | }, 155 | 156 | close: function () { 157 | return '~~'; 158 | } 159 | }, 160 | 161 | 'CODE': { 162 | open: function () { 163 | return '`'; 164 | }, 165 | 166 | close: function () { 167 | return '`'; 168 | } 169 | } 170 | }; 171 | 172 | // A map of draftjs entity types -> markdown open and close characters 173 | // entities are different from block types because they have additional data attached to them. 174 | // an entity object is passed in to both open and close, in case it's needed for string generation. 175 | // 176 | // Both the open and close methods must exist, even if they simply return an empty string. 177 | // They should always return a string. 178 | const EntityItems = { 179 | 'LINK': { 180 | open: function (entity) { 181 | return '['; 182 | }, 183 | 184 | close: function (entity) { 185 | return `](${entity.data.url || entity.data.href})`; 186 | } 187 | } 188 | } 189 | 190 | // Bit of a hack - we normally want a double newline after a block, 191 | // but for list items we just want one (unless it's the _last_ list item in a group.) 192 | const SingleNewlineAfterBlock = [ 193 | 'unordered-list-item', 194 | 'ordered-list-item' 195 | ]; 196 | 197 | function isEmptyBlock(block) { 198 | return block.text.length === 0 && block.entityRanges.length === 0 && Object.keys(block.data || {}).length === 0; 199 | } 200 | 201 | /** 202 | * Generate markdown for a single block javascript object 203 | * DraftJS raw object contains an array of blocks, which is the main "structure" 204 | * of the text. Each block = a new line. 205 | * 206 | * @param {Object} block - block to generate markdown for 207 | * @param {Number} index - index of the block in the blocks array 208 | * @param {Object} rawDraftObject - entire raw draft object (needed for accessing the entityMap) 209 | * @param {Object} options - additional options passed in by the user calling this method. 210 | * 211 | * @return {String} markdown string 212 | **/ 213 | function renderBlock(block, index, rawDraftObject, options) { 214 | var openInlineStyles = [], 215 | markdownToAdd = []; 216 | var markdownString = '', 217 | customStyleItems = options.styleItems || {}, 218 | customEntityItems = options.entityItems || {}, 219 | escapeMarkdownCharacters = options.hasOwnProperty('escapeMarkdownCharacters') ? options.escapeMarkdownCharacters : true; 220 | 221 | var type = block.type; 222 | 223 | var markdownStyleCharactersToEscape = []; 224 | 225 | // draft-js emits empty blocks that have type set… don’t style them unless the user wants to preserve new lines 226 | // (if newlines are preserved each empty line should be "styled" eg in case of blockquote we want to see a blockquote.) 227 | // but if newlines aren’t preserved then we'd end up having double or triple or etc markdown characters, which is a bug. 228 | if (isEmptyBlock(block) && !options.preserveNewlines) { 229 | type = 'unstyled'; 230 | } 231 | 232 | // Render main block wrapping element 233 | if (customStyleItems[type] || StyleItems[type]) { 234 | if (type === 'unordered-list-item' || type === 'ordered-list-item') { 235 | markdownString += ' '.repeat(block.depth * 4); 236 | } 237 | 238 | if (type === 'ordered-list-item') { 239 | orderedListNumber[block.depth] = orderedListNumber[block.depth] || 1; 240 | markdownString += (customStyleItems[type] || StyleItems[type]).open(block, orderedListNumber[block.depth]); 241 | orderedListNumber[block.depth]++; 242 | 243 | // Have to reset the number for orderedListNumber if we are breaking out of a list so that if 244 | // there's another nested list at the same level further down, it starts at 1 again. 245 | // COMPLICATED 😭 246 | if (previousOrderedListDepth > block.depth) { 247 | orderedListNumber[previousOrderedListDepth] = 1; 248 | } 249 | 250 | previousOrderedListDepth = block.depth; 251 | } else { 252 | orderedListNumber = {}; 253 | markdownString += (customStyleItems[type] || StyleItems[type]).open(block); 254 | } 255 | } else { 256 | orderedListNumber = {}; 257 | } 258 | 259 | // A stack to keep track of open tags 260 | var openTags = []; 261 | 262 | function openTag(tag) { 263 | openTags.push(tag); 264 | 265 | if (tag.style) { 266 | // Open inline tag 267 | 268 | if (customStyleItems[tag.style] || StyleItems[tag.style]) { 269 | var styleToAdd = ( 270 | customStyleItems[tag.style] || StyleItems[tag.style] 271 | ).open(); 272 | markdownToAdd.push({ 273 | type: 'style', 274 | style: tag, 275 | value: styleToAdd 276 | }); 277 | } 278 | } else { 279 | // Open entity tag 280 | 281 | var entity = rawDraftObject.entityMap[tag.key]; 282 | if (customEntityItems[entity.type] || EntityItems[entity.type]) { 283 | var entityToAdd = ( 284 | customEntityItems[entity.type] || EntityItems[entity.type] 285 | ).open(entity, block); 286 | markdownToAdd.push({ 287 | type: 'entity', 288 | value: entityToAdd 289 | }); 290 | } 291 | } 292 | } 293 | 294 | function closeTag(tag) { 295 | const popped = openTags.pop(); 296 | if (tag !== popped) { 297 | throw new Error( 298 | 'Invariant violation: Cannot close a tag before all inner tags have been closed' 299 | ); 300 | } 301 | 302 | if (tag.style) { 303 | // Close inline tag 304 | 305 | if (customStyleItems[tag.style] || StyleItems[tag.style]) { 306 | // Have to trim whitespace first and then re-add after because markdown can't handle leading/trailing whitespace 307 | var trailingWhitespace = TRAILING_WHITESPACE.exec(markdownString); 308 | markdownString = markdownString.slice( 309 | 0, 310 | markdownString.length - trailingWhitespace[0].length 311 | ); 312 | 313 | markdownString += ( 314 | customStyleItems[tag.style] || StyleItems[tag.style] 315 | ).close(); 316 | markdownString += trailingWhitespace[0]; 317 | } 318 | } else { 319 | // Close entity tag 320 | 321 | var entity = rawDraftObject.entityMap[tag.key]; 322 | if (customEntityItems[entity.type] || EntityItems[entity.type]) { 323 | markdownString += ( 324 | customEntityItems[entity.type] || EntityItems[entity.type] 325 | ).close(entity, block); 326 | } 327 | } 328 | } 329 | 330 | const compareTagsLastCloseFirst = (a, b) => 331 | b.offset + b.length - (a.offset + a.length); 332 | 333 | // reverse array without mutating the original 334 | const reverse = (array) => array.concat().reverse(); 335 | 336 | // Render text within content, along with any inline styles/entities 337 | Array.from(block.text).some(function (character, characterIndex) { 338 | // Close any tags that need closing, starting from top of the stack 339 | reverse(openTags).forEach(function (tag) { 340 | if (tag.offset + tag.length === characterIndex) { 341 | // Take all tags stacked on top of the current one, meaning they opened after it. 342 | // Since they have not been popped, they'll close only later. So we need to split them. 343 | var tagsToSplit = openTags.slice(openTags.indexOf(tag) + 1); 344 | 345 | // Close in reverse order as they were opened 346 | reverse(tagsToSplit).forEach(closeTag); 347 | 348 | // Now we can close the current tag 349 | closeTag(tag); 350 | 351 | // Reopen split tags, ordered so that tags that close last open first 352 | tagsToSplit.sort(compareTagsLastCloseFirst).forEach(openTag); 353 | } 354 | }); 355 | 356 | // Open any tags that need opening, using the correct nesting order. 357 | var inlineTagsToOpen = block.inlineStyleRanges.filter( 358 | (tag) => tag.offset === characterIndex 359 | ); 360 | var entityTagsToOpen = block.entityRanges.filter( 361 | (tag) => tag.offset === characterIndex 362 | ); 363 | inlineTagsToOpen 364 | .concat(entityTagsToOpen) 365 | .sort(compareTagsLastCloseFirst) 366 | .forEach(openTag); 367 | 368 | // These are all the opening entity and style types being added to the markdown string for this loop 369 | // we store in an array and add here because if the character is WS character, we want to hang onto it and not apply it until the next non-whitespace 370 | // character before adding the markdown, since markdown doesn’t play nice with leading whitespace (eg '** bold**' is no good, whereas ' **bold**' is good.) 371 | if (character !== ' ' && markdownToAdd.length) { 372 | markdownString += markdownToAdd.map(function (item) { 373 | return item.value; 374 | }).join(''); 375 | 376 | markdownToAdd = []; 377 | } 378 | 379 | if (block.type !== 'code-block' && escapeMarkdownCharacters) { 380 | let insideInlineCodeStyle = openTags.find((style) => style.style === 'CODE'); 381 | 382 | if (insideInlineCodeStyle) { 383 | // Todo - The syntax to escape backtics when inside backtic code already is to use MORE backtics wrapping. 384 | // So we need to see how many backtics in a row we have and then when converting to markdown, use that # + 1 385 | 386 | // EG ``Test ` Hllo `` 387 | // OR ```Test `` Hello``` 388 | // OR ````Test ``` Hello ```` 389 | // Similar work has to be done for codeblocks. 390 | } else { 391 | // Special escape logic for blockquotes and heading characters 392 | if (characterIndex === 0 && character === '#' && block.text[1] && block.text[1] === ' ') { 393 | character = character.replace('#', '\\#'); 394 | } else if (characterIndex === 0 && character === '>') { 395 | character = character.replace('>', '\\>'); 396 | } 397 | 398 | // Escaping inline markdown characters 399 | // 🧹 If someone can think of a more elegant solution, I would love that. 400 | // orginally this was just a little char replace using a simple regular expression, but there’s lots of cases where 401 | // a markdown character does not actually get converted to markdown, like this case: http://google.com/i_am_a_link 402 | // so this code now tries to be smart and keeps track of potential “opening” characters as well as potential “closing” 403 | // characters, and only escapes if both opening and closing exist, and they have the correct whitepace-before-open, whitespace-or-end-of-string-after-close pattern 404 | if (MARKDOWN_STYLE_CHARACTERS.includes(character)) { 405 | let openingStyle = markdownStyleCharactersToEscape.find(function (item) { 406 | return item.character === character; 407 | }); 408 | 409 | if (!openingStyle && block.text[characterIndex - 1] === ' ' && block.text[characterIndex + 1] !== ' ') { 410 | markdownStyleCharactersToEscape.push({ 411 | character: character, 412 | index: characterIndex, 413 | markdownStringIndexStart: markdownString.length + character.length - 1, 414 | markdownStringIndexEnd: markdownString.length + character.length 415 | }); 416 | } else if (openingStyle && block.text[characterIndex - 1] === character && characterIndex === openingStyle.index + 1) { 417 | openingStyle.markdownStringIndexEnd += 1; 418 | } else if (openingStyle) { 419 | let openingStyleLength = openingStyle.markdownStringIndexEnd - openingStyle.markdownStringIndexStart; 420 | let escapeCharacter = false; 421 | let popOpeningStyle = false; 422 | if (openingStyleLength === 1 && (block.text[characterIndex + 1] === ' ' || !block.text[characterIndex + 1])) { 423 | popOpeningStyle = true; 424 | escapeCharacter = true; 425 | } 426 | 427 | if (openingStyleLength === 2 && block.text[characterIndex + 1] === character) { 428 | escapeCharacter = true; 429 | } 430 | 431 | if (openingStyleLength === 2 && block.text[characterIndex - 1] === character && (block.text[characterIndex + 1] === ' ' || !block.text[characterIndex + 1])) { 432 | popOpeningStyle = true; 433 | escapeCharacter = true; 434 | } 435 | 436 | if (popOpeningStyle) { 437 | markdownStyleCharactersToEscape.splice(markdownStyleCharactersToEscape.indexOf(openingStyle), 1); 438 | let replacementString = markdownString.slice(openingStyle.markdownStringIndexStart, openingStyle.markdownStringIndexEnd); 439 | replacementString = replacementString.replace(MARKDOWN_STYLE_CHARACTER_REGXP, '\\$1'); 440 | markdownString = (markdownString.slice(0, openingStyle.markdownStringIndexStart) + replacementString + markdownString.slice(openingStyle.markdownStringIndexEnd)); 441 | } 442 | 443 | if (escapeCharacter) { 444 | character = `\\${character}`; 445 | } 446 | } 447 | } 448 | } 449 | } 450 | 451 | if (character === '\n' && type === 'blockquote') { 452 | markdownString += '\n> '; 453 | } else { 454 | markdownString += character; 455 | } 456 | }); 457 | 458 | // Finally, close all remaining open tags 459 | reverse(openTags).forEach(closeTag); 460 | 461 | // Close block level item 462 | if (customStyleItems[type] || StyleItems[type]) { 463 | markdownString += (customStyleItems[type] || StyleItems[type]).close(block); 464 | } 465 | 466 | // Determine how many newlines to add - generally we want 2, but for list items we just want one when they are succeeded by another list item. 467 | if (SingleNewlineAfterBlock.indexOf(type) !== -1 && rawDraftObject.blocks[index + 1] && SingleNewlineAfterBlock.indexOf(rawDraftObject.blocks[index + 1].type) !== -1) { 468 | markdownString += '\n'; 469 | } else if (rawDraftObject.blocks[index + 1]) { 470 | if (rawDraftObject.blocks[index].text) { 471 | if (SingleNewlineAfterBlock.indexOf(type) !== -1 472 | && SingleNewlineAfterBlock.indexOf(rawDraftObject.blocks[index + 1].type) === -1) { 473 | markdownString += '\n\n'; 474 | } else if (!options.preserveNewlines) { 475 | // 2 newlines if not preserving 476 | markdownString += '\n\n'; 477 | } else { 478 | markdownString += '\n'; 479 | } 480 | } else if (options.preserveNewlines) { 481 | markdownString += '\n'; 482 | } 483 | } 484 | 485 | return markdownString; 486 | } 487 | 488 | /** 489 | * Generate markdown for a raw draftjs object 490 | * DraftJS raw object contains an array of blocks, which is the main "structure" 491 | * of the text. Each block = a new line. 492 | * 493 | * @param {Object} rawDraftObject - draftjs object to generate markdown for 494 | * @param {Object} options - optional additional data, see readme for what options can be passed in. 495 | * 496 | * @return {String} markdown string 497 | **/ 498 | function draftToMarkdown(rawDraftObject, options) { 499 | options = options || {}; 500 | var markdownString = ''; 501 | rawDraftObject.blocks.forEach(function (block, index) { 502 | markdownString += renderBlock(block, index, rawDraftObject, options); 503 | }); 504 | 505 | orderedListNumber = {}; // See variable definitions at the top of the page to see why we have to do this sad hack. 506 | return markdownString; 507 | } 508 | 509 | export default draftToMarkdown; 510 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as draftToMarkdown } from './draft-to-markdown'; 2 | export { default as markdownToDraft } from './markdown-to-draft'; 3 | -------------------------------------------------------------------------------- /src/markdown-to-draft.js: -------------------------------------------------------------------------------- 1 | import { Remarkable } from 'remarkable'; 2 | 3 | const TRAILING_NEW_LINE = /\n$/; 4 | 5 | // In DraftJS, string lengths are calculated differently than in JS itself (due 6 | // to surrogate pairs). Instead of importing the entire UnicodeUtils file from 7 | // FBJS, we use a simpler alternative, in the form of `Array.from`. 8 | // 9 | // Alternative: const { strlen } = require('fbjs/lib/UnicodeUtils'); 10 | function strlen(str) { 11 | return Array.from(str).length; 12 | } 13 | 14 | // Block level items, key is Remarkable's key for them, value returned is 15 | // A function that generates the raw draftjs key and block data. 16 | // 17 | // Why a function? Because in some cases (headers) we need additional information 18 | // before we can determine the exact key to return. And blocks may also return data 19 | const DefaultBlockTypes = { 20 | paragraph_open: function (item) { 21 | return { 22 | type: 'unstyled', 23 | text: '', 24 | entityRanges: [], 25 | inlineStyleRanges: [] 26 | }; 27 | }, 28 | 29 | blockquote_open: function (item) { 30 | return { 31 | type: 'blockquote', 32 | text: '' 33 | }; 34 | }, 35 | 36 | ordered_list_item_open: function () { 37 | return { 38 | type: 'ordered-list-item', 39 | text: '' 40 | }; 41 | }, 42 | 43 | unordered_list_item_open: function () { 44 | return { 45 | type: 'unordered-list-item', 46 | text: '' 47 | }; 48 | }, 49 | 50 | fence: function (item) { 51 | return { 52 | type: 'code-block', 53 | data: { 54 | language: item.params || '' 55 | }, 56 | text: (item.content || '').replace(TRAILING_NEW_LINE, ''), // remarkable seems to always append an erronious trailing newline to its codeblock content, so we need to trim it out. 57 | entityRanges: [], 58 | inlineStyleRanges: [] 59 | }; 60 | }, 61 | 62 | heading_open: function (item) { 63 | var type = 'header-' + ({ 64 | 1: 'one', 65 | 2: 'two', 66 | 3: 'three', 67 | 4: 'four', 68 | 5: 'five', 69 | 6: 'six' 70 | })[item.hLevel]; 71 | 72 | return { 73 | type: type, 74 | text: '' 75 | }; 76 | } 77 | }; 78 | 79 | // Entity types. These are things like links or images that require 80 | // additional data and will be added to the `entityMap` 81 | // again. In this case, key is remarkable key, value is 82 | // meethod that returns the draftjs key + any data needed. 83 | const DefaultBlockEntities = { 84 | link_open: function (item) { 85 | return { 86 | type: 'LINK', 87 | mutability: 'MUTABLE', 88 | data: { 89 | url: item.href, 90 | href: item.href 91 | } 92 | }; 93 | } 94 | }; 95 | 96 | // Entity styles. Simple Inline styles that aren't added to entityMap 97 | // key is remarkable key, value is draftjs raw key 98 | const DefaultBlockStyles = { 99 | strong_open: 'BOLD', 100 | em_open: 'ITALIC', 101 | code: 'CODE', 102 | del_open: 'STRIKETHROUGH' 103 | }; 104 | 105 | // Key generator for entityMap items 106 | var idCounter = -1; 107 | function generateUniqueKey() { 108 | idCounter++; 109 | return idCounter; 110 | } 111 | 112 | /* 113 | * Handle inline content in a block level item 114 | * parses for BlockEntities (links, images) and BlockStyles (em, strong) 115 | * doesn't handle block level items (blockquote, ordered list, etc) 116 | * 117 | * @param inlineItem - single object from remarkable data representation of markdown 118 | * @param BlockEntities - key-value object of mappable block entity items. Passed in as param so users can include their own custom stuff 119 | * @param BlockStyles - key-value object of mappable block styles items. Passed in as param so users can include their own custom stuff 120 | * 121 | * @return 122 | * content: Entire text content for the inline item, 123 | * blockEntities: New block eneities to be added to global block entity map 124 | * blockEntityRanges: block-level representation of block entities including key to access the block entity from the global map 125 | * blockStyleRanges: block-level representation of styles (eg strong, em) 126 | */ 127 | function parseInline(inlineItem, BlockEntities, BlockStyles) { 128 | var content = '', blockEntities = {}, blockEntityRanges = [], blockInlineStyleRanges = []; 129 | inlineItem.children.forEach(function (child) { 130 | if (child.type === 'text') { 131 | content += child.content; 132 | } else if (child.type === 'softbreak') { 133 | content += '\n'; 134 | } else if (child.type === 'hardbreak') { 135 | content += '\n'; 136 | } else if (BlockStyles[child.type]) { 137 | var key = generateUniqueKey(); 138 | var styleBlock = { 139 | offset: strlen(content) || 0, 140 | length: 0, 141 | style: BlockStyles[child.type] 142 | }; 143 | 144 | // Edge case hack because code items don't have inline content or open/close, unlike everything else 145 | // sub and sup are also special :) 146 | if (child.type === 'code' || child.type === 'sub' || child.type === 'sup') { 147 | styleBlock.length = strlen(child.content); 148 | content += child.content; 149 | } 150 | 151 | blockInlineStyleRanges.push(styleBlock); 152 | } else if (BlockEntities[child.type]) { 153 | var key = generateUniqueKey(); 154 | 155 | blockEntities[key] = BlockEntities[child.type](child); 156 | 157 | blockEntityRanges.push({ 158 | offset: strlen(content) || 0, 159 | length: 0, 160 | key: key 161 | }); 162 | } else if (child.type.indexOf('_close') !== -1 && BlockEntities[child.type.replace('_close', '_open')]) { 163 | blockEntityRanges[blockEntityRanges.length - 1].length = strlen(content) - blockEntityRanges[blockEntityRanges.length - 1].offset; 164 | } else if (child.type.indexOf('_close') !== -1 && BlockStyles[child.type.replace('_close', '_open')]) { 165 | var type = BlockStyles[child.type.replace('_close', '_open')] 166 | blockInlineStyleRanges = blockInlineStyleRanges 167 | .map(style => { 168 | if (style.length === 0 && style.style === type) { 169 | style.length = strlen(content) - style.offset; 170 | } 171 | return style; 172 | }); 173 | } 174 | }); 175 | 176 | return {content, blockEntities, blockEntityRanges, blockInlineStyleRanges}; 177 | } 178 | 179 | /** 180 | * Convert markdown into raw draftjs object 181 | * 182 | * @param {String} markdown - markdown to convert into raw draftjs object 183 | * @param {Object} options - optional additional data, see readme for what options can be passed in. 184 | * 185 | * @return {Object} rawDraftObject 186 | **/ 187 | function markdownToDraft(string, options = {}) { 188 | const remarkablePreset = options.remarkablePreset || options.remarkableOptions; 189 | const remarkableOptions = typeof options.remarkableOptions === 'object' ? options.remarkableOptions : null; 190 | const md = new Remarkable(remarkablePreset, remarkableOptions); 191 | 192 | // if tables are not explicitly enabled, disable them by default 193 | if ( 194 | !remarkableOptions || 195 | !remarkableOptions.enable || 196 | !remarkableOptions.enable.block || 197 | remarkableOptions.enable.block !== 'table' || 198 | remarkableOptions.enable.block.includes('table') === false 199 | ) { 200 | md.block.ruler.disable('table'); 201 | } 202 | 203 | // disable the specified rules 204 | if (remarkableOptions && remarkableOptions.disable) { 205 | for (let [key, value] of Object.entries(remarkableOptions.disable)) { 206 | md[key].ruler.disable(value); 207 | } 208 | } 209 | 210 | // enable the specified rules 211 | if (remarkableOptions && remarkableOptions.enable) { 212 | for (let [key, value] of Object.entries(remarkableOptions.enable)) { 213 | md[key].ruler.enable(value); 214 | } 215 | } 216 | 217 | // If users want to define custom remarkable plugins for custom markdown, they can be added here 218 | if (options.remarkablePlugins) { 219 | options.remarkablePlugins.forEach(function (plugin) { 220 | md.use(plugin, {}); 221 | }); 222 | } 223 | 224 | var blocks = []; // blocks will be returned as part of the final draftjs raw object 225 | var entityMap = {}; // entitymap will be returned as part of the final draftjs raw object 226 | var parsedData = md.parse(string, {}); // remarkable js takes markdown and makes it an array of style objects for us to easily parse 227 | var currentListType = null; // Because of how remarkable's data is formatted, we need to cache what kind of list we're currently dealing with 228 | var previousBlockEndingLine = 0; 229 | 230 | // Allow user to define custom BlockTypes and Entities if they so wish 231 | const BlockTypes = Object.assign({}, DefaultBlockTypes, options.blockTypes || {}); 232 | const BlockEntities = Object.assign({}, DefaultBlockEntities, options.blockEntities || {}); 233 | const BlockStyles = Object.assign({}, DefaultBlockStyles, options.blockStyles || {}); 234 | 235 | parsedData.forEach(function (item) { 236 | // Because of how remarkable's data is formatted, we need to cache what kind of list we're currently dealing with 237 | if (item.type === 'bullet_list_open') { 238 | currentListType = 'unordered_list_item_open'; 239 | } else if (item.type === 'ordered_list_open') { 240 | currentListType = 'ordered_list_item_open'; 241 | } 242 | 243 | var itemType = item.type; 244 | if (itemType === 'list_item_open') { 245 | itemType = currentListType; 246 | } 247 | 248 | if (itemType === 'inline') { 249 | // Parse inline content and apply it to the most recently created block level item, 250 | // which is where the inline content will belong. 251 | var {content, blockEntities, blockEntityRanges, blockInlineStyleRanges} = parseInline(item, BlockEntities, BlockStyles); 252 | var blockToModify = blocks[blocks.length - 1]; 253 | blockToModify.text = content; 254 | blockToModify.inlineStyleRanges = blockInlineStyleRanges; 255 | blockToModify.entityRanges = blockEntityRanges; 256 | 257 | // The entity map is a master object separate from the block so just add any entities created for this block to the master object 258 | Object.assign(entityMap, blockEntities); 259 | } else if ((itemType.indexOf('_open') !== -1 || itemType === 'fence' || itemType === 'hr' || itemType === 'htmlblock') && BlockTypes[itemType]) { 260 | var depth = 0; 261 | var block; 262 | 263 | if (item.level > 0) { 264 | depth = Math.floor(item.level / 2); 265 | } 266 | 267 | // Draftjs only supports 1 level of blocks, hence the item.level === 0 check 268 | // List items will always be at least `level==1` though so we need a separate check for that 269 | // If there’s nested block level items deeper than that, we need to make sure we capture this by cloning the topmost block 270 | // otherwise we’ll accidentally overwrite its text. (eg if there's a blockquote with 3 nested paragraphs with inline text, without this check, only the last paragraph would be reflected) 271 | if (item.level === 0 || item.type === 'list_item_open') { 272 | block = Object.assign({ 273 | depth: depth 274 | }, BlockTypes[itemType](item)); 275 | } else if (item.level > 0 && blocks[blocks.length - 1].text) { 276 | block = Object.assign({}, blocks[blocks.length - 1]); 277 | } 278 | 279 | if (block && options.preserveNewlines) { 280 | // Re: previousBlockEndingLine.... omg. 281 | // So remarkable strips out empty newlines and doesn't make any entities to parse to restore them 282 | // the only solution I could find is that there's a 2-value array on each block item called "lines" which is the start and end line of the block element. 283 | // by keeping track of the PREVIOUS block element ending line and the NEXT block element starting line, we can find the difference between the new lines and insert 284 | // an appropriate number of extra paragraphs to re-create those newlines in draftjs. 285 | // This is probably my least favourite thing in this file, but not sure what could be better. 286 | var totalEmptyParagraphsToCreate = item.lines[0] - previousBlockEndingLine; 287 | for (var i = 0; i < totalEmptyParagraphsToCreate; i++) { 288 | blocks.push(DefaultBlockTypes.paragraph_open()); 289 | } 290 | } 291 | 292 | if (block) { 293 | previousBlockEndingLine = item.lines[1]; 294 | // reserve one line after list block 295 | if ( 296 | block.type === 'unordered-list-item' || 297 | block.type === 'ordered-list-item' 298 | ) { 299 | previousBlockEndingLine += 1; 300 | } 301 | blocks.push(block); 302 | } 303 | } 304 | 305 | }); 306 | 307 | // EditorState.createWithContent will error if there's no blocks defined 308 | // Remarkable returns an empty array though. So we have to generate a 'fake' 309 | // empty block in this case. 😑 310 | if (!blocks.length) { 311 | blocks.push(DefaultBlockTypes.paragraph_open()); 312 | } 313 | 314 | return { 315 | entityMap, 316 | blocks 317 | }; 318 | } 319 | 320 | export default markdownToDraft; 321 | -------------------------------------------------------------------------------- /test/draft-to-markdown.spec.js: -------------------------------------------------------------------------------- 1 | import { markdownToDraft, draftToMarkdown } from '../src/index'; 2 | 3 | describe('draftToMarkdown', function () { 4 | describe('whitespace', function () { 5 | it('renders inline styled text with leading whitespace correctly', function () { 6 | /* eslint-disable */ 7 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"Test Bold Text Test","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":4,"length":10,"style":"BOLD"}],"entityRanges":[],"data":{}}]}; 8 | /* eslint-enable */ 9 | 10 | var markdown = draftToMarkdown(rawObject); 11 | expect(markdown).toEqual('Test **Bold Text** Test'); 12 | }); 13 | 14 | it('renders inline styled text with trailing whitespace correctly', function () { 15 | /* eslint-disable */ 16 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"Test Bold Text Test","type":"inline","depth":0,"inlineStyleRanges":[{"offset":5,"length":10,"style":"BOLD"}],"entityRanges":[],"data":{}}]}; 17 | /* eslint-enable */ 18 | 19 | var markdown = draftToMarkdown(rawObject); 20 | expect(markdown).toEqual('Test **Bold Text** Test'); 21 | 22 | /* eslint-disable */ 23 | rawObject = {"blocks":[{"key":"4mmbt","text":"Test\n\nI am some bold text\nI will not be bold","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":6,"length":20,"style":"BOLD"}],"entityRanges":[],"data":{}}],"entityMap":{}}; 24 | /* eslint-enable */ 25 | 26 | var markdown = draftToMarkdown(rawObject); 27 | expect(markdown).toEqual('Test\n\n**I am some bold text**\nI will not be bold'); 28 | }); 29 | 30 | it('renders inline styled text with trailing whitespace correctly when trailing whitespace is the last character', function () { 31 | /* eslint-disable */ 32 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"Test Bold Text ","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":5,"length":10,"style":"BOLD"}],"entityRanges":[],"data":{}}]}; 33 | /* eslint-enable */ 34 | 35 | var markdown = draftToMarkdown(rawObject); 36 | expect(markdown).toEqual('Test **Bold Text** '); 37 | }); 38 | 39 | it('renders nested inline styled text with trailing whitespace correctly', function () { 40 | /* eslint-disable */ 41 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"Test Bold Text Test","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":10,"style":"ITALIC"},{"offset":5,"length":9,"style":"BOLD"}],"entityRanges":[],"data":{}}]}; 42 | /* eslint-enable */ 43 | 44 | var markdown = draftToMarkdown(rawObject); 45 | expect(markdown).toEqual('_Test **Bold**_ **Text** Test'); 46 | 47 | /* eslint-disable */ 48 | rawObject = {"entityMap":{},"blocks":[{"key":"84jcj","text":"bold/italic plain","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":11,"style":"BOLD"},{"offset":0,"length":11,"style":"ITALIC"}],"entityRanges":[],"data":{}}]}; 49 | /* eslint-enable */ 50 | 51 | var markdown = draftToMarkdown(rawObject); 52 | expect(markdown).toEqual('**_bold/italic_** plain'); 53 | }); 54 | 55 | it('handles unstyled blank lines', function () { 56 | // draft-js can have blank lines that have block styles. 57 | // This would result in double-application of markdown line prefixes. 58 | 59 | /* eslint-disable */ 60 | const rawObject = {"blocks":[{"key":"8i76c","text":"a","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"3htgs","text":"b","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"c46o4","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"l5v1","text":"c","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"cbsdo","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"9i2da","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"9mr0v","text":"d","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}} 61 | /* eslint-enable */ 62 | 63 | var markdown = draftToMarkdown(rawObject); 64 | expect(markdown).toEqual('a\n\nb\n\nc\n\nd'); 65 | 66 | markdown = draftToMarkdown(rawObject, {preserveNewlines: true}); 67 | expect(markdown).toEqual('a\nb\n\nc\n\n\nd'); 68 | }); 69 | 70 | it('handles blank lines with styled block types', function () { 71 | // draft-js can have blank lines that have block styles. 72 | // This would result in double-application of markdown line prefixes. 73 | 74 | /* eslint-disable */ 75 | const rawObject = { "blocks": [ { "key": "e8ojh", "text": "", "type": "header-three", "depth": 0, "inlineStyleRanges": [], "entityRanges": [], "data": {} }, { "key": "eg79g", "text": "Header 1", "type": "header-three", "depth": 0, "inlineStyleRanges": [], "entityRanges": [], "data": {} }, { "key": "123", "text": "", "type": "header-three", "depth": 0, "inlineStyleRanges": [], "entityRanges": [], "data": {} }, { "key": "456", "text": "Header 2", "type": "header-three", "depth": 0, "inlineStyleRanges": [], "entityRanges": [], "data": {} } ], "entityMap": {} }; 76 | /* eslint-enable */ 77 | 78 | var markdown = draftToMarkdown(rawObject); 79 | expect(markdown).toEqual('### Header 1\n\n### Header 2'); 80 | 81 | markdown = draftToMarkdown(rawObject, {preserveNewlines: true}); 82 | expect(markdown).toEqual('### \n### Header 1\n### \n### Header 2'); 83 | }); 84 | 85 | it('handles blank lines with blockquotes', function () { 86 | // draft-js can have blank lines that have block styles. 87 | // This would result in double-application of markdown line prefixes. 88 | 89 | /* eslint-disable */ 90 | const rawObject = { "blocks": [{ "key": "eg79g", "text": "one\n\nblockquote", "type": "blockquote", "depth": 0, "inlineStyleRanges": [], "entityRanges": [], "data": {} },{"depth":0,"type":"unstyled","text":"Hello :)","entityRanges":[],"inlineStyleRanges":[]}]}; 91 | /* eslint-enable */ 92 | 93 | var markdown = draftToMarkdown(rawObject); 94 | expect(markdown).toEqual('> one\n> \n> blockquote\n\nHello :)'); 95 | 96 | markdown = draftToMarkdown(rawObject, {preserveNewlines: true}); 97 | expect(markdown).toEqual('> one\n> \n> blockquote\nHello :)'); 98 | }); 99 | }); 100 | 101 | describe('entity conversion', function () { 102 | describe('headings', function () { 103 | it ('renders heading-one correctly', function () { 104 | /* eslint-disable */ 105 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"header-one","text":"Test","inlineStyleRanges":[],"entityRanges":[]},{"depth":0,"type":"unstyled","text":"Hello :)","entityRanges":[],"inlineStyleRanges":[]},{"depth":0,"type":"header-one","text":"Test","inlineStyleRanges":[],"entityRanges":[]}]}; 106 | /* eslint-enable */ 107 | 108 | var markdown = draftToMarkdown(rawObject); 109 | 110 | expect(markdown).toEqual('# Test\n\nHello :)\n\n# Test'); 111 | }); 112 | 113 | it ('renders heading-two correctly', function () { 114 | /* eslint-disable */ 115 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"header-two","text":"Test","inlineStyleRanges":[],"entityRanges":[]},{"depth":0,"type":"unstyled","text":"Hello :)","entityRanges":[],"inlineStyleRanges":[]},{"depth":0,"type":"header-two","text":"Test","inlineStyleRanges":[],"entityRanges":[]}]}; 116 | /* eslint-enable */ 117 | 118 | var markdown = draftToMarkdown(rawObject); 119 | 120 | expect(markdown).toEqual('## Test\n\nHello :)\n\n## Test'); 121 | }); 122 | 123 | it ('renders heading-three correctly', function () { 124 | /* eslint-disable */ 125 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"header-three","text":"Test","inlineStyleRanges":[],"entityRanges":[]},{"depth":0,"type":"unstyled","text":"Hello :)","entityRanges":[],"inlineStyleRanges":[]},{"depth":0,"type":"header-three","text":"Test","inlineStyleRanges":[],"entityRanges":[]}]}; 126 | /* eslint-enable */ 127 | 128 | var markdown = draftToMarkdown(rawObject); 129 | 130 | expect(markdown).toEqual('### Test\n\nHello :)\n\n### Test'); 131 | }); 132 | 133 | it ('renders heading-four correctly', function () { 134 | /* eslint-disable */ 135 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"header-four","text":"Test","inlineStyleRanges":[],"entityRanges":[]},{"depth":0,"type":"unstyled","text":"Hello :)","entityRanges":[],"inlineStyleRanges":[]},{"depth":0,"type":"header-four","text":"Test","inlineStyleRanges":[],"entityRanges":[]}]}; 136 | /* eslint-enable */ 137 | 138 | var markdown = draftToMarkdown(rawObject); 139 | 140 | expect(markdown).toEqual('#### Test\n\nHello :)\n\n#### Test'); 141 | }); 142 | 143 | it ('renders heading-five correctly', function () { 144 | /* eslint-disable */ 145 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"header-five","text":"Test","inlineStyleRanges":[],"entityRanges":[]},{"depth":0,"type":"unstyled","text":"Hello :)","entityRanges":[],"inlineStyleRanges":[]},{"depth":0,"type":"header-five","text":"Test","inlineStyleRanges":[],"entityRanges":[]}]}; 146 | /* eslint-enable */ 147 | 148 | var markdown = draftToMarkdown(rawObject); 149 | 150 | expect(markdown).toEqual('##### Test\n\nHello :)\n\n##### Test'); 151 | }); 152 | 153 | it ('renders heading-six correctly', function () { 154 | /* eslint-disable */ 155 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"header-six","text":"Test","inlineStyleRanges":[],"entityRanges":[]},{"depth":0,"type":"unstyled","text":"Hello :)","entityRanges":[],"inlineStyleRanges":[]},{"depth":0,"type":"header-six","text":"Test","inlineStyleRanges":[],"entityRanges":[]}]}; 156 | /* eslint-enable */ 157 | 158 | var markdown = draftToMarkdown(rawObject); 159 | 160 | expect(markdown).toEqual('###### Test\n\nHello :)\n\n###### Test'); 161 | }); 162 | }); 163 | 164 | describe('code', function () { 165 | it ('renders codeblock without syntax correctly', function () { 166 | /* eslint-disable */ 167 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"code-block","data":{},"text":"Test codeblock","entityRanges":[],"inlineStyleRanges":[]}]}; 168 | /* eslint-enable */ 169 | var markdown = draftToMarkdown(rawObject); 170 | 171 | expect(markdown).toEqual('```\nTest codeblock\n```'); 172 | }); 173 | 174 | it ('renders codeblock with syntax correctly', function () { 175 | /* eslint-disable */ 176 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"code-block","data":{"language":"javascript"},"text":"Test codeblock","entityRanges":[],"inlineStyleRanges":[]}]}; 177 | /* eslint-enable */ 178 | var markdown = draftToMarkdown(rawObject); 179 | 180 | expect(markdown).toEqual('```javascript\nTest codeblock\n```'); 181 | }); 182 | 183 | it ('renders inline code correctly', function () { 184 | /* eslint-disable */ 185 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"unstyled","text":"Hello I am some inline code","entityRanges":[],"inlineStyleRanges":[{"offset":6,"length":21,"style":"CODE"}]}]}; 186 | /* eslint-enable */ 187 | var markdown = draftToMarkdown(rawObject); 188 | 189 | expect(markdown).toEqual('Hello `I am some inline code`'); 190 | }); 191 | }); 192 | 193 | describe('inline styles', function () { 194 | it ('renders bold text correctly', function () { 195 | /* eslint-disable */ 196 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"unstyled","text":"Hello I am bold yay","entityRanges":[],"inlineStyleRanges":[{"offset":6,"length":9,"style":"BOLD"}]}]}; 197 | /* eslint-enable */ 198 | var markdown = draftToMarkdown(rawObject); 199 | 200 | expect(markdown).toEqual('Hello **I am bold** yay'); 201 | }); 202 | 203 | it ('renders italic text correctly', function () { 204 | /* eslint-disable */ 205 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"unstyled","text":"Hello There, I am italic yay","entityRanges":[],"inlineStyleRanges":[{"offset":12,"length":12,"style":"ITALIC"}]}]}; 206 | /* eslint-enable */ 207 | var markdown = draftToMarkdown(rawObject); 208 | 209 | expect(markdown).toEqual('Hello There, _I am italic_ yay'); 210 | }); 211 | 212 | it ('renders strikethrough text correctly', function () { 213 | /* eslint-disable */ 214 | var rawObject = {"entityMap":{},"blocks":[{"depth":0,"type":"unstyled","text":"this is strikethrough text","entityRanges":[],"inlineStyleRanges":[{"offset":8,"length":14,"style":"STRIKETHROUGH"}]}]}; 215 | /* eslint-enable */ 216 | var markdown = draftToMarkdown(rawObject); 217 | 218 | expect(markdown).toEqual('this is ~~strikethrough~~ text'); 219 | }); 220 | }); 221 | 222 | it('renders links with a URL correctly', function () { 223 | /* eslint-disable */ 224 | var rawObject = {"entityMap":{"0":{"type":"LINK","mutability":"MUTABLE","data":{"url":"https://google.com"}},"1":{"type":"LINK","mutability":"MUTABLE","data":{"url":"https://facebook.github.io/draft-js/"}}},"blocks":[{"key":"58spd","text":"This is a test of a link","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":18,"length":6,"key":0}],"data":{}},{"key":"9ln6g","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"3euar","text":"And perhaps we should test once more.","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":4,"length":7,"key":1}],"data":{}}]}; 225 | /* eslint-enable */ 226 | var markdown = draftToMarkdown(rawObject); 227 | expect(markdown).toEqual('This is a test of [a link](https://google.com)\n\nAnd [perhaps](https://facebook.github.io/draft-js/) we should test once more.'); 228 | }); 229 | 230 | it('renders links with surrogate pairs (e.g. some emoji) correctly', function () { 231 | /* eslint-disable */ 232 | var rawObject = { 233 | "blocks": [{ 234 | "key": "eubc2", 235 | "text": "🙋 link link", 236 | "type": "unstyled", 237 | "depth": 0, 238 | "inlineStyleRanges": [], 239 | "entityRanges": [{"offset": 2, "length": 4, "key": 0}, {"offset": 7, "length": 4, "key": 1}], 240 | "data": {} 241 | }], 242 | "entityMap": { 243 | "0": { 244 | "type": "LINK", 245 | "mutability": "MUTABLE", 246 | "data": {"url": "https://link.com", "href": "https://link.com"} 247 | }, "1": {"type": "LINK", "mutability": "MUTABLE", "data": {"url": "https://link.com"}} 248 | } 249 | } 250 | /* eslint-enable */ 251 | var markdown = draftToMarkdown(rawObject); 252 | expect(markdown).toEqual('🙋 [link](https://link.com) [link](https://link.com)'); 253 | }); 254 | 255 | it('renders links with a HREF correctly', function () { 256 | /* eslint-disable */ 257 | var rawObject = {"entityMap":{"0":{"type":"LINK","mutability":"MUTABLE","data":{"href":"https://google.com"}},"1":{"type":"LINK","mutability":"MUTABLE","data":{"href":"https://facebook.github.io/draft-js/"}}},"blocks":[{"key":"58spd","text":"This is a test of a link","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":18,"length":6,"key":0}],"data":{}},{"key":"9ln6g","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"3euar","text":"And perhaps we should test once more.","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":4,"length":7,"key":1}],"data":{}}]}; 258 | /* eslint-enable */ 259 | var markdown = draftToMarkdown(rawObject); 260 | expect(markdown).toEqual('This is a test of [a link](https://google.com)\n\nAnd [perhaps](https://facebook.github.io/draft-js/) we should test once more.'); 261 | }); 262 | 263 | it('renders “the kitchen sink” correctly', function () { 264 | /* eslint-disable */ 265 | var rawObject = {"entityMap":{},"blocks":[{"key":"2uvch","text":"Hello!","type":"header-one","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"gcip","text":"My name is Rose :) \nToday, I'm here to talk to you about how great markdown is!\n","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":11,"length":4,"style":"BOLD"}],"entityRanges":[],"data":{}},{"key":"eu8ak","text":"First, here's a few bullet points:","type":"header-two","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"fiti6","text":"One","type":"unordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"d8amu","text":"Two","type":"unordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"7r62d","text":"Three","type":"unordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"3n7hc","text":"A codeblock","type":"code-block","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"9o0hn","text":"And then... some monospace text?\nOr... italics?","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":12,"length":19,"style":"CODE"},{"offset":39,"length":8,"style":"ITALIC"}],"entityRanges":[],"data":{}}]}; 266 | /* eslint-enable */ 267 | var markdown = draftToMarkdown(rawObject); 268 | expect(markdown).toEqual('# Hello!\n\nMy name is **Rose** :) \nToday, I\'m here to talk to you about how great markdown is!\n\n\n## First, here\'s a few bullet points:\n\n- One\n- Two\n- Three\n\n```\nA codeblock\n```\n\nAnd then... `some monospace text`?\nOr... _italics?_'); 269 | }); 270 | 271 | it('renders complex nested items correctly', function () { 272 | /* eslint-disable */ 273 | var rawObject = {"entityMap":{},"blocks":[{"key":"2unrq","text":"asdasdasd","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":9,"style":"BOLD"}],"entityRanges":[],"data":{}},{"key":"62od7","text":"asdasd","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":6,"style":"BOLD"}],"entityRanges":[],"data":{}},{"key":"c5obb","text":"asdasdasdmmmasdads","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":9,"style":"BOLD"},{"offset":0,"length":12,"style":"ITALIC"}],"entityRanges":[],"data":{}}]}; 274 | /* eslint-enable */ 275 | var markdown = draftToMarkdown(rawObject); 276 | expect(markdown).toEqual('**asdasdasd**\n\n**asdasd**\n\n_**asdasdasd**mmm_asdads'); 277 | 278 | /* eslint-disable */ 279 | rawObject = {"entityMap":{"0":{"type":"mention","mutability":"SEGMENTED","data":{"mention":{"name":"Bran Stark","avatar":"https://d1ojh8nvjh9gcx.cloudfront.net/accounts/168181/2e4bff18a328c50404c411c04e608a32a967b1a2/large_2x.png","link":null,"id":168181}}}},"blocks":[{"key":"bkvjj","text":"asdadd adasdasd Bran Stark sadadsadasddasdasdasdsadsadasdsadasdasdasdsa","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":7,"length":9,"style":"BOLD"},{"offset":27,"length":27,"style":"BOLD"},{"offset":38,"length":7,"style":"ITALIC"},{"offset":63,"length":8,"style":"ITALIC"}],"entityRanges":[{"offset":16,"length":10,"key":0}],"data":{}}]}; 280 | /* eslint-enable */ 281 | 282 | var markdown = draftToMarkdown(rawObject, { 283 | entityItems: { 284 | mention: { 285 | open: function () { 286 | return '['; 287 | }, 288 | 289 | close: function (entity) { 290 | return '](@'+ entity.data.mention.id +')'; 291 | } 292 | } 293 | } 294 | }); 295 | 296 | expect(markdown).toEqual('asdadd **adasdasd** [Bran Stark](@168181) **sadadsadasd_dasdasd_asdsadsad**asdsadasd_asdasdsa_'); 297 | 298 | /* eslint-disable */ 299 | rawObject = {"entityMap":{"0":{"type":"mention","mutability":"SEGMENTED","data":{"mention":{"name":"Bran Stark","avatar":"https://d1ojh8nvjh9gcx.cloudfront.net/accounts/168181/2e4bff18a328c50404c411c04e608a32a967b1a2/large_2x.png","link":null,"id":168181}}}},"blocks":[{"key":"42pht","text":"jkhkhj Bran Stark khkjj","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":17,"style":"BOLD"}],"entityRanges":[{"offset":7,"length":10,"key":0}],"data":{}}]}; 300 | /* eslint-enable */ 301 | 302 | var markdown = draftToMarkdown(rawObject, { 303 | entityItems: { 304 | mention: { 305 | open: function () { 306 | return '['; 307 | }, 308 | 309 | close: function (entity) { 310 | return '](@'+ entity.data.mention.id +')'; 311 | } 312 | } 313 | } 314 | }); 315 | 316 | expect(markdown).toEqual('**jkhkhj [Bran Stark](@168181)** khkjj'); 317 | }); 318 | 319 | it('renders complex nested items correctly using entityItems block parameters', function () { 320 | /* eslint-disable */ 321 | var rawObject = { "blocks": [ { "key": "2h685", "text": "A mention in code block @Christophe Hamerling", "type": "code-block", "depth": 0, "inlineStyleRanges": [], "entityRanges": [{ "offset": 24, "length": 21, "key": 0 }], "data": { "language": "" } }, { "key": "34o22", "text": "A mention in unstyled block @Loris Hamerling", "type": "unstyled", "depth": 0, "inlineStyleRanges": [], "entityRanges": [{ "offset": 28, "length": 16, "key": 1 }], "data": {} } ], "entityMap": { "0": { "type": "mention", "mutability": "IMMUTABLE", "data": { "mention": { "id": "8cb18514-a9be-11eb-abce-0242ac120005" } } }, "1": { "type": "mention", "mutability": "IMMUTABLE", "data": { "mention": { "id": "8cb18514-a9be-11eb-abce-0242ac120006" } } } } }; 322 | /* eslint-enable */ 323 | 324 | var markdown = draftToMarkdown(rawObject, { 325 | entityItems: { 326 | mention: { 327 | open: function (entity, block) { 328 | console.log(block); 329 | return block.type === 'code-block' ? `[code@${entity.data.mention.id}:` : '['; 330 | }, 331 | 332 | close: function (entity, block) { 333 | return block.type === 'code-block' ? ']' : '](@'+ entity.data.mention.id +')' 334 | } 335 | } 336 | } 337 | }); 338 | expect(markdown).toEqual('```\nA mention in code block [code@8cb18514-a9be-11eb-abce-0242ac120005:@Christophe Hamerling]\n```\n\nA mention in unstyled block [@Loris Hamerling](@8cb18514-a9be-11eb-abce-0242ac120006)'); 339 | }); 340 | 341 | it('renders custom items correctly', function () { 342 | /* eslint-disable */ 343 | var rawObject = {"entityMap":{},"blocks":[{"key":"f2bpj","text":"OneTwoThree","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":3,"style":"red"},{"offset":3,"length":3,"style":"orange"},{"offset":6,"length":5,"style":"yellow"}],"entityRanges":[],"data":{}}]}; 344 | /* eslint-enable */ 345 | var markdown = draftToMarkdown(rawObject, { 346 | styleItems: { 347 | red: { 348 | open: function () { 349 | return ''; 350 | }, 351 | 352 | close: function () { 353 | return ''; 354 | } 355 | }, 356 | 357 | orange: { 358 | open: function () { 359 | return ''; 360 | }, 361 | 362 | close: function () { 363 | return ''; 364 | } 365 | }, 366 | 367 | yellow: { 368 | open: function () { 369 | return ''; 370 | }, 371 | 372 | close: function () { 373 | return ''; 374 | } 375 | } 376 | } 377 | }); 378 | 379 | expect(markdown).toEqual('OneTwoThree') 380 | }); 381 | 382 | it('allows to retrieve block data', function () { 383 | /* eslint-disable */ 384 | var rawObject = {"entityMap":{},"blocks":[{"key":"fb5f8","text":"","type":"atomic:image","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{"src":"https://example.com"}}]} 385 | /* eslint-enable */ 386 | var markdown = draftToMarkdown(rawObject, { 387 | styleItems: { 388 | 'atomic:image': { 389 | open: (block) => { 390 | const alt = block.data.alt || '' 391 | const title = block.data.title 392 | ? ` "${block.data.title}"` 393 | : '' 394 | return `![${alt}](${block.data.src}${title})` 395 | }, 396 | close: () => '' 397 | } 398 | } 399 | }) 400 | 401 | expect(markdown).toEqual('![](https://example.com)') 402 | }); 403 | 404 | it('renders nested unordered lists', function () { 405 | /* eslint-disable */ 406 | var rawObject = {"entityMap":{},"blocks":[{"key":"fqn68","text":"item","type":"unordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"5p96k","text":"item","type":"unordered-list-item","depth":1,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 407 | /* eslint-enable */ 408 | var markdown = draftToMarkdown(rawObject); 409 | 410 | expect(markdown).toEqual('- item\n - item'); 411 | }); 412 | 413 | it('renders nested ordered lists', function () { 414 | /* eslint-disable */ 415 | var rawObject = {"entityMap":{},"blocks":[{"key":"d9c1d","text":"item","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"meoh","text":"item","type":"ordered-list-item","depth":1,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 416 | /* eslint-enable */ 417 | var markdown = draftToMarkdown(rawObject); 418 | 419 | expect(markdown).toEqual('1. item\n 1. item'); 420 | }); 421 | 422 | it('renders complex nested ordered lists', function () { 423 | /* eslint-disable */ 424 | var rawObject = {"entityMap":{},"blocks":[{"key":"83lsh","text":"Test Item one unnested","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"f8nk7","text":"Test Item one nested","type":"ordered-list-item","depth":1,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"68mnn","text":"Test Item two nested","type":"ordered-list-item","depth":1,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"3lr37","text":"Test item three nested","type":"ordered-list-item","depth":1,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"6t7np","text":"Test item two unnested","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"c6spi","text":"Test item three unnested","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"dm827","text":"Test Item Four unnested","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"bni48","text":"Test item one nested under test item four","type":"ordered-list-item","depth":1,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"d3g6c","text":"Test item one double nested","type":"ordered-list-item","depth":2,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"cshu1","text":"Test item two double nested","type":"ordered-list-item","depth":2,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 425 | /* eslint-enable */ 426 | var markdown = draftToMarkdown(rawObject); 427 | 428 | expect(markdown).toEqual('1. Test Item one unnested\n 1. Test Item one nested\n 2. Test Item two nested\n 3. Test item three nested\n2. Test item two unnested\n3. Test item three unnested\n4. Test Item Four unnested\n 1. Test item one nested under test item four\n 1. Test item one double nested\n 2. Test item two double nested'); 429 | }); 430 | 431 | it('resets ordered lists count when list is interupted by another element', function () { 432 | /* eslint-disable */ 433 | var rawObject = {"entityMap":{},"blocks":[ 434 | {"key":"8omhg","text":"first top level list item","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}, 435 | {"key":"d8k17","text":"second top level list item","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}, 436 | {"key":"ag9fd","text":"another block-level item","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}, 437 | {"key":"b5fol","text":"another top level list item","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}, 438 | ]}; 439 | /* eslint-enable */ 440 | var markdown = draftToMarkdown(rawObject); 441 | expect(markdown).toEqual('1. first top level list item\n2. second top level list item\n\nanother block-level item\n\n1. another top level list item'); 442 | }) 443 | 444 | it('renders unnested ordered lists', function () { 445 | /* eslint-disable */ 446 | var rawObject = {"entityMap":{},"blocks":[{"key":"d9c1d","text":"item","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"meoh","text":"item","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 447 | /* eslint-enable */ 448 | var markdown = draftToMarkdown(rawObject); 449 | 450 | expect(markdown).toEqual('1. item\n2. item'); 451 | }); 452 | 453 | it('renders newlines after ordered lists correctly', function () { 454 | /* eslint-disable */ 455 | var rawObject = {"entityMap":{},"blocks":[{"key":"d9c1d","text":"item","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"meoh","text":"item","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"litt","text":"foo","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 456 | /* eslint-enable */ 457 | var markdown = draftToMarkdown(rawObject); 458 | 459 | expect(markdown).toEqual('1. item\n2. item\n\nfoo'); 460 | }); 461 | 462 | it('renders newlines after unordered lists correctly', function () { 463 | /* eslint-disable */ 464 | var rawObject = {"entityMap":{},"blocks":[{"key":"d9c1d","text":"item","type":"unordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"meoh","text":"item","type":"unordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"litt","text":"foo","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 465 | /* eslint-enable */ 466 | var markdown = draftToMarkdown(rawObject); 467 | 468 | expect(markdown).toEqual('- item\n- item\n\nfoo'); 469 | }); 470 | 471 | it('renders emoji correctly', function () { 472 | /* eslint-disable */ 473 | var rawObject = { 474 | 'entityMap': {}, 475 | 'blocks': [ 476 | { 477 | 'depth': 0, 478 | 'type': 'unstyled', 479 | 'text': 'Testing 👍 italic words words words bold words words words', 480 | 'entityRanges': [], 481 | 'inlineStyleRanges': [ 482 | { 483 | 'offset': 10, 484 | 'length': 6, 485 | 'style': 'ITALIC' 486 | }, 487 | { 488 | 'offset': 35, 489 | 'length': 4, 490 | 'style': 'BOLD' 491 | } 492 | ] 493 | } 494 | ] 495 | } 496 | /* eslint-enable */ 497 | var markdown = draftToMarkdown(rawObject); 498 | expect(markdown).toEqual('Testing 👍 _italic_ words words words **bold** words words words'); 499 | }); 500 | 501 | it('handles overlapping inline style ranges correctly', function () { 502 | var rawObject = { 503 | blocks: [ 504 | { 505 | key: '8ohj1', 506 | text: 'foo bar baz', 507 | type: 'unstyled', 508 | depth: 0, 509 | inlineStyleRanges: [ 510 | { offset: 0, length: 7, style: 'ITALIC' }, 511 | { offset: 4, length: 7, style: 'BOLD' } 512 | ], 513 | entityRanges: [], 514 | data: {} 515 | } 516 | ], 517 | entityMap: {} 518 | }; 519 | var markdown = draftToMarkdown(rawObject); 520 | expect(markdown).toEqual('_foo **bar**_ **baz**'); 521 | }); 522 | 523 | it('handles overlapping entities & inline style ranges correctly', function () { 524 | var rawObject = { 525 | blocks: [ 526 | { 527 | key: '5rq8m', 528 | text: 'foo bar baz', 529 | type: 'unstyled', 530 | depth: 0, 531 | inlineStyleRanges: [{ offset: 4, length: 7, style: 'BOLD' }], 532 | entityRanges: [{ offset: 0, length: 7, key: 0 }], 533 | data: {} 534 | } 535 | ], 536 | entityMap: { 537 | '0': { 538 | type: 'LINK', 539 | mutability: 'MUTABLE', 540 | data: { 541 | url: 'http://localhost:8000' 542 | } 543 | } 544 | } 545 | }; 546 | var markdown = draftToMarkdown(rawObject); 547 | expect(markdown).toEqual('[foo **bar**](http://localhost:8000) **baz**'); 548 | }); 549 | }); 550 | 551 | describe('escaping markdown characters', function () { 552 | 553 | it ('escapes inline markdown characters', function () { 554 | /* eslint-disable */ 555 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"Test _not italic_ Test **not bold**","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 556 | /* eslint-enable */ 557 | 558 | var markdown = draftToMarkdown(rawObject); 559 | expect(markdown).toEqual('Test \\_not italic\\_ Test \\*\\*not bold\\*\\*'); 560 | }); 561 | 562 | it('escapes complex inline markdown characters', function () { 563 | /* eslint-disable */ 564 | var rawObject = { "entityMap": {}, "blocks": [{ "key": "dvfr1", "text": "Test _not **i** t**a**lic_ T_est **not bold** _hi_ ok **notmatching* smile!", "type": "unstyled", "depth": 0, "inlineStyleRanges": [], "entityRanges": [], "data": {} }] }; 565 | /* eslint-enable */ 566 | 567 | var markdown = draftToMarkdown(rawObject); 568 | expect(markdown).toEqual('Test \\_not \\*\\*i\\*\\* t**a**lic\\_ T_est \\*\\*not bold\\*\\* \\_hi\\_ ok **notmatching* smile!'); 569 | }); 570 | 571 | it('doesn’t escape inline markdown characters in cases where they aren’t valid as markdown', function () { 572 | /* eslint-disable */ 573 | var rawObject = { "entityMap": {}, "blocks": [{ "key": "dvfr1", "text": "Test_not italic_ Test**not bold**", "type": "unstyled", "depth": 0, "inlineStyleRanges": [], "entityRanges": [], "data": {} }] }; 574 | /* eslint-enable */ 575 | 576 | var markdown = draftToMarkdown(rawObject); 577 | expect(markdown).toEqual('Test_not italic_ Test**not bold**'); 578 | 579 | /* eslint-disable */ 580 | rawObject = { "entityMap": {}, "blocks": [{ "key": "dvfr1", "text": "Test _not italic Test not bold", "type": "unstyled", "depth": 0, "inlineStyleRanges": [], "entityRanges": [], "data": {} }] }; 581 | /* eslint-enable */ 582 | 583 | markdown = draftToMarkdown(rawObject); 584 | expect(markdown).toEqual('Test _not italic Test not bold'); 585 | }); 586 | 587 | it ('escapes block markdown characters when at start of line', function () { 588 | /* eslint-disable */ 589 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"# Test _not # italic_ Test **not bold**","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 590 | /* eslint-enable */ 591 | 592 | var markdown = draftToMarkdown(rawObject); 593 | expect(markdown).toEqual('\\# Test \\_not # italic\\_ Test \\*\\*not bold\\*\\*'); 594 | }); 595 | 596 | it ('doesn’t escape heading markdown characters when no whitespace afterwards', function () { 597 | /* eslint-disable */ 598 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"#Test","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 599 | /* eslint-enable */ 600 | 601 | var markdown = draftToMarkdown(rawObject); 602 | expect(markdown).toEqual('#Test'); 603 | }); 604 | 605 | it ('does escape blockquote markdown characters when no whitespace afterwards', function () { 606 | /* eslint-disable */ 607 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":">Test","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 608 | /* eslint-enable */ 609 | 610 | var markdown = draftToMarkdown(rawObject); 611 | expect(markdown).toEqual('\\>Test'); 612 | }); 613 | 614 | it ('doesn’t escape markdown characters in inline code blocks', function () { 615 | /* eslint-disable */ 616 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"such special code which contains *special* chars is so important","type":"unstyled","depth":0,"inlineStyleRanges":[{'offset':5,'length':43,'style':'CODE'}],"entityRanges":[],"data":{}}]}; 617 | /* eslint-enable */ 618 | 619 | var markdown = draftToMarkdown(rawObject); 620 | expect(markdown).toEqual('such `special code which contains *special* chars` is so important'); 621 | }); 622 | 623 | it ('doesn’t escape markdown characters in code blocks', function () { 624 | /* eslint-disable */ 625 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"such special _code_ which contains *special* chars *wow*","type":"code-block","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 626 | /* eslint-enable */ 627 | 628 | var markdown = draftToMarkdown(rawObject); 629 | expect(markdown).toEqual('```\nsuch special _code_ which contains *special* chars *wow*\n```'); 630 | }); 631 | }); 632 | 633 | describe('allowing markdown characters', function () { 634 | it('preserves inline markdown characters', function () { 635 | /* eslint-disable */ 636 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"Test _not italic_ Test **not bold**","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 637 | /* eslint-enable */ 638 | 639 | var markdown = draftToMarkdown(rawObject, { escapeMarkdownCharacters: false }); 640 | expect(markdown).toEqual('Test _not italic_ Test **not bold**'); 641 | }); 642 | 643 | it('preserves block markdown characters at begining of a line', function () { 644 | /* eslint-disable */ 645 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"# Test _not # italic_ Test **not bold**","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 646 | /* eslint-enable */ 647 | 648 | var markdown = draftToMarkdown(rawObject, { escapeMarkdownCharacters: false }); 649 | expect(markdown).toEqual('# Test _not # italic_ Test **not bold**'); 650 | }); 651 | 652 | it('preserves blockquotes markdown characters with no trailing whitespace', function () { 653 | /* eslint-disable */ 654 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":">Test","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 655 | /* eslint-enable */ 656 | 657 | var markdown = draftToMarkdown(rawObject, { escapeMarkdownCharacters: false }); 658 | expect(markdown).toEqual('>Test'); 659 | }); 660 | 661 | it('preserves italics markdown characters with no trailing whitespace', function () { 662 | /* eslint-disable */ 663 | var rawObject = {"entityMap":{},"blocks":[{"key":"dvfr1","text":"_Test_","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}; 664 | /* eslint-enable */ 665 | 666 | var markdown = draftToMarkdown(rawObject, { escapeMarkdownCharacters: false }); 667 | expect(markdown).toEqual('_Test_'); 668 | }); 669 | }); 670 | }); 671 | -------------------------------------------------------------------------------- /test/idempotency.spec.js: -------------------------------------------------------------------------------- 1 | import { markdownToDraft, draftToMarkdown } from '../src/index'; 2 | 3 | /* 4 | * Note: These tests are to make sure markdown to draft and then back to markdown have the exact same value before-and-after 5 | * 6 | * However, there are some cases where this will not be possible - 7 | * For example, this is valid in draftjs: 8 | * _I am italic content with a space included at the end _ 9 | * but markdown will struggle with this so we need to convert it to: 10 | * _I am italic content with a space after the closing italic symbol instead of before_ 11 | * The end result should basically look the same to a human, but behind the scenes it has to be a bit different. 12 | * 13 | * Still, for the most part results should match, so these tests are for cases when they _should_ match. 14 | */ 15 | 16 | describe('idempotency', function () { 17 | 18 | it('renders new lines text correctly', function () { 19 | var markdownString = 'Test\n\n\nHello There\n\nSmile\n\n\n\n\n\n\n\nYep Hi'; 20 | var draftJSObject = markdownToDraft(markdownString, {preserveNewlines: true}); 21 | var markdownFromDraft = draftToMarkdown(draftJSObject, {preserveNewlines: true}); 22 | 23 | expect(markdownFromDraft).toEqual(markdownString); 24 | 25 | markdownString = 'a\nb\n\nc\n\n\nd'; 26 | draftJSObject = markdownToDraft(markdownString, {preserveNewlines: true}); 27 | markdownFromDraft = draftToMarkdown(draftJSObject, {preserveNewlines: true}); 28 | 29 | expect(markdownFromDraft).toEqual(markdownString); 30 | 31 | markdownString = '\n\na'; 32 | draftJSObject = markdownToDraft(markdownString, {preserveNewlines: true}); 33 | markdownFromDraft = draftToMarkdown(draftJSObject, {preserveNewlines: true}); 34 | expect(markdownFromDraft).toEqual(markdownString); 35 | 36 | }); 37 | 38 | it('renders new lines text correctly with styled blocks', function () { 39 | var markdownString = '# Test\n\n\nHello There\n\nSmile\n\n\n\n\n\n\n\nYep Hi'; 40 | var draftJSObject = markdownToDraft(markdownString, {preserveNewlines: true}); 41 | var markdownFromDraft = draftToMarkdown(draftJSObject, {preserveNewlines: true}); 42 | 43 | expect(markdownFromDraft).toEqual(markdownString); 44 | }); 45 | 46 | it ('renders preserved new lines after list correctly', function () { 47 | var markdown = '- unordered item\n\n\n1. ordered item\n\n\nparagraph' 48 | var rawDraftConversion = markdownToDraft(markdown, {preserveNewlines: true }); 49 | var markdownConversion = draftToMarkdown(rawDraftConversion, {preserveNewlines: true}); 50 | 51 | expect(markdownConversion).toEqual(markdown); 52 | }); 53 | 54 | it('renders blockquotes correctly', function () { 55 | var markdownString = '> Hello I am Blockquote\n\nI am not\n\n> I am'; 56 | var draftJSObject = markdownToDraft(markdownString, {preserveNewlines: true}); 57 | var markdownFromDraft = draftToMarkdown(draftJSObject, {preserveNewlines: true}); 58 | expect(markdownFromDraft).toEqual(markdownString); 59 | }); 60 | 61 | it('renders italic text correctly', function () { 62 | var markdownString = '_I am italic_ …I am not italic.'; 63 | var draftJSObject = markdownToDraft(markdownString); 64 | var markdownFromDraft = draftToMarkdown(draftJSObject); 65 | 66 | expect(markdownFromDraft).toEqual(markdownString); 67 | }); 68 | 69 | it('renders bold text correctly', function () { 70 | var markdownString = 'Hello **I am bold** I am not bold.'; 71 | var draftJSObject = markdownToDraft(markdownString); 72 | var markdownFromDraft = draftToMarkdown(draftJSObject); 73 | 74 | expect(markdownFromDraft).toEqual(markdownString); 75 | }); 76 | 77 | it('renders nested styles correctly', function () { 78 | var markdownString = '**bold** _italic_ test **bold _italic_** _italic_ _italic **bold** italic **bold italic**_'; 79 | var draftJSObject = markdownToDraft(markdownString); 80 | var markdownFromDraft = draftToMarkdown(draftJSObject); 81 | 82 | expect(markdownFromDraft).toEqual(markdownString); 83 | }); 84 | 85 | it('renders inline code correctly', function () { 86 | var markdownString = 'Test `here is some inline code`'; 87 | var draftJSObject = markdownToDraft(markdownString); 88 | var markdownFromDraft = draftToMarkdown(draftJSObject); 89 | 90 | expect(markdownFromDraft).toEqual(markdownString); 91 | }); 92 | 93 | it('renders code fences correctly', function () { 94 | var markdownString = '```\nHello I am Codefence\n```'; 95 | var draftJSObject = markdownToDraft(markdownString); 96 | var markdownFromDraft = draftToMarkdown(draftJSObject); 97 | 98 | expect(markdownFromDraft).toEqual(markdownString); 99 | }); 100 | 101 | it('renders blockquotes correctly', function () { 102 | var markdownString = '> Hello I am Blockquote'; 103 | var draftJSObject = markdownToDraft(markdownString); 104 | var markdownFromDraft = draftToMarkdown(draftJSObject); 105 | 106 | expect(markdownFromDraft).toEqual(markdownString); 107 | }); 108 | 109 | // TODO this test should pass but markdown-to-draft doesn’t correctly create empty markdown blocks in this case currently. 110 | xit('renders blockquotes with blank lines correctly', function () { 111 | var markdownString = '> Hello I am Blockquote\n> more\n> \n> \n> hey'; 112 | var draftJSObject = markdownToDraft(markdownString); 113 | var markdownFromDraft = draftToMarkdown(draftJSObject); 114 | 115 | expect(markdownFromDraft).toEqual(markdownString); 116 | }); 117 | 118 | it('renders links correctly', function () { 119 | var markdown = 'This is a test of [a link](https://google.com)\n\nAnd [perhaps](https://facebook.github.io/draft-js/) we should test once more.'; 120 | var rawDraftConversion = markdownToDraft(markdown); 121 | var markdownConversion = draftToMarkdown(rawDraftConversion); 122 | 123 | expect(markdownConversion).toEqual(markdown); 124 | }); 125 | 126 | it ('renders codeblock with syntax correctly', function () { 127 | var markdown = '```javascript\nsingle line codeblock\n```'; 128 | var rawDraftConversion = markdownToDraft(markdown); 129 | var markdownConversion = draftToMarkdown(rawDraftConversion); 130 | expect(markdownConversion).toEqual(markdown); 131 | }); 132 | 133 | it('renders "the kitchen sink" correctly', function () { 134 | var markdown = '# Hello!\n\nMy name is **Rose** :)\nToday, I\'m here to talk to you about how great markdown is!\n\n## First, here\'s a few bullet points:\n\n- One\n- Two\n- Three\n\n```\nA codeblock\n```\n\nAnd then... `some monospace text`?\nOr... _italics?_'; 135 | var rawDraftConversion = markdownToDraft(markdown); 136 | var markdownConversion = draftToMarkdown(rawDraftConversion); 137 | 138 | expect(markdownConversion).toEqual(markdown); 139 | }); 140 | 141 | it ('renders escaped italics correctly', function () { 142 | var markdown = 'test \\_not italic\\_ test'; 143 | var rawDraftConversion = markdownToDraft(markdown); 144 | var markdownConversion = draftToMarkdown(rawDraftConversion); 145 | 146 | expect(markdownConversion).toEqual(markdown); 147 | }); 148 | 149 | it ('renders escaped bold correctly', function () { 150 | var markdown = 'test \\*\\*not italic\\*\\* test'; 151 | var rawDraftConversion = markdownToDraft(markdown); 152 | var markdownConversion = draftToMarkdown(rawDraftConversion); 153 | 154 | expect(markdownConversion).toEqual(markdown); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/markdown-to-draft.spec.js: -------------------------------------------------------------------------------- 1 | import { markdownToDraft, draftToMarkdown } from '../src/index'; 2 | 3 | describe('markdownToDraft', function () { 4 | it('renders empty text correctly', function () { 5 | var markdown = ''; 6 | var conversionResult = markdownToDraft(markdown); 7 | 8 | expect(conversionResult.blocks[0].text).toEqual(''); 9 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 10 | }); 11 | 12 | it('renders text according to Remarkable options', function () { 13 | var markdown = '(p)'; 14 | var conversionResult = markdownToDraft(markdown, {remarkableOptions: {typographer: true}}); 15 | expect(conversionResult.blocks[0].text).toEqual('§'); 16 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 17 | }); 18 | 19 | it('renders text according to Remarkable options (with preset)', function () { 20 | var markdown = '(p)'; 21 | var conversionResult = markdownToDraft(markdown, {remarkablePreset: 'full', remarkableOptions: {typographer: true}}); 22 | expect(conversionResult.blocks[0].text).toEqual('§'); 23 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 24 | }); 25 | 26 | it('renders unstyled blank lines correctly', function () { 27 | var markdown = 'a\nb\n\nc\n\n\nd'; 28 | var conversionResult = markdownToDraft(markdown); 29 | expect(conversionResult.blocks[0].text).toEqual('a\nb'); 30 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 31 | expect(conversionResult.blocks[1].text).toEqual('c'); 32 | expect(conversionResult.blocks[1].type).toEqual('unstyled'); 33 | expect(conversionResult.blocks[2].text).toEqual('d'); 34 | expect(conversionResult.blocks[2].type).toEqual('unstyled'); 35 | 36 | conversionResult = markdownToDraft(markdown, {preserveNewlines: true}); 37 | expect(conversionResult.blocks[0].text).toEqual('a\nb'); 38 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 39 | expect(conversionResult.blocks[1].text).toEqual(''); 40 | expect(conversionResult.blocks[1].type).toEqual('unstyled'); 41 | expect(conversionResult.blocks[2].text).toEqual('c'); 42 | expect(conversionResult.blocks[2].type).toEqual('unstyled'); 43 | expect(conversionResult.blocks[3].text).toEqual(''); 44 | expect(conversionResult.blocks[3].type).toEqual('unstyled'); 45 | expect(conversionResult.blocks[4].text).toEqual(''); 46 | expect(conversionResult.blocks[4].type).toEqual('unstyled'); 47 | expect(conversionResult.blocks[5].text).toEqual('d'); 48 | expect(conversionResult.blocks[5].type).toEqual('unstyled'); 49 | }); 50 | 51 | it('renders hardbreaks correctly', function () { 52 | var markdown = 'First line \nSecond line'; 53 | var conversionResult = markdownToDraft(markdown); 54 | expect(conversionResult.blocks[0].text).toEqual('First line\nSecond line'); 55 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 56 | }); 57 | 58 | describe('blockquotes', function () { 59 | it('renders blockquotes correctly', function () { 60 | var markdown = '> Test I am a blockquote'; 61 | var conversionResult = markdownToDraft(markdown); 62 | 63 | expect(conversionResult.blocks[0].type).toEqual('blockquote'); 64 | expect(conversionResult.blocks[0].text).toEqual('Test I am a blockquote'); 65 | }); 66 | 67 | it('can handle an empty blockquote', function () { 68 | var markdown = '>'; 69 | var conversionResult = markdownToDraft(markdown); 70 | 71 | expect(conversionResult.blocks[0].type).toEqual('blockquote'); 72 | expect(conversionResult.blocks[0].text).toEqual(''); 73 | }); 74 | 75 | it('can handle a blockquote with non-blockquote text around it', function () { 76 | var markdown = 'Hey I am not blockquote \n\n I am also not a blockquote \n\n > Test I am a blockquote\n\n more not blockquote'; 77 | var conversionResult = markdownToDraft(markdown); 78 | 79 | expect(conversionResult.blocks[2].type).toEqual('blockquote'); 80 | expect(conversionResult.blocks[2].text).toEqual('Test I am a blockquote'); 81 | }); 82 | 83 | it ('can handle empty blockquote between blockquote with content', function () { 84 | var markdown = '> Testing\n> \n> \n> Hello'; 85 | var conversionResult = markdownToDraft(markdown); 86 | 87 | expect(conversionResult.blocks[0].type).toEqual('blockquote'); 88 | expect(conversionResult.blocks[0].text).toEqual('Testing'); 89 | 90 | expect(conversionResult.blocks[1].type).toEqual('blockquote'); 91 | expect(conversionResult.blocks[1].text).toEqual('Hello'); 92 | }); 93 | }); 94 | 95 | describe('headings', function () { 96 | it ('renders h1 correctly', function () { 97 | var markdown = '# Test I am a heading'; 98 | var conversionResult = markdownToDraft(markdown); 99 | 100 | expect(conversionResult.blocks[0].type).toEqual('header-one'); 101 | expect(conversionResult.blocks[0].text).toEqual('Test I am a heading'); 102 | }); 103 | 104 | it ('renders h2 correctly', function () { 105 | var markdown = '## Test I am a heading'; 106 | var conversionResult = markdownToDraft(markdown); 107 | 108 | expect(conversionResult.blocks[0].type).toEqual('header-two'); 109 | expect(conversionResult.blocks[0].text).toEqual('Test I am a heading'); 110 | }); 111 | 112 | it ('renders h3 correctly', function () { 113 | var markdown = '### Test I am a heading'; 114 | var conversionResult = markdownToDraft(markdown); 115 | 116 | expect(conversionResult.blocks[0].type).toEqual('header-three'); 117 | expect(conversionResult.blocks[0].text).toEqual('Test I am a heading'); 118 | }); 119 | 120 | it ('renders h4 correctly', function () { 121 | var markdown = '#### Test I am a heading'; 122 | var conversionResult = markdownToDraft(markdown); 123 | 124 | expect(conversionResult.blocks[0].type).toEqual('header-four'); 125 | expect(conversionResult.blocks[0].text).toEqual('Test I am a heading'); 126 | }); 127 | 128 | it ('renders h5 correctly', function () { 129 | var markdown = '##### Test I am a heading'; 130 | var conversionResult = markdownToDraft(markdown); 131 | 132 | expect(conversionResult.blocks[0].type).toEqual('header-five'); 133 | expect(conversionResult.blocks[0].text).toEqual('Test I am a heading'); 134 | }); 135 | 136 | it ('renders h6 correctly', function () { 137 | var markdown = '###### Test I am a heading'; 138 | var conversionResult = markdownToDraft(markdown); 139 | 140 | expect(conversionResult.blocks[0].type).toEqual('header-six'); 141 | expect(conversionResult.blocks[0].text).toEqual('Test I am a heading'); 142 | }); 143 | 144 | it('can handle an empty heading', function () { 145 | var markdown = '#'; 146 | var conversionResult = markdownToDraft(markdown); 147 | 148 | expect(conversionResult.blocks[0].type).toEqual('header-one'); 149 | expect(conversionResult.blocks[0].text).toEqual(''); 150 | }); 151 | 152 | it('can handle heading with non-heading text around it', function () { 153 | var markdown = 'Test I am not a heading \n\n # I am a heading \n\n I am more no heading'; 154 | var conversionResult = markdownToDraft(markdown); 155 | 156 | expect(conversionResult.blocks[1].type).toEqual('header-one'); 157 | expect(conversionResult.blocks[1].text).toEqual('I am a heading'); 158 | }); 159 | }); 160 | 161 | describe('lists', function () { 162 | it('can handle an unordered list item', function () { 163 | var markdown = '- Hi I am an unordered List Item'; 164 | var conversionResult = markdownToDraft(markdown); 165 | 166 | expect(conversionResult.blocks[0].type).toEqual('unordered-list-item'); 167 | expect(conversionResult.blocks[0].text).toEqual('Hi I am an unordered List Item'); 168 | }); 169 | 170 | it('can handle an ordered list item', function () { 171 | var markdown = '1. Hi I am a list item'; 172 | var conversionResult = markdownToDraft(markdown); 173 | 174 | expect(conversionResult.blocks[0].type).toEqual('ordered-list-item'); 175 | expect(conversionResult.blocks[0].text).toEqual('Hi I am a list item'); 176 | }); 177 | 178 | it('can handle an empty unordered list item', function () { 179 | var markdown = '-'; 180 | var conversionResult = markdownToDraft(markdown); 181 | 182 | expect(conversionResult.blocks[0].type).toEqual('unordered-list-item'); 183 | expect(conversionResult.blocks[0].text).toEqual(''); 184 | }); 185 | 186 | it('can handle an ordered empty list item', function () { 187 | var markdown = '1.'; 188 | var conversionResult = markdownToDraft(markdown); 189 | 190 | expect(conversionResult.blocks[0].type).toEqual('ordered-list-item'); 191 | expect(conversionResult.blocks[0].text).toEqual(''); 192 | }); 193 | 194 | it('can handle nested unordered lists', function () { 195 | var markdown = '- item\n - item'; 196 | var conversionResult = markdownToDraft(markdown); 197 | 198 | expect(conversionResult.blocks[0].type).toEqual('unordered-list-item'); 199 | expect(conversionResult.blocks[0].depth).toEqual(0); 200 | expect(conversionResult.blocks[1].type).toEqual('unordered-list-item'); 201 | expect(conversionResult.blocks[1].depth).toEqual(1); 202 | }); 203 | 204 | it('can handle nested ordered lists', function () { 205 | var markdown = '1. item\n 1. item'; 206 | var conversionResult = markdownToDraft(markdown); 207 | 208 | expect(conversionResult.blocks[0].type).toEqual('ordered-list-item'); 209 | expect(conversionResult.blocks[0].depth).toEqual(0); 210 | expect(conversionResult.blocks[1].type).toEqual('ordered-list-item'); 211 | expect(conversionResult.blocks[1].depth).toEqual(1); 212 | }); 213 | 214 | it ('can handle complex nested ordered lists', function () { 215 | var markdown = '1. Test Item one unnested\n 1. Test Item one nested\n 2. Test Item two nested\n 3. Test item three nested\n2. Test item two unnested\n3. Test item three unnested\n4. Test Item Four unnested\n 1. Test item one nested under test item four\n 1. Test item one double nested\n 2. Test item two double nested'; 216 | var conversionResult = markdownToDraft(markdown); 217 | 218 | expect(conversionResult.blocks[0].type).toEqual('ordered-list-item'); 219 | expect(conversionResult.blocks[0].depth).toEqual(0); 220 | expect(conversionResult.blocks[1].type).toEqual('ordered-list-item'); 221 | expect(conversionResult.blocks[1].depth).toEqual(1); 222 | 223 | expect(conversionResult.blocks[2].type).toEqual('ordered-list-item'); 224 | expect(conversionResult.blocks[2].depth).toEqual(1); 225 | 226 | expect(conversionResult.blocks[3].type).toEqual('ordered-list-item'); 227 | expect(conversionResult.blocks[3].depth).toEqual(1); 228 | 229 | expect(conversionResult.blocks[4].type).toEqual('ordered-list-item'); 230 | expect(conversionResult.blocks[4].depth).toEqual(0); 231 | 232 | expect(conversionResult.blocks[5].type).toEqual('ordered-list-item'); 233 | expect(conversionResult.blocks[5].depth).toEqual(0); 234 | 235 | expect(conversionResult.blocks[6].type).toEqual('ordered-list-item'); 236 | expect(conversionResult.blocks[6].depth).toEqual(0); 237 | 238 | expect(conversionResult.blocks[7].type).toEqual('ordered-list-item'); 239 | expect(conversionResult.blocks[7].depth).toEqual(1); 240 | 241 | expect(conversionResult.blocks[8].type).toEqual('ordered-list-item'); 242 | expect(conversionResult.blocks[8].depth).toEqual(2); 243 | 244 | expect(conversionResult.blocks[9].type).toEqual('ordered-list-item'); 245 | expect(conversionResult.blocks[9].depth).toEqual(2); 246 | }); 247 | }); 248 | 249 | describe ('codeblocks', function () { 250 | it ('renders single-line codeblock correctly', function () { 251 | var markdown = '```\nsingle line codeblock\n```'; 252 | var conversionResult = markdownToDraft(markdown); 253 | 254 | expect(conversionResult.blocks[0].text).toEqual('single line codeblock'); 255 | expect(conversionResult.blocks[0].type).toEqual('code-block'); 256 | }); 257 | 258 | it ('renders codeblock with syntax correctly', function () { 259 | var markdown = '```javascript\nsingle line codeblock\n```'; 260 | var conversionResult = markdownToDraft(markdown); 261 | 262 | expect(conversionResult.blocks[0].text).toEqual('single line codeblock'); 263 | expect(conversionResult.blocks[0].type).toEqual('code-block'); 264 | expect(conversionResult.blocks[0].data.language).toEqual('javascript'); 265 | }); 266 | 267 | it ('renders single-line codeblock with a single trailing newline correctly', function () { 268 | var markdown = '```\nsingle line codeblock with trailing newline\n\n```'; 269 | var conversionResult = markdownToDraft(markdown); 270 | 271 | expect(conversionResult.blocks[0].text).toEqual('single line codeblock with trailing newline\n'); 272 | expect(conversionResult.blocks[0].type).toEqual('code-block'); 273 | }); 274 | 275 | it ('renders single-line codeblock wrapping newlines correctly', function () { 276 | var markdown = '```\n\nsingle line codeblock with wrapping newlines\n\n```'; 277 | var conversionResult = markdownToDraft(markdown); 278 | 279 | expect(conversionResult.blocks[0].text).toEqual('\nsingle line codeblock with wrapping newlines\n'); 280 | expect(conversionResult.blocks[0].type).toEqual('code-block'); 281 | }); 282 | 283 | it ('renders multi-line codeblock correctly', function () { 284 | var markdown = '```\nTest \n\n here is more \n ok\n```'; 285 | var conversionResult = markdownToDraft(markdown); 286 | 287 | expect(conversionResult.blocks[0].text).toEqual('Test \n\n here is more \n ok'); 288 | expect(conversionResult.blocks[0].type).toEqual('code-block'); 289 | }); 290 | }); 291 | 292 | it('renders links correctly', function () { 293 | var markdown = 'This is a test of [a link](https://google.com)\n\n\n\nAnd [perhaps](https://facebook.github.io/draft-js/) we should test once more.'; 294 | var conversionResult = markdownToDraft(markdown); 295 | expect(conversionResult.blocks[0].text).toEqual('This is a test of a link'); 296 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 297 | expect(conversionResult.blocks[0].inlineStyleRanges).toEqual([]); 298 | expect(conversionResult.blocks[0].entityRanges[0].offset).toEqual(18); 299 | expect(conversionResult.blocks[0].entityRanges[0].length).toEqual(6); 300 | var blockOneKey = conversionResult.blocks[0].entityRanges[0].key; 301 | expect(conversionResult.entityMap[blockOneKey].type).toEqual('LINK'); 302 | expect(conversionResult.entityMap[blockOneKey].data.url).toEqual('https://google.com'); 303 | expect(conversionResult.entityMap[blockOneKey].data.href).toEqual('https://google.com'); 304 | 305 | expect(conversionResult.blocks[1].text).toEqual('And perhaps we should test once more.'); 306 | expect(conversionResult.blocks[1].type).toEqual('unstyled'); 307 | expect(conversionResult.blocks[1].inlineStyleRanges).toEqual([]); 308 | expect(conversionResult.blocks[1].entityRanges[0].offset).toEqual(4); 309 | expect(conversionResult.blocks[1].entityRanges[0].length).toEqual(7); 310 | var blockTwoKey = conversionResult.blocks[1].entityRanges[0].key; 311 | expect(conversionResult.entityMap[blockTwoKey].type).toEqual('LINK'); 312 | expect(conversionResult.entityMap[blockTwoKey].data.url).toEqual('https://facebook.github.io/draft-js/'); 313 | expect(conversionResult.entityMap[blockTwoKey].data.href).toEqual('https://facebook.github.io/draft-js/'); 314 | 315 | }); 316 | 317 | it('renders "the kitchen sink" correctly', function () { 318 | var markdown = '# Hello!\n\nMy name is **Rose** :) \nToday, I\'m here to talk to you about how great markdown is!\n\n## First, here\'s a few bullet points:\n\n- One\n- Two\n- Three\n\n```\nA codeblock\n```\n\nAnd then... `some monospace text`?\nOr... _italics?_'; 319 | var conversionResult = markdownToDraft(markdown); 320 | expect(conversionResult.blocks[0].text).toEqual('Hello!'); 321 | expect(conversionResult.blocks[0].type).toEqual('header-one'); 322 | expect(conversionResult.blocks[0].inlineStyleRanges).toEqual([]); 323 | expect(conversionResult.blocks[0].entityRanges).toEqual([]); 324 | 325 | expect(conversionResult.blocks[1].text).toEqual('My name is Rose :)\nToday, I\'m here to talk to you about how great markdown is!'); 326 | expect(conversionResult.blocks[1].type).toEqual('unstyled'); 327 | expect(conversionResult.blocks[1].inlineStyleRanges[0].offset).toEqual(11); 328 | expect(conversionResult.blocks[1].inlineStyleRanges[0].length).toEqual(4); 329 | expect(conversionResult.blocks[1].inlineStyleRanges[0].style).toEqual('BOLD'); 330 | 331 | expect(conversionResult.blocks[2].text).toEqual('First, here\'s a few bullet points:'); 332 | expect(conversionResult.blocks[2].type).toEqual('header-two'); 333 | expect(conversionResult.blocks[2].inlineStyleRanges).toEqual([]); 334 | expect(conversionResult.blocks[2].entityRanges).toEqual([]); 335 | 336 | expect(conversionResult.blocks[3].text).toEqual('One'); 337 | expect(conversionResult.blocks[3].type).toEqual('unordered-list-item'); 338 | expect(conversionResult.blocks[3].inlineStyleRanges).toEqual([]); 339 | expect(conversionResult.blocks[3].entityRanges).toEqual([]); 340 | 341 | expect(conversionResult.blocks[4].text).toEqual('Two'); 342 | expect(conversionResult.blocks[4].type).toEqual('unordered-list-item'); 343 | expect(conversionResult.blocks[4].inlineStyleRanges).toEqual([]); 344 | expect(conversionResult.blocks[4].entityRanges).toEqual([]); 345 | 346 | expect(conversionResult.blocks[5].text).toEqual('Three'); 347 | expect(conversionResult.blocks[5].type).toEqual('unordered-list-item'); 348 | expect(conversionResult.blocks[5].inlineStyleRanges).toEqual([]); 349 | expect(conversionResult.blocks[5].entityRanges).toEqual([]); 350 | 351 | expect(conversionResult.blocks[6].text).toEqual('A codeblock'); 352 | expect(conversionResult.blocks[6].type).toEqual('code-block'); 353 | expect(conversionResult.blocks[6].inlineStyleRanges).toEqual([]); 354 | expect(conversionResult.blocks[6].entityRanges).toEqual([]); 355 | 356 | expect(conversionResult.blocks[7].text).toEqual('And then... some monospace text?\nOr... italics?'); 357 | expect(conversionResult.blocks[7].type).toEqual('unstyled'); 358 | expect(conversionResult.blocks[7].inlineStyleRanges[0].offset).toEqual(12); 359 | expect(conversionResult.blocks[7].inlineStyleRanges[0].length).toEqual(19); 360 | expect(conversionResult.blocks[7].inlineStyleRanges[0].style).toEqual('CODE'); 361 | expect(conversionResult.blocks[7].inlineStyleRanges[1].offset).toEqual(39); 362 | expect(conversionResult.blocks[7].inlineStyleRanges[1].length).toEqual(8); 363 | expect(conversionResult.blocks[7].inlineStyleRanges[1].style).toEqual('ITALIC'); 364 | expect(conversionResult.blocks[7].entityRanges).toEqual([]); 365 | }); 366 | 367 | it('can handle block entity data', function () { 368 | const MentionRegexp = /^@\[([^\]]*)\]\s*\(([^)]+)\)/; 369 | function mentionWrapper(remarkable) { 370 | remarkable.inline.ruler.push('mention', function mention(state, silent) { 371 | // it is surely not our rule, so we could stop early 372 | if (!state.src || !state.pos) { 373 | return false; 374 | } 375 | 376 | if (state.src[state.pos] !== '@') { 377 | return false; 378 | } 379 | 380 | var match = MentionRegexp.exec(state.src.slice(state.pos)); 381 | if (!match) { 382 | return false; 383 | } 384 | 385 | // in silent mode it shouldn't output any tokens or modify pending 386 | if (!silent) { 387 | state.push({ 388 | type: 'mention_open', 389 | name: match[1], 390 | id: match[2], 391 | level: state.level 392 | }); 393 | 394 | state.push({ 395 | type: 'text', 396 | content: '@' + match[1], 397 | level: state.level + 1 398 | }); 399 | 400 | state.push({ 401 | type: 'mention_close', 402 | level: state.level 403 | }); 404 | } 405 | 406 | // every rule should set state.pos to a position after token"s contents 407 | state.pos += match[0].length; 408 | 409 | return true; 410 | }); 411 | } 412 | 413 | var markdown = 'Test @[Rose](1)'; 414 | var conversionResult = markdownToDraft(markdown, { 415 | remarkablePlugins: [mentionWrapper], 416 | blockEntities: { 417 | mention_open: function (item) { 418 | return { 419 | type: 'MENTION', 420 | mutability: 'IMMUTABLE', 421 | data: { 422 | id: item.id, 423 | name: item.name 424 | } 425 | }; 426 | } 427 | } 428 | }); 429 | 430 | expect(conversionResult.blocks[0].text).toEqual('Test @Rose'); 431 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 432 | expect(conversionResult.blocks[0].inlineStyleRanges).toEqual([]); 433 | expect(conversionResult.blocks[0].entityRanges[0].offset).toEqual(5); 434 | expect(conversionResult.blocks[0].entityRanges[0].length).toEqual(5); 435 | var blockOneKey = conversionResult.blocks[0].entityRanges[0].key; 436 | expect(conversionResult.entityMap[blockOneKey].type).toEqual('MENTION'); 437 | expect(conversionResult.entityMap[blockOneKey].data.id).toEqual('1'); 438 | expect(conversionResult.entityMap[blockOneKey].data.name).toEqual('Rose'); 439 | }); 440 | 441 | it('can handle block data', function () { 442 | var markdown = '```js\ntest()\n```'; 443 | var conversionResult = markdownToDraft(markdown, { 444 | blockTypes: { 445 | fence: function (item) { 446 | return { 447 | type: 'code-block', 448 | data: { 449 | lang: item.params 450 | } 451 | } 452 | } 453 | } 454 | }); 455 | 456 | expect(conversionResult.blocks[0].type).toEqual('code-block'); 457 | expect(conversionResult.blocks[0].data.lang).toEqual('js'); 458 | }); 459 | 460 | it('can handle simple nested styles', function () { 461 | var markdown = '__*hello* world__'; 462 | var conversionResult = markdownToDraft(markdown); 463 | 464 | expect(conversionResult.blocks[0].inlineStyleRanges[0].length).toBe(11); 465 | expect(conversionResult.blocks[0].inlineStyleRanges[1].length).toBe(5); 466 | 467 | }); 468 | 469 | it('can handle more complex nested styles', function () { 470 | var markdown = '**bold _bolditalic_** _italic_ regular'; 471 | var conversionResult = markdownToDraft(markdown); 472 | 473 | expect(conversionResult).toEqual({ 474 | 'entityMap': {}, 475 | 'blocks': [ 476 | { 477 | 'depth': 0, 478 | 'type': 'unstyled', 479 | 'text': 'bold bolditalic italic regular', 480 | 'entityRanges': [], 481 | 'inlineStyleRanges': [ 482 | { 483 | 'offset': 0, 484 | 'length': 15, 485 | 'style': 'BOLD' 486 | }, 487 | { 488 | 'offset': 5, 489 | 'length': 10, 490 | 'style': 'ITALIC' 491 | }, 492 | { 493 | 'offset': 16, 494 | 'length': 6, 495 | 'style': 'ITALIC' 496 | } 497 | ] 498 | } 499 | ] 500 | }); 501 | }); 502 | 503 | it ('ignores tables by default', function () { 504 | var markdown = 'this is the first line.\n' + 505 | '\n' + 506 | 'this is the second line.\n' + 507 | '\n' + 508 | '| foo | bar |\n' + 509 | '| --- | --- |\n' + 510 | '| baz | bim |\n' + 511 | '\n' + 512 | 'This is another line under the table.'; 513 | var conversionResult = markdownToDraft(markdown); 514 | 515 | expect(conversionResult.blocks[0].text).toEqual('this is the first line.'); 516 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 517 | 518 | expect(conversionResult.blocks[1].text).toEqual('this is the second line.'); 519 | expect(conversionResult.blocks[1].type).toEqual('unstyled'); 520 | 521 | expect(conversionResult.blocks[2].text).toEqual('| foo | bar |\n| --- | --- |\n| baz | bim |'); 522 | expect(conversionResult.blocks[2].type).toEqual('unstyled'); 523 | 524 | expect(conversionResult.blocks[3].text).toEqual('This is another line under the table.'); 525 | expect(conversionResult.blocks[3].type).toEqual('unstyled'); 526 | }); 527 | 528 | it('can handle emoji', function () { 529 | // Note `'👍'.length === 2` 530 | var markdown = 'Testing 👍 _italic_ words words words **bold** words words words'; 531 | var conversionResult = markdownToDraft(markdown); 532 | 533 | expect(conversionResult).toEqual({ 534 | 'entityMap': {}, 535 | 'blocks': [ 536 | { 537 | 'depth': 0, 538 | 'type': 'unstyled', 539 | 'text': 'Testing 👍 italic words words words bold words words words', 540 | 'entityRanges': [], 541 | 'inlineStyleRanges': [ 542 | { 543 | 'offset': 10, 544 | 'length': 6, 545 | 'style': 'ITALIC' 546 | }, 547 | { 548 | 'offset': 35, 549 | 'length': 4, 550 | 'style': 'BOLD' 551 | } 552 | ] 553 | } 554 | ] 555 | }); 556 | }); 557 | 558 | it('can handle hr', function () { 559 | var markdown = 'this is the first line.\n' + 560 | '\n' + 561 | '---\n' + 562 | '\n' + 563 | 'this is the second line.'; 564 | var conversionResult = markdownToDraft(markdown, { 565 | blockTypes: { 566 | hr: function (_item) { 567 | return { 568 | type: 'HR', 569 | text: '' 570 | }; 571 | } 572 | } 573 | }); 574 | 575 | expect(conversionResult.blocks[0].text).toEqual('this is the first line.'); 576 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 577 | 578 | expect(conversionResult.blocks[1].text).toEqual(''); 579 | expect(conversionResult.blocks[1].type).toEqual('HR'); 580 | 581 | expect(conversionResult.blocks[2].text).toEqual('this is the second line.'); 582 | expect(conversionResult.blocks[2].type).toEqual('unstyled'); 583 | }); 584 | 585 | it('can disable rules', () => { 586 | var markdown = 'This is a test of [a link](https://google.com)'; 587 | var remarkableOptions = { 588 | remarkableOptions: { 589 | disable: { 590 | inline: 'links' 591 | } 592 | } 593 | } 594 | var conversionResult = markdownToDraft(markdown, remarkableOptions); 595 | 596 | expect(conversionResult.blocks[0].text).toEqual( 597 | 'This is a test of [a link](https://google.com)' 598 | ); 599 | }); 600 | 601 | it('can enable rules', () => { 602 | var markdown = 'H~2~O is a liquid. 2^10^ is 1024.'; 603 | var remarkableOptions = { 604 | remarkableOptions: { 605 | enable: { 606 | inline: ['sub', 'sup'] 607 | } 608 | } 609 | } 610 | var conversionResult = markdownToDraft(markdown, remarkableOptions); 611 | 612 | expect(conversionResult.blocks[0].text).toEqual( 613 | 'HO is a liquid. 2 is 1024.' 614 | ); 615 | }); 616 | 617 | it ('can enable tables with a string', function () { 618 | var markdown = 'this is the first line.\n' + 619 | '\n' + 620 | 'this is the second line.\n' + 621 | '\n' + 622 | '| foo | bar |\n' + 623 | '| --- | --- |\n' + 624 | '| baz | bim |\n' + 625 | '\n' + 626 | 'This is another line under the table.'; 627 | var remarkableOptions = { 628 | remarkableOptions: { 629 | enable: { 630 | block: 'table' 631 | } 632 | } 633 | } 634 | var conversionResult = markdownToDraft(markdown, remarkableOptions); 635 | expect(conversionResult.blocks[0].text).toEqual('this is the first line.'); 636 | expect(conversionResult.blocks[1].text).toEqual('bim'); 637 | expect(conversionResult.blocks[2].text).toEqual('This is another line under the table.'); 638 | }); 639 | 640 | it ('can enable tables with an array', function () { 641 | var markdown = 'this is the first line.\n' + 642 | '\n' + 643 | 'this is the second line.\n' + 644 | '\n' + 645 | '| foo | bar |\n' + 646 | '| --- | --- |\n' + 647 | '| baz | bim |\n' + 648 | '\n' + 649 | 'This is another line under the table.'; 650 | var remarkableOptions = { 651 | remarkableOptions: { 652 | enable: { 653 | block: ['table'] 654 | } 655 | } 656 | } 657 | var conversionResult = markdownToDraft(markdown, remarkableOptions); 658 | expect(conversionResult.blocks[0].text).toEqual('this is the first line.'); 659 | expect(conversionResult.blocks[1].text).toEqual('bim'); 660 | expect(conversionResult.blocks[2].text).toEqual('This is another line under the table.'); 661 | }); 662 | 663 | it ('can handle strikethrough', function () { 664 | var markdown = 'this is ~~strikethrough~~ text'; 665 | var conversionResult = markdownToDraft(markdown); 666 | 667 | expect(conversionResult.blocks[0].inlineStyleRanges.length).toBe(1); 668 | expect(conversionResult.blocks[0].inlineStyleRanges[0].length).toBe(13); 669 | expect(conversionResult.blocks[0].inlineStyleRanges[0].style).toBe('STRIKETHROUGH'); 670 | }); 671 | 672 | describe ('sub and sup', function () { 673 | it ('renders sup and sub correctly when configured', function () { 674 | var markdown = 'H~2~O^TM^'; 675 | var conversionResult = markdownToDraft(markdown, { 676 | remarkablePreset: 'full', 677 | blockStyles: { 678 | sub: 'SUB', 679 | sup: 'SUP' 680 | } 681 | }); 682 | 683 | expect(conversionResult.blocks[0].text).toEqual('H2OTM'); 684 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 685 | expect(conversionResult.blocks[0].inlineStyleRanges[0]).toEqual({ 686 | offset: 1, 687 | length: 1, 688 | style: 'SUB' 689 | }); 690 | expect(conversionResult.blocks[0].inlineStyleRanges[1]).toEqual({ 691 | offset: 3, 692 | length: 2, 693 | style: 'SUP' 694 | }); 695 | }); 696 | }); 697 | 698 | describe ('htmlblock', function () { 699 | it ('renders htmlblock correctly when configured', function () { 700 | var markdown = '
some html
'; 701 | var conversionResult = markdownToDraft(markdown, { 702 | remarkableOptions: { 703 | html: true 704 | }, 705 | blockTypes: { 706 | htmlblock: function (item) { 707 | return { 708 | text: 'HTML transformed into text', 709 | type: 'unstyled' 710 | }; 711 | } 712 | } 713 | }); 714 | 715 | expect(conversionResult.blocks[0].text).toEqual('HTML transformed into text'); 716 | expect(conversionResult.blocks[0].type).toEqual('unstyled'); 717 | expect(conversionResult.blocks[0].depth).toEqual(0); 718 | }); 719 | }); 720 | 721 | }); 722 | --------------------------------------------------------------------------------