mirror of
https://github.com/penpot/penpot.git
synced 2026-10-03 17:26:16 +00:00
* ✨ Add rich HTML paste to the v3 text editor Pasting HTML into the v3 text editor keeps bold, italic, decoration and text-transform, and drops fonts, colors and links. Gated behind text-editor-wasm/v1-html-paste. Refs #10465 AI-assisted-by: claude-opus-5-5 * ✨ Paste HTML on the canvas as a styled text shape With v3 HTML paste on, pasting HTML on the canvas creates a text shape with the same emphasis. Clipboard options are now read at paste time. Refs #10465 AI-assisted-by: claude-opus-5-5
147 lines
7.1 KiB
Clojure
147 lines
7.1 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
|
|
|
(ns frontend-tests.render-wasm.text-paste-test
|
|
"Restyling pasted text. Test content already holds the pasted text in the
|
|
style of the span it went into, as after `text-editor-sync-content`."
|
|
(:require
|
|
[app.render-wasm.text-paste :as text-paste]
|
|
[cljs.test :as t :include-macros true]))
|
|
|
|
(def ^:private regular
|
|
{:font-id "sourcesanspro" :font-variant-id "regular"
|
|
:font-weight "400" :font-style "normal"})
|
|
|
|
(defn- span
|
|
([text] (span text {}))
|
|
([text attrs] (merge regular attrs {:text text})))
|
|
|
|
(defn- content [& paragraphs]
|
|
{:type "root"
|
|
:children [{:type "paragraph-set"
|
|
:children (mapv (fn [spans] {:type "paragraph" :children spans})
|
|
paragraphs)}]})
|
|
|
|
(defn- spans-of [content]
|
|
(mapv :children (-> content :children first :children)))
|
|
|
|
(defn- fragment-paragraph [& runs]
|
|
{:attrs {} :children (vec runs)})
|
|
|
|
(defn- run
|
|
([text] (run text {}))
|
|
([text attrs] {:text text :attrs attrs}))
|
|
|
|
(def ^:private bold-variant
|
|
{:font-weight "700" :font-style "normal" :font-variant-id "bold"})
|
|
|
|
(t/deftest fragment->text
|
|
(t/testing "paragraphs are joined by newlines"
|
|
(t/is (= "ab\n\nc"
|
|
(text-paste/fragment->text
|
|
[(fragment-paragraph (run "a") (run "b"))
|
|
(fragment-paragraph)
|
|
(fragment-paragraph (run "c"))])))))
|
|
|
|
(t/deftest bold-run-resolves-to-a-font-variant
|
|
(t/testing "a bold run inside a paragraph gets the bold variant of the span's font"
|
|
(let [pasted (content [(span "Hello big world")])
|
|
fragment [(fragment-paragraph (run "big " {:font-weight "700"}))]
|
|
result (text-paste/apply-fragment-styles pasted fragment {:para 0 :offset 6})]
|
|
(t/is (= [[(span "Hello ") (span "big " bold-variant) (span "world")]]
|
|
(spans-of result))))))
|
|
|
|
(t/deftest italic-run-keeps-the-span-weight
|
|
(t/testing "italic over a bold span picks the bold italic variant"
|
|
(let [pasted (content [(span "x" bold-variant)])
|
|
fragment [(fragment-paragraph (run "x" {:font-style "italic"}))]
|
|
result (text-paste/apply-fragment-styles pasted fragment {:para 0 :offset 0})]
|
|
(t/is (= [[(span "x" {:font-weight "700" :font-style "italic"
|
|
:font-variant-id "bolditalic"})]]
|
|
(spans-of result))))))
|
|
|
|
(t/deftest html-emphasis-replaces-the-caret-emphasis
|
|
(let [no-emphasis {:font-weight "400" :font-style "normal"
|
|
:text-decoration "none" :text-transform "none"}]
|
|
(t/testing "plain source text pasted into bold, underlined text comes out plain"
|
|
(let [pasted (content [(span "aXb" (assoc bold-variant :text-decoration "underline"))])
|
|
fragment [(fragment-paragraph (run "X" no-emphasis))]
|
|
result (text-paste/apply-fragment-styles pasted fragment {:para 0 :offset 1})]
|
|
(t/is (= [[(span "a" (assoc bold-variant :text-decoration "underline"))
|
|
(span "X" {:text-decoration "none" :text-transform "none"})
|
|
(span "b" (assoc bold-variant :text-decoration "underline"))]]
|
|
(spans-of result)))))
|
|
|
|
(t/testing "not bold leaves a light weight as it is"
|
|
(let [light {:font-weight "300" :font-style "normal" :font-variant-id "300"}
|
|
pasted (content [(span "X" light)])
|
|
fragment [(fragment-paragraph (run "X" no-emphasis))]
|
|
result (text-paste/apply-fragment-styles pasted fragment {:para 0 :offset 0})]
|
|
(t/is (= [[(span "X" (assoc light :text-decoration "none" :text-transform "none"))]]
|
|
(spans-of result)))))))
|
|
|
|
(t/deftest unknown-font-keeps-its-weight
|
|
(t/testing "a font we have no data for cannot resolve a variant, so it stays"
|
|
(let [unknown (span "x" {:font-id "missing-font"})
|
|
fragment [(fragment-paragraph (run "x" {:font-weight "700"}))]
|
|
result (text-paste/apply-fragment-styles (content [unknown]) fragment {:para 0 :offset 0})]
|
|
(t/is (= [[unknown]] (spans-of result))))))
|
|
|
|
(t/deftest typography-link
|
|
(let [linked {:typography-ref-id "typo-id" :typography-ref-file "file-id"}]
|
|
(t/testing "a span changed by the paste leaves its typography"
|
|
(let [fragment [(fragment-paragraph (run "x" {:text-decoration "underline"}))]
|
|
result (text-paste/apply-fragment-styles
|
|
(content [(span "x" linked)]) fragment {:para 0 :offset 0})]
|
|
(t/is (= [[(span "x" {:text-decoration "underline"})]]
|
|
(spans-of result)))))
|
|
|
|
(t/testing "a span the paste leaves as it was keeps its typography"
|
|
(let [already (span "x" (merge bold-variant linked))
|
|
fragment [(fragment-paragraph (run "x" {:font-weight "700"}))]
|
|
result (text-paste/apply-fragment-styles (content [already]) fragment {:para 0 :offset 0})]
|
|
(t/is (= [[already]] (spans-of result)))))))
|
|
|
|
(t/deftest ranges-across-paragraphs
|
|
(t/testing "runs after the first paragraph start at offset 0 of their paragraph"
|
|
(let [pasted (content [(span "abX")] [(span "Ycd")])
|
|
fragment [(fragment-paragraph (run "X" {:font-weight "700"}))
|
|
(fragment-paragraph (run "Y" {:font-weight "700"}))]
|
|
result (text-paste/apply-fragment-styles pasted fragment {:para 0 :offset 2})]
|
|
(t/is (= [[(span "ab") (span "X" bold-variant)]
|
|
[(span "Y" bold-variant) (span "cd")]]
|
|
(spans-of result))))))
|
|
|
|
(t/deftest ranges-count-utf16-units
|
|
(t/testing "an emoji in the fragment moves later runs by its two UTF-16 units"
|
|
(let [pasted (content [(span "😀b")])
|
|
fragment [(fragment-paragraph (run "😀") (run "b" {:font-weight "700"}))]
|
|
result (text-paste/apply-fragment-styles pasted fragment {:para 0 :offset 0})]
|
|
(t/is (= [[(span "😀") (span "b" bold-variant)]]
|
|
(spans-of result)))))
|
|
|
|
(t/testing "a paste after an emoji starts at the UTF-16 offset WASM reports"
|
|
(let [pasted (content [(span "😀Xb")])
|
|
fragment [(fragment-paragraph (run "X" {:font-weight "700"}))]
|
|
result (text-paste/apply-fragment-styles pasted fragment {:para 0 :offset 2})]
|
|
(t/is (= [[(span "😀") (span "X" bold-variant) (span "b")]]
|
|
(spans-of result))))))
|
|
|
|
(t/deftest fragment->content
|
|
(t/testing "runs become spans over the base style, and empty paragraphs keep one empty span"
|
|
(let [result (text-paste/fragment->content
|
|
[(fragment-paragraph (run "Hello ") (run "bold" {:font-weight "700"}))
|
|
(fragment-paragraph)]
|
|
regular)]
|
|
(t/is (= [[(span "Hello ") (span "bold" bold-variant)]
|
|
[(span "")]]
|
|
(spans-of result)))))
|
|
|
|
(t/testing "paragraphs carry the base style"
|
|
(let [result (text-paste/fragment->content [(fragment-paragraph (run "x"))] regular)]
|
|
(t/is (= (assoc regular :type "paragraph")
|
|
(dissoc (-> result :children first :children first) :children))))))
|