alonso.torres 75aeea32fb polishing
2026-07-15 17:27:15 +02:00

282 lines
11 KiB
Clojure
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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