;; 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"))))