♻️ Migrate fo-text and html-text renderers to modern component syntax (#9385)

Step toward issue #9260 (incremental migration of legacy UI
components to the modern `*`-suffixed syntax, removing the per-render
JS-to-Clojure props conversion overhead).

Twin namespaces with parallel structure: each defines six components
that drive a recursive text rendering pass over the editor's content
tree (root -> paragraph-set -> paragraph -> node -> text). Both files
were uniformly legacy: every component carried `::mf/wrap-props
false` and read its props with `(obj/get props "key")`. None had
`::mf/register`, `unchecked-get` or `obj/merge!`, so they qualify as
clean Case-A migrations.

frontend/src/app/main/ui/shapes/text/fo_text.cljs (6 components)
----------------------------------------------------------------

- `render-text`           -> `render-text*`
- `render-root`           -> `render-root*`
- `render-paragraph-set`  -> `render-paragraph-set*`
- `render-paragraph`      -> `render-paragraph*`
- `render-node`           -> `render-node*`     (forward-props case,
                                                 see below)
- `text-shape`            -> `text-shape*`      (`::mf/forward-ref`
                                                 preserved)

The four leaf components switch from `[props]` + per-key
`(obj/get props "key")` to standard destructuring. `text-shape`
already used destructuring under `::mf/props :obj`; that legacy
metadata is dropped because the modern `*` form handles props
automatically. Its single `::mf/forward-ref true` is kept per the
prompt's "preserve forward-ref" rule.

`render-node` is the recursive driver. It needs to forward all of
its incoming props to the matched paragraph-* / text component and
then to a child `render-node*` after overriding `:node`, `:index`
and `:key`. The migrated form uses `::mf/props :obj` together with
`{:keys [node] :as props}` to keep the JS-object props symbol
available, and `(mf/spread-props props {…})` replaces the previous
`obj/clone` + `obj/set!` chain.

`app.util.object` is no longer required by this namespace and the
`(:require ... [app.util.object :as obj] ...)` line is removed.

frontend/src/app/main/ui/shapes/text/html_text.cljs (6 components)
-----------------------------------------------------------------

Identical six-component shape as `fo_text.cljs`, plus a `code?`
flag threaded through every component to switch the rendering path
between regular shapes and code-style shapes.

- `render-text`           -> `render-text*`
- `render-root`           -> `render-root*`
- `render-paragraph-set`  -> `render-paragraph-set*`
- `render-paragraph`      -> `render-paragraph*`
- `render-node`           -> `render-node*`     (same forward-props
                                                 treatment as above,
                                                 plus `is-code` in
                                                 the spread)
- `text-shape`            -> `text-shape*`      (`::mf/forward-ref`
                                                 preserved)

The `code?` boolean prop is renamed to `is-code` per the migration
prompt's "?-suffixed boolean -> `is-` prefix" rule. The rename is
applied at every read site (5 components) and at the `text-shape*`
internal call to `render-node*`, so the prop is consistent inside
the namespace.

`app.util.object` is no longer required by this namespace either
and the corresponding `:require` line is dropped.

External call sites (3 files, 4 sites)
--------------------------------------

- `frontend/src/app/main/ui/shapes/text.cljs` - the legacy
  text-shape wrapper (intentionally kept legacy in this PR because
  it dispatches to `svg/text-shape`, which is still being touched by
  the in-flight PR #9016) now calls `[:> fo/text-shape* props]`.
  The `props` symbol is the wrapper's incoming JS-object; modern
  destructured components accept JS-object props at the call site
  via `[:>` so this works unchanged.

- `frontend/src/app/util/code_gen/markup_html.cljs` -
  `(mf/element text/text-shape #js {:shape shape :code? true})`
  becomes
  `(mf/element text/text-shape* #js {:shape shape :is-code true})`
  (component renamed and the `code?` JS key updated to match the
  renamed prop).

- `frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs`
  - `[:& html/text-shape {…}]` -> `[:> html/text-shape* {…}]`.

Behavior preserved verbatim
---------------------------

Same render output, same forward-ref forwarding semantics, same
recursive children-by-index keying, same default `:dir "auto"` on
`render-paragraph*`. The visible-prop changes are only the `code?`
-> `is-code` rename, all driven from this namespace and its single
caller in `markup_html.cljs`.

Github #9260

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
This commit is contained in:
FairyPiggyDev 2026-05-07 09:03:51 -04:00 committed by GitHub
parent defeeab054
commit fa06efa84d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 85 additions and 131 deletions

View File

@ -43,4 +43,4 @@
;; will give a tainted canvas error because the `foreignObject` cannot be ;; will give a tainted canvas error because the `foreignObject` cannot be
;; rendered. ;; rendered.
(and (nil? position-data) (or is-component? is-render?)) (and (nil? position-data) (or is-component? is-render?))
[:> fo/text-shape props]))) [:> fo/text-shape* props])))

View File

@ -11,73 +11,54 @@
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.types.color :as cc] [app.common.types.color :as cc]
[app.main.ui.shapes.text.styles :as sts] [app.main.ui.shapes.text.styles :as sts]
[app.util.object :as obj]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc render-text (mf/defc render-text*
{::mf/wrap-props false} [{:keys [node parent shape]}]
[props] (let [text (:text node)
(let [node (obj/get props "node") style (if (= text "")
parent (obj/get props "parent") (sts/generate-text-styles shape parent)
shape (obj/get props "shape") (sts/generate-text-styles shape node))]
text (:text node)
style (if (= text "")
(sts/generate-text-styles shape parent)
(sts/generate-text-styles shape node))]
[:span.text-node {:style style} [:span.text-node {:style style}
(if (= text "") "\u00A0" text)])) (if (= text "") "\u00A0" text)]))
(mf/defc render-root (mf/defc render-root*
{::mf/wrap-props false} [{:keys [node children shape]}]
[props] (let [style (sts/generate-root-styles shape node)]
(let [node (obj/get props "node")
children (obj/get props "children")
shape (obj/get props "shape")
style (sts/generate-root-styles shape node)]
[:div.root.rich-text [:div.root.rich-text
{:style style {:style style
:xmlns "http://www.w3.org/1999/xhtml"} :xmlns "http://www.w3.org/1999/xhtml"}
children])) children]))
(mf/defc render-paragraph-set (mf/defc render-paragraph-set*
{::mf/wrap-props false} [{:keys [children shape]}]
[props] (let [style (sts/generate-paragraph-set-styles shape)]
(let [children (obj/get props "children")
shape (obj/get props "shape")
style (sts/generate-paragraph-set-styles shape)]
[:div.paragraph-set {:style style} children])) [:div.paragraph-set {:style style} children]))
(mf/defc render-paragraph (mf/defc render-paragraph*
{::mf/wrap-props false} [{:keys [node children shape]}]
[props] (let [style (sts/generate-paragraph-styles shape node)
(let [node (obj/get props "node") dir (:text-direction node "auto")]
shape (obj/get props "shape")
children (obj/get props "children")
style (sts/generate-paragraph-styles shape node)
dir (:text-direction node "auto")]
[:p.paragraph {:style style :dir dir} children])) [:p.paragraph {:style style :dir dir} children]))
;; -- Text nodes ;; -- Text nodes
(mf/defc render-node (mf/defc render-node*
{::mf/wrap-props false} {::mf/props :obj}
[props] [{:keys [node] :as props}]
(let [{:keys [type text children]} (obj/get props "node")] (let [{:keys [type text children]} node]
(if (string? text) (if (string? text)
[:> render-text props] [:> render-text* props]
(let [component (case type (let [component (case type
"root" render-root "root" render-root*
"paragraph-set" render-paragraph-set "paragraph-set" render-paragraph-set*
"paragraph" render-paragraph "paragraph" render-paragraph*
nil)] nil)]
(when component (when component
[:> component props [:> component props
(for [[index node] (d/enumerate children)] (for [[index child-node] (d/enumerate children)]
(let [props (-> (obj/clone props) [:> render-node*
(obj/set! "node" node) (mf/spread-props props {:node child-node :index index :key index})])])))))
(obj/set! "index" index)
(obj/set! "key" index))]
[:> render-node props]))])))))
(defn- next-color (defn- next-color
"Given a set of colors try to get a color not yet used" "Given a set of colors try to get a color not yet used"
@ -168,9 +149,8 @@
[colors color-mapping color-mapping-inverse])) [colors color-mapping color-mapping-inverse]))
(mf/defc text-shape (mf/defc text-shape*
{::mf/props :obj {::mf/forward-ref true}
::mf/forward-ref true}
[{:keys [shape grow-type]} ref] [{:keys [shape grow-type]} ref]
(let [transform (gsh/transform-str shape) (let [transform (gsh/transform-str shape)
id (dm/get-prop shape :id) id (dm/get-prop shape :id)
@ -196,6 +176,6 @@
;; `background-clip` ;; `background-clip`
[:style ".text-node { background-clip: text; [:style ".text-node { background-clip: text;
-webkit-background-clip: text; }"] -webkit-background-clip: text; }"]
[:& render-node {:index 0 [:> render-node* {:index 0
:shape shape :shape shape
:node content}]])) :node content}]]))

View File

@ -10,99 +10,73 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.text :as legacy.txt] [app.common.text :as legacy.txt]
[app.main.ui.shapes.text.styles :as sts] [app.main.ui.shapes.text.styles :as sts]
[app.util.object :as obj]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc render-text (mf/defc render-text*
{::mf/wrap-props false} [{:keys [node parent shape is-code]}]
[props] (let [text (:text node)
(let [node (obj/get props "node") style (if (= text "")
parent (obj/get props "parent") (sts/generate-text-styles shape parent)
shape (obj/get props "shape") (sts/generate-text-styles shape node))
code? (obj/get props "code?") class (when is-code (:$id node))]
text (:text node)
style (if (= text "")
(sts/generate-text-styles shape parent)
(sts/generate-text-styles shape node))
class (when code? (:$id node))]
[:span.text-node {:style style :class class} [:span.text-node {:style style :class class}
(if (= text "") "\u00A0" text)])) (if (= text "") "\u00A0" text)]))
(mf/defc render-root (mf/defc render-root*
{::mf/wrap-props false} [{:keys [node children shape is-code]}]
[props] (let [style (sts/generate-root-styles shape node is-code)
(let [node (obj/get props "node") class (when is-code (:$id node))]
children (obj/get props "children")
shape (obj/get props "shape")
code? (obj/get props "code?")
style (sts/generate-root-styles shape node code?)
class (when code? (:$id node))]
[:div.root.rich-text [:div.root.rich-text
{:style style {:style style
:class class :class class
:xmlns "http://www.w3.org/1999/xhtml"} :xmlns "http://www.w3.org/1999/xhtml"}
children])) children]))
(mf/defc render-paragraph-set (mf/defc render-paragraph-set*
{::mf/wrap-props false} [{:keys [node children shape is-code]}]
[props] (let [style (when-not is-code (sts/generate-paragraph-set-styles shape))
(let [node (obj/get props "node") class (when is-code (:$id node))]
children (obj/get props "children")
shape (obj/get props "shape")
code? (obj/get props "code?")
style (when-not code? (sts/generate-paragraph-set-styles shape))
class (when code? (:$id node))]
[:div.paragraph-set {:style style :class class} children])) [:div.paragraph-set {:style style :class class} children]))
(mf/defc render-paragraph (mf/defc render-paragraph*
{::mf/wrap-props false} [{:keys [node children shape is-code]}]
[props] (let [style (when-not is-code (sts/generate-paragraph-styles shape node))
(let [node (obj/get props "node") class (when is-code (:$id node))
shape (obj/get props "shape") dir (:text-direction node "auto")]
children (obj/get props "children")
code? (obj/get props "code?")
style (when-not code? (sts/generate-paragraph-styles shape node))
class (when code? (:$id node))
dir (:text-direction node "auto")]
[:p.paragraph {:style style :dir dir :class class} children])) [:p.paragraph {:style style :dir dir :class class} children]))
;; -- Text nodes ;; -- Text nodes
(mf/defc render-node (mf/defc render-node*
{::mf/wrap-props false} {::mf/props :obj}
[props] [{:keys [node is-code] :as props}]
(let [{:keys [type text children] :as parent} (obj/get props "node") (let [{:keys [type text children] :as parent} node]
code? (obj/get props "code?")]
(if (string? text) (if (string? text)
[:> render-text props] [:> render-text* props]
(let [component (case type (let [component (case type
"root" render-root "root" render-root*
"paragraph-set" render-paragraph-set "paragraph-set" render-paragraph-set*
"paragraph" render-paragraph "paragraph" render-paragraph*
nil)] nil)]
(when component (when component
[:> component props [:> component props
(for [[index node] (d/enumerate children)] (for [[index child-node] (d/enumerate children)]
(let [props (-> (obj/clone props) [:> render-node*
(obj/set! "node" node) (mf/spread-props props
(obj/set! "parent" parent) {:node child-node
(obj/set! "index" index) :parent parent
(obj/set! "key" index) :index index
(obj/set! "code?" code?))] :key index
[:> render-node props]))]))))) :is-code is-code})])])))))
(mf/defc text-shape (mf/defc text-shape*
{::mf/wrap-props false {::mf/forward-ref true}
::mf/forward-ref true} [{:keys [shape grow-type is-code]} ref]
[props ref] (let [{:keys [id x y width height content]} shape
(let [shape (obj/get props "shape")
grow-type (obj/get props "grow-type")
code? (obj/get props "code?")
{:keys [id x y width height content]} shape
content (if code? (legacy.txt/index-content content) content) content (if is-code (legacy.txt/index-content content) content)
style style
(when-not code? (when-not is-code
#js {:position "fixed" #js {:position "fixed"
:left 0 :left 0
:top 0 :top 0
@ -118,10 +92,10 @@
:style style} :style style}
;; We use a class here because react has a bug that won't use the appropriate selector for ;; We use a class here because react has a bug that won't use the appropriate selector for
;; `background-clip` ;; `background-clip`
(when (not code?) (when (not is-code)
[:style ".text-node { background-clip: text; [:style ".text-node { background-clip: text;
-webkit-background-clip: text; }"]) -webkit-background-clip: text; }"])
[:& render-node {:index 0 [:> render-node* {:index 0
:shape shape :shape shape
:node content :node content
:code? code?}]])) :is-code is-code}]]))

View File

@ -130,10 +130,10 @@
(when (some? node) (when (some? node)
(on-update shape node))))] (on-update shape node))))]
[:& html/text-shape {:key (str "shape-" (:id shape)) [:> html/text-shape* {:key (str "shape-" (:id shape))
:ref handle-update :ref handle-update
:shape shape :shape shape
:grow-type (:grow-type shape)}])) :grow-type (:grow-type shape)}]))
(defn text-properties-equal? (defn text-properties-equal?
[shape other] [shape other]

View File

@ -38,7 +38,7 @@
indent)) indent))
(cfh/text-shape? shape) (cfh/text-shape? shape)
(let [text-shape-html (rds/renderToStaticMarkup (mf/element text/text-shape #js {:shape shape :code? true})) (let [text-shape-html (rds/renderToStaticMarkup (mf/element text/text-shape* #js {:shape shape :is-code true}))
text-shape-html (str/replace text-shape-html #"style\s*=\s*[\"'][^\"']*[\"']" "")] text-shape-html (str/replace text-shape-html #"style\s*=\s*[\"'][^\"']*[\"']" "")]
(dm/fmt "%<div class=\"%\">\n%\n%</div>" (dm/fmt "%<div class=\"%\">\n%\n%</div>"
indent indent