@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.
@@ -5,131 +5,93 @@
5
5
  /**
6
6
  * @module markdown-gfm/html2markdown/html2markdown
7
7
  */
8
- import Turndown from 'turndown';
9
- // There no avaialble types for 'turndown-plugin-gfm' module and it's not worth to generate them on our own.
10
- /* eslint-disable @typescript-eslint/ban-ts-comment */
11
- // @ts-ignore
12
- import { gfm } from 'turndown-plugin-gfm';
13
- const autolinkRegex = /* #__PURE__ */ new RegExp(
14
- // Prefix.
15
- /\b(?:(?:https?|ftp):\/\/|www\.)/.source +
16
- // Domain name.
17
- /(?![-_])(?:[-_a-z0-9\u00a1-\uffff]{1,63}\.)+(?:[a-z\u00a1-\uffff]{2,63})/.source +
18
- // The rest.
19
- /(?:[^\s<>]*)/.source, 'gi');
20
- class UpdatedTurndown extends Turndown {
21
- escape(string) {
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
- * Trimming end of link.
51
- * https://github.github.com/gfm/#autolinks-extension-
52
- */
53
- *_matchAutolink(string) {
54
- for (const match of string.matchAll(autolinkRegex)) {
55
- const matched = match[0];
56
- const length = this._autolinkFindEnd(matched);
57
- yield Object.assign([matched.substring(0, length)], { index: match.index });
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 the new length of the link (after it would trim trailing characters).
34
+ * Returns handlers for raw HTML tags that should be kept in the Markdown output.
63
35
  */
64
- _autolinkFindEnd(string) {
65
- let length = string.length;
66
- while (length > 0) {
67
- const char = string[length - 1];
68
- if ('?!.,:*_~\'"'.includes(char)) {
69
- length--;
70
- }
71
- else if (char == ')') {
72
- let openBrackets = 0;
73
- for (let i = 0; i < length; i++) {
74
- if (string[i] == '(') {
75
- openBrackets++;
76
- }
77
- else if (string[i] == ')') {
78
- openBrackets--;
79
- }
80
- }
81
- // If there is fewer opening brackets then closing ones we should remove a closing bracket.
82
- if (openBrackets < 0) {
83
- length--;
84
- }
85
- else {
86
- break;
87
- }
88
- }
89
- else {
90
- break;
91
- }
92
- }
93
- return length;
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
- * This is a helper class used by the {@link module:markdown-gfm/markdown Markdown feature} to convert HTML to Markdown.
87
+ * Removes `<label>` element from TODO lists, so that `<input>` and `text` are direct children of `<li>`.
98
88
  */
99
- export class HtmlToMarkdown {
100
- _parser;
101
- constructor() {
102
- this._parser = this._createParser();
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 { default as Markdown } from './markdown.js';
9
- export { default as PasteFromMarkdownExperimental } from './pastefrommarkdownexperimental.js';
10
- export { default as GFMDataProcessor } from './gfmdataprocessor.js';
11
- export { MarkdownToHtml } from './markdown2html/markdown2html.js';
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 { default as Markdown } from './markdown.js';
9
- export { default as PasteFromMarkdownExperimental } from './pastefrommarkdownexperimental.js';
10
- export { default as GFMDataProcessor } from './gfmdataprocessor.js';
11
- export { MarkdownToHtml } from './markdown2html/markdown2html.js';
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 default class Markdown extends Plugin {
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 GFMDataProcessor from './gfmdataprocessor.js';
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 default class Markdown extends Plugin {
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 GFMDataProcessor(editor.data.viewDocument);
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 MarkdownToHtml {
9
- private _parser;
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 { marked } from 'marked';
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 MarkdownToHtml {
13
- _parser;
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
- // Overrides.
23
- marked.use({
24
- tokenizer: {
25
- // Disable the autolink rule in the lexer.
26
- autolink: () => null,
27
- url: () => null
28
- },
29
- renderer: {
30
- checkbox(...args) {
31
- // Remove bogus space after <input type="checkbox"> because it would be preserved
32
- // by DomConverter as it's next to an inline object.
33
- return Object.getPrototypeOf(this).checkbox.call(this, ...args).trimRight();
34
- },
35
- code(...args) {
36
- // Since marked v1.2.8, every <code> gets a trailing "\n" whether it originally
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._parser.parse(markdown, this._options);
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 default class PasteFromMarkdownExperimental extends Plugin {
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 GFMDataProcessor from './gfmdataprocessor.js';
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 default class PasteFromMarkdownExperimental extends Plugin {
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 GFMDataProcessor(editor.data.viewDocument);
27
+ this._gfmDataProcessor = new MarkdownGfmDataProcessor(editor.data.viewDocument);
28
28
  }
29
29
  /**
30
30
  * @inheritDoc