mirror of
https://github.com/penpot/penpot.git
synced 2026-10-03 01:06:14 +00:00
338 lines
14 KiB
Clojure
338 lines
14 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 INC Sucursal en España SL
|
||
|
||
(ns frontend-tests.render-wasm.texts-test
|
||
"Unit tests for CJK script classification and the Han-unification
|
||
resolution policy used to pick Noto fallback fonts."
|
||
(:require
|
||
[app.common.fonts :as cfnt]
|
||
[app.common.render-wasm.text-content :as tc]
|
||
[app.common.render-wasm.wasm :as wasm]
|
||
[app.main.ui.workspace.shapes.text.v3-editor :as v3-editor]
|
||
[app.render-wasm.api :as api]
|
||
[app.render-wasm.api.texts :as texts]
|
||
[app.render-wasm.text-editor :as text-editor]
|
||
[cljs.test :as t :include-macros true]))
|
||
|
||
(defn- write-spans
|
||
[offset dview spans paragraph]
|
||
(@#'tc/write-spans offset dview spans paragraph cfnt/font-id->uuid))
|
||
|
||
(defn- langs [text]
|
||
(cfnt/collect-used-languages #{} text))
|
||
|
||
(defn- font-ids
|
||
[fonts]
|
||
(into #{} (map :font-id) fonts))
|
||
|
||
(defn- editor-content
|
||
[& paragraphs]
|
||
{:type "root"
|
||
:children [{:type "paragraph-set"
|
||
:children (mapv (fn [spans]
|
||
{:type "paragraph"
|
||
:children spans})
|
||
paragraphs)}]})
|
||
|
||
(defn- selection
|
||
[anchor-para anchor-offset focus-para focus-offset]
|
||
{:anchor-para anchor-para
|
||
:anchor-offset anchor-offset
|
||
:focus-para focus-para
|
||
:focus-offset focus-offset})
|
||
|
||
(t/deftest japanese-styles-follow-the-selected-span
|
||
(let [content (editor-content
|
||
[{:text "日" :ruby "にち" :text-emphasis "filled-dot"}
|
||
{:text "本"}])]
|
||
(t/is (= {:text-combine-upright "none"
|
||
:text-emphasis "filled-dot"
|
||
:ruby "にち"
|
||
:ruby-hidden false
|
||
:ruby-size "half"
|
||
:ruby-align "space-around"
|
||
:ruby-overhang "auto"
|
||
:ruby-side "over"
|
||
:warichu "none"
|
||
:font-features "none"
|
||
:annotation-clearance "none"}
|
||
(text-editor/selection-japanese-styles
|
||
content (selection 0 0 0 1))))
|
||
(t/is (= {:text-combine-upright "none"
|
||
:text-emphasis "none"
|
||
:ruby ""
|
||
:ruby-hidden false
|
||
:ruby-size "half"
|
||
:ruby-align "space-around"
|
||
:ruby-overhang "auto"
|
||
:ruby-side "over"
|
||
:warichu "none"
|
||
:font-features "none"
|
||
:annotation-clearance "none"}
|
||
(text-editor/selection-japanese-styles
|
||
content (selection 0 1 0 2))))))
|
||
|
||
(t/deftest japanese-styles-report-mixed-ranges-and-follow-the-caret
|
||
(let [content (editor-content
|
||
[{:text "平成"
|
||
:text-combine-upright "digits2"
|
||
:text-emphasis "filled-dot"
|
||
:ruby "へいせい"
|
||
:ruby-hidden true
|
||
:ruby-size "third"
|
||
:ruby-align "center"
|
||
:ruby-overhang "none"
|
||
:ruby-side "under"
|
||
:annotation-clearance "auto"}
|
||
{:text "年"
|
||
:ruby-hidden false
|
||
:warichu "warichu"
|
||
:font-features "vpal"}])]
|
||
(t/is (= {:text-combine-upright :multiple
|
||
:text-emphasis :multiple
|
||
:ruby :multiple
|
||
:ruby-hidden :multiple
|
||
:ruby-size :multiple
|
||
:ruby-align :multiple
|
||
:ruby-overhang :multiple
|
||
:ruby-side :multiple
|
||
:warichu :multiple
|
||
:font-features :multiple
|
||
:annotation-clearance :multiple}
|
||
(text-editor/selection-japanese-styles
|
||
content (selection 0 0 0 3))))
|
||
;; A caret at the boundary belongs to the preceding span, as in WASM.
|
||
(t/is (= "digits2"
|
||
(:text-combine-upright
|
||
(text-editor/selection-japanese-styles
|
||
content (selection 0 2 0 2)))))
|
||
(t/is (= "warichu"
|
||
(:warichu
|
||
(text-editor/selection-japanese-styles
|
||
content (selection 0 3 0 3)))))))
|
||
|
||
(t/deftest japanese-selection-offsets-are-utf16-code-units
|
||
(let [content (editor-content
|
||
[{:text "😀"}
|
||
{:text "日" :ruby "にち"}])]
|
||
(t/is (= "にち"
|
||
(:ruby
|
||
(text-editor/selection-japanese-styles
|
||
content (selection 0 2 0 3)))))))
|
||
|
||
(t/deftest composition-caret-goes-to-the-end-of-the-changed-part
|
||
;; Typing appends, converting replaces everything, and picking a candidate
|
||
;; for a middle clause changes only that clause.
|
||
(t/is (= 1 (v3-editor/changed-span-end "" "に")))
|
||
(t/is (= 2 (v3-editor/changed-span-end "に" "にほ")))
|
||
(t/is (= 3 (v3-editor/changed-span-end "にほんご" "日本語")))
|
||
(t/is (= 5 (v3-editor/changed-span-end "日本語を書く" "日本語を掻く")))
|
||
(t/is (= 3 (v3-editor/changed-span-end "ああ" "あああ")))
|
||
(t/is (= 0 (v3-editor/changed-span-end "にほ" "")))
|
||
(t/is (= 2 (v3-editor/changed-span-end "😀" "😀😀"))))
|
||
|
||
(t/deftest whole-shape-paragraph-attrs-apply-to-every-paragraph
|
||
(let [content (-> (editor-content [{:text "一"}] [{:text "二"}] [{:text "三"}])
|
||
(assoc-in [:children 0 :children 2 :text-orientation] "upright"))
|
||
result (text-editor/apply-paragraph-attrs-to-range
|
||
content
|
||
(selection 1 0 1 0)
|
||
{:writing-mode "vertical-rl"
|
||
:text-orientation nil
|
||
:text-align "center"})
|
||
paragraphs (-> result :children first :children)]
|
||
(t/is (every? #(= "vertical-rl" (:writing-mode %)) paragraphs))
|
||
(t/is (not-any? #(contains? % :text-orientation) paragraphs))
|
||
(t/is (= [nil "center" nil] (mapv :text-align paragraphs)))))
|
||
|
||
(t/deftest write-spans-serializes-font-features-in-reserved-byte
|
||
(let [sentinel (js-obj)
|
||
previous (if (.hasOwnProperty wasm/serializers "font-features")
|
||
(unchecked-get wasm/serializers "font-features")
|
||
sentinel)]
|
||
(try
|
||
(aset wasm/serializers "font-features" #js {"none" 0 "palt" 1 "vpal" 2})
|
||
(let [buffer (js/ArrayBuffer. 256)
|
||
dview (js/DataView. buffer)
|
||
span {:text "日本語"
|
||
:font-size "16"
|
||
:font-weight "400"
|
||
:font-features "vpal"}
|
||
paragraph {:font-size "16"
|
||
:font-weight "400"
|
||
:line-height "1"}]
|
||
(write-spans 0 dview [span] paragraph)
|
||
(t/is (= 2 (.getUint8 dview 8)))
|
||
(t/is (= 0 (.getUint8 dview 9)))
|
||
(t/is (= 0 (.getUint8 dview 14)))
|
||
(t/is (= 0 (.getUint8 dview 15))))
|
||
(finally
|
||
(if (identical? sentinel previous)
|
||
(js-delete wasm/serializers "font-features")
|
||
(aset wasm/serializers "font-features" previous))))))
|
||
|
||
(t/deftest write-spans-serializes-annotation-clearance-in-reserved-byte
|
||
(let [sentinel (js-obj)
|
||
previous (if (.hasOwnProperty wasm/serializers "annotation-clearance")
|
||
(unchecked-get wasm/serializers "annotation-clearance")
|
||
sentinel)]
|
||
(try
|
||
(aset wasm/serializers "annotation-clearance" #js {"none" 0 "auto" 1})
|
||
(let [buffer (js/ArrayBuffer. 256)
|
||
dview (js/DataView. buffer)
|
||
span {:text "漢字"
|
||
:font-size "16"
|
||
:font-weight "400"
|
||
:annotation-clearance "auto"}
|
||
paragraph {:font-size "16"
|
||
:font-weight "400"
|
||
:line-height "1"}]
|
||
(write-spans 0 dview [span] paragraph)
|
||
(t/is (= 1 (.getUint8 dview 9)))
|
||
(t/is (= 0 (.getUint8 dview 14)))
|
||
(t/is (= 0 (.getUint8 dview 15))))
|
||
(finally
|
||
(if (identical? sentinel previous)
|
||
(js-delete wasm/serializers "annotation-clearance")
|
||
(aset wasm/serializers "annotation-clearance" previous))))))
|
||
|
||
(t/deftest write-spans-serializes-ruby-customization-bytes
|
||
(let [buffer (js/ArrayBuffer. 256)
|
||
dview (js/DataView. buffer)
|
||
span {:text "漢字"
|
||
:font-size "16"
|
||
:font-weight "400"
|
||
:ruby-size "quarter"
|
||
:ruby-align "space-between"
|
||
:ruby-overhang "none"
|
||
:ruby-side "under"}
|
||
paragraph {:font-size "16"
|
||
:font-weight "400"
|
||
:line-height "1"}]
|
||
(write-spans 0 dview [span] paragraph)
|
||
(t/is (= [2 3 1 1 0 0]
|
||
(mapv #(.getUint8 dview %) (range 10 16))))))
|
||
|
||
(t/deftest write-spans-omits-hidden-ruby-bytes
|
||
(let [buffer (js/ArrayBuffer. 256)
|
||
dview (js/DataView. buffer)
|
||
paragraph {:font-size "16"
|
||
:font-weight "400"
|
||
:line-height "1"}]
|
||
(write-spans 0 dview [{:text "日"
|
||
:ruby "にち"
|
||
:ruby-hidden true
|
||
:font-size "16"
|
||
:font-weight "400"}]
|
||
paragraph)
|
||
(t/is (= 0 (.getInt32 dview 72 true)))
|
||
(write-spans 0 dview [{:text "日"
|
||
:ruby "にち"
|
||
:ruby-hidden false
|
||
:font-size "16"
|
||
:font-weight "400"}]
|
||
paragraph)
|
||
(t/is (pos? (.getInt32 dview 72 true)))))
|
||
|
||
(t/deftest write-spans-serializes-counted-digits-tcy
|
||
(let [sentinel (js-obj)
|
||
previous (if (.hasOwnProperty wasm/serializers "text-combine-upright")
|
||
(unchecked-get wasm/serializers "text-combine-upright")
|
||
sentinel)]
|
||
(try
|
||
(aset wasm/serializers "text-combine-upright"
|
||
#js {"none" 0 "all" 1 "digits" 2 "digits2" 3 "digits3" 4})
|
||
(let [buffer (js/ArrayBuffer. 256)
|
||
dview (js/DataView. buffer)
|
||
span {:text "平成31年"
|
||
:font-size "16"
|
||
:font-weight "400"
|
||
:text-combine-upright "digits2"}
|
||
paragraph {:font-size "16"
|
||
:font-weight "400"
|
||
:line-height "1"}]
|
||
(write-spans 0 dview [span] paragraph)
|
||
;; Byte 5 of the span attr block carries text-combine-upright.
|
||
(t/is (= 3 (.getUint8 dview 5))))
|
||
(finally
|
||
(if (identical? sentinel previous)
|
||
(js-delete wasm/serializers "text-combine-upright")
|
||
(aset wasm/serializers "text-combine-upright" previous))))))
|
||
|
||
(t/deftest ruby-text-participates-in-live-and-reload-fallback-discovery
|
||
(let [content {:children
|
||
[{:children
|
||
[{:children
|
||
[{:text "Penpot"
|
||
:ruby "ぺんぽっと😀"}]}]}]}
|
||
expected #{"gfont-noto-sans-jp" "gfont-noto-color-emoji"}]
|
||
(with-redefs [texts/write-shape-text (fn [& _])]
|
||
(t/is (every? (font-ids (api/fonts-from-text-content content true)) expected)))
|
||
(t/is (every? (font-ids (api/fonts-from-text-content content false)) expected))))
|
||
|
||
(t/deftest classification-kana
|
||
;; Hiragana/katakana are unambiguously Japanese.
|
||
(t/is (= #{:japanese} (langs "ひらがなとカタカナ")))
|
||
;; Half-width katakana too.
|
||
(t/is (= #{:japanese} (langs "デザイン"))))
|
||
|
||
(t/deftest classification-han-is-ambiguous
|
||
;; Kanji-only text (han-unification fixture) must NOT classify as a
|
||
;; concrete language; it is ambiguous Han.
|
||
(t/is (= #{:han} (langs "東京都渋谷区神南一丁目"))))
|
||
|
||
(t/deftest classification-cjk-punctuation
|
||
;; CJK punctuation and full-width forms match the shared class
|
||
;; (previously they matched no range at all).
|
||
(t/is (= #{:cjk-punctuation} (langs "、。「」『』()")))
|
||
(t/is (= #{:cjk-punctuation} (langs "!?:;123ABC"))))
|
||
|
||
(t/deftest classification-mixed-japanese
|
||
(t/is (= #{:japanese :han :cjk-punctuation}
|
||
(langs "「こんにちは」と彼は言った。"))))
|
||
|
||
(t/deftest classification-korean
|
||
(t/is (= #{:korean} (langs "안녕하세요"))))
|
||
|
||
(t/deftest resolve-kana-wins-over-locale
|
||
;; Kana in the same content implies Japanese regardless of locale.
|
||
(t/is (= #{:japanese}
|
||
(cfnt/resolve-ambiguous-cjk #{:japanese :han :cjk-punctuation} "zh")))
|
||
(t/is (= #{:japanese}
|
||
(cfnt/resolve-ambiguous-cjk #{:japanese :han} "en"))))
|
||
|
||
(t/deftest resolve-hangul-wins-over-locale
|
||
(t/is (= #{:korean}
|
||
(cfnt/resolve-ambiguous-cjk #{:korean :han} "ja"))))
|
||
|
||
(t/deftest resolve-han-only-uses-locale
|
||
(t/is (= #{:japanese} (cfnt/resolve-ambiguous-cjk #{:han} "ja")))
|
||
(t/is (= #{:japanese} (cfnt/resolve-ambiguous-cjk #{:han} "ja_paid")))
|
||
(t/is (= #{:korean} (cfnt/resolve-ambiguous-cjk #{:han} "ko")))
|
||
(t/is (= #{:chinese} (cfnt/resolve-ambiguous-cjk #{:han} "zh_cn"))))
|
||
|
||
(t/deftest resolve-han-only-defaults-to-chinese
|
||
;; Without kana/hangul and without a CJK locale, keep the previous
|
||
;; behavior (Noto Sans SC).
|
||
(t/is (= #{:chinese} (cfnt/resolve-ambiguous-cjk #{:han} "en")))
|
||
(t/is (= #{:chinese} (cfnt/resolve-ambiguous-cjk #{:han} nil))))
|
||
|
||
(t/deftest resolve-punctuation-only
|
||
(t/is (= #{:japanese} (cfnt/resolve-ambiguous-cjk #{:cjk-punctuation} "ja")))
|
||
(t/is (= #{:chinese} (cfnt/resolve-ambiguous-cjk #{:cjk-punctuation} "en"))))
|
||
|
||
(t/deftest resolve-leaves-unambiguous-sets-alone
|
||
(t/is (= #{:latin-ext} (cfnt/resolve-ambiguous-cjk #{:latin-ext} "ja")))
|
||
(t/is (= #{} (cfnt/resolve-ambiguous-cjk #{} "ja")))
|
||
;; Other detected languages survive resolution.
|
||
(t/is (= #{:japanese :cyrillic}
|
||
(cfnt/resolve-ambiguous-cjk #{:han :cyrillic} "ja"))))
|
||
|
||
(t/deftest resolve-han-unification
|
||
;; A kanji-only Japanese address resolves to Japanese under a ja locale.
|
||
(t/is (= #{:japanese}
|
||
(cfnt/resolve-ambiguous-cjk (langs "東京都渋谷区神南一丁目") "ja"))))
|