alonso.torres 11d4cc6459 refactor
2026-10-02 16:38:30 +02:00

285 lines
12 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
"Japanese text support in the WASM bridge: span serialization, editor
selection styles, IME composition caret placement, and the CJK script
classification used to pick Noto fallback fonts."
(:require
[app.common.fonts :as cfnt]
[app.common.render-wasm.text-content :as tc]
[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)))))
;; Byte offsets inside one span's attribute block (see `write-spans` and
;; RawTextSpan in render-wasm).
(def ^:private span-attr-offset
{:text-combine-upright 5
:font-features 8
:annotation-clearance 9
:ruby-size 10
:ruby-align 11
:ruby-overhang 12
:ruby-side 13
:padding [14 15]
:ruby-length 72})
(def ^:private span-paragraph
{:font-size "16" :font-weight "400" :line-height "1"})
(defn- serialized-span
"DataView over the attribute block `write-spans` produces for `span`."
[span]
(let [dview (js/DataView. (js/ArrayBuffer. 256))]
(write-spans 0 dview [(merge {:font-size "16" :font-weight "400"} span)] span-paragraph)
dview))
(defn- span-byte
[dview attr]
(.getUint8 dview (get span-attr-offset attr)))
(defn- padding-bytes
[dview]
(mapv #(.getUint8 dview %) (:padding span-attr-offset)))
(t/deftest write-spans-serializes-font-features
(let [dview (serialized-span {:text "日本語" :font-features "vpal"})]
(t/is (= 2 (span-byte dview :font-features)))
(t/is (= 0 (span-byte dview :annotation-clearance)))
(t/is (= [0 0] (padding-bytes dview)))))
(t/deftest write-spans-serializes-annotation-clearance
(let [dview (serialized-span {:text "漢字" :annotation-clearance "auto"})]
(t/is (= 1 (span-byte dview :annotation-clearance)))
(t/is (= [0 0] (padding-bytes dview)))))
(t/deftest write-spans-serializes-ruby-customization
(let [dview (serialized-span {:text "漢字"
:ruby-size "quarter"
:ruby-align "space-between"
:ruby-overhang "none"
:ruby-side "under"})]
(t/is (= [2 3 1 1]
(mapv #(span-byte dview %) [:ruby-size :ruby-align :ruby-overhang :ruby-side])))
(t/is (= [0 0] (padding-bytes dview)))))
(t/deftest write-spans-omits-hidden-ruby
(let [ruby-length #(.getInt32 % (:ruby-length span-attr-offset) true)]
(t/is (= 0 (ruby-length (serialized-span {:text "日" :ruby "にち" :ruby-hidden true}))))
(t/is (pos? (ruby-length (serialized-span {:text "日" :ruby "にち" :ruby-hidden false}))))))
(t/deftest write-spans-serializes-counted-digits-tcy
(let [dview (serialized-span {:text "平成31年" :text-combine-upright "digits2"})]
(t/is (= 3 (span-byte dview :text-combine-upright)))))
(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"))))