diff --git a/.flowconfig b/.flowconfig index 6f52e39..0575b6a 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,6 +1,7 @@ [ignore] .*node_modules/babel.* .*node_modules/fbjs.* +.*node_modules/json5.* [include] diff --git a/LICENSE b/LICENSE index 2684d70..9197c59 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,5 @@ +ISC License + Copyright (c) 2016, Simon Sturmer Permission to use, copy, modify, and/or distribute this software for any diff --git a/README.md b/README.md index cd2d83c..6fe4451 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,4 @@ This project is still under development. If you want to help out, please open an ## License -This software is [BSD Licensed](/LICENSE). +This software is [ISC Licensed](/LICENSE). diff --git a/package.json b/package.json index 7017cf1..1ed5b17 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,40 @@ { "name": "draft-js-export-markdown", - "version": "0.1.2", + "version": "0.2.0", "description": "DraftJS: Export ContentState to Markdown", "main": "lib/main.js", "scripts": { "build": "babel src --ignore '_*' --out-dir lib", "lint": "eslint --max-warnings 0 .", + "typecheck": "flow", "prepublish": "npm run build", - "test": "npm run lint && npm run test-src", - "test-src": "mocha" + "test": "npm run lint && npm run typecheck && npm run test-src", + "test-src": "mocha src/__tests__/*.js src/**/__tests__/*.js" }, "dependencies": { - "draft-js-tools": "^0.1.2" + "draft-js-utils": "^0.1.5" + }, + "peerDependencies": { + "draft-js": ">=0.5.0", + "immutable": "3.x.x" }, "devDependencies": { - "babel-core": "^6.7.2", - "babel-eslint": "^5.0.0", - "babel-plugin-transform-class-properties": "^6.6.0", - "babel-preset-es2015": "^6.6.0", + "babel-cli": "^6.9.0", + "babel-core": "^6.9.0", + "babel-eslint": "^6.0.4", + "babel-plugin-transform-class-properties": "^6.9.0", + "babel-preset-es2015": "^6.9.0", "babel-preset-react": "^6.5.0", "babel-preset-stage-2": "^6.5.0", - "draft-js": "^0.2.2", - "eslint": "2.2.0", - "eslint-plugin-babel": "^3.1.0", - "eslint-plugin-flow-vars": "^0.2.1", - "eslint-plugin-react": "^4.2.1", - "expect": "^1.15.2", - "immutable": "^3.7.6", + "eslint": "^2.10.2", + "eslint-plugin-babel": "^3.2.0", + "eslint-plugin-flow-vars": "^0.4.0", + "eslint-plugin-react": "^5.1.1", + "expect": "^1.20.1", + "flow-bin": "^0.25.0", "mocha": "^2.4.5", - "react": "^0.14.7", - "react-dom": "^0.14.7" + "react": "^15.0.2", + "react-dom": "^15.0.2" }, "repository": { "type": "git", @@ -40,6 +45,16 @@ "export-markdown" ], "author": "sstur@me.com", + "contributors": [ + { + "name": "Freddy Harris", + "url": "https://github.com/Freddy03h" + }, + { + "name": "Simon Sturmer", + "url": "https://github.com/sstur" + } + ], "license": "ISC", "bugs": { "url": "https://github.com/sstur/draft-js-export-markdown/issues" diff --git a/src/__tests__/utilities-test.js b/src/__tests__/utilities-test.js new file mode 100644 index 0000000..d3e6c2d --- /dev/null +++ b/src/__tests__/utilities-test.js @@ -0,0 +1,46 @@ +/* @flow */ +const {describe, it} = global; +import expect from 'expect'; +import { + encodeContent, + encodeURL, + escapeTitle, +} from '../utilities'; + +describe('utilities', () => { + describe('encodeContent', () => { + it('should escape the * character', () => { + var result = encodeContent('Test * String ** Testing*'); + expect(result).toEqual('Test \\* String \\*\\* Testing\\*'); + }); + + it('should escape the _ character', () => { + var result = encodeContent('Test _ String __ Testing_'); + expect(result).toEqual('Test \\_ String \\_\\_ Testing\\_'); + }); + + it('should escape the ` character', () => { + var result = encodeContent('Test ` String `` Testing`'); + expect(result).toEqual('Test \\` String \\`\\` Testing\\`'); + }); + + it('should escape *, _, and ` characters', () => { + var result = encodeContent('Test ` String ** Testing_'); + expect(result).toEqual('Test \\` String \\*\\* Testing\\_'); + }); + }); + + describe('encodeURL', () => { + it('should escape the ) character', () => { + var result = encodeURL('https://google.com/hello)'); + expect(result).toEqual('https://google.com/hello%29'); + }); + }); + + describe('escapeTitle', () => { + it('should escape the " character', () => { + var result = escapeTitle('Test "Hello" Test'); + expect(result).toEqual('Test \\"Hello\\" Test'); + }); + }); +}); diff --git a/src/stateToMarkdown.js b/src/stateToMarkdown.js index 1bfe776..1c69b20 100644 --- a/src/stateToMarkdown.js +++ b/src/stateToMarkdown.js @@ -5,8 +5,13 @@ import { BLOCK_TYPE, ENTITY_TYPE, INLINE_STYLE, -} from 'draft-js-tools'; +} from 'draft-js-utils'; import {Entity} from 'draft-js'; +import { + encodeContent, + encodeURL, + escapeTitle, +} from './utilities'; import type {ContentState, ContentBlock} from 'draft-js'; @@ -63,6 +68,21 @@ class MarkupGenerator { this.output.push('### ' + this.renderBlockContent(block) + '\n'); break; } + case BLOCK_TYPE.HEADER_FOUR: { + this.insertLineBreaks(1); + this.output.push('#### ' + this.renderBlockContent(block) + '\n'); + break; + } + case BLOCK_TYPE.HEADER_FIVE: { + this.insertLineBreaks(1); + this.output.push('##### ' + this.renderBlockContent(block) + '\n'); + break; + } + case BLOCK_TYPE.HEADER_SIX: { + this.insertLineBreaks(1); + this.output.push('###### ' + this.renderBlockContent(block) + '\n'); + break; + } case BLOCK_TYPE.UNORDERED_LIST_ITEM: { let blockDepth = block.getDepth(); let lastBlock = this.getLastBlock(); @@ -210,6 +230,11 @@ class MarkupGenerator { let url = data.url || ''; let title = data.title ? ` "${escapeTitle(data.title)}"` : ''; return `[${content}](${encodeURL(url)}${title})`; + } else if (entity != null && entity.getType() === ENTITY_TYPE.IMAGE) { + let data = entity.getData(); + let src = data.src || ''; + let alt = data.alt ? ` "${escapeTitle(data.alt)}"` : ''; + return `![${alt}](${encodeURL(src)})`; } else { return content; } @@ -227,21 +252,6 @@ function canHaveDepth(blockType: any): boolean { } } -function encodeContent(text) { - return text.replace(/[*_`]/g, '\\$&'); -} - -// Encode chars that would normally be allowed in a URL but would conflict with -// our markdown syntax: `[foo](http://foo/)` -function encodeURL(url) { - return url.replace(/\)/g, '%29'); -} - -// Escape quotes using backslash. -function escapeTitle(text) { - return text.replace(/"/g, '\\"'); -} - export default function stateToMarkdown(content: ContentState): string { return new MarkupGenerator(content).generate(); } diff --git a/src/utilities.js b/src/utilities.js new file mode 100644 index 0000000..7e36c5a --- /dev/null +++ b/src/utilities.js @@ -0,0 +1,32 @@ +/* @flow */ +const BASIC_MARKDOWN_CHARS = /[*_`]/g; +const URL_MARKDOWN_CHARS = /\)/g; +const QUOTATIONS = /"/g; + +/** + * Escape chars (*, _) that have meaning in markdown so that they aren't interpreted as part of the markdown + * @param {string} text - the text to be replaced. + * @returns {string} +*/ +export function encodeContent(text: string) { + return text.replace(BASIC_MARKDOWN_CHARS, '\\$&'); +} + +/** + * Encode chars that would normally be allowed in a URL but would conflict with + * our markdown syntax: `[foo](http://foo/)` + * @param {string} url The url to be encoded. + * @returns {string} +*/ +export function encodeURL(url: string) { + return url.replace(URL_MARKDOWN_CHARS, '%29'); +} + +/** + * Escape quotes using backslash + * @param {string} text The string to be escaped + * @returns {string} +*/ +export function escapeTitle(text: string) { + return text.replace(QUOTATIONS, '\\"'); +} diff --git a/test/mocha.opts b/test/mocha.opts index 67c5129..ccd1f09 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,2 +1 @@ --compilers js:babel-core/register -src/__tests__/*.js src/**/__tests__/*.js diff --git a/test/test-cases.txt b/test/test-cases.txt index baca361..799d13a 100644 --- a/test/test-cases.txt +++ b/test/test-cases.txt @@ -14,6 +14,14 @@ _**BoldItalic**_ {"entityMap":{},"blocks":[{"key":"9nc73","text":"BoldItalic","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":4,"length":6,"style":"BOLD"},{"offset":0,"length":4,"style":"ITALIC"}],"entityRanges":[]}]} _Bold_**Italic** +>> Image with alt +{"entityMap":{"0":{"type":"IMAGE","mutability":"MUTABLE","data":{"src":"/a.jpg","alt":"x"}}},"blocks":[{"key":"f131g","text":"Hello World.","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":5,"length":1,"key":0}]}]} +Hello![ "x"](/a.jpg)World. + +>> Image with empty alt +{"entityMap":{"0":{"type":"IMAGE","mutability":"MUTABLE","data":{"src":"/a.jpg","alt":""}}},"blocks":[{"key":"f131g","text":"Hello World.","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":5,"length":1,"key":0}]}]} +Hello![](/a.jpg)World. + >> Link without title {"entityMap":{"0":{"type":"LINK","mutability":"MUTABLE","data":{"url":"/a","foo":"x"}}},"blocks":[{"key":"f131g","text":"Hello World.","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":6,"length":5,"key":0}]}]} Hello [World](/a).