diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc index cd96d5faa9..f452c7b66e 100644 --- a/common/src/app/common/schema.cljc +++ b/common/src/app/common/schema.cljc @@ -842,38 +842,6 @@ choices))] {:pred pred}))}) -;; (register! -;; {:type ::inst -;; :pred tm/instant? -;; :type-properties -;; {:title "inst" -;; :description "Satisfies Inst protocol" -;; :error/message "should be an instant" -;; :gen/gen (->> (sg/small-int :min 0 :max 100000) -;; (sg/fmap (fn [v] (tm/parse-inst v)))) - -;; :decode/string tm/parse-inst -;; :encode/string tm/format-inst -;; :decode/json tm/parse-inst -;; :encode/json tm/format-inst -;; ::oapi/type "string" -;; ::oapi/format "iso"}}) - -;; (register! -;; {:type ::timestamp -;; :pred tm/instant? -;; :type-properties -;; {:title "inst" -;; :description "Satisfies Inst protocol, the same as ::inst but encodes to epoch" -;; :error/message "should be an instant" -;; :gen/gen (->> (sg/small-int) -;; (sg/fmap (fn [v] (tm/parse-inst v)))) -;; :decode/string tm/parse-inst -;; :encode/string inst-ms -;; :decode/json tm/parse-inst -;; :encode/json inst-ms -;; ::oapi/type "string" -;; ::oapi/format "number"}}) #?(:clj (register! @@ -951,7 +919,7 @@ :pred #(and (string? %) (not (str/blank? %))) :property-pred (fn [{:keys [min max] :as props}] - (if (seq props) + (if (or min max) (fn [value] (let [size (count value)] (cond diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 4ba774358d..7b92b188da 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -65,7 +65,7 @@ RUN set -eux; \ FROM base AS setup-jvm -ENV CLOJURE_VERSION=1.12.2.1565 +ENV CLOJURE_VERSION=1.12.3.1577 RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 757055af41..481ae826aa 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -12,7 +12,7 @@ http { sendfile on; tcp_nopush on; tcp_nodelay on; - keepalive_timeout 65; + keepalive_timeout 0; types_hash_max_size 2048; server_tokens off; @@ -223,16 +223,6 @@ http { add_header X-Cache-Status $upstream_cache_status; } - location ~ ^/js/config.js$ { - add_header Cache-Control "no-store, no-cache, max-age=0" always; - } - - location ~* \.(js|css|jpg|svg|png|mjs|map)$ { - # We set no cache only on devenv - add_header Cache-Control "no-store, no-cache, max-age=0" always; - # add_header Cache-Control "max-age=604800" always; # 7 days - } - location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) { } @@ -240,9 +230,10 @@ http { return 301 " /404"; } - add_header Last-Modified $date_gmt; - add_header Cache-Control "no-store, no-cache, max-age=0" always; - if_modified_since off; + add_header Cache-Control "no-store"; + add_header Connection close always; + # This header is what we need to use on prod + # add_header Cache-Control "public, must-revalidate, max-age=0"; try_files $uri /index.html$is_args$args /index.html =404; } } diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index 440190f26a..e1bbebbacb 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -499,7 +499,7 @@ test.describe("Tokens: Tokens Tab", () => { await valueField.fill(""); // TODO: We need to fix this translation await expect( - tokensUpdateCreateModal.getByText("Empty field"), + tokensUpdateCreateModal.getByText("Token value cannot be empty"), ).toBeVisible(); await valueSaturationSelector.click({ position: { x: 50, y: 50 } }); await expect(valueField).toHaveValue(/^#[A-Fa-f\d]+$/); diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs index 4862b5a7e6..0d94379e85 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs @@ -33,6 +33,7 @@ [app.util.object :as obj] [app.util.text.content :as content] [app.util.text.content.styles :as styles] + [cuerdas.core :as str] [rumext.v2 :as mf])) (defn get-contrast-color [background-color] @@ -268,7 +269,12 @@ "bottom" "flex-end" nil)) -;; +(defn- font-family-from-font-id [font-id] + (if (str/includes? font-id "gfont-noto-sans") + (let [lang (str/replace font-id #"gfont\-noto\-sans\-" "")] + (if (>= (count lang) 3) (str/capital lang) (str/upper lang))) + "Noto Color Emoji")) + ;; Text Editor Wrapper ;; This is an SVG element that wraps the HTML editor. ;; @@ -281,6 +287,10 @@ (let [shape-id (dm/get-prop shape :id) modifiers (dm/get-in modifiers [shape-id :modifiers]) + fallback-fonts (wasm.api/fonts-from-text-content (:content shape) false) + fallback-families (map (fn [font] + (font-family-from-font-id (:font-id font))) fallback-fonts) + clip-id (dm/str "text-edition-clip" shape-id) text-modifier-ref @@ -341,7 +351,8 @@ render-wasm? (obj/merge! #js {"--editor-container-width" (dm/str width "px") - "--editor-container-height" (dm/str height "px")}) + "--editor-container-height" (dm/str height "px") + "--fallback-families" (dm/str (str/join ", " fallback-families))}) (not render-wasm?) (obj/merge! diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs index 8c4b501f40..fb1473a8e1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs @@ -32,6 +32,12 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) +(defn- token-value-error-fn + [{:keys [value]}] + (when (or (str/empty? value) + (str/blank? value)) + (tr "workspace.tokens.empty-input"))) + (defn- make-schema [tokens-tree] (sm/schema @@ -44,7 +50,7 @@ [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} #(not (cft/token-name-path-exists? % tokens-tree))]]] - [:value ::sm/text] + [:value [::sm/text {:error/fn token-value-error-fn}]] [:resolved-value ::sm/any] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs index 90eb0c78a3..e5f70ed99f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs @@ -32,6 +32,12 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) +(defn- token-value-error-fn + [{:keys [value]}] + (when (or (str/empty? value) + (str/blank? value)) + (tr "workspace.tokens.empty-input"))) + (defn- make-schema [tokens-tree] (sm/schema @@ -44,7 +50,7 @@ [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} #(not (cft/token-name-path-exists? % tokens-tree))]]] - [:value ::sm/text] + [:value [::sm/text {:error/fn token-value-error-fn}]] [:resolved-value ::sm/any] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs index aa429de437..ebefb01d88 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs @@ -32,6 +32,12 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) +(defn- token-value-error-fn + [{:keys [value]}] + (when (or (str/empty? value) + (str/blank? value)) + (tr "workspace.tokens.empty-input"))) + (defn- make-schema [tokens-tree] (sm/schema @@ -44,7 +50,7 @@ [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} #(not (cft/token-name-path-exists? % tokens-tree))]]] - [:value ::sm/text] + [:value [::sm/text {:error/fn token-value-error-fn}]] [:resolved-value ::sm/any] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs index 1e31503c53..a6c1db54cf 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs @@ -205,6 +205,7 @@ ;; TODO: Review this value vs default-value :value (or value "") :hint-message (:message hint) + :variant "comfortable" :slot-start swatch :hint-type (:type hint)}) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs index 64d0a32124..2147c38f98 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs @@ -83,6 +83,7 @@ (mf/spread-props props {:on-change on-change :default-value value :hint-message (:message hint) + :variant "comfortable" :hint-type (:type hint)}) props diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs index 2eb3776d54..ba0ca455aa 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs @@ -32,6 +32,12 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) +(defn- token-value-error-fn + [{:keys [value]}] + (when (or (str/empty? value) + (str/blank? value)) + (tr "workspace.tokens.empty-input"))) + (defn- make-schema [tokens-tree] (sm/schema @@ -44,7 +50,7 @@ [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} #(not (cft/token-name-path-exists? % tokens-tree))]]] - [:value ::sm/text] + [:value [::sm/text {:error/fn token-value-error-fn}]] [:resolved-value ::sm/any] diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 8e6f8f0e14..be85dc1fd7 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -785,19 +785,10 @@ hidden))) shadows)) -(defn set-shape-text-content - "This function sets shape text content and returns a stream that loads the needed fonts asynchronously" - [shape-id content] - - (h/call wasm/internal-module "_clear_shape_text") - - (set-shape-vertical-align (get content :vertical-align)) - +(defn fonts-from-text-content [content fallback-fonts-only?] (let [paragraph-set (first (get content :children)) paragraphs (get paragraph-set :children) - fonts (fonts/get-content-fonts content) total (count paragraphs)] - (loop [index 0 emoji? false langs #{}] @@ -814,20 +805,36 @@ emoji? (if emoji? emoji? (t/contains-emoji? text)) langs (t/collect-used-languages langs text)] - (t/write-shape-text spans paragraph text) + ;; FIXME: this should probably be somewhere else + (when fallback-fonts-only? (t/write-shape-text spans paragraph text)) + (recur (inc index) emoji? langs)))) (let [updated-fonts - (-> fonts + (-> #{} (cond-> ^boolean emoji? (f/add-emoji-font)) (f/add-noto-fonts langs)) - result (f/store-fonts shape-id updated-fonts)] + fallback-fonts (filter #(get % :is-fallback) updated-fonts)] - (h/call wasm/internal-module "_update_shape_text_layout") + (if fallback-fonts-only? updated-fonts fallback-fonts)))))) - result))))) +(defn set-shape-text-content + "This function sets shape text content and returns a stream that loads the needed fonts asynchronously" + [shape-id content] + + (h/call wasm/internal-module "_clear_shape_text") + + (set-shape-vertical-align (get content :vertical-align)) + + (let [fonts (fonts/get-content-fonts content) + fallback-fonts (fonts-from-text-content content true) + all-fonts (concat fonts fallback-fonts) + result (f/store-fonts shape-id all-fonts)] + (f/load-fallback-fonts-for-editor! fallback-fonts) + (h/call wasm/internal-module "_update_shape_text_layout") + result)) (defn set-shape-grow-type [grow-type] diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs index 1e3bcc6ca5..ea47a06b2b 100644 --- a/frontend/src/app/render_wasm/api/fonts.cljs +++ b/frontend/src/app/render_wasm/api/fonts.cljs @@ -266,6 +266,12 @@ (store-font-id shape-id font-data asset-id emoji? fallback?))) +;; FIXME: This is a temporary function to load the fallback fonts for the editor. +;; Once we render the editor content within wasm, we can remove this function. +(defn load-fallback-fonts-for-editor! + [fonts] + (doseq [font fonts] + (fonts/ensure-loaded! (:font-id font) (:font-variant-id font)))) (defn store-fonts [shape-id fonts] @@ -277,7 +283,8 @@ :font-variant-id "regular" :style 0 :weight 400 - :is-emoji true})) + :is-emoji true + :is-fallback true})) (def noto-fonts {:japanese {:font-id "gfont-noto-sans-jp" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} diff --git a/frontend/src/app/util/text/content/styles.cljs b/frontend/src/app/util/text/content/styles.cljs index a750f8b7ca..8cab737417 100644 --- a/frontend/src/app/util/text/content/styles.cljs +++ b/frontend/src/app/util/text/content/styles.cljs @@ -38,7 +38,8 @@ (str v "px") (and (= k :font-family) (seq v)) - (str/quote v) + ;; pick just first family, avoid quoting twice, and add var(--fallback-families) + (str/concat (str/quote (str/unquote (first (str/split v ",")))) ", var(--fallback-families)") :else v)) @@ -53,7 +54,7 @@ (str/slice v 0 -2) (= k :font-family) - (str/unquote v) + (str/unquote (str/replace v ", var(--fallback-families)" "")) :else v)) diff --git a/frontend/text-editor/src/editor/content/dom/Content.js b/frontend/text-editor/src/editor/content/dom/Content.js index 9c1f70d5f6..89bd0cd0b5 100644 --- a/frontend/text-editor/src/editor/content/dom/Content.js +++ b/frontend/text-editor/src/editor/content/dom/Content.js @@ -13,6 +13,7 @@ import { isLikeParagraph, } from "./Paragraph.js"; import { isDisplayBlock, normalizeStyles } from "./Style.js"; +import { sanitizeFontFamily } from "./Style.js"; const DEFAULT_FONT_SIZE = "14px"; const DEFAULT_FONT_WEIGHT = 400; @@ -87,16 +88,16 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) { styleDefaults?.getPropertyValue("font-size") ?? DEFAULT_FONT_SIZE, ); } - const fontFamily = textSpan.style.getPropertyValue("font-family"); + let fontFamily = textSpan.style.getPropertyValue("font-family"); if (!fontFamily) { console.warn("font-family", fontFamily); - const fontFamilyValue = + fontFamily = styleDefaults?.getPropertyValue("font-family") ?? DEFAULT_FONT_FAMILY; - const quotedFontFamily = fontFamilyValue.startsWith('"') - ? fontFamilyValue - : `"${fontFamilyValue}"`; - textSpan.style.setProperty("font-family", quotedFontFamily); } + + fontFamily = sanitizeFontFamily(fontFamily); + textSpan.style.setProperty("font-family", fontFamily); + const fontWeight = textSpan.style.getPropertyValue("font-weight"); if (!fontWeight) { console.warn("font-weight", fontWeight); @@ -144,18 +145,29 @@ export function htmlToText(html) { tmp.innerHTML = html; const blockTags = [ - "P", "DIV", "SECTION", "ARTICLE", "HEADER", "FOOTER", - "UL", "OL", "LI", "TABLE", "TR", "TD", "TH", "PRE" + "P", + "DIV", + "SECTION", + "ARTICLE", + "HEADER", + "FOOTER", + "UL", + "OL", + "LI", + "TABLE", + "TR", + "TD", + "TH", + "PRE", ]; function walk(node) { let text = ""; - node.childNodes.forEach(child => { + node.childNodes.forEach((child) => { if (child.nodeType === Node.TEXT_NODE) { text += child.textContent; } else if (child.nodeType === Node.ELEMENT_NODE) { - if (child.tagName === "BR") { text += "\n"; } @@ -178,7 +190,6 @@ export function htmlToText(html) { return result.trim(); } - /** * Maps any HTML into a valid content DOM element. * @@ -187,10 +198,14 @@ export function htmlToText(html) { * @param {boolean} [allowHTMLPaste=false] * @returns {DocumentFragment} */ -export function mapContentFragmentFromHTML(html, styleDefaults, allowHTMLPaste) { +export function mapContentFragmentFromHTML( + html, + styleDefaults, + allowHTMLPaste, +) { if (allowHTMLPaste) { try { - const parser = new DOMParser() + const parser = new DOMParser(); const document = parser.parseFromString(html, "text/html"); return mapContentFragmentFromDocument(document, styleDefaults); } catch (error) { diff --git a/frontend/text-editor/src/editor/content/dom/Style.js b/frontend/text-editor/src/editor/content/dom/Style.js index ec421cee6a..63027d6b0c 100644 --- a/frontend/text-editor/src/editor/content/dom/Style.js +++ b/frontend/text-editor/src/editor/content/dom/Style.js @@ -19,10 +19,31 @@ const DEFAULT_FONT_WEIGHT = "400"; * @param {string} value */ export function sanitizeFontFamily(value) { - if (value && value.length > 0 && !value.startsWith('"')) { - return `"${value}"`; - } else { + // NOTE: This is a fix for a bug introduced earlier that have might modified the font-family in the model + // adding extra double quotes. + if (value && value.startsWith('""')) { + //remove the first and last quotes + value = value.slice(1).replace(/"([^"]*)$/, "$1"); + + // remove quotes from font-family in 1-word font-families + // and repeated values + value = [ + ...new Set( + value + .split(", ") + .map((x) => (x.includes(" ") ? x : x.replace(/"/g, ""))), + ), + ].join(", "); + } + + if (!value || value === "") { + return "var(--fallback-families)"; + } else if (value.endsWith(" var(--fallback-families)")) { return value; + } else if (value.startsWith('"')) { + return `${value}, var(--fallback-families)`; + } else { + return `"${value}", var(--fallback-families)`; } }