@ckeditor/ckeditor5-markdown-gfm 0.0.0-internal-20241017.0

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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module markdown-gfm/pastefrommarkdownexperimental
7
+ */
8
+ import { Plugin } from 'ckeditor5/src/core.js';
9
+ import { ClipboardPipeline } from 'ckeditor5/src/clipboard.js';
10
+ import GFMDataProcessor from './gfmdataprocessor.js';
11
+ const ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS = ['SPAN', 'BR', 'PRE', 'CODE'];
12
+ /**
13
+ * The GitHub Flavored Markdown (GFM) paste plugin.
14
+ *
15
+ * For a detailed overview, check the {@glink features/pasting/paste-markdown Paste Markdown feature} guide.
16
+ */
17
+ export default class PasteFromMarkdownExperimental extends Plugin {
18
+ /**
19
+ * @inheritDoc
20
+ */
21
+ constructor(editor) {
22
+ super(editor);
23
+ this._gfmDataProcessor = new GFMDataProcessor(editor.data.viewDocument);
24
+ }
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ static get pluginName() {
29
+ return 'PasteFromMarkdownExperimental';
30
+ }
31
+ /**
32
+ * @inheritDoc
33
+ */
34
+ static get isOfficialPlugin() {
35
+ return true;
36
+ }
37
+ /**
38
+ * @inheritDoc
39
+ */
40
+ static get requires() {
41
+ return [ClipboardPipeline];
42
+ }
43
+ /**
44
+ * @inheritDoc
45
+ */
46
+ init() {
47
+ const editor = this.editor;
48
+ const view = editor.editing.view;
49
+ const viewDocument = view.document;
50
+ const clipboardPipeline = editor.plugins.get('ClipboardPipeline');
51
+ let shiftPressed = false;
52
+ this.listenTo(viewDocument, 'keydown', (evt, data) => {
53
+ shiftPressed = data.shiftKey;
54
+ });
55
+ this.listenTo(clipboardPipeline, 'inputTransformation', (evt, data) => {
56
+ if (shiftPressed) {
57
+ return;
58
+ }
59
+ const dataAsTextHtml = data.dataTransfer.getData('text/html');
60
+ if (!dataAsTextHtml) {
61
+ const dataAsTextPlain = data.dataTransfer.getData('text/plain');
62
+ data.content = this._gfmDataProcessor.toView(dataAsTextPlain);
63
+ return;
64
+ }
65
+ const markdownFromHtml = this._parseMarkdownFromHtml(dataAsTextHtml);
66
+ if (markdownFromHtml) {
67
+ data.content = this._gfmDataProcessor.toView(markdownFromHtml);
68
+ }
69
+ });
70
+ }
71
+ /**
72
+ * Determines if the code copied from a website in the `text/html` type can be parsed as Markdown.
73
+ * It removes any OS-specific HTML tags, for example, <meta> on macOS and <!--StartFragment--> on Windows.
74
+ * Then removes a single wrapper HTML tag or wrappers for sibling tags, and if there are no more tags left,
75
+ * returns the remaining text. Returns null if there are any remaining HTML tags detected.
76
+ *
77
+ * @param htmlString Clipboard content in the `text/html` type format.
78
+ */
79
+ _parseMarkdownFromHtml(htmlString) {
80
+ const withoutOsSpecificTags = this._removeOsSpecificTags(htmlString);
81
+ if (!this._containsOnlyAllowedFirstLevelTags(withoutOsSpecificTags)) {
82
+ return null;
83
+ }
84
+ const withoutWrapperTag = this._removeFirstLevelWrapperTagsAndBrs(withoutOsSpecificTags);
85
+ if (this._containsAnyRemainingHtmlTags(withoutWrapperTag)) {
86
+ return null;
87
+ }
88
+ return this._replaceHtmlReservedEntitiesWithCharacters(withoutWrapperTag);
89
+ }
90
+ /**
91
+ * Removes OS-specific tags.
92
+ *
93
+ * @param htmlString Clipboard content in the `text/html` type format.
94
+ */
95
+ _removeOsSpecificTags(htmlString) {
96
+ // Removing the <meta> tag present on Mac.
97
+ const withoutMetaTag = htmlString.replace(/^<meta\b[^>]*>/, '').trim();
98
+ // Removing the <html> tag present on Windows.
99
+ const withoutHtmlTag = withoutMetaTag.replace(/^<html>/, '').replace(/<\/html>$/, '').trim();
100
+ // Removing the <body> tag present on Windows.
101
+ const withoutBodyTag = withoutHtmlTag.replace(/^<body>/, '').replace(/<\/body>$/, '').trim();
102
+ // Removing the <!--StartFragment--> tag present on Windows.
103
+ return withoutBodyTag.replace(/^<!--StartFragment-->/, '').replace(/<!--EndFragment-->$/, '').trim();
104
+ }
105
+ /**
106
+ * If the input HTML string contains any first-level formatting tags
107
+ * like <b>, <strong>, or <i>, we should not treat it as Markdown.
108
+ *
109
+ * @param htmlString Clipboard content.
110
+ */
111
+ _containsOnlyAllowedFirstLevelTags(htmlString) {
112
+ const parser = new DOMParser();
113
+ const { body: tempElement } = parser.parseFromString(htmlString, 'text/html');
114
+ const tagNames = Array.from(tempElement.children).map(el => el.tagName);
115
+ return tagNames.every(el => ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS.includes(el));
116
+ }
117
+ /**
118
+ * Removes multiple HTML wrapper tags from a list of sibling HTML tags.
119
+ *
120
+ * @param htmlString Clipboard content without any OS-specific tags.
121
+ */
122
+ _removeFirstLevelWrapperTagsAndBrs(htmlString) {
123
+ const parser = new DOMParser();
124
+ const { body: tempElement } = parser.parseFromString(htmlString, 'text/html');
125
+ const brElements = tempElement.querySelectorAll('br');
126
+ for (const br of brElements) {
127
+ br.replaceWith('\n');
128
+ }
129
+ const outerElements = tempElement.querySelectorAll(':scope > *');
130
+ for (const element of outerElements) {
131
+ const elementClone = element.cloneNode(true);
132
+ element.replaceWith(...elementClone.childNodes);
133
+ }
134
+ return tempElement.innerHTML;
135
+ }
136
+ /**
137
+ * Determines if a string contains any HTML tags.
138
+ */
139
+ _containsAnyRemainingHtmlTags(str) {
140
+ return str.includes('<');
141
+ }
142
+ /**
143
+ * Replaces the reserved HTML entities with the actual characters.
144
+ *
145
+ * @param htmlString Clipboard content without any tags.
146
+ */
147
+ _replaceHtmlReservedEntitiesWithCharacters(htmlString) {
148
+ return htmlString
149
+ .replace(/&gt;/g, '>')
150
+ .replace(/&lt;/g, '<')
151
+ .replace(/&nbsp;/g, ' ');
152
+ }
153
+ }