Skip to content

Instantly share code, notes, and snippets.

@nsfmc
Last active July 8, 2016 18:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nsfmc/ac3ab1cc8b8525185a094e845a9fce21 to your computer and use it in GitHub Desktop.
Save nsfmc/ac3ab1cc8b8525185a094e845a9fce21 to your computer and use it in GitHub Desktop.
a barely working node module that uses markdown-it (and lodash) to build draft content states
import MarkdownIt from 'markdown-it';
import {convertFromRaw} from 'draft-js';
import util from 'util';
import _ from 'lodash';
const flog = (whatever) => {
console.log(util.inspect(whatever, {depth: null, colors: true}));
};
function parseToken(token, struct, tree, treeIndex) {
if (token.type === 'paragraph_open') {
struct.blocks.push({type: 'unstyled', depth: 0});
}
if (token.type === 'heading_open') {
// do this because our editor doesn't support < h2
let style = token.tag === 'h1' ? 'header-one' : 'header-two';
struct.blocks.push({type: style, depth: 0});
}
// When the parser encounters an inline token, it needs to run this
// parser over all its children
if (token.type === 'inline') {
for (let j = 0; j < token.children.length; j += 1) {
let inlineToken = token.children[j];
struct = parseToken(inlineToken, struct);
}
}
if (token.type === 'text') {
let lastBlock = struct.blocks[struct.blocks.length - 1];
lastBlock.text = lastBlock.text || '';
lastBlock.text += token.content;
}
if (token.type === 'em_open') {
let lastBlock = struct.blocks[struct.blocks.length - 1];
lastBlock.inlineStyleRanges = lastBlock.inlineStyleRanges || [];
lastBlock.inlineStyleRanges.push({
style: 'ITALIC',
offset: lastBlock.text.length,
});
}
if (token.type === 'em_close') {
let lastBlock = struct.blocks[struct.blocks.length - 1];
lastBlock.inlineStyleRanges = lastBlock.inlineStyleRanges.map(styleRange => {
let lengthObject = {};
if (styleRange.style === 'ITALIC') {
let length = lastBlock.text.length - styleRange.offset;
lengthObject.length = length;
}
let newRange = Object.assign({}, styleRange, lengthObject);
return newRange;
});
}
if (token.type === 'strong_open') {
let lastBlock = struct.blocks[struct.blocks.length - 1];
lastBlock.inlineStyleRanges = lastBlock.inlineStyleRanges || [];
lastBlock.inlineStyleRanges.push({
style: 'BOLD',
offset: lastBlock.text.length,
});
// how do we inform the next text block that it appends to this inlineStyleRange?
}
if (token.type === 'strong_close') {
let lastBlock = struct.blocks[struct.blocks.length - 1];
lastBlock.inlineStyleRanges = lastBlock.inlineStyleRanges.map(styleRange => {
let lengthObject = {};
if (styleRange.style === 'BOLD') {
let length = lastBlock.text.length - styleRange.offset;
lengthObject.length = length;
}
let newRange = Object.assign({}, styleRange, lengthObject);
return newRange;
});
}
if (token.type === 'image') {
let attrs = _.fromPairs(token.attrs);
let entityKey = Object.keys(struct.entityMap).map(key => parseInt(key, 10)).sort((a,b) => a - b)[0] + 1 || 0;
struct.entityMap[entityKey] = {
type: 'image',
mutability: 'IMMUTABLE',
data: {
src: attrs.src,
},
};
let lastBlock = struct.blocks[struct.blocks.length - 1];
const atomicBlock = Object.assign({}, lastBlock);
atomicBlock.text = ' ';
atomicBlock.type = 'atomic';
atomicBlock.entityRanges = [
{
offset: 0,
length: 1,
key: entityKey, // replace this to find new max entity key
},
];
if (lastBlock.text && _.trim(lastBlock.text.length) > 0) {
// if the last block had actual text, end it and add this
// image as a new block
struct.blocks.push(atomicBlock);
} else {
// otherwise, replace empty blocks
struct.blocks[struct.blocks.length - 1] = atomicBlock;
}
}
if (token.type === 'link_open') {
let lastBlock = struct.blocks[struct.blocks.length - 1];
let entityKey = Object.keys(struct.entityMap).map(key => parseInt(key, 10)).sort((a,b) => a - b)[0] + 1 || 0;
let attrs = _.fromPairs(token.attrs);
lastBlock.entityRanges = lastBlock.entityRanges || [];
struct.entityMap[entityKey] = {
type: 'LINK',
mutability: 'MUTABLE',
data: {
url: attrs.href,
},
};
lastBlock.entityRanges.push({
offset: lastBlock.text && lastBlock.text.length || 0,
key: String(entityKey),
});
// how do we inform the next text block that it appends to this inlineStyleRange?
}
if (token.type === 'link_close') {
let lastBlock = struct.blocks[struct.blocks.length - 1];
let currentEntity = lastBlock.entityRanges[lastBlock.entityRanges.length - 1];
let linkLength = lastBlock.text.length - currentEntity.offset;
currentEntity = Object.assign(currentEntity, {length: linkLength});
}
return struct;
}
export function parseMarkdown(markdownText) {
const md = new MarkdownIt();
let structure = {blocks: [], entityMap: {}};
const tree = md.parse(markdownText);
for (let i = 0; i < tree.length; i += 1) {
let token = tree[i];
structure = parseToken(token, structure, tree, i);
}
return structure;
}
export function convertFromMarkdown(markdownText) {
return convertFromRaw(parseMarkdown(markdownText));
}
import {expect} from 'chai';
import MarkdownIt from 'markdown-it';
import {parseMarkdown} from './convert-from-markdown';
import {convertFromRaw} from 'draft-js';
import util from 'util';
const flog = (whatever) => {
console.log(util.inspect(whatever, {depth: null, colors: true}));
};
describe('markdownit -> draft', function () {
it('makes a link', function () {
const markdown = '[foo](bar)';
const raw = parseMarkdown(markdown);
// one entity
expect(Object.keys(raw.entityMap).length).to.equal(1);
// one block
expect(raw.blocks.length).to.equal(1);
// it's a link
expect(raw.entityMap['0'].type).to.equal('LINK');
// goes to 'bar'
expect(raw.entityMap['0'].data.url).to.equal('bar');
});
it('makes a link in a paragraph', function () {
const markdown = 'oh [foo](bar)...';
const raw = parseMarkdown(markdown);
// one entity
expect(Object.keys(raw.entityMap).length).to.equal(1);
// one block
expect(raw.blocks.length).to.equal(1);
// it's a link
expect(raw.entityMap['0'].type).to.equal('LINK');
// goes to 'bar'
expect(raw.entityMap['0'].data.url).to.equal('bar');
// still 3 chars long
expect(raw.blocks[0].entityRanges[0]['length']).to.equal(3);
// offset by 3 chars
expect(raw.blocks[0].entityRanges[0].offset).to.equal(3);
});
it('makes an image, no alt text', function () {
const markdown = '![](imgsrc)';
const raw = parseMarkdown(markdown);
// one entity
expect(Object.keys(raw.entityMap).length).to.equal(1);
// one block
expect(raw.blocks.length).to.equal(1);
// atomic
expect(raw.blocks[0].type).to.equal('atomic');
// has image
expect(raw.entityMap['0'].type).to.equal('image');
// with correct src
expect(raw.entityMap['0'].data.src).to.equal('imgsrc');
});
// it('makes an image, with alt text', function () {
// const markdown = '![alt](imgsrc)';
// const raw = parseMarkdown(markdown);
// // with correct src
// expect(raw.entityMap['0'].data.alt).to.equal('alt');
// });
it('makes two blocks when finding an inline image', function () {
const markdown = 'here\'s some text ![alt](imgsrc)';
const raw = parseMarkdown(markdown);
// one entity
expect(Object.keys(raw.entityMap).length).to.equal(1);
// twe blocks
expect(raw.blocks.length).to.equal(2);
});
it('parses h1s', function () {
const markdown = '# foo';
const raw = parseMarkdown(markdown);
// one block
expect(raw.blocks.length).to.equal(1);
// of type h1
expect(raw.blocks[0].type).to.equal('header-one');
});
it('parses h2s', function () {
const markdown = '# foo';
const raw = parseMarkdown(markdown);
// one block
expect(raw.blocks.length).to.equal(1);
// of type h1
expect(raw.blocks[0].type).to.equal('header-one');
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment