0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-14 08:41:48 -05:00

♻️ Refactor embed resouces

This commit is contained in:
alonso.torres 2021-05-21 14:51:02 +02:00 committed by Andrey Antukh
parent 42072f2584
commit 6a68e9c118
15 changed files with 312 additions and 260 deletions

View file

@ -26,7 +26,8 @@
[app.main.ui.shapes.text :as text]
[app.main.ui.shapes.group :as group]
[app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.shape :refer [shape-container]]))
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.embed :as embed]))
(def ^:private default-color "#E8E9EA") ;; $color-canvas
@ -43,8 +44,9 @@
[{:keys [objects] :as data} vport]
(let [shapes (cp/select-toplevel-shapes objects {:include-frames? true})
to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val))
rect (->> (gsh/selection-rect shapes)
(gal/adjust-to-viewport vport))]
rect (cond->> (gsh/selection-rect shapes)
(some? vport)
(gal/adjust-to-viewport vport))]
(-> rect
(update :x to-finite 0)
(update :y to-finite 0)
@ -121,13 +123,14 @@
(mf/defc page-svg
{::mf/wrap [mf/memo]}
[{:keys [data width height thumbnails?] :as props}]
[{:keys [data width height thumbnails? embed?] :as props}]
(let [objects (:objects data)
root (get objects uuid/zero)
shapes (->> (:shapes root)
(map #(get objects %)))
vport {:width width :height height}
vport (when (and (some? width) (some? height))
{:width width :height height})
dim (calculate-dimensions data vport)
vbox (get-viewbox dim)
background-color (get-in data [:options :background] default-color)
@ -140,29 +143,31 @@
(mf/use-memo
(mf/deps objects)
#(shape-wrapper-factory objects))]
[:svg {:view-box vbox
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& background {:vbox dim :color background-color}]
(for [item shapes]
(let [frame? (= (:type item) :frame)]
(cond
(and frame? thumbnails? (some? (:thumbnail item)))
[:image {:xlinkHref (:thumbnail item)
:x (:x item)
:y (:y item)
:width (:width item)
:height (:height item)
;; DEBUG
;; :style {:filter "sepia(1)"}
}]
frame?
[:& frame-wrapper {:shape item
:key (:id item)}]
:else
[:& shape-wrapper {:shape item
:key (:id item)}])))]))
[:& (mf/provider embed/context) {:value embed?}
[:svg {:view-box vbox
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns"}
[:& background {:vbox dim :color background-color}]
(for [item shapes]
(let [frame? (= (:type item) :frame)]
(cond
(and frame? thumbnails? (some? (:thumbnail item)))
[:image {:xlinkHref (:thumbnail item)
:x (:x item)
:y (:y item)
:width (:width item)
:height (:height item)
;; DEBUG
;; :style {:filter "sepia(1)"}
}]
frame?
[:& frame-wrapper {:shape item
:key (:id item)}]
:else
[:& shape-wrapper {:shape item
:key (:id item)}])))]]))
(mf/defc frame-svg
{::mf/wrap [mf/memo]}

View file

@ -8,17 +8,19 @@
"Fonts management and loading logic."
(:require-macros [app.main.fonts :refer [preload-gfonts]])
(:require
[app.config :as cf]
[app.common.data :as d]
[app.common.text :as txt]
[app.config :as cf]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.logging :as log]
[app.util.object :as obj]
[app.util.timers :as ts]
[app.util.logging :as log]
[lambdaisland.uri :as u]
[goog.events :as gev]
[beicon.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[goog.events :as gev]
[lambdaisland.uri :as u]
[okulary.core :as l]
[promesa.core :as p]))
@ -216,3 +218,55 @@
(or
(d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants)
(first variants)))
;; Font embedding functions
;; Template for a CSS font face
(def font-face-template "
/* latin */
@font-face {
font-family: '%(family)s';
font-style: %(style)s;
font-weight: %(weight)s;
font-display: block;
src: url(/fonts/%(family)s-%(suffix)s.woff) format('woff');
}
")
(defn get-content-fonts
"Extracts the fonts used by the content of a text shape"
[{font-id :font-id children :children :as content}]
(let [current-font
(if (some? font-id)
#{(select-keys content [:font-id :font-variant-id])}
#{(select-keys txt/default-text-attrs [:font-id :font-variant-id])})
children-font (->> children (mapv get-content-fonts))]
(reduce set/union (conj children-font current-font))))
(defn fetch-font-css
"Given a font and the variant-id, retrieves the fontface CSS"
[{:keys [font-id font-variant-id]
:or {font-variant-id "regular"}}]
(let [{:keys [backend family variants]} (get @fontsdb font-id)]
(if (= :google backend)
(-> (generate-gfonts-url
{:family family
:variants [{:id font-variant-id}]})
(http/fetch-text))
(let [{:keys [weight style suffix] :as variant}
(d/seek #(= (:id %) font-variant-id) variants)
font-data {:family family
:style style
:suffix (or suffix font-variant-id)
:weight weight}]
(rx/of (str/fmt font-face-template font-data))))))
(defn extract-fontface-urls
"Parses the CSS and retrieves the font urls"
[^string css]
(->> (re-seq #"url\(([^)]+)\)" css)
(mapv second)))

View file

@ -8,7 +8,6 @@
(:require
[rumext.alpha :as mf]))
(def embed-ctx (mf/create-context false))
(def render-ctx (mf/create-context nil))
(def def-ctx (mf/create-context false))

View file

@ -225,3 +225,28 @@
(fn []
(mf/set-ref-val! ref value)))
(mf/ref-val ref)))
(defn use-equal-memo
[val]
(let [ref (mf/use-ref nil)]
(when-not (= (mf/ref-val ref) val)
(mf/set-ref-val! ref val))
(mf/ref-val ref)))
(defn- ssr?
"Checks if the current environment is run under a SSR context"
[]
(try
(not js/window)
(catch :default e
;; When exception accessing window we're in ssr
true)))
(defn use-effect-ssr
"Use effect that handles SSR"
[deps effect-fn]
(if (ssr?)
(let [ret (effect-fn)]
(when (fn? ret) (ret)))
(mf/use-effect deps effect-fn)))

View file

@ -14,7 +14,7 @@
[app.common.uuid :as uuid]
[app.main.exports :as exports]
[app.main.repo :as repo]
[app.main.ui.context :as muc]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.shape :refer [shape-container]]
[beicon.core :as rx]
@ -71,7 +71,7 @@
#(exports/shape-wrapper-factory objects))
]
[:& (mf/provider muc/embed-ctx) {:value true}
[:& (mf/provider embed/context) {:value true}
[:svg {:id "screenshot"
:view-box vbox
:width width

View file

@ -0,0 +1,39 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.shapes.embed
(:require
[app.main.ui.hooks :as hooks]
[app.util.http :as http]
[beicon.core :as rx]
[rumext.alpha :as mf]))
(def context (mf/create-context false))
(defn use-data-uris [urls]
(let [embed? (mf/use-ctx context)
urls (hooks/use-equal-memo urls)
uri-data (mf/use-ref {})
state (mf/use-state 0)]
(hooks/use-effect-ssr
(mf/deps embed? urls)
(fn []
(let [sub (when embed?
(->> (rx/from urls)
(rx/merge-map http/fetch-data-uri)
(rx/reduce conj {})
(rx/subs (fn [data]
(when-not (= data (mf/ref-val uri-data))
(mf/set-ref-val! uri-data data)
(reset! state inc))))))]
#(when sub
(rx/dispose! sub)))))
;; Use ref so if the urls are cached will return inmediately instead of the
;; next render
(when embed?
(mf/ref-val uri-data))))

View file

@ -8,10 +8,9 @@
(:require
[app.common.geom.shapes :as gsh]
[app.config :as cfg]
[app.main.ui.shapes.embed :as embed]
[app.util.object :as obj]
[rumext.alpha :as mf]
[app.common.geom.point :as gpt]
[app.main.ui.shapes.image :as image]))
[rumext.alpha :as mf]))
(mf/defc fill-image-pattern
{::mf/wrap-props false}
@ -22,8 +21,8 @@
(when (contains? shape :fill-image)
(let [{:keys [x y width height]} (:selrect shape)
fill-image-id (str "fill-image-" render-id)
media (:fill-image shape)
uri (image/use-image-uri media)
uri (cfg/resolve-file-media (:fill-image shape))
embed (embed/use-data-uris [uri])
transform (gsh/transform-matrix shape)]
[:pattern {:id fill-image-id
@ -33,6 +32,6 @@
:height height
:width width
:patternTransform transform}
[:image {:xlinkHref uri
[:image {:xlinkHref (get embed uri uri)
:width width
:height height}]]))))

View file

@ -7,13 +7,16 @@
(ns app.main.ui.shapes.frame
(:require
[app.common.data :as d]
[app.common.geom.shapes :as geom]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.text.fontfaces :as ff]
[app.util.object :as obj]
[rumext.alpha :as mf]))
(def frame-default-props {:fill-color "#ffffff"})
(defn is-text? [{type :type}]
(= :text type))
(defn frame-shape
[shape-wrapper]
(mf/fnc frame-shape
@ -23,6 +26,8 @@
shape (unchecked-get props "shape")
{:keys [id width height]} shape
text-childs (->> childs (filterv is-text?))
props (-> (merge frame-default-props shape)
(attrs/extract-style-attrs)
(obj/merge!
@ -32,6 +37,7 @@
:height height
:className "frame-background"}))]
[:*
[:& ff/fontfaces-style {:shapes text-childs}]
[:> :rect props]
(for [[i item] (d/enumerate childs)]
[:& shape-wrapper {:frame shape

View file

@ -7,35 +7,13 @@
(ns app.main.ui.shapes.image
(:require
[app.common.geom.shapes :as geom]
[app.config :as cfg]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.embed :as se]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.object :as obj]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[rumext.alpha :as mf]))
(defn use-image-uri
[media]
(let [uri (mf/use-memo (mf/deps (:id media))
#(cfg/resolve-file-media media))
embed-resources? (mf/use-ctx muc/embed-ctx)
data-uri (mf/use-state (when (not embed-resources?) uri))]
(mf/use-effect
(mf/deps uri)
(fn []
(if embed-resources?
(->> (http/send! {:method :get
:uri uri
:response-type :blob})
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)
(rx/subs #(reset! data-uri %))))))
(or @data-uri uri)))
[rumext.alpha :as mf]
[app.config :as cfg]
[app.main.ui.shapes.embed :as embed]))
(mf/defc image-shape
{::mf/wrap-props false}
@ -43,7 +21,8 @@
(let [shape (unchecked-get props "shape")
{:keys [id x y width height rotation metadata]} shape
uri (use-image-uri metadata)]
uri (cfg/resolve-file-media metadata)
embed (embed/use-data-uris [uri])]
(let [transform (geom/transform-matrix shape)
props (-> (attrs/extract-style-attrs shape)
@ -60,5 +39,5 @@
[:> "image" (obj/merge!
props
#js {:xlinkHref uri
#js {:xlinkHref (get embed uri uri)
:onDragStart on-drag-start})])))

View file

@ -1,144 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.shapes.text.embed
(:refer-clojure :exclude [memoize])
(:require
[app.common.data :as d]
[app.common.text :as txt]
[app.main.fonts :as fonts]
[app.util.http :as http]
[app.util.time :as dt]
[app.util.webapi :as wapi]
[app.util.object :as obj]
[clojure.set :as set]
[cuerdas.core :as str]
[promesa.core :as p]
[beicon.core :as rx]
[rumext.alpha :as mf]))
(defonce cache (atom {}))
(defn with-cache
[{:keys [key max-age]} observable]
(let [entry (get @cache key)
age (when entry
(dt/diff (dt/now)
(:created-at entry)))]
(if (and (some? entry)
(< age max-age))
(rx/of (:data entry))
(->> observable
(rx/tap (fn [data]
(let [entry {:created-at (dt/now) :data data}]
(swap! cache assoc key entry))))))))
(def font-face-template "
/* latin */
@font-face {
font-family: '%(family)s';
font-style: %(style)s;
font-weight: %(weight)s;
font-display: block;
src: url(/fonts/%(family)s-%(suffix)s.woff) format('woff');
}
")
;; -- Embed fonts into styles
(defn get-node-fonts
[node]
(let [current-font (if (not (nil? (:font-id node)))
#{(select-keys node [:font-id :font-variant-id])}
#{(select-keys txt/default-text-attrs [:font-id :font-variant-id])})
children-font (map get-node-fonts (:children node))]
(reduce set/union (conj children-font current-font))))
(defn get-font-css
"Given a font and the variant-id, retrieves the style CSS for it."
[{:keys [id backend family variants] :as font} font-variant-id]
(if (= :google backend)
(let [uri (fonts/generate-gfonts-url {:family family :variants [{:id font-variant-id}]})]
(->> (http/send! {:method :get
:mode :cors
:omit-default-headers true
:uri uri
:response-type :text})
(rx/map :body)
(http/as-promise)))
(let [{:keys [name weight style suffix] :as variant} (d/seek #(= (:id %) font-variant-id) variants)
result (str/fmt font-face-template {:family family
:style style
:suffix (or suffix font-variant-id)
:weight weight})]
(p/resolved result))))
(defn- to-promise
[observable]
(p/create (fn [resolve reject]
(->> (rx/take 1 observable)
(rx/subs resolve reject)))))
(defn fetch-font-data
"Parses the CSS and retrieves the font data as DataURI."
[^string css]
(let [uris (->> (re-seq #"url\(([^)]+)\)" css)
(mapv second))]
(with-cache {:key uris :max-age (dt/duration {:hours 4})}
(->> (rx/from (seq uris))
(rx/mapcat (fn [uri]
(->> (http/send! {:method :get :uri uri :response-type :blob :omit-default-headers true})
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)
(rx/map #(vector uri %)))))
(rx/reduce conj [])))))
(defn get-font-data
"Parses the CSS and retrieves the font data as DataURI."
[^string css]
(->> (fetch-font-data css)
(http/as-promise)))
(defn embed-font
"Given a font-id and font-variant-id, retrieves the CSS for it and
convert all external urls to embedded data URI's."
[{:keys [font-id font-variant-id] :or {font-variant-id "regular"}}]
(let [{:keys [backend family] :as font} (get @fonts/fontsdb font-id)]
(p/let [css (get-font-css font font-variant-id)
url-to-data (get-font-data css)
replace-text (fn [text [url data]] (str/replace text url data))]
(reduce replace-text css url-to-data))))
;; NOTE: we can't move this to generic hooks namespace because that
;; namespace imports some code incompatible with webworkers and this
;; font embbeding should be able run on browser and webworker
;; contexts.
(defn- memoize
[val]
(let [ref (mf/use-ref #js {})]
(when-not (= (mf/ref-val ref) val)
(mf/set-ref-val! ref val))
(mf/ref-val ref)))
(mf/defc embed-fontfaces-style
{::mf/wrap-props false
::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
[props]
(let [shapes (obj/get props "shapes")
node {:children (->> shapes (map :content))}
fonts (-> node get-node-fonts memoize)
style (mf/use-state nil)]
(mf/use-effect
(mf/deps fonts)
(fn []
(-> (p/all (map embed-font fonts))
(p/then (fn [result]
(reset! style (str/join "\n" result)))))))
(when (some? @style)
[:style @style])))

View file

@ -0,0 +1,79 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.shapes.text.fontfaces
(:require
[app.main.fonts :as fonts]
[app.main.ui.hooks :as hooks]
[app.main.ui.shapes.embed :as embed]
[app.util.object :as obj]
[beicon.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(defn replace-embeds
"Replace into the font-faces of a CSS the URL's that are present in `embed-data` by its
data-uri"
[css urls embed-data]
(letfn [(replace-url [css url]
(str/replace css url (get embed-data url url)))]
(->> urls
(reduce replace-url css))))
(defn use-fonts-css
"Hook that retrieves the CSS of the fonts passed as parameter"
[fonts]
(let [fonts-css-ref (mf/use-ref "")
redraw (mf/use-state 0)]
(hooks/use-effect-ssr
(mf/deps fonts)
(fn []
(let [sub
(->> (rx/from fonts)
(rx/merge-map fonts/fetch-font-css)
(rx/reduce conj [])
(rx/subs
(fn [result]
(let [css (str/join "\n" result)]
(when-not (= (mf/ref-val fonts-css-ref) css)
(mf/set-ref-val! fonts-css-ref css)
(reset! redraw inc))))))]
#(rx/dispose! sub))))
(mf/ref-val fonts-css-ref)))
(mf/defc fontfaces-style
{::mf/wrap-props false
::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
[props]
(let [shapes (obj/get props "shapes")
content (->> shapes (mapv :content))
;; Retrieve the fonts ids used by the text shapes
fonts (->> content
(mapv fonts/get-content-fonts)
(reduce set/union #{})
(hooks/use-equal-memo))
;; Fetch its CSS fontfaces
fonts-css (use-fonts-css fonts)
;; Extract from the CSS the URL's to embed
fonts-urls (mf/use-memo
(mf/deps fonts-css)
#(fonts/extract-fontface-urls fonts-css))
;; Calculate the data-uris for these fonts
fonts-embed (embed/use-data-uris fonts-urls)
;; Creates a style tag by replacing the urls with the data uri
style (replace-embeds fonts-css fonts-urls fonts-embed)]
(when (and (some? style) (not (empty? style)))
[:style style])))

View file

@ -6,45 +6,15 @@
(ns app.main.ui.workspace.shapes.frame
(:require
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.data.workspace :as dw]
[app.main.data.workspace.changes :as dch]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as muc]
[app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.text.embed :as ste]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.timers :as ts]
[beicon.core :as rx]
[okulary.core :as l]
[rumext.alpha :as mf]))
(def obs-config
#js {:attributes true
:childList true
:subtree true
:characterData true})
(defn make-is-moving-ref
[id]
(let [check-moving (fn [local]
(and (= :move (:transform local))
(contains? (:selected local) id)))]
(l/derived check-moving refs/workspace-local)))
(defn check-props
([props] (check-props props =))
([props eqfn?]
(fn [np op]
(every? #(eqfn? (unchecked-get np %)
(unchecked-get op %))
props))))
(defn check-frame-props
"Checks for changes in the props of a frame"
[new-props old-props]
@ -107,14 +77,9 @@
thumbnail? (unchecked-get props "thumbnail?")
edition (mf/deref refs/selected-edition)
embed-fonts? (mf/use-ctx muc/embed-ctx)
shape (gsh/transform-shape shape)
children (mapv #(get objects %) (:shapes shape))
text-childs (->> objects
vals
(filterv #(and (= :text (:type %))
(= (:id shape) (:frame-id %)))))
ds-modifier (get-in shape [:modifiers :displacement])
@ -131,12 +96,7 @@
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
(when-not show-thumbnail?
[:> shape-container {:shape shape
:ref on-dom}
(when embed-fonts?
[:& ste/embed-fontfaces-style {:shapes text-childs}])
[:> shape-container {:shape shape :ref on-dom}
[:& frame-shape {:shape shape
:childs children}]])

View file

@ -7,12 +7,13 @@
(ns app.main.ui.workspace.viewport
(:require
[app.common.data :as d]
[app.common.pages :as cp]
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.main.refs :as refs]
[app.main.ui.context :as ctx]
[app.main.ui.context :as muc]
[app.main.ui.measurements :as msr]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.workspace.shapes :as shapes]
[app.main.ui.workspace.shapes.text.editor :as editor]
[app.main.ui.workspace.viewport.actions :as actions]
@ -187,7 +188,7 @@
:style {:background-color (get options :background "#E8E9EA")
:pointer-events "none"}}
[:& (mf/provider muc/embed-ctx) {:value true}
[:& (mf/provider embed/context) {:value true}
;; Render root shape
[:& shapes/root-shape {:key page-id
:objects objects

View file

@ -0,0 +1,26 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.util.cache
(:require
[app.util.time :as dt]
[beicon.core :as rx]))
(defonce cache (atom {}))
(defn with-cache
[{:keys [key max-age]} observable]
(let [entry (get @cache key)
age (when entry
(dt/diff (dt/now)
(:created-at entry)))]
(if (and (some? entry) (< age max-age))
(rx/of (:data entry))
(->> observable
(rx/tap
(fn [data]
(let [entry {:created-at (dt/now) :data data}]
(swap! cache assoc key entry))))))))

View file

@ -10,9 +10,12 @@
[app.common.data :as d]
[app.common.uri :as u]
[app.config :as cfg]
[app.util.cache :as c]
[app.util.globals :as globals]
[app.util.object :as obj]
[app.util.time :as dt]
[app.util.transit :as t]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[cuerdas.core :as str]
[promesa.core :as p]))
@ -152,6 +155,27 @@
(defn as-promise
[observable]
(p/create (fn [resolve reject]
(->> (rx/take 1 observable)
(rx/subs resolve reject)))))
(p/create
(fn [resolve reject]
(->> (rx/take 1 observable)
(rx/subs resolve reject)))))
(defn fetch-data-uri [uri]
(c/with-cache {:key uri :max-age (dt/duration {:hours 4})}
(->> (send! {:method :get
:uri uri
:response-type :blob
:omit-default-headers true})
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)
(rx/map #(hash-map uri %)))))
(defn fetch-text [url]
(c/with-cache {:key url :max-age (dt/duration {:hours 4})}
(->> (send!
{:method :get
:mode :cors
:omit-default-headers true
:uri url
:response-type :text})
(rx/map :body))))