mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)"; \
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]+$/);
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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)})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user