diff --git a/src/blocks/placeholder.lisp b/src/blocks/placeholder.lisp
new file mode 100644
index 0000000..d0c92b1
--- /dev/null
+++ b/src/blocks/placeholder.lisp
@@ -0,0 +1,12 @@
+(uiop:define-package #:reblocks-text-editor/blocks/placeholder
+ (:use #:cl)
+ (:import-from #:common-doc
+ #:define-node))
+(in-package #:reblocks-text-editor/blocks/placeholder)
+
+
+(define-node placeholder (common-doc:document-node)
+ ()
+ (:tag-name "placeholder")
+ (:documentation "This node will be replaced by a real one with the same reference."))
+
diff --git a/src/document/ops.lisp b/src/document/ops.lisp
index c4f1959..6db4e20 100644
--- a/src/document/ops.lisp
+++ b/src/document/ops.lisp
@@ -21,6 +21,10 @@
#:length<
#:length>
#:slice)
+ (:import-from #:common-doc.ops
+ #:with-document-traversal)
+ (:import-from #:reblocks-text-editor/blocks/placeholder
+ #:placeholder)
(:local-nicknames (#:dom #:reblocks-text-editor/dom/ops)))
(in-package #:reblocks-text-editor/document/ops)
@@ -180,6 +184,8 @@
(error "Probably we should't get here.")))))
(common-doc:image
;; Just skip it
+ (setf last-visited-node-content-length
+ 0)
node))))
(recursive-find node)
@@ -364,7 +370,7 @@
(text-after-cursor (subseq plain-text (min cursor-position
(length plain-text))))
(new-paragraph (prepare-new-content document text-after-cursor)))
-
+
(cond
((and
;; When user presses Option + Enter, we want to stay within
@@ -642,6 +648,7 @@
"Parses text-nodes inside the tree as a scriba documents."
(flet ((parse (node depth)
(declare (ignore depth))
+ (log:error "TRAEA" node)
(typecase node
(common-doc:text-node
(let* ((text (common-doc:text node))
@@ -655,15 +662,21 @@
((length= 0 content)
node)
((length= 1 content)
-
- ;; If there we were able to split text into separate
- ;; nodes, then return them as a single content-node
- (let ((content-node (first content)))
- (if (and (typep content-node 'node-with-children)
- (serapeum:length< 1 (common-doc:children content-node)))
- content-node
- ;; otherwise just return original text node
- node)))
+
+ ;; Previously here was used this code, but
+ ;; I decided just return the first item
+ ;; because sometimes content might be a single @placeholder
+ ;; node when there is image or something like that is on the line.
+ ;;
+ ;; ;; If there we were able to split text into separate
+ ;; ;; nodes, then return them as a single content-node
+ ;; (let ((content-node (first content)))
+ ;; (if (and (typep content-node 'node-with-children)
+ ;; (serapeum:length< 1 (common-doc:children content-node)))
+ ;; content-node
+ ;; ;; otherwise just return original text node
+ ;; node))
+ (first content))
(t
(error "Why does content has more than one node? This is unexpected.")))))
(t node))))
@@ -682,6 +695,35 @@
;; (map-document root-node #'parse)))
+(defun replace-placeholders (editable-document root-node)
+ "Replaces PLACEHOLDER nodes inside the nodes tree starting from ROOT-NODE.
+
+ Usually ROOT-NODE will point to a new nodes, created from the current
+ paragraph, modified by a user.
+
+ Whereas EDITABLE-DOCUMENT is a full document. Replacements are
+ collected from the EDITABLE-DOCUMENT and matched to a placeholder by reference."
+
+ (let ((id-to-node (make-hash-table :test 'equal)))
+ (with-document-traversal (editable-document node)
+ (setf (gethash (common-doc:reference node) id-to-node)
+ node))
+
+ (map-document
+ root-node
+ (lambda (node depth)
+ (declare (ignore depth))
+ (cond
+ ((typep node 'placeholder)
+ (let* ((id (common-doc:reference node))
+ (replacement (gethash id id-to-node)))
+ (unless replacement
+ (error "Unable to find node to replace placeholder with id: ~S" id))
+ replacement))
+ (t
+ node))))))
+
+
(defun prepare-new-content (document text)
(let ((node
(cond
@@ -689,8 +731,10 @@
(common-doc:make-code-block nil
(common-doc:make-text +zero-width-space+)))
(t
- (parse-scriba-nodes
- (from-markdown text))))))
+ (replace-placeholders
+ document
+ (parse-scriba-nodes
+ (from-markdown text)))))))
(add-reference-ids document
:to-node node)))
@@ -830,7 +874,8 @@
caret-position)
(cond
(node
- (dom::move-cursor node new-caret-position :from-the-end from-the-end)
+ (dom::move-cursor node new-caret-position
+ :from-the-end from-the-end)
(setf (caret-position document)
(list node new-caret-position)))
(t
diff --git a/src/frontend/js.lisp b/src/frontend/js.lisp
index bcbf37b..4f7b06c 100644
--- a/src/frontend/js.lisp
+++ b/src/frontend/js.lisp
@@ -108,6 +108,17 @@
(element
(chain document
(get-element-by-id element-id)))
+ (element-non-editable
+ (chain element
+ class-list
+ (contains "noneditable")))
+ (element-to-select (if element-non-editable
+ element
+ ;; Selecting the first text node
+ ;; inside the element:
+ (@ element
+ child-nodes
+ 0)))
(range (chain document (create-range)))
(sel (chain window (get-selection))))
@@ -115,9 +126,7 @@
(element
(chain range
(set-start
- (@ element
- child-nodes
- 0)
+ element-to-select
(if from-the-end
(- (chain element
inner-text
diff --git a/src/html.lisp b/src/html.lisp
index 795d2d3..bfc182a 100644
--- a/src/html.lisp
+++ b/src/html.lisp
@@ -51,18 +51,19 @@
(defmethod to-html ((node common-doc:content-node))
(let* ((node-type (class-of node))
- (tag (or (loop for key being the hash-key of common-doc::*registry*
- using (hash-value value)
- when (eql value node-type)
- do (return (or (alexandria:assoc-value *common-doc-key-to-html-tag* key
- :test #'string-equal)
- key)))
- "div")))
+ (tag (loop for key being the hash-key of common-doc::*registry*
+ using (hash-value value)
+ when (eql value node-type)
+ do (return (or (alexandria:assoc-value *common-doc-key-to-html-tag* key
+ :test #'string-equal)
+ key)))))
(reblocks/html:with-html
- ;; TODO: add a check that there is no nodes prohibited as a span's content
- (:tag :name tag
- :id (common-doc:reference node)
- (to-html (common-doc:children node))))))
+ (if tag
+ (:tag :name tag
+ :id (common-doc:reference node)
+ (to-html (common-doc:children node)))
+ ;; by default, content node has no HTML representation
+ (to-html (common-doc:children node))))))
(defmethod to-html ((node common-doc:unordered-list))
(reblocks/html:with-html
@@ -116,14 +117,6 @@
(common-doc:children node))))))))
-(defmethod to-html ((node common-doc:image))
- (reblocks/html:with-html
- (:img :id (common-doc:reference node)
- :class (html-class node)
- :src (common-doc:source node)
- :title (common-doc:description node))))
-
-
(defmethod to-html ((node commondoc-markdown/raw-html:raw-inline-html))
(reblocks/html:with-html
(:raw (commondoc-markdown/raw-html:html node))))
diff --git a/src/html/image.lisp b/src/html/image.lisp
new file mode 100644
index 0000000..c16d735
--- /dev/null
+++ b/src/html/image.lisp
@@ -0,0 +1,20 @@
+(uiop:define-package #:reblocks-text-editor/html/image
+ (:use #:cl)
+ (:import-from #:reblocks-text-editor/html
+ #:to-html
+ #:html-class))
+(in-package #:reblocks-text-editor/html/image)
+
+
+(defmethod to-html ((node common-doc:image))
+ (reblocks/html:with-html
+ (let ((classes (remove nil
+ (list "noneditable"
+ (html-class node)))))
+ (:img :id (common-doc:reference node)
+ :class (format nil "~{~A~^ ~}"
+ classes)
+ :src (when (slot-boundp node 'common-doc:source)
+ (common-doc:source node))
+ :title (when (slot-boundp node 'common-doc:description)
+ (common-doc:description node))))))
diff --git a/src/utils/markdown.lisp b/src/utils/markdown.lisp
index c2e6eb1..362ca22 100644
--- a/src/utils/markdown.lisp
+++ b/src/utils/markdown.lisp
@@ -31,18 +31,57 @@
result)))
+(defun replace-scriba-nodes-with-tmp-placeholders (text)
+ (let ((current-id 0)
+ (pieces (make-hash-table :test 'equal)))
+ (values
+ (cl-ppcre:regex-replace-all "@[^ ]+(\\[[^]]*\\])?\\([^)]*\\)" text
+ (lambda (text start end match-start match-end &rest rest)
+ (declare (ignore start end rest))
+ (let* ((matched (subseq text match-start match-end))
+ (id (incf current-id))
+ (replacement (format nil "SCRIBA-NODE-~A" id)))
+ (setf (gethash replacement pieces)
+ matched)
+ replacement)))
+ pieces)))
+
+
+(defun replace-placeholders-with-scriba-nodes (text pieces)
+ "Makes opposite to REPLACE-SCRIBA-NODES-WITH-TMP-PLACEHOLDERS."
+ (cl-ppcre:regex-replace-all "SCRIBA-NODE-[0-9]+" text
+ (lambda (text start end match-start match-end &rest rest)
+ (declare (ignore start end rest))
+ (let* ((matched (subseq text match-start match-end))
+ (scriba-node (gethash matched pieces)))
+ (unless scriba-node
+ (error "Unable to find Scriba node for placeholder ~S." matched))
+ scriba-node))))
+
+
(defun from-markdown (text)
- (let ((node (common-doc.format:parse-document (make-instance 'commondoc-markdown:markdown)
- text)))
- ;; This is a workaround for a bug inside commondoc-markdown or 3bmd
- ;; When it parses an empty text, it returns an empty CONTENT-NODE,
- ;; But we need a PARAGRAPH.
- (cond
- ((and (typep node 'common-doc:content-node)
- (null (common-doc:children node)))
- (common-doc:make-paragraph
- (list
- ;; Without this hack with ​
- ;; we'll be unable to put cursor into a new paragraph :(
- (common-doc:make-text reblocks-text-editor/utils/text::+zero-width-space+))))
- (t node))))
+ ;; We need this hack with Scriba nodes replacement because
+ ;; otherwise markdown parser will replace nodes like @some[ref=dsd]() with
+ ;; a web-link.
+ (multiple-value-bind (text-without-scriba-nodes scriba-nodes)
+ (replace-scriba-nodes-with-tmp-placeholders text)
+ (let ((node (common-doc.format:parse-document (make-instance 'commondoc-markdown:markdown)
+ text-without-scriba-nodes)))
+ (common-doc.ops:with-document-traversal (node current-node)
+ (when (typep current-node 'common-doc:text-node)
+ (let ((text (common-doc:text current-node)))
+ (setf (common-doc:text current-node)
+ (replace-placeholders-with-scriba-nodes text scriba-nodes)))))
+
+ ;; This is a workaround for a bug inside commondoc-markdown or 3bmd
+ ;; When it parses an empty text, it returns an empty CONTENT-NODE,
+ ;; But we need a PARAGRAPH.
+ (cond
+ ((and (typep node 'common-doc:content-node)
+ (null (common-doc:children node)))
+ (common-doc:make-paragraph
+ (list
+ ;; Without this hack with ​
+ ;; we'll be unable to put cursor into a new paragraph :(
+ (common-doc:make-text reblocks-text-editor/utils/text::+zero-width-space+))))
+ (t node)))))
diff --git a/src/utils/text.lisp b/src/utils/text.lisp
index 31b0945..69b9b00 100644
--- a/src/utils/text.lisp
+++ b/src/utils/text.lisp
@@ -25,7 +25,45 @@
""))))
+(defun %remove-tags (html-string)
+ (with-output-to-string (s)
+ (plump:traverse (plump:parse html-string)
+ (lambda (node)
+ (let* ((class-attr (when (plump:element-p node)
+ (plump:attribute node "class")))
+ (id (when (plump:element-p node)
+ (plump:attribute node "id")))
+ (classes (when class-attr
+ (str:split #\Space class-attr)) ))
+ (cond
+ ((plump:text-node-p node)
+ (write-string (plump:text node) s))
+ ((and (member "noneditable" classes
+ :test #'string=)
+ id)
+ (format s "@placeholder[ref=~A]()"
+ id))))))))
+
+
(defun remove-html-tags (html-string &key (remove-new-lines t))
+ (let* ((result (%remove-tags html-string)
+ ;; (cl-ppcre:regex-replace-all "<[^>]+>" html-string
+ ;; "")
+ )
+ (result (remove-zero-spaces-unless-string-is-empty result))
+ ;; TODO: check if we still need this:
+ (result (plump:decode-entities result))
+ (result (if remove-new-lines
+ ;; For some strange reason sometimes browser starts
+ ;; passing newlines even inside span elements. Why? Don't know.
+ ;; Thus by default we are removing new lines.
+ ;; However, when processing content of code blocks
+ ;; it is useful to keep newlines.
+ (str:replace-all '(#\Newline) "" result)
+ result)))
+ result))
+
+(defun remove-html-tags-old (html-string &key (remove-new-lines t))
(let* ((result (cl-ppcre:regex-replace-all "<[^>]+>" html-string
""))
(result (remove-zero-spaces-unless-string-is-empty result))
diff --git a/t/utils/text.lisp b/t/utils/text.lisp
index 81ecf3d..c9b2488 100644
--- a/t/utils/text.lisp
+++ b/t/utils/text.lisp
@@ -4,6 +4,7 @@
#:ok
#:deftest)
(:import-from #:reblocks-text-editor/utils/text
+ #:remove-html-tags
#:caret-position-from-beginning-of-the-line
#:move-caret-on-the-next-line))
(in-package #:reblocks-text-editor-tests/utils/text)
@@ -39,3 +40,17 @@ second line
middle line
last line" 17)))
(ok (= pos 3))))
+
+
+
+(deftest test-remove-html-tags-1
+ (let ((result (remove-html-tags "Text other"))
+ (expected "Text other"))
+ (ok (equal result expected))))
+
+
+(deftest test-remove-html-tags-2
+ "It should replace noneditable nodes with a Scriba placeholder."
+ (let ((result (remove-html-tags "
other"))
+ (expected "@placeholder[ref=some-ref]() other"))
+ (ok (equal result expected))))