mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
🐛 Join text spans left with the same style after range styling
Styling a range splits spans at its ends, and nothing joined them back, so a text kept invisible boundaries between spans with equal styles. Two CJK features depend on those boundaries: tate-chu-yoko `all` combines per span, so "2" + "027" rendered as two upright blocks, and a ruby reading or warichu note over a range that crossed a hidden boundary was rejected. `merge-equal-spans` joins neighbouring spans of a paragraph whose styles match, counting a missing attr as its default value. Spans with a reading or a warichu note are never joined: each one is its own group. Both places that split spans use it: `update-text-range-attrs` (plugin API) and `apply-attrs-to-paragraph` (WASM editor styling). AI-assisted-by: claude-opus-5-5
This commit is contained in:
parent
f99d7d22b8
commit
996f498e9b
@ -247,6 +247,41 @@
|
||||
item))
|
||||
root)))
|
||||
|
||||
(defn- span-style
|
||||
"The text node attrs of `span` that differ from their default value, with
|
||||
legacy numeric values as strings."
|
||||
[span]
|
||||
(reduce (fn [style attr]
|
||||
(let [value (get span attr)
|
||||
value (if (number? value) (str value) value)]
|
||||
(if (or (nil? value)
|
||||
(and (or (string? value) (coll? value)) (empty? value))
|
||||
(= value (get default-text-attrs attr))
|
||||
(= value (get cjk/span-attr-defaults attr)))
|
||||
style
|
||||
(assoc style attr value))))
|
||||
{}
|
||||
text-node-attrs))
|
||||
|
||||
(defn- mergeable-spans?
|
||||
[a b]
|
||||
(and (not (cjk/annotated-span? a))
|
||||
(not (cjk/annotated-span? b))
|
||||
(= (span-style a) (span-style b))))
|
||||
|
||||
(defn merge-equal-spans
|
||||
"Joins neighbouring `spans` of a paragraph that have the same style, so
|
||||
span boundaries only exist where the style changes. Spans with a ruby
|
||||
reading or a warichu note are never joined: each one is its own group."
|
||||
[spans]
|
||||
(reduce (fn [result span]
|
||||
(let [prev (peek result)]
|
||||
(if (and (some? prev) (mergeable-spans? prev span))
|
||||
(conj (pop result) (update prev :text str (:text span)))
|
||||
(conj result span))))
|
||||
[]
|
||||
spans))
|
||||
|
||||
(defn update-text-content
|
||||
[shape pred-fn update-fn attrs]
|
||||
(let [update-attrs-fn #(update-fn % attrs)
|
||||
|
||||
@ -540,3 +540,25 @@
|
||||
(t/is (< (:y first-normal) (:y last-normal)))
|
||||
(t/is (> (:x tone) (:x last-normal)))
|
||||
(t/is (< (:y tone) (:y last-normal)))))
|
||||
|
||||
(t/deftest merge-equal-spans-joins-neighbours-with-the-same-style
|
||||
(t/testing "a missing attr equals its default, CJK defaults included"
|
||||
(t/is (= [{:text "2027" :text-combine-upright "all"}]
|
||||
(cttx/merge-equal-spans [{:text "2" :text-combine-upright "all"}
|
||||
{:text "027" :text-combine-upright "all"}])))
|
||||
(t/is (= [{:text "abcd" :font-size 14}]
|
||||
(cttx/merge-equal-spans [{:text "ab" :font-size 14}
|
||||
{:text "cd" :text-combine-upright "none" :ruby nil}]))))
|
||||
|
||||
(t/testing "spans with different styles stay apart"
|
||||
(t/is (= [{:text "ab"} {:text "cd" :font-weight "700"} {:text "ef"}]
|
||||
(cttx/merge-equal-spans [{:text "ab"} {:text "cd" :font-weight "700"} {:text "ef"}]))))
|
||||
|
||||
(t/testing "readings and warichu notes are never joined, even when equal"
|
||||
(let [spans [{:text "人" :ruby "hito"} {:text "々" :ruby "hito"} {:text "x"}]]
|
||||
(t/is (= spans (cttx/merge-equal-spans spans))))
|
||||
(let [spans [{:text "注" :warichu "warichu"} {:text "釈" :warichu "warichu"}]]
|
||||
(t/is (= spans (cttx/merge-equal-spans spans)))))
|
||||
|
||||
(t/testing "no spans give no spans"
|
||||
(t/is (= [] (cttx/merge-equal-spans [])))))
|
||||
|
||||
@ -595,14 +595,17 @@
|
||||
|
||||
(defn update-text-range-attrs
|
||||
"Applies `attrs` to the characters [start, end) of a text shape. A range
|
||||
that cuts a ruby or warichu span covers the whole span."
|
||||
that cuts a ruby or warichu span covers the whole span. Spans left with
|
||||
the same style as their neighbour are joined back."
|
||||
[shape start end attrs]
|
||||
(let [content (decorate-range-info (:content shape))
|
||||
[start end] (annotated-span-bounds content start end)
|
||||
new-content (-> content
|
||||
(split-content-at start)
|
||||
(split-content-at end)
|
||||
(update-content-range start end attrs))]
|
||||
new-content (->> (-> content
|
||||
(split-content-at start)
|
||||
(split-content-at end)
|
||||
(update-content-range start end attrs))
|
||||
(txt/transform-nodes txt/is-paragraph-node?
|
||||
#(d/update-when % :children txt/merge-equal-spans)))]
|
||||
(assoc shape :content new-content)))
|
||||
|
||||
(defn update-text-range
|
||||
|
||||
@ -975,7 +975,8 @@
|
||||
[start end]))
|
||||
|
||||
(defn apply-attrs-to-paragraph
|
||||
"Apply `styles` (attrs map, or a fn per span) within [sel-start, sel-end), splitting spans."
|
||||
"Apply `styles` (attrs map, or a fn per span) within [sel-start, sel-end),
|
||||
splitting spans and joining back the ones left with their neighbour's style."
|
||||
[para sel-start sel-end styles]
|
||||
(let [[sel-start sel-end] (annotation-style-range para sel-start sel-end styles)
|
||||
spans (:children para)
|
||||
@ -1009,7 +1010,7 @@
|
||||
(recur (rest spans) span-end
|
||||
(-> acc
|
||||
(into (keep identity [before selected after])))))))))]
|
||||
(assoc para :children result)))
|
||||
(assoc para :children (txt/merge-equal-spans result))))
|
||||
|
||||
(defn selection-fills
|
||||
"The selection's fills: shared vector if all spans match, `:multiple` if not, nil if empty."
|
||||
|
||||
@ -920,3 +920,31 @@
|
||||
(rx/subs! #(swap! events conj %)))
|
||||
(let [updates (filter #(= ::dwsh/update-shapes (ptk/type %)) @events)]
|
||||
(t/is (= 1 (count updates))))))
|
||||
|
||||
(defn- spans-of
|
||||
[shape]
|
||||
(->> (:content shape) (txt/node-seq txt/is-text-node?) (mapv #(select-keys % [:text :ruby :text-combine-upright]))))
|
||||
|
||||
(defn- one-span-text
|
||||
[text]
|
||||
{:type "root"
|
||||
:children [{:type "paragraph-set"
|
||||
:children [{:type "paragraph"
|
||||
:children [{:text text :font-size "20"}]}]}]})
|
||||
|
||||
(t/deftest update-text-range-joins-spans-left-with-the-same-style
|
||||
(t/testing "the same style set over overlapping ranges ends in one span"
|
||||
(let [shape (-> {:content (one-span-text "ab123c")}
|
||||
(dwt/update-text-range-attrs 3 5 {:text-combine-upright "all"})
|
||||
(dwt/update-text-range-attrs 2 5 {:text-combine-upright "all"}))]
|
||||
(t/is (= [{:text "ab"} {:text "123" :text-combine-upright "all"} {:text "c"}]
|
||||
(spans-of shape)))))
|
||||
|
||||
(t/testing "removing per-character readings lets a group reading cover the word"
|
||||
(let [shape (reduce (fn [shape [idx reading]]
|
||||
(dwt/update-text-range-attrs shape idx (inc idx) {:ruby reading}))
|
||||
{:content (one-span-text "一盅兩件")}
|
||||
(map-indexed vector ["jat1" "zung1" "loeng5" "gin6"]))
|
||||
shape (dwt/update-text-range-attrs shape 0 4 {:ruby nil})]
|
||||
(t/is (= [{:text "一盅兩件"}] (spans-of shape)))
|
||||
(t/is (dwt/single-span-range? (:content shape) 0 4)))))
|
||||
|
||||
@ -100,3 +100,15 @@
|
||||
(t/testing "a collapsed selection has no selected spans"
|
||||
(let [c (content [(para [(span "hello" [red])])])]
|
||||
(t/is (nil? (text-editor/selection-fills c (selection 0 2 0 2)))))))
|
||||
|
||||
(t/deftest apply-attrs-joins-spans-left-with-the-same-style
|
||||
(t/testing "extending a style over its neighbour leaves one span"
|
||||
(let [para {:children [{:text "ab"} {:text "12" :text-combine-upright "all"} {:text "3c"}]}
|
||||
result (apply-attrs-to-paragraph para 4 5 {:text-combine-upright "all"})]
|
||||
(t/is (= [{:text "ab"} {:text "123" :text-combine-upright "all"} {:text "c"}]
|
||||
(:children result)))))
|
||||
|
||||
(t/testing "resetting a style to its default rejoins the text"
|
||||
(let [para {:children [{:text "ab"} {:text "cd" :text-emphasis "filled-dot"} {:text "ef"}]}
|
||||
result (apply-attrs-to-paragraph para 2 4 {:text-emphasis "none"})]
|
||||
(t/is (= [{:text "abcdef"}] (:children result))))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user