Copy and paste html support

pull/645/head
Tienson Qin 2020-07-05 18:18:14 +08:00
parent ff06116b7f
commit 2a2d6f5abb
9 changed files with 358 additions and 16 deletions

View File

@ -16,6 +16,7 @@
com.andrewmcveigh/cljs-time {:mvn/version "0.5.2"}
cljs-drag-n-drop {:mvn/version "0.1.0"}
borkdude/sci {:mvn/version "0.0.13-alpha.27"}
hickory {:mvn/version "0.7.1"}
}
:aliases {:cljs {:extra-paths ["src/dev-cljs/"]

View File

@ -28,3 +28,4 @@ dummy.fillText = function() {};
dummy.beginPath = function() {};
dummy.arc = function() {};
dummy.fill = function() {};
dummy.getData = function() {};

View File

@ -27,7 +27,8 @@
[cljs-time.coerce :as tc]
[cljs-drag-n-drop.core :as dnd]
[frontend.search :as search]
["/frontend/utils" :as utils]))
["/frontend/utils" :as utils]
[frontend.extensions.html-parser :as html-parser]))
;; TODO: refactor the state, it is already too complex.
(defonce *last-edit-heading (atom nil))
@ -556,6 +557,14 @@
(set-last-edit-heading! (:heading/uuid heading) value)
(handler/save-heading-if-changed! heading new-value)))
(defn- append-paste-doc!
[format event]
(when-let [html (util/get-clipboard-as-html event)]
(let [doc-text (html-parser/parse format html)]
(when-not (string/blank? doc-text)
(util/stop event)
(state/append-current-edit-content! doc-text)))))
(rum/defc box < rum/reactive
(mixins/event-mixin
(fn [state]
@ -563,6 +572,8 @@
input-id id
input (gdom/getElement input-id)
repo (:heading/repo heading)]
(.addEventListener input "paste" (fn [event]
(append-paste-doc! format event)))
(mixins/hide-when-esc-or-outside
state
:on-hide
@ -736,9 +747,12 @@
:will-unmount (fn [state]
(let [{:keys [id value format heading repo dummy?]} (get-state state)]
(when-let [input (gdom/getElement id)]
(.removeEventListener input "paste" (fn [event]
(append-paste-doc! format event)))
(dnd/unsubscribe!
input
:upload-images))
:upload-images)
)
(clear-when-saved!))
state)}
[content {:keys [on-hide dummy? node format heading]

View File

@ -282,12 +282,18 @@
(let [{:keys [url label title]} link]
(match url
["Search" s]
(case (first s)
\#
(cond
(= \# (first s))
(->elem :a {:href (str "#" (anchor-link (subs s 1)))} (map-inline config label))
;; FIXME: same headline, see more https://orgmode.org/manual/Internal-Links.html
\*
(= \* (first s))
(->elem :a {:href (str "#" (anchor-link (subs s 1)))} (map-inline config label))
(re-find #"^https://" s)
(->elem :a {:href s}
(map-inline config label))
:else
;; page reference
[:span.page-reference
[:span.text-gray-500 "[["]
@ -1110,6 +1116,7 @@
[:div.headings-container {:style {:margin-left -24}}
(build-headings headings config)])
;; headers to hiccup
(rum/defc ->hiccup < rum/reactive
;; (mixins/perf-measure-mixin "hiccup")
[headings config option]

View File

@ -84,6 +84,76 @@
"")))
(defn get-hr
[format]
(let [format (or format (keyword (state/get-preferred-format)))]
(case format
:org
"-----"
:markdown
"---"
"")))
(defn get-bold
[format]
(let [format (or format (keyword (state/get-preferred-format)))]
(case format
:org
"*"
:markdown
"**"
"")))
(defn get-italic
[format]
(let [format (or format (keyword (state/get-preferred-format)))]
(case format
:org
"/"
:markdown
"__"
"")))
(defn get-underline
[format]
(let [format (or format (keyword (state/get-preferred-format)))]
(case format
:org
"/"
:markdown
"__"
"")))
(defn get-strike-through
[format]
(let [format (or format (keyword (state/get-preferred-format)))]
(case format
:org
"+"
:markdown
"~~"
"")))
(defn get-highlight
[format]
"^^")
(defn get-code
[format]
(let [format (or format (keyword (state/get-preferred-format)))]
(case format
:org
"~"
:markdown
"`"
"")))
(defn get-subscript
[format]
"_")
(defn get-superscript
[format]
"^")
(defn default-empty-heading
([format]
(default-empty-heading format 2))

View File

@ -0,0 +1,223 @@
(ns frontend.extensions.html-parser
(:require [goog.object :as gobj]
[hickory.core :as hickory]
[cljs.core.match :refer-macros [match]]
[frontend.config :as config]
[frontend.util :as util]
[clojure.string :as string]
[clojure.walk :as walk]))
(defonce *inside-pre? (atom false))
(defn- hiccup-without-style
[hiccup]
(walk/postwalk (fn [f]
(if (map? f)
(dissoc f :style)
f)) hiccup))
(defn- export-hiccup
[hiccup]
(util/format "#+BEGIN_EXPORT hiccup\n%s\n#+END_EXPORT"
(str (hiccup-without-style hiccup))))
(defn hiccup->doc-inner
[format hiccup]
(let [transform-fn (fn [hiccup]
(hiccup->doc-inner format hiccup))
heading-pattern (config/get-heading-pattern format)
map-join (fn [children] (apply str (map transform-fn children)))
heading-transform (fn [level children]
(str (apply str (repeat level heading-pattern))
" "
(->> (map transform-fn children)
(string/join " "))
"\n"))
emphasis-transform (fn [tag attrs children]
(let [pattern (cond
(contains? #{:b :strong} tag)
(config/get-bold format)
(contains? #{:i :em} tag)
(config/get-italic format)
(contains? #{:ins} tag)
(config/get-underline format)
(contains? #{:del} tag)
(config/get-strike-through format)
(contains? #{:mark} tag)
(config/get-highlight format)
:else
nil)]
(str pattern (map-join children) pattern)))
wrapper (fn [tag content]
(cond
(contains? #{:p :hr :ul :ol :dl :table :pre :blockquote :aside :canvas
:center :figure :figcaption :fieldset :div :footer
:header} tag)
(str "\n\n" content "\n\n")
(contains? #{:thead :tr :li :dt :dd :dl} tag)
(str content "\n")
:else
content))
single-hiccup-transform
(fn [x]
(do
(cond
(vector? x)
(let [[tag attrs & children] x
result (match tag
:head nil
:h1 (heading-transform 1 children)
:h2 (heading-transform 2 children)
:h3 (heading-transform 3 children)
:h4 (heading-transform 4 children)
:h5 (heading-transform 5 children)
:h6 (heading-transform 6 children)
:a (let [href (:href attrs)
title (:title attrs)
label (map-join children)
has-img-tag? (re-find #"\[:img" (str x))]
(if has-img-tag?
(export-hiccup x)
(case format
:markdown (util/format "[%s](%s)" label href)
:org (util/format "[[%s][%s]]" href label)
nil)))
:img (let [src (:src attrs)
alt (:alt attrs)]
(case format
:markdown (util/format "![%s](%s)" alt src)
:org (util/format "[[%s][%s]]" src alt)
nil))
:p (util/format "%s"
(map-join children))
:hr (config/get-hr format)
(_ :guard #(contains? #{:b :strong
:i :em
:ins
:del
:mark} %))
(emphasis-transform tag attrs children)
:code (if @*inside-pre?
(map-join children)
(let [pattern (config/get-code format)]
(str " "
(str pattern (first children) pattern)
" ")))
:pre
(do
(reset! *inside-pre? true)
(let [content (string/trim (doall (map-join children)))]
(reset! *inside-pre? false)
(case format
:markdown (if (string/starts-with? content "```")
content
(str "```\n" content "\n```"))
:org (if (string/starts-with? content "#+BEGIN_SRC")
content
(util/format "#+BEGIN_SRC\n%s\n#+END_SRC" content))
nil)))
:blockquote
(case format
:markdown (str "> " (map-join children))
:org (util/format "#+BEGIN_QUOTE\n%s\n#+END_QUOTE" (map-join children))
nil)
:li
(str "- " (map-join children))
:dt
(case format
:org (str "- " (map-join children) " ")
:markdown (map-join children)
nil)
:dd
(case format
:markdown (str ": " (map-join children) "\n")
:org (str ":: " (map-join children) "\n")
nil)
:thead
(case format
:markdown (let [columns (count (last (first children)))]
(str
(map-join children)
(str "| " (string/join " | "
(repeat columns "----"))
" |")))
:org (let [columns (count (last (first children)))]
(str
(map-join children)
(str "|" (string/join "+"
(repeat columns "----"))
"|")))
nil)
:tr
(str "| "
(->> (map transform-fn children)
(string/join " | "))
" |")
(_ :guard #(contains? #{:aside :center :figure :figcaption :fieldset :footer :header} %))
(export-hiccup x)
:else (map-join children)
)]
(wrapper tag result))
(string? x)
x
:else
(println "hiccup->doc error: " x))))
result (if (vector? (first hiccup))
(for [x hiccup]
(single-hiccup-transform x))
(single-hiccup-transform hiccup))]
(apply str result)))
(defn hiccup->doc
[format hiccup]
(let [s (hiccup->doc-inner format hiccup)]
(if (string/blank? s)
""
(-> s
(string/trim)
(string/replace "\n\n\n\n" "\n\n")
(string/replace "\n\n\n" "\n\n")))))
(defn html-decode-hiccup
[hiccup]
(walk/postwalk (fn [f]
(if (string? f)
(goog.string.unescapeEntities f)
f)) hiccup))
(defonce debug-hiccup (atom nil))
(defn parse
[format html]
(when-not (string/blank? html)
(let [hiccup (hickory/as-hiccup (hickory/parse html))
decoded-hiccup (html-decode-hiccup hiccup)]
(reset! debug-hiccup decoded-hiccup)
(hiccup->doc format decoded-hiccup))))
(def img-link
[:a {:href "https://www.markdownguide.org/book/", :style "box-sizing: border-box; color: rgb(0, 123, 255); text-decoration: none; background-color: transparent;"} [:img {:src "https://d33wubrfki0l68.cloudfront.net/cb41dd8e38b0543a305f9c56db89b46caa802263/25192/assets/images/book-cover.jpg", :class "card-img", :alt "Markdown Guide book cover", :style "box-sizing: border-box; vertical-align: middle; border-style: none; flex-shrink: 0; width: 205.75px; border-radius: calc(0.25rem - 1px);"}]])
(comment
;; | Syntax | Description | Test Text |``
;; | :--- | :----: | ---: |
;; | Header | Title | Here's this |
;; | Paragraph | Text | And more |
(def img-link
[:a {:href "https://www.markdownguide.org/book/", :style "box-sizing: border-box; color: rgb(0, 123, 255); text-decoration: none; background-color: transparent;"} [:img {:src "https://d33wubrfki0l68.cloudfront.net/cb41dd8e38b0543a305f9c56db89b46caa802263/25192/assets/images/book-cover.jpg", :class "card-img", :alt "Markdown Guide book cover", :style "box-sizing: border-box; vertical-align: middle; border-style: none; flex-shrink: 0; width: 205.75px; border-radius: calc(0.25rem - 1px);"}]])
)

View File

@ -0,0 +1,5 @@
(ns frontend.regex)
;; Get this from XRegExp("^\\pL+$")
(def valid-tag-pattern
#"^[0-9A-Za-z_\-@%ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙՠ-ֈא-תׯ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࡠ-ࡪࢠ-ࢴࢶ-ࢽऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱৼਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡૹଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘ-ౚౠౡಀಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൔ-ൖൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄຆ-ຊຌ-ຣລວ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏽᏸ-ᏽᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛱ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡸᢀ-ᢄᢇ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᲀ-ᲈᲐ-ᲺᲽ-Ჿᳩ-ᳬᳮ-ᳳᳵᳶᳺᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ--ℝℤΩℨK--ℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々〆〱-〵〻〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄯㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿯ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚝꚠ-ꛥꜗ-ꜟꜢ-ꞈꞋ-ꞿꟂ-Ᶎꟷ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꣽꣾꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭧꭰ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA--zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]+$")

View File

@ -146,6 +146,25 @@
(update-state! :editor/content (fn [m]
(assoc m input-id value)))))
(defn get-edit-input-id
[]
(ffirst (:editor/editing? @state)))
(defn append-current-edit-content!
[append-text]
(when-not (string/blank? append-text)
(when-let [input-id (get-edit-input-id)]
(when-let [input (gdom/getElement input-id)]
(let [value (gobj/get input "value")
new-value (str value append-text)
new-value (if (or (= (last value) " ")
(= (last value) "\n"))
new-value
(str "\n" new-value))]
(js/document.execCommand "insertText" false append-text)
(update-state! :editor/content (fn [m]
(assoc m input-id new-value))))))))
(defn get-cursor-range
[]
(:cursor-range @state))
@ -236,10 +255,6 @@
(fn [m]
(and input-id {input-id true}))))
(defn get-edit-input-id
[]
(ffirst (:editor/editing? @state)))
(defn sub-edit-input-id
[]
(ffirst (util/react (rum/cursor state :editor/editing?))))

View File

@ -13,7 +13,8 @@
[dommy.core :as d]
[cljs-time.core :as t]
[cljs-time.coerce :as tc]
[cljs-time.format :as format]))
[cljs-time.format :as format]
[frontend.regex :as regex]))
(defn format
[fmt & args]
@ -664,15 +665,20 @@
[]
(str (rand-str 6) (rand-str 3)))
;; Get this from XRegExp("^\\pL+$")
(def valid-tag-pattern
#"^[0-9A-Za-z_\-@%ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙՠ-ֈא-תׯ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࡠ-ࡪࢠ-ࢴࢶ-ࢽऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱৼਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡૹଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘ-ౚౠౡಀಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൔ-ൖൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄຆ-ຊຌ-ຣລວ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏽᏸ-ᏽᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛱ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡸᢀ-ᢄᢇ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᲀ-ᲈᲐ-ᲺᲽ-Ჿᳩ-ᳬᳮ-ᳳᳵᳶᳺᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ--ℝℤΩℨK--ℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々〆〱-〵〻〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄯㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿯ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚝꚠ-ꛥꜗ-ꜟꜢ-ꞈꞋ-ꞿꟂ-Ᶎꟷ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꣽꣾꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭧꭰ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA--zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]+$")
(defn tag-valid?
[tag-name]
(re-find valid-tag-pattern tag-name))
(re-find regex/valid-tag-pattern tag-name))
;; TODO: emoji, unicode alphanum, spaces, _, -
;; (defn page-title-valid?
;; [page-title]
;; )
(defn- get-clipboard-as-html
[event]
(if-let [c (gobj/get event "clipboardData")]
(.getData c "text/html")
(if-let [c (gobj/getValueByKeys event "originalEvent" "clipboardData")]
(.getData c "text/html")
(if-let [c (gobj/get js/window "clipboardData")]
(.getData c "Text")))))