mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 02:46:20 +00:00
🐛 Fix invalid shape due to missing fonts (#10942)
* 🐛 Fix setting a font-related properties to nil * ✨ Repair already-corrupted text nodes * 🐛 Fix deleted fonts appearing in Recents and as the auto-selected font for new text shapes
This commit is contained in:
parent
2cb8813862
commit
be5d9f9e9c
@ -145,6 +145,29 @@
|
|||||||
|
|
||||||
;; -- Content helpers
|
;; -- Content helpers
|
||||||
|
|
||||||
|
;; Style attrs typed as `::sm/text` in the content schema (see
|
||||||
|
;; app.common.types.shape.text/schema:content): when the key is present its
|
||||||
|
;; value must be a non-blank string, so an explicit nil fails backend
|
||||||
|
;; `validate-shape`. `:key` is likewise a plain `:string`.
|
||||||
|
(def ^:private non-nilable-style-attrs
|
||||||
|
#{:font-family :font-size :font-style :font-weight
|
||||||
|
:direction :text-direction :text-decoration :text-transform :key})
|
||||||
|
|
||||||
|
(defn- remove-nil-style-attrs
|
||||||
|
"Strip nil-valued non-nilable style attrs from every node in a content tree.
|
||||||
|
Repairs content already corrupted with e.g. nil :font-family/:font-weight/
|
||||||
|
:font-style (from an unloaded font) so it can pass the backend schema again."
|
||||||
|
[content]
|
||||||
|
(txt/transform-nodes
|
||||||
|
(fn [node]
|
||||||
|
(reduce (fn [node k]
|
||||||
|
(if (and (contains? node k) (nil? (get node k)))
|
||||||
|
(dissoc node k)
|
||||||
|
node))
|
||||||
|
node
|
||||||
|
non-nilable-style-attrs))
|
||||||
|
content))
|
||||||
|
|
||||||
(defn ensure-valid-text-content
|
(defn ensure-valid-text-content
|
||||||
"Repair structurally incomplete text :content to a canonical
|
"Repair structurally incomplete text :content to a canonical
|
||||||
root -> paragraph-set -> paragraph -> span tree. Returns the
|
root -> paragraph-set -> paragraph -> span tree. Returns the
|
||||||
@ -153,7 +176,11 @@
|
|||||||
A `nil` content, a root with no :children, or a root with an empty
|
A `nil` content, a root with no :children, or a root with an empty
|
||||||
:children vector all fail the backend `validate-shape` schema
|
:children vector all fail the backend `validate-shape` schema
|
||||||
(children must contain at least one paragraph-set). This helper
|
(children must contain at least one paragraph-set). This helper
|
||||||
is the defensive normalizer used by content-commit paths."
|
is the defensive normalizer used by content-commit paths.
|
||||||
|
|
||||||
|
It also scrubs nil-valued non-nilable style attrs (e.g. nil
|
||||||
|
:font-family/:font-weight/:font-style left over from an unloaded font),
|
||||||
|
so already-corrupted content self-heals on the next commit."
|
||||||
[content]
|
[content]
|
||||||
(if (and (map? content)
|
(if (and (map? content)
|
||||||
(= "root" (:type content))
|
(= "root" (:type content))
|
||||||
@ -161,7 +188,7 @@
|
|||||||
(empty? (:children content))))
|
(empty? (:children content))))
|
||||||
(let [base (tc/v2-default-text-content)]
|
(let [base (tc/v2-default-text-content)]
|
||||||
(d/txt-merge base (select-keys content txt/root-attrs)))
|
(d/txt-merge base (select-keys content txt/root-attrs)))
|
||||||
content))
|
(remove-nil-style-attrs content)))
|
||||||
|
|
||||||
(defn- v2-content-has-text?
|
(defn- v2-content-has-text?
|
||||||
[content]
|
[content]
|
||||||
@ -294,7 +321,8 @@
|
|||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [text-state (some->> content ted/import-content)
|
(let [text-state (some->> content ted/import-content)
|
||||||
attrs (merge (txt/get-default-text-attrs)
|
attrs (merge (txt/get-default-text-attrs)
|
||||||
(get-in state [:workspace-global :default-font]))
|
(fonts/valid-default-font
|
||||||
|
(get-in state [:workspace-global :default-font])))
|
||||||
editor (cond-> (ted/create-editor-state text-state decorator)
|
editor (cond-> (ted/create-editor-state text-state decorator)
|
||||||
(and (nil? content) (some? attrs))
|
(and (nil? content) (some? attrs))
|
||||||
(ted/update-editor-current-block-data attrs))]
|
(ted/update-editor-current-block-data attrs))]
|
||||||
@ -1122,7 +1150,8 @@
|
|||||||
;; Avoid swapping the global store when the computed styles are unchanged,
|
;; Avoid swapping the global store when the computed styles are unchanged,
|
||||||
;; otherwise we can end up in store->rerender->selectionchange loops.
|
;; otherwise we can end up in store->rerender->selectionchange loops.
|
||||||
(let [merged-styles (merge (txt/get-default-text-attrs)
|
(let [merged-styles (merge (txt/get-default-text-attrs)
|
||||||
(get-in state [:workspace-global :default-font])
|
(fonts/valid-default-font
|
||||||
|
(get-in state [:workspace-global :default-font]))
|
||||||
new-styles)
|
new-styles)
|
||||||
prev (get-in state [:workspace-v2-editor-state id])]
|
prev (get-in state [:workspace-v2-editor-state id])]
|
||||||
(if (= merged-styles prev)
|
(if (= merged-styles prev)
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
(ns app.main.data.workspace.texts-v3
|
(ns app.main.data.workspace.texts-v3
|
||||||
(:require
|
(:require
|
||||||
[app.common.types.text :as txt]
|
[app.common.types.text :as txt]
|
||||||
|
[app.main.fonts :as fonts]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(defn v3-update-text-editor-styles
|
(defn v3-update-text-editor-styles
|
||||||
@ -15,6 +16,7 @@
|
|||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [merged-styles (merge (txt/get-default-text-attrs)
|
(let [merged-styles (merge (txt/get-default-text-attrs)
|
||||||
(get-in state [:workspace-global :default-font])
|
(fonts/valid-default-font
|
||||||
|
(get-in state [:workspace-global :default-font]))
|
||||||
new-styles)]
|
new-styles)]
|
||||||
(update-in state [:workspace-wasm-editor-styles id] (fnil merge {}) merged-styles)))))
|
(update-in state [:workspace-wasm-editor-styles id] (fnil merge {}) merged-styles)))))
|
||||||
|
|||||||
@ -73,6 +73,25 @@
|
|||||||
(defn get-font-data [id]
|
(defn get-font-data [id]
|
||||||
(get @fontsdb id))
|
(get @fontsdb id))
|
||||||
|
|
||||||
|
(defn installed?
|
||||||
|
"True when a font with `font-id` is currently registered in `fontsdb`."
|
||||||
|
[font-id]
|
||||||
|
(contains? @fontsdb font-id))
|
||||||
|
|
||||||
|
(defn valid-default-font
|
||||||
|
"Return the remembered default-font attrs only when its font is still
|
||||||
|
installed in `fontsdb`; otherwise nil, so callers fall back to the built-in
|
||||||
|
default text attrs (Source Sans Pro).
|
||||||
|
|
||||||
|
A new text shape inherits the last-used font (`[:workspace-global
|
||||||
|
:default-font]`). If that font is gone (deleted, unavailable in the current
|
||||||
|
team, or never resolved), its attrs carry a missing/nil font-family, which
|
||||||
|
produces content that fails the backend `validate-shape` schema."
|
||||||
|
[default-font]
|
||||||
|
(when (and (some? default-font)
|
||||||
|
(installed? (:font-id default-font)))
|
||||||
|
default-font))
|
||||||
|
|
||||||
(defn find-font-data [data]
|
(defn find-font-data [data]
|
||||||
(d/seek
|
(d/seek
|
||||||
(fn [font]
|
(fn [font]
|
||||||
|
|||||||
@ -86,9 +86,11 @@
|
|||||||
canvas-node
|
canvas-node
|
||||||
(mf/ref-val canvas-ref)
|
(mf/ref-val canvas-ref)
|
||||||
|
|
||||||
;; Gets the default font from the workspace refs.
|
;; Gets the default font from the workspace refs. Ignore it when the
|
||||||
|
;; remembered font is no longer installed, falling back to the built-in
|
||||||
|
;; default so a new text shape never inherits a missing font.
|
||||||
default-font
|
default-font
|
||||||
(deref refs/default-font)
|
(fonts/valid-default-font (deref refs/default-font))
|
||||||
|
|
||||||
style-defaults
|
style-defaults
|
||||||
(styles/get-style-defaults
|
(styles/get-style-defaults
|
||||||
|
|||||||
@ -178,15 +178,24 @@
|
|||||||
flist (mf/use-ref)
|
flist (mf/use-ref)
|
||||||
input (mf/use-ref)
|
input (mf/use-ref)
|
||||||
|
|
||||||
fonts (mf/deref fonts/fonts)
|
all-fonts (mf/deref fonts/fonts)
|
||||||
fonts (mf/with-memo [state fonts]
|
fonts (mf/with-memo [state all-fonts]
|
||||||
(filter-fonts state fonts))
|
(filter-fonts state all-fonts))
|
||||||
|
|
||||||
|
;; Ids currently installed in fontsdb. Recent fonts that are no longer
|
||||||
|
;; available (deleted, or belonging to another team) must be hidden:
|
||||||
|
;; selecting one applies a missing/nil font-family and corrupts the
|
||||||
|
;; text content (fails the backend `validate-shape` schema).
|
||||||
|
installed-ids (mf/with-memo [all-fonts]
|
||||||
|
(into #{} (map :id) all-fonts))
|
||||||
|
|
||||||
sprite-status (:status (mf/deref fonts/preview-sprite))
|
sprite-status (:status (mf/deref fonts/preview-sprite))
|
||||||
|
|
||||||
recent-fonts (mf/deref refs/recent-fonts)
|
recent-fonts (mf/deref refs/recent-fonts)
|
||||||
recent-fonts (mf/with-memo [state recent-fonts]
|
recent-fonts (mf/with-memo [state recent-fonts installed-ids]
|
||||||
(filter-fonts state recent-fonts))
|
(->> recent-fonts
|
||||||
|
(filter #(contains? installed-ids (:id %)))
|
||||||
|
(filter-fonts state)))
|
||||||
|
|
||||||
|
|
||||||
full-size? (boolean (and full-size show-recent))
|
full-size? (boolean (and full-size show-recent))
|
||||||
@ -332,12 +341,19 @@
|
|||||||
(fn [new-font-id]
|
(fn [new-font-id]
|
||||||
(let [{:keys [family] :as font} (get fonts new-font-id)
|
(let [{:keys [family] :as font} (get fonts new-font-id)
|
||||||
{:keys [id name weight style]} (fonts/get-default-variant font)]
|
{:keys [id name weight style]} (fonts/get-default-variant font)]
|
||||||
(on-change {:font-id new-font-id
|
;; Guard against a font that is not present in fontsdb (unloaded
|
||||||
:font-family family
|
;; custom font, deleted font, shared library not yet resolved).
|
||||||
:font-variant-id (or id name)
|
;; Without it `family`/`weight`/`style` come back nil and get written
|
||||||
:font-weight weight
|
;; onto the text spans, producing content that fails the backend
|
||||||
:font-style style})
|
;; `validate-shape` schema (`:font-family`/`:font-weight`/`:font-style`
|
||||||
(mf/set-ref-val! last-font font))))
|
;; must be non-blank strings when present).
|
||||||
|
(when (and (some? font) (some? family))
|
||||||
|
(on-change {:font-id new-font-id
|
||||||
|
:font-family family
|
||||||
|
:font-variant-id (or id name)
|
||||||
|
:font-weight weight
|
||||||
|
:font-style style})
|
||||||
|
(mf/set-ref-val! last-font font)))))
|
||||||
|
|
||||||
on-font-size-change
|
on-font-size-change
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|||||||
@ -708,7 +708,13 @@
|
|||||||
(defn apply-styles-to-selection
|
(defn apply-styles-to-selection
|
||||||
[attrs use-shape-fn set-shape-text-content-fn]
|
[attrs use-shape-fn set-shape-text-content-fn]
|
||||||
(when (wasm/ready?)
|
(when (wasm/ready?)
|
||||||
(let [shape-id (text-editor-get-active-shape-id)
|
(let [;; Drop nil-valued attrs so they are never merged onto text spans.
|
||||||
|
;; The DOM editor path strips these in `attrs->styles`; the WASM merge
|
||||||
|
;; here (`apply-attrs-to-paragraph`) does not, so an unresolved attr
|
||||||
|
;; (e.g. nil :font-family/:font-weight/:font-style from an unloaded
|
||||||
|
;; font) would corrupt the span and fail the backend schema.
|
||||||
|
attrs (into {} (remove (comp nil? val)) attrs)
|
||||||
|
shape-id (text-editor-get-active-shape-id)
|
||||||
selection (text-editor-get-selection)]
|
selection (text-editor-get-selection)]
|
||||||
|
|
||||||
(when (and shape-id selection)
|
(when (and shape-id selection)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user