diff --git a/frontend/src/utils/markdownParser.js b/frontend/src/utils/markdownParser.js
index a5b10ae7..493bcb47 100644
--- a/frontend/src/utils/markdownParser.js
+++ b/frontend/src/utils/markdownParser.js
@@ -34,7 +34,9 @@ export class Markdown {
}
static get pasteConfig() {
- return { tags: ['P'] }
+ return {
+ tags: ['P'],
+ }
}
render() {
@@ -52,11 +54,277 @@ export class Markdown {
this._togglePlaceholder()
)
this.wrapper.addEventListener('keydown', (e) => this._onKeyDown(e))
+ this.wrapper.addEventListener(
+ 'paste',
+ (e) => this._onNativePaste(e),
+ true
+ )
}
return this.wrapper
}
+ _onNativePaste(event) {
+ const clipboardData = event.clipboardData || window.clipboardData
+ if (!clipboardData) return
+
+ const pastedText = clipboardData.getData('text/plain')
+
+ if (pastedText && this._looksLikeMarkdown(pastedText)) {
+ event.preventDefault()
+ event.stopPropagation()
+ event.stopImmediatePropagation()
+
+ this._insertMarkdownAsBlocks(pastedText)
+ }
+ }
+
+ _looksLikeMarkdown(text) {
+ const markdownPatterns = [
+ /^#{1,6}\s+/m,
+ /^[\-\*]\s+/m,
+ /^\d+\.\s+/m,
+ /```[\s\S]*```/,
+ ]
+
+ return markdownPatterns.some((pattern) => pattern.test(text))
+ }
+
+ async _insertMarkdownAsBlocks(markdown) {
+ const blocks = this._parseMarkdownToBlocks(markdown)
+
+ if (blocks.length === 0) return
+
+ const currentIndex = this.api.blocks.getCurrentBlockIndex()
+
+ for (let i = 0; i < blocks.length; i++) {
+ try {
+ await this.api.blocks.insert(
+ blocks[i].type,
+ blocks[i].data,
+ {},
+ currentIndex + i,
+ false
+ )
+ } catch (error) {
+ console.error('Failed to insert block:', blocks[i], error)
+ }
+ }
+
+ try {
+ await this.api.blocks.delete(currentIndex + blocks.length)
+ } catch (error) {
+ console.error('Failed to delete original block:', error)
+ }
+
+ setTimeout(() => {
+ this.api.caret.setToBlock(currentIndex, 'end')
+ }, 100)
+ }
+
+ _parseMarkdownToBlocks(markdown) {
+ const lines = markdown.split('\n')
+ const blocks = []
+ let i = 0
+
+ while (i < lines.length) {
+ const line = lines[i]
+
+ if (line.trim() === '') {
+ i++
+ continue
+ }
+
+ if (line.trim().startsWith('```')) {
+ const codeBlock = this._parseCodeBlock(lines, i)
+ blocks.push(codeBlock.block)
+ i = codeBlock.nextIndex
+ continue
+ }
+
+ if (/^#{1,6}\s+/.test(line)) {
+ blocks.push(this._parseHeading(line))
+ i++
+ continue
+ }
+
+ if (/^[\s]*[-*+]\s+/.test(line)) {
+ const listBlock = this._parseUnorderedList(lines, i)
+ blocks.push(listBlock.block)
+ i = listBlock.nextIndex
+ continue
+ }
+
+ if (/^[\s]*(\d+)\.\s+/.test(line)) {
+ const listBlock = this._parseOrderedList(lines, i)
+ blocks.push(listBlock.block)
+ i = listBlock.nextIndex
+ continue
+ }
+
+ blocks.push({
+ type: 'paragraph',
+ data: { text: this._parseInlineMarkdown(line) },
+ })
+ i++
+ }
+
+ return blocks
+ }
+
+ _parseHeading(line) {
+ const match = line.match(/^(#{1,6})\s+(.*)$/)
+ const level = match[1].length
+ const text = match[2]
+
+ return {
+ type: 'header',
+ data: {
+ text: this._parseInlineMarkdown(text),
+ level: level,
+ },
+ }
+ }
+
+ _parseUnorderedList(lines, startIndex) {
+ const items = []
+ let i = startIndex
+
+ while (i < lines.length) {
+ const line = lines[i]
+
+ if (/^[\s]*[-*+]\s+/.test(line)) {
+ const text = line.replace(/^[\s]*[-*+]\s+/, '')
+ items.push({
+ content: this._parseInlineMarkdown(text),
+ items: [],
+ })
+ i++
+ } else if (line.trim() === '') {
+ i++
+ if (i < lines.length && /^[\s]*[-*+]\s+/.test(lines[i])) {
+ continue
+ } else {
+ break
+ }
+ } else {
+ break
+ }
+ }
+
+ return {
+ block: {
+ type: 'list',
+ data: {
+ style: 'unordered',
+ items: items,
+ },
+ },
+ nextIndex: i,
+ }
+ }
+
+ _parseOrderedList(lines, startIndex) {
+ const items = []
+ let i = startIndex
+
+ while (i < lines.length) {
+ const line = lines[i]
+
+ const match = line.match(/^[\s]*(\d+)\.\s+(.*)$/)
+
+ if (match) {
+ const number = match[1]
+ const text = match[2]
+
+ if (number === '1') {
+ if (items.length > 0) {
+ break
+ }
+ }
+
+ items.push({
+ content: this._parseInlineMarkdown(text),
+ items: [],
+ })
+ i++
+ } else if (line.trim() === '') {
+ i++
+ if (i < lines.length && /^[\s]*(\d+)\.\s+/.test(lines[i])) {
+ continue
+ } else {
+ break
+ }
+ } else {
+ break
+ }
+ }
+
+ return {
+ block: {
+ type: 'list',
+ data: {
+ style: 'ordered',
+ items: items,
+ },
+ },
+ nextIndex: i,
+ }
+ }
+
+ _parseCodeBlock(lines, startIndex) {
+ let i = startIndex + 1
+ const codeLines = []
+ let language = lines[startIndex].trim().substring(3).trim()
+
+ while (i < lines.length) {
+ if (lines[i].trim().startsWith('```')) {
+ i++
+ break
+ }
+ codeLines.push(lines[i])
+ i++
+ }
+
+ return {
+ block: {
+ type: 'codeBox',
+ data: {
+ code: codeLines.join('\n'),
+ language: language || 'plaintext',
+ },
+ },
+ nextIndex: i,
+ }
+ }
+
+ _parseInlineMarkdown(text) {
+ if (!text) return ''
+
+ let html = this._escapeHtml(text)
+
+ html = html.replace(/`([^`]+)`/g, '$1')
+
+ html = html.replace(/\*\*([^\*\n]+?)\*\*/g, '$1')
+ html = html.replace(/__([^_\n]+?)__/g, '$1')
+
+ html = html.replace(/\*([^\*\n]+?)\*/g, '$1')
+ html = html.replace(/(?$1')
+
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+
+ return html
+ }
+
+ _escapeHtml(text) {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ }
+
_togglePlaceholder() {
const blocks = document.querySelectorAll(
'.cdx-block.ce-paragraph[data-placeholder]'
diff --git a/lms/lms/utils.py b/lms/lms/utils.py
index 88bc96d0..c5fc4acf 100644
--- a/lms/lms/utils.py
+++ b/lms/lms/utils.py
@@ -8,7 +8,6 @@ from frappe import _
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
from frappe.desk.notifications import extract_mentions
-from frappe.pulse.utils import get_frappe_version
from frappe.rate_limiter import rate_limit
from frappe.utils import (
add_months,
@@ -17,6 +16,7 @@ from frappe.utils import (
fmt_money,
format_datetime,
get_datetime,
+ get_frappe_version,
get_fullname,
get_time_str,
getdate,