From 7f4cadb3263cd1e9cee756188df7b48c7c5a8360 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Oct 2026 17:08:59 +0200 Subject: [PATCH] :sparkles: Define Japanese text layout attributes Add schemas, defaults and helpers for vertical writing, ruby, emphasis and warichu. Keep readings and notes tied to their own characters when copying styles, and preserve base text when pasting HTML ruby. Cover the model, property copying, clipboard and library file builder. --- common/src/app/common/types/shape.cljc | 6 +- common/src/app/common/types/shape/text.cljc | 35 +++- common/src/app/common/types/text.cljc | 16 +- .../src/app/common/types/text/cjk_layout.cljc | 187 ++++++++++++++++++ common/test/common_tests/types/text_test.cljc | 113 +++++++++++ frontend/src/app/util/text/clipboard.cljs | 9 +- .../tokens/copy_paste_props_test.cljs | 17 ++ .../util_text_clipboard_test.cljs | 11 ++ library/test/builder.test.js | 38 ++++ 9 files changed, 422 insertions(+), 10 deletions(-) create mode 100644 common/src/app/common/types/text/cjk_layout.cljc diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 80f2e608f6..5279707c7b 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -795,7 +795,7 @@ :r4}) (def ^:private text-extract-props - (into #{} cat [txt/root-attrs txt/paragraph-attrs txt/text-node-attrs])) + (into #{} cat [txt/root-attrs txt/paragraph-attrs txt/text-node-style-attrs])) (def ^:private layout-extract-props (set ctsl/layout-attrs)) @@ -846,7 +846,7 @@ (assoc-props node txt/paragraph-attrs) (txt/is-text-node? node) - (assoc-props node txt/text-node-attrs))) + (assoc-props node txt/text-node-style-attrs))) props))) (extract-layout-attrs [props shape] @@ -889,7 +889,7 @@ (d/patch-object (select-keys props txt/paragraph-attrs)) (txt/is-text-node? node) - (d/patch-object (select-keys props txt/text-node-attrs)))))))))) + (d/patch-object (select-keys props txt/text-node-style-attrs)))))))))) (patch-layout-props [shape props] (let [shape (d/patch-object shape (select-keys props ctsl/layout-attrs))] diff --git a/common/src/app/common/types/shape/text.cljc b/common/src/app/common/types/shape/text.cljc index 122aae80a8..b17ddbd77c 100644 --- a/common/src/app/common/types/shape/text.cljc +++ b/common/src/app/common/types/shape/text.cljc @@ -7,7 +7,8 @@ (ns app.common.types.shape.text (:require [app.common.schema :as sm] - [app.common.types.fills :refer [schema:fills]])) + [app.common.types.fills :refer [schema:fills]] + [app.common.types.text.cjk-layout :as cjk])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SCHEMA @@ -15,10 +16,17 @@ (def node-types #{"root" "paragraph-set" "paragraph"}) +;; Values of a Japanese layout enum attr; paragraphs set the orientation +;; of the whole shape and spans carry a copy. +(defn- schema:jl-enum + [attr] + (into [:enum] (get cjk/enum-values attr))) + (def schema:content [:map [:type [:= "root"]] [:key {:optional true} :string] + [:line-adjustment {:optional true} (schema:jl-enum :line-adjustment)] [:children [:vector {:min 1 :gen/max 2 :gen/min 1} [:map @@ -36,6 +44,8 @@ [:font-style {:optional true} ::sm/text] [:font-weight {:optional true} ::sm/text] [:direction {:optional true} ::sm/text] + [:writing-mode {:optional true} (schema:jl-enum :writing-mode)] + [:text-orientation {:optional true} (schema:jl-enum :text-orientation)] [:text-decoration {:optional true} ::sm/text] [:text-transform {:optional true} ::sm/text] [:typography-ref-id {:optional true} [:maybe ::sm/uuid]] @@ -52,6 +62,18 @@ [:font-style {:optional true} ::sm/text] [:font-weight {:optional true} ::sm/text] [:direction {:optional true} ::sm/text] + [:text-combine-upright {:optional true} (schema:jl-enum :text-combine-upright)] + [:text-emphasis {:optional true} (schema:jl-enum :text-emphasis)] + [:ruby {:optional true} :string] + [:ruby-hidden {:optional true} :boolean] + [:ruby-size {:optional true} (schema:jl-enum :ruby-size)] + [:ruby-align {:optional true} (schema:jl-enum :ruby-align)] + [:ruby-overhang {:optional true} (schema:jl-enum :ruby-overhang)] + [:ruby-side {:optional true} (schema:jl-enum :ruby-side)] + [:warichu {:optional true} (schema:jl-enum :warichu)] + [:font-features {:optional true} (schema:jl-enum :font-features)] + [:annotation-clearance {:optional true} (schema:jl-enum :annotation-clearance)] + [:text-orientation {:optional true} (schema:jl-enum :text-orientation)] [:text-decoration {:optional true} ::sm/text] [:text-transform {:optional true} ::sm/text] [:typography-ref-id {:optional true} [:maybe ::sm/uuid]] @@ -73,6 +95,17 @@ [:font-style {:optional true} ::sm/text] [:font-weight {:optional true} ::sm/text] [:rtl {:optional true} :boolean] + [:writing-mode {:optional true} ::sm/text] + [:text-orientation {:optional true} ::sm/text] + [:text-combine-upright {:optional true} ::sm/text] + [:emphasis-mark {:optional true} :boolean] + [:ruby {:optional true} :string] + [:ruby-size {:optional true} ::sm/text] + [:ruby-align {:optional true} ::sm/text] + [:ruby-overhang {:optional true} ::sm/text] + [:ruby-side {:optional true} ::sm/text] + [:warichu {:optional true} ::sm/text] + [:font-features {:optional true} ::sm/text] [:text {:optional true} :string] [:text-decoration {:optional true} ::sm/text] [:text-transform {:optional true} ::sm/text]]]) diff --git a/common/src/app/common/types/text.cljc b/common/src/app/common/types/text.cljc index 4e53c9b753..40f70afc16 100644 --- a/common/src/app/common/types/text.cljc +++ b/common/src/app/common/types/text.cljc @@ -12,6 +12,7 @@ [app.common.math :as mth] [app.common.types.color :as clr] [app.common.types.fills :as types.fills] + [app.common.types.text.cjk-layout :as cjk] [clojure.set :as set] [clojure.walk :as walk] [cuerdas.core :as str])) @@ -50,6 +51,10 @@ (def text-valign-attrs [:vertical-align]) +;; How the shape fits lines slightly too long (see `cjk/enum-values`). +(def text-line-adjustment-attrs + [:line-adjustment]) + (def text-decoration-attrs [:text-decoration]) @@ -97,22 +102,29 @@ [:grow-type]) (def root-attrs - text-valign-attrs) + (d/concat-vec text-valign-attrs text-line-adjustment-attrs)) (def paragraph-attrs (d/concat-vec text-align-attrs - text-direction-attrs)) + text-direction-attrs + cjk/whole-shape-paragraph-attrs)) (def text-node-attrs (d/concat-vec text-typography-attrs text-font-attrs text-spacing-attrs + cjk/span-attrs text-decoration-attrs text-transform-attrs text-fills)) +(def text-node-style-attrs + "Text node attrs that style characters, without the attrs the characters + own (ruby reading, warichu)." + (vec (remove (set cjk/text-content-attrs) text-node-attrs))) + (def text-span-attrs "Inline text span attrs. Line-height is paragraph-level in the DOM editor; it may still be stored redundantly on span nodes." diff --git a/common/src/app/common/types/text/cjk_layout.cljc b/common/src/app/common/types/text/cjk_layout.cljc new file mode 100644 index 0000000000..afa6b40576 --- /dev/null +++ b/common/src/app/common/types/text/cjk_layout.cljc @@ -0,0 +1,187 @@ +;; 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 app.common.types.text.cjk-layout + (:require + [app.common.data.macros :as dm] + [cuerdas.core :as str])) + +;; Values of the enum attrs, in the order of the renderer's discriminants +;; (`Raw*` enums in render-wasm). The first value is the default. +(def enum-values + {;; Vertical writing (tategaki). + :writing-mode ["horizontal-tb" "vertical-rl"] + :text-orientation ["mixed" "upright"] + :text-combine-upright ["none" "all" "digits" "digits2" "digits3"] + ;; Emphasis mark (圏点 / bouten) applied per span. + :text-emphasis ["none" "filled-dot" "open-dot" "filled-circle" + "open-circle" "filled-sesame" "open-sesame"] + ;; Warichu (割注): two half-size lines in one line position. + :warichu ["none" "warichu"] + :font-features ["none" "palt" "vpal"] + ;; "auto" adds the ruby (at its size) and emphasis marks to the line + ;; height; "none" keeps it, so annotations sit in the line gap. + :annotation-clearance ["none" "auto"] + :ruby-size ["half" "third" "quarter"] + :ruby-align ["space-around" "center" "start" "space-between"] + :ruby-overhang ["auto" "none"] + :ruby-side ["over" "under"] + ;; Root attr: how lines slightly too long are fitted (Adobe's Kinsoku + ;; Type). It decides squeezing; alignment decides the space left over. + :line-adjustment ["push-in-first" "push-out-first" "push-out-only"]}) + +(def ^:private enum-value-sets + (update-vals enum-values set)) + +(defn enum-default + "Default value of the enum `attr`." + [attr] + (first (get enum-values attr))) + +(defn valid-enum-value? + "True when `value` is one of the values of the enum `attr`." + [attr value] + (contains? (get enum-value-sets attr) value)) + +;; Paragraph attrs. They apply to the whole shape: every paragraph carries +;; the first one's value. +(def whole-shape-paragraph-attrs + [:writing-mode :text-orientation]) + +;; Ruby attrs that change how a reading looks, not the reading itself. +(def ruby-presentation-attrs + [:ruby-hidden :ruby-size :ruby-align :ruby-overhang :ruby-side]) + +;; Span attrs; `:ruby` holds the reading (furigana) of the span. +(def span-attrs + (into [:text-combine-upright :text-emphasis :ruby] + (concat ruby-presentation-attrs + [:warichu :font-features :annotation-clearance]))) + +;; Span attrs owned by the span's characters, not its style: a reading or a +;; warichu note annotates one span and is never copied to other text. +(def text-content-attrs + [:ruby :warichu]) + +;; Values of the span attrs above when a span does not store them. +(def span-attr-defaults + (into {:ruby "" :ruby-hidden false} + (comp (filter #(contains? enum-values %)) + (map (juxt identity enum-default))) + span-attrs)) + +;; Values of the paragraph attrs when a paragraph does not store them. +(def paragraph-attr-defaults + (into {} (map (juxt identity enum-default)) whole-shape-paragraph-attrs)) + +;; Glyph of each emphasis style, per CSS `text-emphasis-style`. +(def ^:private emphasis-mark-chars + {"filled-dot" "•" + "open-dot" "◦" + "filled-circle" "●" + "open-circle" "○" + "filled-sesame" "﹅" + "open-sesame" "﹆"}) + +(defn emphasis-mark-char + "Mark glyph of a text-emphasis value, or nil for none." + [text-emphasis] + (get emphasis-mark-chars text-emphasis)) + +;; Emphasis mark font size relative to the base font size. +(def emphasis-font-scale 0.5) + +;; Warichu sub-line font size relative to the base font size. +(def warichu-font-scale 0.5) + +(defn ruby-font-scale + [ruby-size] + (case ruby-size + "third" (/ 1 3) + "quarter" 0.25 + 0.5)) + +(defn visible-ruby + "Ruby annotation text of a text node, or nil when absent, hidden or on a + warichu note, which shows no ruby." + [node] + (let [ruby (:ruby node)] + (when (and (string? ruby) + (not (str/blank? ruby)) + (not (true? (:ruby-hidden node))) + (not= "warichu" (:warichu node))) + ruby))) + +(defn western-reading? + "True when a ruby reading holds no CJK, kana or full-width character: it is + set as words, sideways in vertical text, and never spread letter by + letter." + [ruby] + (and (string? ruby) + (not (str/blank? ruby)) + (not (re-find #"[\u2E80-\u9FFF\uF900-\uFAFF\uFF00-\uFFEF]" ruby)))) + +(defn ruby-span? + "True when a text node carries a ruby reading, hidden or not." + [node] + (not (str/blank? (:ruby node)))) + +(defn annotated-span? + "True when a text node carries a ruby reading or is a warichu note." + [node] + (or (ruby-span? node) + (= "warichu" (:warichu node)))) + +(defn- digit? + [c] + (let [code #?(:clj (int c) :cljs (.charCodeAt c 0))] + (or (<= 48 code 57) (<= 0xFF10 code 0xFF19)))) + +;; Longest digit run each `digits` tate-chu-yoko value combines. +(def ^:private digit-combine-max-lengths + {"digits" 4 "digits2" 2 "digits3" 3}) + +(defn digit-combine? + "True for the tate-chu-yoko values that combine digit runs only." + [value] + (contains? digit-combine-max-lengths value)) + +(defn digit-combine-segments + "Text split for a `digits` tate-chu-yoko value: `[text combine?]` pairs, + where runs of two up to the value's maximum ASCII or full-width digits + combine and longer runs stay ordinary text. Nil for other values." + [text value] + (when-let [max-len (get digit-combine-max-lengths value)] + (->> (partition-by digit? (seq text)) + (mapv (fn [chars] + (let [run (apply str chars)] + [run (and (digit? (first chars)) (<= 2 (count chars) max-len))])))))) + +(defn warichu-text? + "True when a text node renders as warichu, which needs at least two + characters for its two sub-lines." + [node] + (let [text (:text node)] + (and (= "warichu" (:warichu node)) + (string? text) + (>= (count text) 2)))) + +(defn whole-shape-attr + "Value of a whole-shape paragraph `attr` (see `whole-shape-paragraph-attrs`) + in a text content: stored per paragraph, the first paragraph's value + decides. Nil when unset." + [content attr] + (dm/get-in content [:children 0 :children 0 attr])) + +(defn content-writing-mode + "Writing mode of a text content, or nil when unset." + [content] + (whole-shape-attr content :writing-mode)) + +(defn vertical-text-content? + "True when the text content flows vertically (vertical-rl)." + [content] + (= "vertical-rl" (content-writing-mode content))) diff --git a/common/test/common_tests/types/text_test.cljc b/common/test/common_tests/types/text_test.cljc index ace36f0c4d..ab7f15a668 100644 --- a/common/test/common_tests/types/text_test.cljc +++ b/common/test/common_tests/types/text_test.cljc @@ -8,7 +8,9 @@ (:require [app.common.types.shape :as cts] + [app.common.types.shape.text :as ctst] [app.common.types.text :as cttx] + [app.common.types.text.cjk-layout :as cjk] [clojure.test :as t :include-macros true])) (def content-base @@ -368,3 +370,114 @@ (t/is (every? #(= % "14") original-font-sizes)) (t/is (every? #(= % "32") updated-font-sizes)))) + +(defn- with-span-attrs + [attrs] + (update-in content-base [:children 0 :children 0 :children 0] merge attrs)) + +(defn- with-paragraph-attrs + [attrs] + (update-in content-base [:children 0 :children 0] merge attrs)) + +(t/deftest japanese-span-attrs-accept-supported-values + (t/is (ctst/valid-content? + (with-span-attrs {:text-combine-upright "digits2" + :text-emphasis "open-sesame" + :ruby "かんじ" + :ruby-hidden true + :ruby-size "quarter" + :ruby-align "space-between" + :ruby-overhang "none" + :ruby-side "under" + :warichu "warichu" + :font-features "vpal" + :annotation-clearance "auto" + :text-orientation "upright"}))) + (t/is (ctst/valid-content? + (with-paragraph-attrs {:writing-mode "vertical-rl" :text-orientation "mixed"})))) + +(t/deftest japanese-attrs-reject-unknown-values + (doseq [attrs [{:text-combine-upright "digits5"} + {:text-emphasis "dot"} + {:ruby-hidden "yes"} + {:ruby-size "full"} + {:ruby-align "end"} + {:ruby-overhang "always"} + {:ruby-side "left"} + {:warichu "yes"} + {:font-features "kern"} + {:annotation-clearance "max"} + {:text-orientation "sideways"}]] + (t/is (not (ctst/valid-content? (with-span-attrs attrs))) (pr-str attrs))) + (t/is (not (ctst/valid-content? (with-paragraph-attrs {:writing-mode "vertical-lr"}))))) + +(t/deftest digit-combine-segments-combine-short-digit-runs + (t/is (= [["平成" false] ["31" true] ["年" false]] + (cjk/digit-combine-segments "平成31年" "digits2"))) + (t/is (= [["第" false] ["123" true]] + (cjk/digit-combine-segments "第123" "digits3"))) + (t/is (= [["12345" false]] (cjk/digit-combine-segments "12345" "digits"))) + (t/is (= [["1" false]] (cjk/digit-combine-segments "1" "digits"))) + (t/is (nil? (cjk/digit-combine-segments "31" "all")))) + +(t/deftest western-reading-test + (t/is (true? (cjk/western-reading? "editor"))) + (t/is (true? (cjk/western-reading? "gross domestic product"))) + (t/is (false? (cjk/western-reading? "エディター"))) + (t/is (false? (cjk/western-reading? "GDP"))) + (t/is (false? (cjk/western-reading? "かnji"))) + (t/is (false? (cjk/western-reading? ""))) + (t/is (false? (cjk/western-reading? nil)))) + +(t/deftest schema-accepts-every-japanese-enum-value + (doseq [[attr values] (dissoc cjk/enum-values :writing-mode :line-adjustment) + value values] + (t/is (ctst/valid-content? (with-span-attrs {attr value})) (pr-str attr value))) + (doseq [value (:writing-mode cjk/enum-values)] + (t/is (ctst/valid-content? (with-paragraph-attrs {:writing-mode value})) value)) + (doseq [value (:line-adjustment cjk/enum-values)] + (t/is (ctst/valid-content? (assoc content-base :line-adjustment value)) value)) + (t/is (not (ctst/valid-content? (assoc content-base :line-adjustment "squeeze"))))) + +(t/deftest japanese-enum-defaults-are-the-first-values + (t/is (= "horizontal-tb" (cjk/enum-default :writing-mode))) + (t/is (= "space-around" (cjk/enum-default :ruby-align))) + (t/is (= "auto" (cjk/enum-default :ruby-overhang))) + (t/is (= "push-in-first" (cjk/enum-default :line-adjustment))) + (t/is (= {:writing-mode "horizontal-tb" :text-orientation "mixed"} + cjk/paragraph-attr-defaults))) + +(t/deftest span-attr-defaults-cover-every-span-attr + (t/is (= (set cjk/span-attrs) (set (keys cjk/span-attr-defaults)))) + (t/is (= "" (:ruby cjk/span-attr-defaults))) + (t/is (false? (:ruby-hidden cjk/span-attr-defaults))) + (t/is (= "half" (:ruby-size cjk/span-attr-defaults)))) + +(t/deftest valid-enum-value-checks-the-attr-values + (t/is (cjk/valid-enum-value? :ruby-side "under")) + (t/is (not (cjk/valid-enum-value? :ruby-side "left"))) + (t/is (not (cjk/valid-enum-value? :ruby-side nil)))) + +(t/deftest digit-combine-covers-the-digits-values-only + (t/is (every? cjk/digit-combine? ["digits" "digits2" "digits3"])) + (t/is (not-any? cjk/digit-combine? ["none" "all" nil]))) + +(t/deftest warichu-notes-show-no-ruby + (t/is (= "かん" (cjk/visible-ruby {:text "漢字" :ruby "かん"}))) + (t/is (nil? (cjk/visible-ruby {:text "漢字" :ruby "かん" :warichu "warichu"}))) + (t/is (nil? (cjk/visible-ruby {:text "漢字" :ruby "かん" :ruby-hidden true})))) + +(t/deftest ruby-span-is-true-for-hidden-readings + (t/is (cjk/ruby-span? {:ruby "かん" :ruby-hidden true})) + (t/is (not (cjk/ruby-span? {:ruby " "}))) + (t/is (not (cjk/ruby-span? {})))) + +(t/deftest whole-shape-attr-reads-the-first-paragraph + (let [content {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" :writing-mode "vertical-rl" + :children [{:text "a"}]} + {:type "paragraph" :writing-mode "horizontal-tb" + :children [{:text "b"}]}]}]}] + (t/is (= "vertical-rl" (cjk/whole-shape-attr content :writing-mode))) + (t/is (nil? (cjk/whole-shape-attr content :text-orientation))))) diff --git a/frontend/src/app/util/text/clipboard.cljs b/frontend/src/app/util/text/clipboard.cljs index f8368aa34a..98de96504f 100644 --- a/frontend/src/app/util/text/clipboard.cljs +++ b/frontend/src/app/util/text/clipboard.cljs @@ -25,13 +25,14 @@ "SUMMARY" "TABLE" "TBODY" "TFOOT" "THEAD" "TR" "UL"}) ;; Elements whose text is read; any other element is skipped with its children. -;; `O:P` is Word's paragraph filler, which marks its empty lines. +;; `O:P` is Word's paragraph filler, which marks its empty lines. Ruby keeps its +;; base text (`RUBY`, `RB`); the reading (`RT`, `RTC`) and fallback `RP` are skipped. (def ^:private allowed-tags (into block-tags #{"A" "ABBR" "B" "BDI" "BDO" "BIG" "BR" "CENTER" "CITE" "CODE" "DATA" - "DEL" "DFN" "EM" "FONT" "I" "INS" "KBD" "LABEL" "MARK" "O:P" "Q" "S" - "SAMP" "SMALL" "SPAN" "STRIKE" "STRONG" "SUB" "SUP" "TD" "TH" "TIME" - "TT" "U" "VAR" "WBR"})) + "DEL" "DFN" "EM" "FONT" "I" "INS" "KBD" "LABEL" "MARK" "O:P" "Q" "RB" + "RUBY" "S" "SAMP" "SMALL" "SPAN" "STRIKE" "STRONG" "SUB" "SUP" "TD" + "TH" "TIME" "TT" "U" "VAR" "WBR"})) (def ^:private bold-tags #{"B" "STRONG" "TH" "H1" "H2" "H3" "H4" "H5" "H6"}) (def ^:private italic-tags #{"I" "EM" "CITE" "VAR" "DFN"}) diff --git a/frontend/test/frontend_tests/tokens/copy_paste_props_test.cljs b/frontend/test/frontend_tests/tokens/copy_paste_props_test.cljs index 86da4c21c0..e582530271 100644 --- a/frontend/test/frontend_tests/tokens/copy_paste_props_test.cljs +++ b/frontend/test/frontend_tests/tokens/copy_paste_props_test.cljs @@ -276,3 +276,20 @@ (t/testing "tokens shared by both shapes are applied" (t/is (= "opacity.50" (get-in result [:applied-tokens :opacity])))))) + +;; --------------------------------------------------------------------------- +;; Ruby readings and warichu notes belong to their own characters +;; --------------------------------------------------------------------------- + +(t/deftest copy-props-leaves-ruby-reading-and-warichu-behind + (let [source (text {:content (content {:text "漢字" + :font-size "20" + :ruby "かんじ" + :ruby-size "third" + :warichu "warichu"})}) + target (text {}) + node (-> (paste-props source target) :content :children first :children first :children first)] + (t/is (= "20" (:font-size node))) + (t/is (= "third" (:ruby-size node))) + (t/is (nil? (:ruby node))) + (t/is (nil? (:warichu node))))) diff --git a/frontend/test/frontend_tests/util_text_clipboard_test.cljs b/frontend/test/frontend_tests/util_text_clipboard_test.cljs index 88b1cbdcfe..a31320866b 100644 --- a/frontend/test/frontend_tests/util_text_clipboard_test.cljs +++ b/frontend/test/frontend_tests/util_text_clipboard_test.cljs @@ -144,6 +144,17 @@ (html->fragment "
a b
cd
"))))) +(t/deftest ruby-keeps-the-base-text + (t/testing "the annotated text is pasted and the reading is dropped" + (t/is (= [(paragraph (run "漢字です"))] + (html->fragment "

漢字かんじです

")))) + + (t/testing "rb bases and rp fallback parentheses" + (t/is (= [(paragraph (run "漢字"))] + (html->fragment + (str "漢(かん)" + "字(じ)")))))) + (t/deftest hidden-content-is-skipped (t/testing "style, script and display:none give no text" (t/is (= [(paragraph (run "shown"))] diff --git a/library/test/builder.test.js b/library/test/builder.test.js index e4c7bf8a37..5dce3340d7 100644 --- a/library/test/builder.test.js +++ b/library/test/builder.test.js @@ -110,6 +110,44 @@ test("create context with file and page", () => { assert.equal(rootShape.id, "00000000-0000-0000-0000-000000000000"); }); +test("create vertical text", () => { + const context = penpot.createBuildContext(); + + const fileId = context.addFile({name: "file 1"}); + const pageId = context.addPage({name: "page 1"}); + const textId = context.addText({ + name: "Vertical text", + x: 10, + y: 20, + width: 100, + height: 200, + content: { + type: "root", + children: [ + { + type: "paragraph-set", + children: [ + { + type: "paragraph", + writingMode: "vertical-rl", + textOrientation: "upright", + children: [{text: "縦書き"}], + }, + ], + }, + ], + }, + }); + + const internalState = context.getInternalState(); + const paragraph = + internalState.files[fileId].data.pagesIndex[pageId].objects[textId].content + .children[0].children[0]; + + assert.equal(paragraph.writingMode, "vertical-rl"); + assert.equal(paragraph.textOrientation, "upright"); +}); + test("create context with color", () => { const context = penpot.createBuildContext();