Skip to content

Commit

Permalink
Merge pull request #7307 from ckeditor/i/5988
Browse files Browse the repository at this point in the history
Feature (markdown-gfm): The markdown data processor has been totally revamped and updated to the most recent conversion libraries. Closes #5988.
  • Loading branch information
jodator authored Aug 17, 2020
2 parents a7ef65c + d7dbe8c commit 3881349
Show file tree
Hide file tree
Showing 25 changed files with 858 additions and 2,710 deletions.
5 changes: 4 additions & 1 deletion packages/ckeditor5-markdown-gfm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"ckeditor5-plugin"
],
"dependencies": {
"@ckeditor/ckeditor5-engine": "^21.0.0"
"@ckeditor/ckeditor5-engine": "^21.0.0",
"marked": "^0.7.0",
"turndown": "^6.0.0",
"turndown-plugin-gfm": "^1.0.2"
},
"engines": {
"node": ">=12.0.0",
Expand Down
31 changes: 17 additions & 14 deletions packages/ckeditor5-markdown-gfm/src/gfmdataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
* @module markdown-gfm/gfmdataprocessor
*/

import marked from './lib/marked/marked';
import toMarkdown from './lib/to-markdown/to-markdown';
import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
import GFMRenderer from './lib/marked/renderer';
import converters from './lib/to-markdown/converters';

import markdown2html from './markdown2html/markdown2html';
import html2markdown, { turndownService } from './html2markdown/html2markdown';

/**
* This data processor implementation uses GitHub Flavored Markdown as input/output data.
Expand All @@ -36,21 +35,26 @@ export default class GFMDataProcessor {
this._htmlDP = new HtmlDataProcessor( document );
}

/**
* Keeps the specified element in the output as HTML. This is useful if the editor contains
* features that produce HTML that are not part of the markdon standards.
*
* By default, all HTML tags are removed.
*
* @param element {String} The element name to be kept.
*/
keepHtml( element ) {
turndownService.keep( [ element ] );
}

/**
* Converts the provided Markdown string to view tree.
*
* @param {String} data A Markdown string.
* @returns {module:engine/view/documentfragment~DocumentFragment} The converted view element.
*/
toView( data ) {
const html = marked.parse( data, {
gfm: true,
breaks: true,
tables: true,
xhtml: true,
renderer: new GFMRenderer()
} );

const html = markdown2html( data );
return this._htmlDP.toView( html );
}

Expand All @@ -63,7 +67,6 @@ export default class GFMDataProcessor {
*/
toData( viewFragment ) {
const html = this._htmlDP.toData( viewFragment );

return toMarkdown( html, { gfm: true, converters } );
return html2markdown( html );
}
}
93 changes: 93 additions & 0 deletions packages/ckeditor5-markdown-gfm/src/html2markdown/html2markdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module markdown-gfm/html2markdown
*/

import TurndownService from 'turndown';
import { gfm } from 'turndown-plugin-gfm';

// Override the original escape method by not escaping links.
const originalEscape = TurndownService.prototype.escape;

function escape( string ) {
string = originalEscape( string );

// Escape "<".
string = string.replace( /</g, '\\<' );

return string;
}

// eslint-disable-next-line max-len
const regex = /\b(?:https?:\/\/|www\.)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()[\]{};:'".,<>?«»])/g;

TurndownService.prototype.escape = function( string ) {
// Urls should not be escaped. Our strategy is using a regex to find them and escape everything
// which is out of the matches parts.

let escaped = '';
let lastLinkEnd = 0;

for ( const match of string.matchAll( regex ) ) {
const index = match.index;

// Append the substring between the last match and the current one (if anything).
if ( index > lastLinkEnd ) {
escaped += escape( string.substring( lastLinkEnd, index ) );
}

const matchedURL = match[ 0 ];

escaped += matchedURL;

lastLinkEnd = index + matchedURL.length;
}

// Add text after the last link or at the string start if no matches.
if ( lastLinkEnd < string.length ) {
escaped += escape( string.substring( lastLinkEnd, string.length ) );
}

return escaped;
};

const turndownService = new TurndownService( {
codeBlockStyle: 'fenced',
hr: '---',
headingStyle: 'atx'
} );

turndownService.use( [
gfm,
todoList
] );

/**
* Parses HTML to a markdown.
*
* @param {String} html
* @returns {String}
*/
export default function html2markdown( html ) {
return turndownService.turndown( html );
}

export { turndownService };

// This is a copy of the original taskListItems rule from turdown-plugin-gfm, with minor changes.
function todoList( turndownService ) {
turndownService.addRule( 'taskListItems', {
filter( node ) {
return node.type === 'checkbox' &&
// Changes here as CKEditor outputs a deeper structure.
( node.parentNode.nodeName === 'LI' || node.parentNode.parentNode.nodeName === 'LI' );
},
replacement( content, node ) {
return ( node.checked ? '[x]' : '[ ]' ) + ' ';
}
} );
}
Loading

0 comments on commit 3881349

Please sign in to comment.