@ckeditor/ckeditor5-markdown-gfm 45.2.1 → 46.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +18 -5
- package/build/markdown-gfm.js +1 -1
- package/dist/index.js +157 -150
- package/dist/index.js.map +1 -1
- package/package.json +20 -10
- package/src/gfmdataprocessor.d.ts +3 -3
- package/src/gfmdataprocessor.js +8 -8
- package/src/html2markdown/html2markdown.d.ts +9 -12
- package/src/html2markdown/html2markdown.js +79 -117
- package/src/index.d.ts +5 -4
- package/src/index.js +5 -4
- package/src/markdown.d.ts +1 -1
- package/src/markdown.js +3 -3
- package/src/markdown2html/markdown2html.d.ts +2 -3
- package/src/markdown2html/markdown2html.js +83 -34
- package/src/pastefrommarkdownexperimental.d.ts +1 -1
- package/src/pastefrommarkdownexperimental.js +3 -3
@@ -5,131 +5,93 @@
|
|
5
5
|
/**
|
6
6
|
* @module markdown-gfm/html2markdown/html2markdown
|
7
7
|
*/
|
8
|
-
import
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
import
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
const originalEscape = super.escape;
|
23
|
-
function escape(string) {
|
24
|
-
string = originalEscape(string);
|
25
|
-
// Escape "<".
|
26
|
-
string = string.replace(/</g, '\\<');
|
27
|
-
return string;
|
28
|
-
}
|
29
|
-
// Urls should not be escaped. Our strategy is using a regex to find them and escape everything
|
30
|
-
// which is out of the matches parts.
|
31
|
-
let escaped = '';
|
32
|
-
let lastLinkEnd = 0;
|
33
|
-
for (const match of this._matchAutolink(string)) {
|
34
|
-
const index = match.index;
|
35
|
-
// Append the substring between the last match and the current one (if anything).
|
36
|
-
if (index > lastLinkEnd) {
|
37
|
-
escaped += escape(string.substring(lastLinkEnd, index));
|
38
|
-
}
|
39
|
-
const matchedURL = match[0];
|
40
|
-
escaped += matchedURL;
|
41
|
-
lastLinkEnd = index + matchedURL.length;
|
42
|
-
}
|
43
|
-
// Add text after the last link or at the string start if no matches.
|
44
|
-
if (lastLinkEnd < string.length) {
|
45
|
-
escaped += escape(string.substring(lastLinkEnd, string.length));
|
46
|
-
}
|
47
|
-
return escaped;
|
8
|
+
import { unified } from 'unified';
|
9
|
+
import rehypeParse from 'rehype-dom-parse';
|
10
|
+
import rehypeRemark from 'rehype-remark';
|
11
|
+
import remarkBreaks from 'remark-breaks';
|
12
|
+
import remarkGfm from 'remark-gfm';
|
13
|
+
import remarkStringify from 'remark-stringify';
|
14
|
+
import { visit } from 'unist-util-visit';
|
15
|
+
import { h } from 'hastscript';
|
16
|
+
import { toHtml } from 'hast-util-to-html';
|
17
|
+
export class MarkdownGfmHtmlToMd {
|
18
|
+
_processor;
|
19
|
+
_keepRawTags = [];
|
20
|
+
constructor() {
|
21
|
+
this._buildProcessor();
|
48
22
|
}
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
// We could adjust regex.lastIndex but it's not needed because what we skipped is for sure not a valid URL.
|
59
|
-
}
|
23
|
+
keep(tagName) {
|
24
|
+
this._keepRawTags.push(tagName.toLowerCase());
|
25
|
+
this._buildProcessor();
|
26
|
+
}
|
27
|
+
parse(html) {
|
28
|
+
return this._processor
|
29
|
+
.processSync(html)
|
30
|
+
.toString()
|
31
|
+
.trim();
|
60
32
|
}
|
61
33
|
/**
|
62
|
-
* Returns
|
34
|
+
* Returns handlers for raw HTML tags that should be kept in the Markdown output.
|
63
35
|
*/
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
36
|
+
_getRawTagsHandlers() {
|
37
|
+
return this._keepRawTags.reduce((handlers, tagName) => {
|
38
|
+
handlers[tagName] = (state, node) => {
|
39
|
+
const tag = toHtml(h(node.tagName, node.properties), {
|
40
|
+
allowDangerousHtml: true,
|
41
|
+
closeSelfClosing: true
|
42
|
+
});
|
43
|
+
const endOfOpeningTagIndex = tag.indexOf('>');
|
44
|
+
const openingTag = tag.slice(0, endOfOpeningTagIndex + 1);
|
45
|
+
const closingTag = tag.slice(endOfOpeningTagIndex + 1);
|
46
|
+
return [
|
47
|
+
{ type: 'html', value: openingTag },
|
48
|
+
...state.all(node),
|
49
|
+
{ type: 'html', value: closingTag }
|
50
|
+
];
|
51
|
+
};
|
52
|
+
return handlers;
|
53
|
+
}, {});
|
54
|
+
}
|
55
|
+
_buildProcessor() {
|
56
|
+
this._processor = unified()
|
57
|
+
// Parse HTML to an abstract syntax tree (AST).
|
58
|
+
.use(rehypeParse)
|
59
|
+
// Removes `<label>` element from TODO lists.
|
60
|
+
.use(removeLabelFromCheckboxes)
|
61
|
+
// Turns HTML syntax tree into Markdown syntax tree.
|
62
|
+
.use(rehypeRemark, {
|
63
|
+
// Keeps allowed HTML tags.
|
64
|
+
handlers: this._getRawTagsHandlers()
|
65
|
+
})
|
66
|
+
// Adds support for GitHub Flavored Markdown (GFM).
|
67
|
+
.use(remarkGfm, {
|
68
|
+
singleTilde: true
|
69
|
+
})
|
70
|
+
// Replaces line breaks with `<br>` tags.
|
71
|
+
.use(remarkBreaks)
|
72
|
+
// Serializes Markdown syntax tree to Markdown string.
|
73
|
+
.use(remarkStringify, {
|
74
|
+
resourceLink: true,
|
75
|
+
emphasis: '_',
|
76
|
+
rule: '-',
|
77
|
+
handlers: {
|
78
|
+
break: () => '\n'
|
79
|
+
},
|
80
|
+
unsafe: [
|
81
|
+
{ character: '<' }
|
82
|
+
]
|
83
|
+
});
|
94
84
|
}
|
95
85
|
}
|
96
86
|
/**
|
97
|
-
*
|
87
|
+
* Removes `<label>` element from TODO lists, so that `<input>` and `text` are direct children of `<li>`.
|
98
88
|
*/
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
parse(html) {
|
105
|
-
return this._parser.turndown(html);
|
106
|
-
}
|
107
|
-
keep(elements) {
|
108
|
-
this._parser.keep(elements);
|
109
|
-
}
|
110
|
-
_createParser() {
|
111
|
-
const parser = new UpdatedTurndown({
|
112
|
-
codeBlockStyle: 'fenced',
|
113
|
-
hr: '---',
|
114
|
-
headingStyle: 'atx'
|
115
|
-
});
|
116
|
-
parser.use([
|
117
|
-
gfm,
|
118
|
-
this._todoList
|
119
|
-
]);
|
120
|
-
return parser;
|
121
|
-
}
|
122
|
-
// This is a copy of the original taskListItems rule from turndown-plugin-gfm, with minor changes.
|
123
|
-
_todoList(turndown) {
|
124
|
-
turndown.addRule('taskListItems', {
|
125
|
-
filter(node) {
|
126
|
-
return node.type === 'checkbox' &&
|
127
|
-
// Changes here as CKEditor outputs a deeper structure.
|
128
|
-
(node.parentNode.nodeName === 'LI' || node.parentNode.parentNode.nodeName === 'LI');
|
129
|
-
},
|
130
|
-
replacement(content, node) {
|
131
|
-
return (node.checked ? '[x]' : '[ ]') + ' ';
|
89
|
+
function removeLabelFromCheckboxes() {
|
90
|
+
return function (tree) {
|
91
|
+
visit(tree, 'element', (node, index, parent) => {
|
92
|
+
if (index !== null && node.tagName === 'label' && parent.type === 'element' && parent.tagName === 'li') {
|
93
|
+
parent.children.splice(index, 1, ...node.children);
|
132
94
|
}
|
133
95
|
});
|
134
|
-
}
|
96
|
+
};
|
135
97
|
}
|
package/src/index.d.ts
CHANGED
@@ -5,8 +5,9 @@
|
|
5
5
|
/**
|
6
6
|
* @module markdown-gfm
|
7
7
|
*/
|
8
|
-
export {
|
9
|
-
export {
|
10
|
-
export {
|
11
|
-
export {
|
8
|
+
export { Markdown } from './markdown.js';
|
9
|
+
export { PasteFromMarkdownExperimental } from './pastefrommarkdownexperimental.js';
|
10
|
+
export { MarkdownGfmDataProcessor } from './gfmdataprocessor.js';
|
11
|
+
export { MarkdownGfmMdToHtml } from './markdown2html/markdown2html.js';
|
12
|
+
export { MarkdownGfmHtmlToMd } from './html2markdown/html2markdown.js';
|
12
13
|
import './augmentation.js';
|
package/src/index.js
CHANGED
@@ -5,8 +5,9 @@
|
|
5
5
|
/**
|
6
6
|
* @module markdown-gfm
|
7
7
|
*/
|
8
|
-
export {
|
9
|
-
export {
|
10
|
-
export {
|
11
|
-
export {
|
8
|
+
export { Markdown } from './markdown.js';
|
9
|
+
export { PasteFromMarkdownExperimental } from './pastefrommarkdownexperimental.js';
|
10
|
+
export { MarkdownGfmDataProcessor } from './gfmdataprocessor.js';
|
11
|
+
export { MarkdownGfmMdToHtml } from './markdown2html/markdown2html.js';
|
12
|
+
export { MarkdownGfmHtmlToMd } from './html2markdown/html2markdown.js';
|
12
13
|
import './augmentation.js';
|
package/src/markdown.d.ts
CHANGED
@@ -11,7 +11,7 @@ import { Plugin, type Editor } from 'ckeditor5/src/core.js';
|
|
11
11
|
*
|
12
12
|
* For a detailed overview, check the {@glink features/markdown Markdown feature} guide.
|
13
13
|
*/
|
14
|
-
export
|
14
|
+
export declare class Markdown extends Plugin {
|
15
15
|
/**
|
16
16
|
* @inheritDoc
|
17
17
|
*/
|
package/src/markdown.js
CHANGED
@@ -6,19 +6,19 @@
|
|
6
6
|
* @module markdown-gfm/markdown
|
7
7
|
*/
|
8
8
|
import { Plugin } from 'ckeditor5/src/core.js';
|
9
|
-
import
|
9
|
+
import { MarkdownGfmDataProcessor } from './gfmdataprocessor.js';
|
10
10
|
/**
|
11
11
|
* The GitHub Flavored Markdown (GFM) plugin.
|
12
12
|
*
|
13
13
|
* For a detailed overview, check the {@glink features/markdown Markdown feature} guide.
|
14
14
|
*/
|
15
|
-
export
|
15
|
+
export class Markdown extends Plugin {
|
16
16
|
/**
|
17
17
|
* @inheritDoc
|
18
18
|
*/
|
19
19
|
constructor(editor) {
|
20
20
|
super(editor);
|
21
|
-
editor.data.processor = new
|
21
|
+
editor.data.processor = new MarkdownGfmDataProcessor(editor.data.viewDocument);
|
22
22
|
}
|
23
23
|
/**
|
24
24
|
* @inheritDoc
|
@@ -5,9 +5,8 @@
|
|
5
5
|
/**
|
6
6
|
* This is a helper class used by the {@link module:markdown-gfm/markdown Markdown feature} to convert Markdown to HTML.
|
7
7
|
*/
|
8
|
-
export declare class
|
9
|
-
private
|
10
|
-
private _options;
|
8
|
+
export declare class MarkdownGfmMdToHtml {
|
9
|
+
private _processor;
|
11
10
|
constructor();
|
12
11
|
parse(markdown: string): string;
|
13
12
|
}
|
@@ -5,45 +5,94 @@
|
|
5
5
|
/**
|
6
6
|
* @module markdown-gfm/markdown2html/markdown2html
|
7
7
|
*/
|
8
|
-
import {
|
8
|
+
import { unified } from 'unified';
|
9
|
+
import remarkGfm from 'remark-gfm';
|
10
|
+
import remarkParse from 'remark-parse';
|
11
|
+
import remarkRehype from 'remark-rehype';
|
12
|
+
import remarkBreaks from 'remark-breaks';
|
13
|
+
import rehypeStringify from 'rehype-dom-stringify';
|
14
|
+
import { visit } from 'unist-util-visit';
|
15
|
+
import { toHtml } from 'hast-util-to-html';
|
16
|
+
import { fromDom } from 'hast-util-from-dom';
|
9
17
|
/**
|
10
18
|
* This is a helper class used by the {@link module:markdown-gfm/markdown Markdown feature} to convert Markdown to HTML.
|
11
19
|
*/
|
12
|
-
export class
|
13
|
-
|
14
|
-
_options = {
|
15
|
-
gfm: true,
|
16
|
-
breaks: true,
|
17
|
-
tables: true,
|
18
|
-
xhtml: true,
|
19
|
-
headerIds: false
|
20
|
-
};
|
20
|
+
export class MarkdownGfmMdToHtml {
|
21
|
+
_processor;
|
21
22
|
constructor() {
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
// ended with one or not (see https://github.com/markedjs/marked/issues/1884 to learn why).
|
38
|
-
// This results in a redundant soft break in the model when loaded into the editor, which
|
39
|
-
// is best prevented at this stage. See https://github.com/ckeditor/ckeditor5/issues/11124.
|
40
|
-
return Object.getPrototypeOf(this).code.call(this, ...args).replace('\n</code>', '</code>');
|
41
|
-
}
|
42
|
-
}
|
43
|
-
});
|
44
|
-
this._parser = marked;
|
23
|
+
this._processor = unified()
|
24
|
+
// Parses Markdown to an abstract syntax tree (AST).
|
25
|
+
.use(remarkParse)
|
26
|
+
// Adds support for GitHub Flavored Markdown (GFM).
|
27
|
+
.use(remarkGfm, { singleTilde: true })
|
28
|
+
// Replaces line breaks with `<br>` tags.
|
29
|
+
.use(remarkBreaks)
|
30
|
+
// Turns markdown syntax tree to HTML syntax tree, ignoring embedded HTML.
|
31
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
32
|
+
// Handles HTML embedded in Markdown.
|
33
|
+
.use(rehypeDomRaw)
|
34
|
+
// Removes classes from list elements.
|
35
|
+
.use(deleteClassesFromToDoLists)
|
36
|
+
// Serializes HTML syntax tree to HTML string.
|
37
|
+
.use(rehypeStringify);
|
45
38
|
}
|
46
39
|
parse(markdown) {
|
47
|
-
return this.
|
40
|
+
return this._processor
|
41
|
+
.processSync(markdown)
|
42
|
+
.toString()
|
43
|
+
.replaceAll('\n</code>', '</code>');
|
48
44
|
}
|
49
45
|
}
|
46
|
+
/**
|
47
|
+
* Rehype plugin that improves handling of the To-do lists by removing:
|
48
|
+
* * default classes added to `<ul>`, `<ol>`, and `<li>` elements.
|
49
|
+
* * bogus space after <input type="checkbox"> because it would be preserved by ViewDomConverter as it's next to an inline object.
|
50
|
+
*/
|
51
|
+
function deleteClassesFromToDoLists() {
|
52
|
+
return (tree) => {
|
53
|
+
visit(tree, 'element', (node) => {
|
54
|
+
if (node.tagName === 'ul' || node.tagName === 'ol' || node.tagName === 'li') {
|
55
|
+
node.children = node.children.filter(child => child.type !== 'text' || !!child.value.trim());
|
56
|
+
delete node.properties.className;
|
57
|
+
}
|
58
|
+
});
|
59
|
+
};
|
60
|
+
}
|
61
|
+
/**
|
62
|
+
* Rehype plugin to parse raw HTML nodes inside Markdown. This plugin is used instead of `rehype-raw` or `rehype-stringify`,
|
63
|
+
* because those plugins rely on `parse5` DOM parser which is heavy and redundant in the browser environment where we can
|
64
|
+
* use the native DOM APIs.
|
65
|
+
*
|
66
|
+
* This plugins finds any node (root or element) whose children include `raw` nodes and reparses them like so:
|
67
|
+
* 1. Serializes its children to an HTML string.
|
68
|
+
* 2. Reparses the HTML string using a `<template>` element.
|
69
|
+
* 3. Converts each parsed DOM node back into HAST nodes.
|
70
|
+
* 4. Replaces the original children with the newly created HAST nodes.
|
71
|
+
*/
|
72
|
+
function rehypeDomRaw() {
|
73
|
+
return (tree) => {
|
74
|
+
visit(tree, ['root', 'element'], (node) => {
|
75
|
+
/* istanbul ignore next -- @preserve */
|
76
|
+
if (!isNodeRootOrElement(node)) {
|
77
|
+
return;
|
78
|
+
}
|
79
|
+
// Only act on nodes with at least one raw child.
|
80
|
+
if (!node.children.some(child => child.type === 'raw')) {
|
81
|
+
return;
|
82
|
+
}
|
83
|
+
const template = document.createElement('template');
|
84
|
+
// Serialize all children to an HTML fragment.
|
85
|
+
template.innerHTML = toHtml({ type: 'root', children: node.children }, { allowDangerousHtml: true });
|
86
|
+
// Convert each parsed DOM node back into HAST and replace the original children.
|
87
|
+
node.children = Array
|
88
|
+
.from(template.content.childNodes)
|
89
|
+
.map(domNode => fromDom(domNode));
|
90
|
+
});
|
91
|
+
};
|
92
|
+
}
|
93
|
+
/**
|
94
|
+
* Only needed for the type guard.
|
95
|
+
*/
|
96
|
+
function isNodeRootOrElement(node) {
|
97
|
+
return (node.type === 'root' || node.type === 'element') && node.children;
|
98
|
+
}
|
@@ -12,7 +12,7 @@ import { ClipboardPipeline } from 'ckeditor5/src/clipboard.js';
|
|
12
12
|
*
|
13
13
|
* For a detailed overview, check the {@glink features/pasting/paste-markdown Paste Markdown feature} guide.
|
14
14
|
*/
|
15
|
-
export
|
15
|
+
export declare class PasteFromMarkdownExperimental extends Plugin {
|
16
16
|
/**
|
17
17
|
* @internal
|
18
18
|
*/
|
@@ -7,14 +7,14 @@
|
|
7
7
|
*/
|
8
8
|
import { Plugin } from 'ckeditor5/src/core.js';
|
9
9
|
import { ClipboardPipeline } from 'ckeditor5/src/clipboard.js';
|
10
|
-
import
|
10
|
+
import { MarkdownGfmDataProcessor } from './gfmdataprocessor.js';
|
11
11
|
const ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS = ['SPAN', 'BR', 'PRE', 'CODE'];
|
12
12
|
/**
|
13
13
|
* The GitHub Flavored Markdown (GFM) paste plugin.
|
14
14
|
*
|
15
15
|
* For a detailed overview, check the {@glink features/pasting/paste-markdown Paste Markdown feature} guide.
|
16
16
|
*/
|
17
|
-
export
|
17
|
+
export class PasteFromMarkdownExperimental extends Plugin {
|
18
18
|
/**
|
19
19
|
* @internal
|
20
20
|
*/
|
@@ -24,7 +24,7 @@ export default class PasteFromMarkdownExperimental extends Plugin {
|
|
24
24
|
*/
|
25
25
|
constructor(editor) {
|
26
26
|
super(editor);
|
27
|
-
this._gfmDataProcessor = new
|
27
|
+
this._gfmDataProcessor = new MarkdownGfmDataProcessor(editor.data.viewDocument);
|
28
28
|
}
|
29
29
|
/**
|
30
30
|
* @inheritDoc
|