From 0d0f6b99e59e1580ef6ecdbf74444eedbd8959c1 Mon Sep 17 00:00:00 2001 From: Fanny Cheung Date: Thu, 12 Nov 2020 23:02:03 +0100 Subject: [PATCH] Add Remarkable standalone blocks configuration to enable customization --- README.md | 34 ++++++++++- src/markdown-to-draft.js | 9 ++- test/markdown-to-draft.spec.js | 105 +++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8ccaadb..2f814c1 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,9 @@ Since entities can also contain additional custom information - in this case, th What if you wanted to go the opposite direction? markdownToDraft uses [Remarkable](https://github.com/jonschlinkert/remarkable) for defining custom markdown types. -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` - +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`. You can find an example of Remarkable plugin in [this repository tests](https://github.com/Rosey/markdown-draft-js/blob/main/test/markdown-to-draft.spec.js#L369). +Example to convert a custom Remarkable token `mention_open` to a Draft entity: ```javascript var rawDraftJSObject = markdownToDraft(markdownString, { remarkablePlugins: [remarkableMentionPlugin], @@ -146,10 +147,39 @@ var rawDraftJSObject = markdownToDraft(markdownString, { } }; } - } + }, +}); +``` +Example to convert a custom Remarkable token `atomic` to a Draft block: +```javascript +var rawDraftJSObject = markdownToDraft(markdownString, { + remarkablePlugins: [remarkableMentionPlugin], + blockTypes: { + atomic_block: function (item) { + return { + type: "atomic", + text: item.content, + entityRanges: [], + inlineStyleRanges: [], + } + } + }, }); ``` +If your custom Remarkable plugin returns a standalone token, you can specify it +in the configuration: +```javascript +var rawDraftJSObject = markdownToDraft(markdownString, { + remarkablePlugins: [remarkableMentionPlugin], + blockTypes: { + atomic_block: function (item) { + ... + } + }, + remarkableStandaloneBlocks: ['atomic_block'] +}); +``` ## Additional options diff --git a/src/markdown-to-draft.js b/src/markdown-to-draft.js index 7495694..f472cc8 100644 --- a/src/markdown-to-draft.js +++ b/src/markdown-to-draft.js @@ -101,6 +101,12 @@ const DefaultBlockStyles = { code: 'CODE' }; +// Remarkable blocks that stands alone. +const DefaultRemarkableStandaloneBlocks = [ + 'hr', + 'fence' +] + // Key generator for entityMap items var idCounter = -1; function generateUniqueKey() { @@ -229,6 +235,7 @@ function markdownToDraft(string, options = {}) { const BlockTypes = Object.assign({}, DefaultBlockTypes, options.blockTypes || {}); const BlockEntities = Object.assign({}, DefaultBlockEntities, options.blockEntities || {}); const BlockStyles = Object.assign({}, DefaultBlockStyles, options.blockStyles || {}); + const RemarkableStandaloneBlocks = DefaultRemarkableStandaloneBlocks.concat(options.remarkableStandaloneBlocks) parsedData.forEach(function (item) { // Because of how remarkable's data is formatted, we need to cache what kind of list we're currently dealing with @@ -254,7 +261,7 @@ function markdownToDraft(string, options = {}) { // The entity map is a master object separate from the block so just add any entities created for this block to the master object Object.assign(entityMap, blockEntities); - } else if ((itemType.indexOf('_open') !== -1 || itemType === 'fence' || itemType === 'hr') && BlockTypes[itemType]) { + } else if ((itemType.indexOf('_open') !== -1 || RemarkableStandaloneBlocks.includes(itemType)) && BlockTypes[itemType]) { var depth = 0; var block; diff --git a/test/markdown-to-draft.spec.js b/test/markdown-to-draft.spec.js index 80dc8b9..363cc87 100644 --- a/test/markdown-to-draft.spec.js +++ b/test/markdown-to-draft.spec.js @@ -457,6 +457,111 @@ describe('markdownToDraft', function () { expect(conversionResult.blocks[0].data.lang).toEqual('js'); }); + it('can handle custom standalone block data', function () { + var delimiter = '---'; + var blockRule = function (state, startLine, endLine, silent) { + var marker, + len, + nextLine, + mem, + content, + haveEndMarker = false, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine] + + // Check if the current line is the first line + if (startLine !== 0) { return false } + + // Check if the line contains at least 3 characters + if (pos + 3 > max) { return false } + + // Check if the first character is a '-' + marker = state.src.charCodeAt(pos) + + if (marker !== 0x2D) { return false } + + // Check marker length + mem = pos + pos = state.skipChars(pos, marker) + len = pos - mem + + if (len < 3) { return false } + + // Since start is found, we can report success here in validation mode + if (silent) { return true } + + nextLine = startLine + + for (;;) { + nextLine++ + + // Break if the current line is the last line + if (nextLine >= endLine) { + break + } + + pos = mem = state.bMarks[nextLine] + state.tShift[nextLine] + max = state.eMarks[nextLine] + + // Skip to next line if the current line does not start with the + // marker + if (state.src.charCodeAt(pos) !== marker) { continue } + + // Skip to next line if the closing dash is indented with more than + // 4 spaces + if (state.tShift[nextLine] - state.blkIndent >= 4) { continue } + + pos = state.skipChars(pos, marker) + + // Skip to next line if the number of closing dashed is not the same + // as the opening ones. + if (pos - mem < len) { continue } + + // Skip to next line if there are more characters after possible + // closing dashes + pos = state.skipSpaces(pos) + + if (pos < max) { continue } + + // Mark block end + haveEndMarker = true + + break + } + + state.line = nextLine + (haveEndMarker ? 1 : 0) + content = state.getLines(startLine + 1, nextLine, state.blkIndent, false).trim() + + state.tokens.push({ + type: 'yaml_frontmatter', + content: content, + level: state.level, + lines: [ startLine, state.line ] + }) + + return true + } + var frontmatterYamlWrapper = function (remarkable) { + remarkable.block.ruler.before('hr', 'yaml_frontmatter', blockRule) + } + var markdown = '---\nsomeMetadata: content\n---'; + var conversionResult = markdownToDraft(markdown, { + remarkablePlugins: [frontmatterYamlWrapper], + blockTypes: { + yaml_frontmatter: function (item) { + return { + type: 'atomic', + content: item.content + } + } + }, + remarkableStandaloneBlocks: ['yaml_frontmatter'] + }); + + expect(conversionResult.blocks[0].type).toEqual('atomic'); + expect(conversionResult.blocks[0].content).toEqual('someMetadata: content'); + }); + it('can handle simple nested styles', function () { var markdown = '__*hello* world__'; var conversionResult = markdownToDraft(markdown);