mirror of
https://github.com/penpot/penpot.git
synced 2026-08-08 05:48:50 +00:00
282 lines
11 KiB
Clojure
282 lines
11 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.render-wasm.api :as api]
|
||
[app.render-wasm.api.texts :as texts]
|
||
[app.render-wasm.text-editor :as text-editor]
|
||
[app.render-wasm.wasm :as wasm]
|
||
[cljs.test :as t :include-macros true]))
|
||
|
||
(def ^:private write-spans @#'texts/write-spans)
|
||
|
||
(defn- langs [text]
|
||
(texts/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-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-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-size "third"
|
||
:ruby-align "center"
|
||
:ruby-overhang "none"
|
||
:ruby-side "under"
|
||
:annotation-clearance "auto"}
|
||
{:text "年"
|
||
:warichu "warichu"
|
||
:font-features "vpal"}])]
|
||
(t/is (= {:text-combine-upright :multiple
|
||
:text-emphasis :multiple
|
||
:ruby :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-count-non-bmp-text-as-one-character
|
||
(let [content (editor-content
|
||
[{:text "😀"}
|
||
{:text "日" :ruby "にち"}])]
|
||
(t/is (= "にち"
|
||
(:ruby
|
||
(text-editor/selection-japanese-styles
|
||
content (selection 0 1 0 2)))))))
|
||
|
||
(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-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}
|
||
(texts/resolve-ambiguous-cjk #{:japanese :han :cjk-punctuation} "zh")))
|
||
(t/is (= #{:japanese}
|
||
(texts/resolve-ambiguous-cjk #{:japanese :han} "en"))))
|
||
|
||
(t/deftest resolve-hangul-wins-over-locale
|
||
(t/is (= #{:korean}
|
||
(texts/resolve-ambiguous-cjk #{:korean :han} "ja"))))
|
||
|
||
(t/deftest resolve-han-only-uses-locale
|
||
(t/is (= #{:japanese} (texts/resolve-ambiguous-cjk #{:han} "ja")))
|
||
(t/is (= #{:japanese} (texts/resolve-ambiguous-cjk #{:han} "ja_paid")))
|
||
(t/is (= #{:korean} (texts/resolve-ambiguous-cjk #{:han} "ko")))
|
||
(t/is (= #{:chinese} (texts/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} (texts/resolve-ambiguous-cjk #{:han} "en")))
|
||
(t/is (= #{:chinese} (texts/resolve-ambiguous-cjk #{:han} nil))))
|
||
|
||
(t/deftest resolve-punctuation-only
|
||
(t/is (= #{:japanese} (texts/resolve-ambiguous-cjk #{:cjk-punctuation} "ja")))
|
||
(t/is (= #{:chinese} (texts/resolve-ambiguous-cjk #{:cjk-punctuation} "en"))))
|
||
|
||
(t/deftest resolve-leaves-unambiguous-sets-alone
|
||
(t/is (= #{:latin-ext} (texts/resolve-ambiguous-cjk #{:latin-ext} "ja")))
|
||
(t/is (= #{} (texts/resolve-ambiguous-cjk #{} "ja")))
|
||
;; Other detected languages survive resolution.
|
||
(t/is (= #{:japanese :cyrillic}
|
||
(texts/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}
|
||
(texts/resolve-ambiguous-cjk (langs "東京都渋谷区神南一丁目") "ja"))))
|