Compare commits

..

17 Commits

Author SHA1 Message Date
Francis Santiago
6703402207 Revert "add new configurations" 2025-05-12 16:47:43 +02:00
Francis Santiago
f93059412a Merge pull request #6463 from sancfc/develop
add new configurations
2025-05-12 16:04:01 +02:00
Francis Santiago
a65aa5ea44 Merge pull request #1 from sancfc/github-actions-bundle-build
GitHub actions bundle build
2025-05-07 10:57:56 +02:00
fsantiago
689063cfb2 add initial functionality 2025-05-07 10:05:54 +02:00
fsantiago
e146ce7be4 🧪 temporary debug test 2025-05-07 10:05:17 +02:00
fsantiago
0fbd9812b3 🧪 basic execution test 2025-05-07 10:05:07 +02:00
fsantiago
ccd7b3bdce 🔧 🧹 minor code cleanup 2025-05-07 10:04:58 +02:00
fsantiago
60f8cfd492 add more tests for zip flow 2025-05-07 10:04:41 +02:00
fsantiago
7359b800ce 🔧 🧹 reorganize zip type logic 2025-05-07 10:04:29 +02:00
fsantiago
0032639831 🐛 🐛 correct zip type validation 2025-05-07 10:04:13 +02:00
fsantiago
d9cdd020e6 ♻️ ♻️ improve zip type definition 2025-05-07 10:04:01 +02:00
fsantiago
10d021b15e implement zip type logic 2025-05-07 10:03:48 +02:00
fsantiago
3be750410e 🔧 🔧 update file paths in project 2025-05-07 10:03:36 +02:00
fsantiago
47552830b1 🧪 add temporary integration test 2025-05-07 10:03:24 +02:00
fsantiago
0fb41f54b0 add initial tests for zip feature 2025-05-07 10:02:56 +02:00
fsantiago
5b777921a6 📝 📝 update reference to PENPOT 2025-05-07 10:02:43 +02:00
fsantiago
42dcc81767 🔧 🔧 add initial pipeline configuration 2025-05-07 10:01:56 +02:00
421 changed files with 32196 additions and 37473 deletions

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check Commit Type
uses: gsactions/commit-message-checker@v2
with:
pattern: '^(Merge|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):)\s[A-Z].*[^.]$'
pattern: '^:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):\s[A-Z].*[^.]$'
flags: 'gm'
error: 'Commit should match CONTRIBUTING.md guideline'
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request

2
.gitignore vendored
View File

@@ -68,8 +68,6 @@
/vendor/**/target
/vendor/svgclean/bundle*.js
/web
/library/target/
clj-profiler/
node_modules
/test-results/

View File

@@ -8,84 +8,42 @@
**Breaking changes on penpot library:**
The library entrypoint API object has been changed. From now you start creating a new
build context, from where you can add multiple files and attach media. This change add the
ability to build more than one file at same time and export them in an unique .penpot
file.
```js
const context = penpot.createBuildContext()
context.addFile({name:"aa"})
context.addPage({name:"aa"})
context.closePage()
context.closeFile()
;; barray is instance of Uint8Array
const barray = penpot.exportAsBytes(context);
```
The previous `file.export()` method has been removed and several alternatives are
added as first level functions on penpot library API entrypoint:
- `exportAsBytes(BuildContext context) -> Promise<Uint8Array>`
- `exportAsBlob(BuildContext context) -> Promise<Blob>`
- `exportStream(BuildContext context, WritableStream stream) -> Promise<Void>`
The stream variant allows writting data as it is generated to the stream, without the need
to store the generated output entirelly in the memory.
There are also relevant semantic changes in how components should be created: this
refactor removes all notions of the old components (v1). Since v2, the shapes that are
part of a component live on a page. So, from now on, to create a component, you should
first create a frame, then add shapes and/or groups to that frame, and then create a
component by declaring that frame as the component root.
A non exhaustive list of changes:
- Change the signature of the `addPage` method: it now accepts an object (as a single argument) where you can pass `id`,
`name`, and `background` props (instead of the previous positional arguments)
- Rename the `createRect` method to `addRect`
- Rename the `createCircle` method to `addCircle`
- Rename the `createPath` method to `addPath`
- Rename the `createText` method to `addText`
- Rename the `addArtboard` method to `addBoard`
- Rename `startComponent` to `addComponent` (to preserve the naming style)
- Rename `createComponentInstance` to `addComponentInstance` (to preserve the naming style)
- Remove `lookupShape`
- Remove `asMap`
- Remove `updateLibraryColor` (use `addLibraryColor` if you just need to replace a color)
- Remove `deleteLibraryColor` (this library is intended to build files)
- Remove `updateLibraryTypography` (use `addLibraryTypography` if you just need to replace a typography)
- Remove `deleteLibraryTypography` (this library is intended to build files)
- Remove `add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components)
- Remove `deleteObject` (this library is intended to build files)
- Remove `updateObject` (this library is intended to build files)
- Remove `finishComponent` (it is no longer necessary; see below for more details on component creation changes)
- Rename the `file.createRect` method to `file.addRect`
- Rename the `file.createCircle` method to `file.addCircle`
- Rename the `file.createPath` method to `file.addPath`
- Rename the `file.createText` method to `file.addText`
- Rename `file.startComponent` to `file.addComponent` (to preserve the naming style)
- Rename `file.createComponentInstance` to `file.addComponentInstance` (to preserve the naming style)
- Rename `file.lookupShape` to `file.getShape`
- Rename `file.asMap` to `file.toMap`
- Remove `file.updateLibraryColor` (use `file.addLibraryColor` if you just need to replace a color)
- Remove `file.deleteLibraryColor` (this library is intended to build files)
- Remove `file.updateLibraryTypography` (use `file.addLibraryTypography` if you just need to replace a typography)
- Remove `file.deleteLibraryTypography` (this library is intended to build files)
- Remove `file.add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components)
- Remove `file.deleteObject` (this library is intended to build files)
- Remove `file.updateObject` (this library is intended to build files)
- Remove `file.finishComponent` (it is no longer necessary; see below for more details on component creation changes)
- Change the `file.getCurrentPageId` function to a read-only `file.currentPageId` property
- Add `file.currentFrameId` read-only property
- Add `file.lastId` read-only property
- Change the `getCurrentPageId` function to a read-only `currentPageId` property
- Add `currentFileId` read-only property
- Add `currentFrameId` read-only property
- Add `lastId` read-only property
There are also relevant semantic changes in how components should be created: this refactor removes
all notions of the old components (v1). Since v2, the shapes that are part of a component live on a
page. So, from now on, to create a component, you should first create a frame, then add shapes
and/or groups to that frame, and then create a component by declaring that frame as the component
root.
### :heart: Community contributions (Thank you!)
### :sparkles: New features
- Optimize profile setup flow for better user experience [Taiga #10028](https://tree.taiga.io/project/penpot/us/10028)
- Update base image for Docker Backend and Exporter to Ubuntu 24.04
- Update base image for Docker Frontend to Nginx 1.28.0
- Allow multi file token import [Github #27](https://github.com/tokens-studio/penpot/issues/27)
- Create `input*` wrapper component, and `label*`, `input-field*` and `hint-message*` components [Taiga #10713](https://tree.taiga.io/project/penpot/us/10713)
- Deselect layers (and path nodes) with Ctrl+Shift+Drag [Github #2509](https://github.com/penpot/penpot/issues/2509)
- Copy to SVG from contextual menu [Github #838](https://github.com/penpot/penpot/issues/838)
- Add styles for Inkeep Chat at workspace [Taiga #10708](https://tree.taiga.io/project/penpot/us/10708)
### :bug: Bugs fixed
- Fix spacing / sizes of different elements in the measurements section of the design tab [Taiga #11076](https://tree.taiga.io/project/penpot/issue/11076)
- Fix selection of short paths [Github #4472](https://github.com/penpot/penpot/issues/4472)
- Fix element positioning on the right side to adjust to grid [#11073](https://tree.taiga.io/project/penpot/issue/11073)
## 2.7.0 (Unreleased)
@@ -105,10 +63,10 @@ A non exhaustive list of changes:
- Duplicate token sets [Taiga #10694](https://tree.taiga.io/project/penpot/issue/10694)
- Add set selection in create Token themes flow [Taiga #10746](https://tree.taiga.io/project/penpot/issue/10746)
- Display indicator on not active sets [Taiga #10668](https://tree.taiga.io/project/penpot/issue/10668)
- Create `input*` wrapper component, and `label*`, `input-field*` and `hint-message*` components [Taiga #10713](https://tree.taiga.io/project/penpot/us/10713)
### :bug: Bugs fixed
- Fix "at" icon to match all icons on app [Taiga #11136](https://tree.taiga.io/project/penpot/issue/11136)
- Fix problem in viewer with the back button [Taiga #10907](https://tree.taiga.io/project/penpot/issue/10907)
- Fix resize bar background on tokens panel [Taiga #10811](https://tree.taiga.io/project/penpot/issue/10811)
- Fix shortcut for history version panel [Taiga #11006](https://tree.taiga.io/project/penpot/issue/11006)
@@ -135,13 +93,6 @@ A non exhaustive list of changes:
- Fix cannot rename Design Token Sets when group of same name exists [Taiga Issue #10773](https://tree.taiga.io/project/penpot/issue/10773)
- Fix problem when duplicating grid layout [Github #6391](https://github.com/penpot/penpot/issues/6391)
- Fix issue that makes workspace shortcuts stop working [Taiga #11062](https://tree.taiga.io/project/penpot/issue/11062)
- Fix problem while syncing library colors and typographies [Taiga #11068](https://tree.taiga.io/project/penpot/issue/11068)
- Fix problem with path edition of shapes [Taiga #9496](https://tree.taiga.io/project/penpot/issue/9496)
- Fix exception on paste invalid html [Taiga #11047](https://tree.taiga.io/project/penpot/issue/11047)
- Fix share button being displayed with no permissions [Taiga #11086](https://tree.taiga.io/project/penpot/issue/11086)
- Fix inline styles in code tab [Taiga Issue #7583](https://tree.taiga.io/project/penpot/issue/7583)
- Fix exception on returning openapi.json
- Fix json encoding of TokensLib [Taiga #10994](https://tree.taiga.io/project/penpot/issue/10994)
## 2.6.2

View File

@@ -1,59 +1,62 @@
# Contributing Guide #
Thank you for your interest in contributing to Penpot. This is a
generic guide that details how to contribute to the project in a way that
is efficient for everyone. If you are looking for specific documentation on
different parts of the platform, please refer to the `docs/` directory,
or the rendered version at the [Help Center](https://help.penpot.app/).
generic guide that details how to contribute to Penpot in a way that
is efficient for everyone. If you want a specific documentation for
different parts of the platform, please refer to `docs/` directory.
## Reporting Bugs ##
We are using [GitHub Issues](https://github.com/penpot/penpot/issues)
for our public bugs. We keep a close eye on them and try to make it
for our public bugs. We keep a close eye on this and try to make it
clear when we have an internal fix in progress. Before filing a new
task, try to make sure your problem doesn't already exist.
If you found a bug, please report it, as far as possible, with:
If you found a bug, please report it, as far as possible with:
- a detailed explanation of steps to reproduce the error
- the browser and browser version used
- a dev tools console exception stack trace (if available)
- a browser and the browser version used
- a dev tools console exception stack trace (if it is available)
If you found a bug which you think is better to discuss in private (for
example, security bugs), consider first sending an email to
If you found a bug that you consider better discuss in private (for
example: security bugs), consider first send an email to
`support@penpot.app`.
**We don't have a formal bug bounty program for security reports; this
is an open source application, and your contribution will be recognized
**We don't have formal bug bounty program for security reports; this
is an open source application and your contribution will be recognized
in the changelog.**
## Pull Requests ##
## Pull requests ##
If you want to propose a change or bug fix via a pull request (PR),
you should first carefully read the section **Developer's Certificate of
Origin**. You must also format your code and commits according to the
instructions below.
If you want propose a change or bug fix with the Pull-Request system
firstly you should carefully read the **DCO** section and format your
commits accordingly.
If you intend to fix a bug, it's fine to submit a pull request right
away, but we still recommend filing an issue detailing what you're
If you intend to fix a bug it's fine to submit a pull request right
away but we still recommend to file an issue detailing what you're
fixing. This is helpful in case we don't accept that specific fix but
want to keep track of the issue.
If you want to implement or start working on a new feature, please
open a **question*- / **discussion*- issue for it. No PR
will be accepted without a prior discussion about the changes,
whether it is a new feature, an already planned one, or a quick win.
If you want to implement or start working in a new feature, please
open a **question** / **discussion** issue for it. No pull-request
will be accepted without previous chat about the changes,
independently if it is a new feature, already planned feature or small
quick win.
If it is your first PR, you can learn how to proceed from
[this free video
series](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
If is going to be your first pull request, You can learn how from this
free video series:
https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github
We will use the `easy fix` mark for tag for indicate issues that are
easy for beginners.
We use the `easy fix` tag to indicate issues that are appropriate for beginners.
## Commit Guidelines ##
We have very precise rules on how our git commit messages must be formatted.
We have very precise rules over how our git commit messages can be formatted.
The commit message format is:
@@ -68,37 +71,34 @@ The commit message format is:
Where type is:
- :bug: `:bug:` a commit that fixes a bug
- :sparkles: `:sparkles:` a commit that adds an improvement
- :tada: `:tada:` a commit with a new feature
- :sparkles: `:sparkles:` a commit that an improvement
- :tada: `:tada:` a commit with new feature
- :recycle: `:recycle:` a commit that introduces a refactor
- :lipstick: `:lipstick:` a commit with cosmetic changes
- :ambulance: `:ambulance:` a commit that fixes a critical bug
- :ambulance: `:ambulance:` a commit that fixes critical bug
- :books: `:books:` a commit that improves or adds documentation
- :construction: `:construction:` a WIP commit
- :construction: `:construction:`: a wip commit
- :boom: `:boom:` a commit with breaking changes
- :wrench: `:wrench:` a commit for config updates
- :zap: `:zap:` a commit with performance improvements
- :whale: `:whale:` a commit for Docker-related stuff
- :paperclip: `:paperclip:` a commit with other non-relevant changes
- :arrow_up: `:arrow_up:` a commit with dependency updates
- :arrow_down: `:arrow_down:` a commit with dependency downgrades
- :whale: `:whale:` a commit for docker related stuff
- :paperclip: `:paperclip:` a commit with other not relevant changes
- :arrow_up: `:arrow_up:` a commit with dependencies updates
- :arrow_down: `:arrow_down:` a commit with dependencies downgrades
- :fire: `:fire:` a commit that removes files or code
- :globe_with_meridians: `:globe_with_meridians:` a commit that adds or updates
translations
More info:
- https://gist.github.com/parmentf/035de27d6ed1dce0b36a
- https://gist.github.com/rxaviers/7360908
Each commit should have:
- A concise subject using the imperative mood.
- The subject should capitalize the first letter, omit the period
at the end, and be no longer than 65 characters.
- A concise subject using imperative mood.
- The subject should have capitalized the first letter, without period
at the end and no larger than 65 characters.
- A blank line between the subject line and the body.
- An entry in the CHANGES.md file if applicable, referencing the
GitHub or Taiga issue/user story using these same rules.
- An entry on the CHANGES.md file if applicable, referencing the
github or taiga issue/user-story using the these same rules.
Examples of good commit messages:
@@ -111,30 +111,8 @@ Examples of good commit messages:
- `:ambulance: Fix critical bug on user registration process`
- `:tada: Add new approach for user registration`
## Formatting and Linting ##
You will want to make sure your code is formatted and linted before submitting
a PR. We use [cljfmt](https://github.com/weavejester/cljfmt) and
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for this. After installing
them on your system, you can run them with:
```bash
# Check formatting
yarn fmt:clj:check
# Check and fix formatting
yarn fmt:clj
# Run the linter
yarn lint:clj
```
There are more choices in `package.json`.
Ideally, you should run these commands as git pre-commit hooks. A convenient way
of defining them is to use [Husky](https://typicode.github.io/husky/#/).
## Code of Conduct ##
## Code of conduct ##
As contributors and maintainers of this project, we pledge to respect
all people who contribute through reporting issues, posting feature
@@ -154,11 +132,11 @@ unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit,
or reject comments, commits, code, wiki edits, issues, and other
contributions that are not aligned with this Code of Conduct. Project
contributions that are not aligned to this Code of Conduct. Project
maintainers who do not follow the Code of Conduct may be removed from
the project team.
This Code of Conduct applies both within project spaces and in public
This code of conduct applies both within project spaces and in public
spaces when an individual is representing the project or its
community.
@@ -167,11 +145,12 @@ may be reported by opening an issue or contacting one or more of the
project maintainers.
This Code of Conduct is adapted from the Contributor Covenant, version
1.1.0, available from [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
1.1.0, available from http://contributor-covenant.org/version/1/1/0/
## Developer's Certificate of Origin (DCO)
By submitting code you agree to and can certify the following:
## Developer's Certificate of Origin (DCO) ##
By submitting code you are agree and can certify the below:
Developer's Certificate of Origin 1.1
@@ -199,15 +178,13 @@ By submitting code you agree to and can certify the following:
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
Then, all your code patches (**documentation is excluded**) should
Then, all your code patches (**documentation are excluded**) should
contain a sign-off at the end of the patch/commit description body. It
can be automatically added by adding the `-s` parameter to `git commit`.
can be automatically added on adding `-s` parameter to `git commit`.
This is an example of what the line should look like:
This is an example of the aspect of the line:
```
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
```
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Please, use your real name (sorry, no pseudonyms or anonymous
contributions are allowed).

View File

@@ -34,7 +34,7 @@
<br />
[Penpot video](https://github.com/user-attachments/assets/08b83119-c090-4a74-86ed-7bfbdda9a793)
[Penpot video](https://github.com/penpot/penpot/assets/5446186/b8ad0764-585e-4ddc-b098-9b4090d337cc)
<br />

View File

@@ -6,7 +6,7 @@
org.clojure/clojure {:mvn/version "1.12.0"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
com.github.luben/zstd-jni {:mvn/version "1.5.6-9"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.6.0.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.5.2.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti
@@ -27,15 +27,15 @@
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc
{:mvn/version "1.3.1002"}
metosin/reitit-core {:mvn/version "0.8.0"}
{:mvn/version "1.3.994"}
metosin/reitit-core {:mvn/version "0.7.2"}
nrepl/nrepl {:mvn/version "1.3.1"}
cider/cider-nrepl {:mvn/version "0.55.7"}
cider/cider-nrepl {:mvn/version "0.52.0"}
org.postgresql/postgresql {:mvn/version "42.7.5"}
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
org.xerial/sqlite-jdbc {:mvn/version "3.48.0.0"}
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
com.zaxxer/HikariCP {:mvn/version "6.2.1"}
io.whitfin/siphash {:mvn/version "2.0.0"}
@@ -44,7 +44,7 @@
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.0"}
org.jsoup/jsoup {:mvn/version "1.20.1"}
org.jsoup/jsoup {:mvn/version "1.18.3"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
@@ -55,11 +55,11 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.12.3"}
markdown-clj/markdown-clj {:mvn/version "1.12.2"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.31.48"}}
software.amazon.awssdk/s3 {:mvn/version "2.28.26"}}
:paths ["src" "resources" "target/classes"]
:aliases
@@ -74,7 +74,7 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
{io.github.clojure/tools.build {:git/tag "v0.10.6" :git/sha "52cf7d6"}}
:ns-default build}
:test

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538",
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"

View File

@@ -31,8 +31,8 @@ export PENPOT_FLAGS="\
enable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions \
enable-subscriptions-old";
enable-subscriptons \
enable-subscriptons-old";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"

View File

@@ -24,8 +24,8 @@ export PENPOT_FLAGS="\
enable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions \
enable-subscriptions-old ";
enable-subscriptons \
enable-subscriptons-old ";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"

View File

@@ -10,6 +10,7 @@
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.features.components-v2 :as feat.compv2]
[clojure.set :as set]
[cuerdas.core :as str]))
@@ -27,11 +28,13 @@
(defn apply-pending-migrations!
"Apply alredy registered pending migrations to files"
[_cfg]
(doseq [[feature _file-id] (-> bfc/*state* deref :pending-to-migrate)]
[cfg]
(doseq [[feature file-id] (-> bfc/*state* deref :pending-to-migrate)]
(case feature
"components/v2"
nil
(feat.compv2/migrate-file! cfg file-id
:validate? (::validate cfg true)
:skip-on-graphic-error? true)
"fdata/shape-data-type"
nil

View File

@@ -18,7 +18,6 @@
[app.common.files.migrations :as-alias fmg]
[app.common.json :as json]
[app.common.logging :as l]
[app.common.media :as cmedia]
[app.common.schema :as sm]
[app.common.thumbnails :as cth]
[app.common.types.color :as ctcl]
@@ -74,7 +73,7 @@
[:size ::sm/int]
[:content-type :string]
[:bucket [::sm/one-of {:format :string} sto/valid-buckets]]
[:hash {:optional true} :string]])
[:hash :string]])
(def ^:private schema:file-thumbnail
[:map {:title "FileThumbnail"}
@@ -89,19 +88,13 @@
ctf/schema:file
[:map [:options {:optional true} ctf/schema:options]]])
;; --- HELPERS
(defn- default-now
[o]
(or o (dt/now)))
;; --- ENCODERS
(def encode-file
(sm/encoder schema:file sm/json-transformer))
(def encode-page
(sm/encoder ctp/schema:page sm/json-transformer))
(sm/encoder ::ctp/page sm/json-transformer))
(def encode-shape
(sm/encoder ::cts/shape sm/json-transformer))
@@ -136,7 +129,7 @@
(sm/decoder schema:manifest sm/json-transformer))
(def decode-media
(sm/decoder ctf/schema:media sm/json-transformer))
(sm/decoder ::ctf/media sm/json-transformer))
(def decode-component
(sm/decoder ::ctc/component sm/json-transformer))
@@ -236,13 +229,27 @@
:always
(bfc/clean-file-features))))))
(defn- resolve-extension
[mtype]
(case mtype
"image/png" ".png"
"image/jpeg" ".jpg"
"image/gif" ".gif"
"image/svg+xml" ".svg"
"image/webp" ".webp"
"font/woff" ".woff"
"font/woff2" ".woff2"
"font/ttf" ".ttf"
"font/otf" ".otf"
"application/octet-stream" ".bin"))
(defn- export-storage-objects
[{:keys [::output] :as cfg}]
(let [storage (sto/resolve cfg)]
(doseq [id (-> bfc/*state* deref :storage-objects not-empty)]
(let [sobject (sto/get-object storage id)
smeta (meta sobject)
ext (cmedia/mtype->extension (:content-type smeta))
ext (resolve-extension (:content-type smeta))
path (str "objects/" id ".json")
params (-> (meta sobject)
(assoc :id (:id sobject))
@@ -567,13 +574,7 @@
(let [object (->> (read-entry input entry)
(decode-media)
(validate-media))
object (-> object
(assoc :file-id file-id)
(update :created-at default-now)
;; FIXME: this is set default to true for
;; setting a value, this prop is no longer
;; relevant;
(assoc :is-local true))]
object (assoc object :file-id file-id)]
(if (= id (:id object))
(conj result object)
result)))
@@ -799,7 +800,7 @@
:expected-id (str id)
:found-id (str (:id object))))
(let [ext (cmedia/mtype->extension (:content-type object))
(let [ext (resolve-extension (:content-type object))
path (str "objects/" id ext)
content (->> path
(get-zip-entry input)
@@ -813,14 +814,13 @@
:expected-size (:size object)
:found-size (sto/get-size content)))
(when-let [hash (get object :hash)]
(when (not= hash (sto/get-hash content))
(ex/raise :type :validation
:code :inconsistent-penpot-file
:hint "found corrupted storage object: hash does not match"
:path path
:expected-hash (:hash object)
:found-hash (sto/get-hash content))))
(when (not= (:hash object) (sto/get-hash content))
(ex/raise :type :validation
:code :inconsistent-penpot-file
:hint "found corrupted storage object: hash does not match"
:path path
:expected-hash (:hash object)
:found-hash (sto/get-hash content)))
(let [params (-> object
(dissoc :id :size)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
(:refer-clojure :exclude [tap])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.transit :as t]
[app.http.errors :as errors]
@@ -53,20 +54,18 @@
::yres/status 200
::yres/body (yres/stream-body
(fn [_ output]
(let [channel (sp/chan :buf buf :xf (keep encode))
listener (events/start-listener
channel
(partial write! output)
(partial pu/close! output))]
(try
(binding [events/*channel* channel]
(binding [events/*channel* (sp/chan :buf buf :xf (keep encode))]
(let [listener (events/start-listener
(partial write! output)
(partial pu/close! output))]
(try
(let [result (handler)]
(events/tap :end result)))
(catch Throwable cause
(let [result (errors/handle' cause request)]
(events/tap channel :error result)))
(finally
(sp/close! channel)
(px/await! listener))))))}))
(events/tap :end result))
(catch Throwable cause
(events/tap :error (errors/handle' cause request))
(when-not (ex/instance? java.io.EOFException cause)
(binding [l/*context* (errors/request->context request)]
(l/err :hint "unexpected error on processing sse response" :cause cause))))
(finally
(sp/close! events/*channel*)
(px/await! listener)))))))}))

View File

@@ -8,11 +8,12 @@
"Media & Font postprocessing."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.schema :as sm]
[app.common.schema.openapi :as-alias oapi]
[app.common.spec :as us]
[app.common.svg :as csvg]
[app.config :as cf]
[app.db :as-alias db]
[app.storage :as-alias sto]
@@ -21,38 +22,39 @@
[buddy.core.bytes :as bb]
[buddy.core.codecs :as bc]
[clojure.java.shell :as sh]
[clojure.xml :as xml]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[datoteka.io :as io])
(:import
clojure.lang.XMLHandler
java.io.InputStream
javax.xml.XMLConstants
javax.xml.parsers.SAXParserFactory
org.apache.commons.io.IOUtils
org.im4java.core.ConvertCmd
org.im4java.core.IMOperation
org.im4java.core.Info))
(def schema:upload
(sm/register!
^{::sm/type ::upload}
[:map {:title "Upload"}
[:filename :string]
[:size ::sm/int]
[:path ::fs/path]
[:mtype {:optional true} :string]
[:headers {:optional true}
[:map-of :string :string]]]))
(s/def ::path fs/path?)
(s/def ::filename string?)
(s/def ::size integer?)
(s/def ::headers (s/map-of string? string?))
(s/def ::mtype string?)
(def ^:private schema:input
[:map {:title "Input"}
[:path ::fs/path]
[:mtype {:optional true} ::sm/text]])
(s/def ::upload
(s/keys :req-un [::filename ::size ::path]
:opt-un [::mtype ::headers]))
(def ^:private check-input
(sm/check-fn schema:input))
;; A subset of fields from the ::upload spec
(s/def ::input
(s/keys :req-un [::path]
:opt-un [::mtype]))
(sm/register!
^{::sm/type ::upload}
[:map {:title "Upload"}
[:filename :string]
[:size ::sm/int]
[:path ::fs/path]
[:mtype {:optional true} :string]
[:headers {:optional true}
[:map-of :string :string]]])
(defn validate-media-type!
([upload] (validate-media-type! upload cm/valid-image-types))
@@ -95,44 +97,17 @@
(catch Throwable e
(process-error e))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SVG PARSING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- secure-parser-factory
[^InputStream input ^XMLHandler handler]
(.. (doto (SAXParserFactory/newInstance)
(.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
(.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
(newSAXParser)
(parse input handler)))
(defn- strip-doctype
[data]
(cond-> data
(str/includes? data "<!DOCTYPE")
(str/replace #"<\!DOCTYPE[^>]*>" "")))
(defn- parse-svg
[text]
(let [text (strip-doctype text)]
(dm/with-open [istream (IOUtils/toInputStream text "UTF-8")]
(xml/parse istream secure-parser-factory))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IMAGE THUMBNAILS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:thumbnail-params
[:map {:title "ThumbnailParams"}
[:input schema:input]
[:format [:enum :jpeg :webp :png]]
[:quality [:int {:min 1 :max 100}]]
[:width :int]
[:height :int]])
(s/def ::width integer?)
(s/def ::height integer?)
(s/def ::format #{:jpeg :webp :png})
(s/def ::quality #(< 0 % 101))
(def ^:private check-thumbnail-params
(sm/check-fn schema:thumbnail-params))
(s/def ::thumbnail-params
(s/keys :req-un [::input ::format ::width ::height]))
;; Related info on how thumbnails generation
;; http://www.imagemagick.org/Usage/thumbnails/
@@ -154,38 +129,30 @@
:data tmp)))
(defmethod process :generic-thumbnail
[params]
(let [{:keys [quality width height] :as params}
(check-thumbnail-params params)
operation
(doto (IMOperation.)
(.addImage)
(.autoOrient)
(.strip)
(.thumbnail ^Integer (int width) ^Integer (int height) ">")
(.quality (double quality))
(.addImage))]
(generic-process (assoc params :operation operation))))
[{:keys [quality width height] :as params}]
(us/assert ::thumbnail-params params)
(let [op (doto (IMOperation.)
(.addImage)
(.autoOrient)
(.strip)
(.thumbnail ^Integer (int width) ^Integer (int height) ">")
(.quality (double quality))
(.addImage))]
(generic-process (assoc params :operation op))))
(defmethod process :profile-thumbnail
[params]
(let [{:keys [quality width height] :as params}
(check-thumbnail-params params)
operation
(doto (IMOperation.)
(.addImage)
(.autoOrient)
(.strip)
(.thumbnail ^Integer (int width) ^Integer (int height) "^")
(.gravity "center")
(.extent (int width) (int height))
(.quality (double quality))
(.addImage))]
(generic-process (assoc params :operation operation))))
[{:keys [quality width height] :as params}]
(us/assert ::thumbnail-params params)
(let [op (doto (IMOperation.)
(.addImage)
(.autoOrient)
(.strip)
(.thumbnail ^Integer (int width) ^Integer (int height) "^")
(.gravity "center")
(.extent (int width) (int height))
(.quality (double quality))
(.addImage))]
(generic-process (assoc params :operation op))))
(defn get-basic-info-from-svg
[{:keys [tag attrs] :as data}]
@@ -217,9 +184,10 @@
(defmethod process :info
[{:keys [input] :as params}]
(let [{:keys [path mtype] :as input} (check-input input)]
(us/assert ::input input)
(let [{:keys [path mtype]} input]
(if (= mtype "image/svg+xml")
(let [info (some-> path slurp parse-svg get-basic-info-from-svg)]
(let [info (some-> path slurp csvg/parse get-basic-info-from-svg)]
(when-not info
(ex/raise :type :validation
:code :invalid-svg-file

View File

@@ -92,9 +92,9 @@
[:string {:max 250}]
[::sm/one-of {:format "string"} valid-event-types]]]
[:props
[:map-of :keyword ::sm/any]]
[:map-of :keyword :any]]
[:context {:optional true}
[:map-of :keyword ::sm/any]]])
[:map-of :keyword :any]]])
(def schema:push-audit-events
[:map {:title "push-audit-events"}

View File

@@ -115,8 +115,7 @@
(db/update! pool :project
{:modified-at (dt/now)}
{:id project-id}
{::db/return-keys false})
{:id project-id})
result))

View File

@@ -189,7 +189,7 @@
[:is-shared ::sm/boolean]
[:project-id ::sm/uuid]
[:created-at ::dt/instant]
[:data {:optional true} ::sm/any]])
[:data {:optional true} :any]])
(def schema:permissions-mixin
[:map {:title "PermissionsMixin"}

View File

@@ -14,6 +14,7 @@
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.features.components-v2 :as feat.compv2]
[app.features.fdata :as fdata]
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
@@ -109,7 +110,7 @@
;; --- MUTATION COMMAND: persist-temp-file
(defn persist-temp-file
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [id ::rpc/profile-id] :as params}]
(let [file (files/get-file cfg id
:migrate? false
:lock-for-update? true)]
@@ -118,6 +119,7 @@
(ex/raise :type :validation
:code :cant-persist-already-persisted-file))
(let [changes (->> (db/cursor conn
(sql/select :file-change {:file-id id}
{:order-by [[:revn :asc]]})
@@ -145,6 +147,19 @@
:revn 1
:data (blob/encode (:data file))}
{:id id})
(let [team (teams/get-team conn :profile-id profile-id :project-id (:project-id file))
file-features (:features file)
team-features (cfeat/get-team-enabled-features cf/flags team)]
(when (and (contains? team-features "components/v2")
(not (contains? file-features "components/v2")))
;; Migrate components v2
(feat.compv2/migrate-file! cfg
(:id file)
:max-procs 2
:validate? true
:throw-on-validate? true)))
nil)))
(def ^:private schema:persist-temp-file

View File

@@ -80,9 +80,9 @@
(def ^:private schema:create-font-variant
[:map {:title "create-font-variant"}
[:team-id ::sm/uuid]
[:data [:map-of ::sm/text ::sm/any]]
[:data [:map-of :string :any]]
[:font-id ::sm/uuid]
[:font-family ::sm/text]
[:font-family :string]
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
[:font-style [::sm/one-of {:format "string"} valid-style]]])

View File

@@ -114,6 +114,18 @@
;; --- Query: Teams
(declare get-teams)
(def ^:private schema:get-teams
[:map {:title "get-teams"}])
(sv/defmethod ::get-teams
{::doc/added "1.17"
::sm/params schema:get-teams}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)]
(get-teams conn profile-id)))
(def sql:get-teams-with-permissions
"SELECT t.*,
tp.is_owner,
@@ -179,37 +191,6 @@
(->> (db/exec! conn [sql (:default-team-id profile) profile-id])
(into [] xform:process-teams))))
(def ^:private schema:get-teams
[:map {:title "get-teams"}])
(sv/defmethod ::get-teams
{::doc/added "1.17"
::sm/params schema:get-teams}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)]
(get-teams conn profile-id)))
(def ^:private sql:get-owned-teams
"SELECT t.id, t.name,
(SELECT count(*) FROM team_profile_rel WHERE team_id=t.id) AS total_members
FROM team AS t
JOIN team_profile_rel AS tpr ON (tpr.team_id = t.id)
WHERE t.is_default IS false
AND tpr.is_owner IS true
AND tpr.profile_id = ?
AND t.deleted_at IS NULL")
(defn- get-owned-teams
[cfg profile-id]
(->> (db/exec! cfg [sql:get-owned-teams profile-id])
(into [] (map decode-row))))
(sv/defmethod ::get-owned-teams
{::doc/added "2.8.0"
::sm/params schema:get-teams}
[cfg {:keys [::rpc/profile-id]}]
(get-owned-teams cfg profile-id))
;; --- Query: Team (by ID)
(declare get-team)

View File

@@ -9,7 +9,6 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.json :as json]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as smdj]
@@ -20,6 +19,7 @@
[app.http.sse :as-alias sse]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.util.json :as json]
[app.util.services :as sv]
[app.util.template :as tmpl]
[clojure.java.io :as io]
@@ -86,7 +86,7 @@
(fn [request]
(let [params (:query-params request)
pstyle (:type params "js")
context (assoc @context :param-style pstyle)]
context (assoc context :param-style pstyle)]
{::yres/status 200
::yres/body (-> (io/resource "app/templates/api-doc.tmpl")
@@ -178,7 +178,8 @@
(fn [_]
{::yres/status 200
::yres/headers {"content-type" "application/json; charset=utf-8"}
::yres/body (json/encode @context)})
::yres/body (json/encode context)})
(fn [_]
{::yres/status 404})))
@@ -208,7 +209,7 @@
(defmethod ig/init-key ::routes
[_ {:keys [::rpc/methods] :as cfg}]
[(let [context (delay (prepare-doc-context methods))]
[(let [context (prepare-doc-context methods)]
[["/_doc"
{:handler (doc-handler context)
:allowed-methods #{:get}}]
@@ -216,7 +217,7 @@
{:handler (doc-handler context)
:allowed-methods #{:get}}]])
(let [context (delay (prepare-openapi-context methods))]
(let [context (prepare-openapi-context methods)]
[["/openapi"
{:handler (openapi-handler)
:allowed-methods #{:get}}]

View File

@@ -0,0 +1,306 @@
;; 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) KALEIDOS INC
(ns app.srepl.components-v2
(:require
[app.common.fressian :as fres]
[app.common.logging :as l]
[app.db :as db]
[app.features.components-v2 :as feat]
[app.main :as main]
[app.srepl.helpers :as h]
[app.util.events :as events]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[datoteka.fs :as fs]
[datoteka.io :as io]
[promesa.exec :as px]
[promesa.exec.semaphore :as ps]
[promesa.util :as pu]))
(def ^:dynamic *scope* nil)
(def ^:dynamic *semaphore* nil)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PRIVATE HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-files-by-created-at
"SELECT id, features,
row_number() OVER (ORDER BY created_at DESC) AS rown
FROM file
WHERE deleted_at IS NULL
ORDER BY created_at DESC")
(defn- get-files
[conn]
(->> (db/cursor conn [sql:get-files-by-created-at] {:chunk-size 500})
(map feat/decode-row)
(remove (fn [{:keys [features]}]
(contains? features "components/v2")))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn migrate-file!
[file-id & {:keys [rollback? validate? label cache skip-on-graphic-error?]
:or {rollback? true
validate? false
skip-on-graphic-error? true}}]
(l/dbg :hint "migrate:start" :rollback rollback?)
(let [tpoint (dt/tpoint)
file-id (h/parse-uuid file-id)]
(binding [feat/*stats* (atom {})
feat/*cache* cache]
(try
(-> (assoc main/system ::db/rollback rollback?)
(feat/migrate-file! file-id
:validate? validate?
:skip-on-graphic-error? skip-on-graphic-error?
:label label))
(-> (deref feat/*stats*)
(assoc :elapsed (dt/format-duration (tpoint))))
(catch Throwable cause
(l/wrn :hint "migrate:error" :cause cause))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
(defn migrate-team!
[team-id & {:keys [rollback? skip-on-graphic-error? validate? label cache]
:or {rollback? true
validate? true
skip-on-graphic-error? true}}]
(l/dbg :hint "migrate:start" :rollback rollback?)
(let [team-id (h/parse-uuid team-id)
stats (atom {})
tpoint (dt/tpoint)]
(binding [feat/*stats* stats
feat/*cache* cache]
(try
(-> (assoc main/system ::db/rollback rollback?)
(feat/migrate-team! team-id
:label label
:validate? validate?
:skip-on-graphics-error? skip-on-graphic-error?))
(-> (deref feat/*stats*)
(assoc :elapsed (dt/format-duration (tpoint))))
(catch Throwable cause
(l/dbg :hint "migrate:error" :cause cause))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
(defn migrate-files!
"A REPL helper for migrate all files.
This function starts multiple concurrent file migration processes
until thw maximum number of jobs is reached which by default has the
value of `1`. This is controled with the `:max-jobs` option.
If you want to run this on multiple machines you will need to specify
the total number of partitions and the current partition.
In order to get the report table populated, you will need to provide
a correct `:label`. That label is also used for persist a file
snaphot before continue with the migration."
[& {:keys [max-jobs max-items rollback? validate?
cache skip-on-graphic-error?
label partitions current-partition]
:or {validate? false
rollback? true
max-jobs 1
current-partition 1
skip-on-graphic-error? true
max-items Long/MAX_VALUE}}]
(when (int? partitions)
(when-not (int? current-partition)
(throw (IllegalArgumentException. "missing `current-partition` parameter")))
(when-not (<= 0 current-partition partitions)
(throw (IllegalArgumentException. "invalid value on `current-partition` parameter"))))
(let [stats (atom {})
tpoint (dt/tpoint)
factory (px/thread-factory :virtual false :prefix "penpot/migration/")
executor (px/cached-executor :factory factory)
sjobs (ps/create :permits max-jobs)
migrate-file
(fn [file-id rown]
(try
(db/tx-run! (assoc main/system ::db/rollback rollback?)
(fn [system]
(db/exec-one! system ["SET LOCAL idle_in_transaction_session_timeout = 0"])
(feat/migrate-file! system file-id
:rown rown
:label label
:validate? validate?
:skip-on-graphic-error? skip-on-graphic-error?)))
(catch Throwable cause
(l/wrn :hint "unexpected error on processing file (skiping)"
:file-id (str file-id))
(events/tap :error
(ex-info "unexpected error on processing file (skiping)"
{:file-id file-id}
cause))
(swap! stats update :errors (fnil inc 0)))
(finally
(ps/release! sjobs))))
process-file
(fn [{:keys [id rown]}]
(ps/acquire! sjobs)
(px/run! executor (partial migrate-file id rown)))]
(l/dbg :hint "migrate:start"
:label label
:rollback rollback?
:max-jobs max-jobs
:max-items max-items)
(binding [feat/*stats* stats
feat/*cache* cache]
(try
(db/tx-run! main/system
(fn [{:keys [::db/conn] :as system}]
(db/exec! conn ["SET LOCAL statement_timeout = 0"])
(db/exec! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
(run! process-file
(->> (get-files conn)
(filter (fn [{:keys [rown] :as row}]
(if (int? partitions)
(= current-partition (inc (mod rown partitions)))
true)))
(take max-items)))
;; Close and await tasks
(pu/close! executor)))
(-> (deref stats)
(assoc :elapsed (dt/format-duration (tpoint))))
(catch Throwable cause
(l/dbg :hint "migrate:error" :cause cause)
(events/tap :error cause))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end"
:rollback rollback?
:elapsed elapsed)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CACHE POPULATE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def sql:sobjects-for-cache
"SELECT id,
row_number() OVER (ORDER BY created_at) AS index
FROM storage_object
WHERE (metadata->>'~:bucket' = 'file-media-object' OR
metadata->>'~:bucket' IS NULL)
AND metadata->>'~:content-type' = 'image/svg+xml'
AND deleted_at IS NULL
AND size < 1135899
ORDER BY created_at ASC")
(defn populate-cache!
"A REPL helper for migrate all files.
This function starts multiple concurrent file migration processes
until thw maximum number of jobs is reached which by default has the
value of `1`. This is controled with the `:max-jobs` option.
If you want to run this on multiple machines you will need to specify
the total number of partitions and the current partition.
In order to get the report table populated, you will need to provide
a correct `:label`. That label is also used for persist a file
snaphot before continue with the migration."
[& {:keys [max-jobs] :or {max-jobs 1}}]
(let [tpoint (dt/tpoint)
factory (px/thread-factory :virtual false :prefix "penpot/cache/")
executor (px/cached-executor :factory factory)
sjobs (ps/create :permits max-jobs)
retrieve-sobject
(fn [id index]
(let [path (feat/get-sobject-cache-path id)
parent (fs/parent path)]
(try
(when-not (fs/exists? parent)
(fs/create-dir parent))
(if (fs/exists? path)
(l/inf :hint "create cache entry" :status "exists" :index index :id (str id) :path (str path))
(let [svg-data (feat/get-optimized-svg id)]
(with-open [^java.lang.AutoCloseable stream (io/output-stream path)]
(let [writer (fres/writer stream)]
(fres/write! writer svg-data)))
(l/inf :hint "create cache entry" :status "created"
:index index
:id (str id)
:path (str path))))
(catch Throwable cause
(l/wrn :hint "create cache entry"
:status "error"
:index index
:id (str id)
:path (str path)
:cause cause))
(finally
(ps/release! sjobs)))))
process-sobject
(fn [{:keys [id index]}]
(ps/acquire! sjobs)
(px/run! executor (partial retrieve-sobject id index)))]
(l/dbg :hint "migrate:start"
:max-jobs max-jobs)
(try
(binding [feat/*system* main/system]
(run! process-sobject
(db/exec! main/system [sql:sobjects-for-cache]))
;; Close and await tasks
(pu/close! executor))
{:elapsed (dt/format-duration (tpoint))}
(catch Throwable cause
(l/dbg :hint "populate:error" :cause cause))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "populate:end"
:elapsed elapsed))))))

View File

@@ -13,6 +13,7 @@
[app.common.files.migrations :as fmg]
[app.common.files.validate :as cfv]
[app.db :as db]
[app.features.components-v2 :as feat.comp-v2]
[app.main :as main]
[app.rpc.commands.files :as files]
[app.rpc.commands.files-snapshot :as fsnap]
@@ -61,27 +62,6 @@
{:id id})
team))
(def ^:private sql:get-and-lock-team-files
"SELECT f.id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE p.team_id = ?
AND p.deleted_at IS NULL
AND f.deleted_at IS NULL
FOR UPDATE")
(defn get-team
[conn team-id]
(-> (db/get conn :team {:id team-id}
{::db/remove-deleted false
::db/check-deleted false})
(update :features db/decode-pgarray #{})))
(defn get-and-lock-team-files
[conn team-id]
(transduce (map :id) conj []
(db/plan conn [sql:get-and-lock-team-files team-id])))
(defn reset-file-data!
"Hardcode replace of the data of one file."
[system id data]
@@ -116,7 +96,7 @@
(defn take-team-snapshot!
[system team-id label]
(let [conn (db/get-connection system)]
(->> (get-and-lock-team-files conn team-id)
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
(reduce (fn [result file-id]
(let [file (fsnap/get-file-snapshots system file-id)]
(fsnap/create-file-snapshot! system file
@@ -128,16 +108,19 @@
(defn restore-team-snapshot!
[system team-id label]
(let [conn (db/get-connection system)
ids (->> (get-and-lock-team-files conn team-id)
ids (->> (feat.comp-v2/get-and-lock-team-files conn team-id)
(into #{}))
snap (search-file-snapshots conn ids label)
ids' (into #{} (map :file-id) snap)]
ids' (into #{} (map :file-id) snap)
team (-> (feat.comp-v2/get-team conn team-id)
(update :features disj "components/v2"))]
(when (not= ids ids')
(throw (RuntimeException. "no uniform snapshot available")))
(feat.comp-v2/update-team! conn team)
(reduce (fn [result {:keys [file-id id]}]
(fsnap/restore-file-snapshot! system file-id id)
(inc result))

View File

@@ -22,6 +22,7 @@
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.components-v2 :as feat.comp-v2]
[app.features.fdata :as feat.fdata]
[app.loggers.audit :as audit]
[app.main :as main]
@@ -438,7 +439,7 @@
(binding [h/*system* system
db/*conn* (db/get-connection system)]
(->> (h/get-and-lock-team-files conn team-id)
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
(reduce (fn [result file-id]
(if (h/process-file! system file-id update-fn opts)
(inc result)

View File

@@ -10,6 +10,7 @@
to them. Mainly used in http.sse for progress reporting."
(:refer-clojure :exclude [tap run!])
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[promesa.exec :as px]
@@ -17,30 +18,33 @@
(def ^:dynamic *channel* nil)
(defn channel
[]
(sp/chan :buf 32))
(defn tap
([type data]
(when-let [channel *channel*]
(sp/put! channel [type data])
nil))
([channel type data]
(when channel
(sp/put! channel [type data])
nil)))
[type data]
(when-let [channel *channel*]
(sp/put! channel [type data])
nil))
(defn start-listener
[channel on-event on-close]
(assert (sp/chan? channel) "expected active events channel")
[on-event on-close]
(dm/assert!
"expected active events channel"
(sp/chan? *channel*))
(px/thread
{:virtual true}
(try
(loop []
(when-let [event (sp/take! channel)]
(when-let [event (sp/take! *channel*)]
(let [result (ex/try! (on-event event))]
(if (ex/exception? result)
(do
(l/wrn :hint "unexpected exception" :cause result)
(sp/close! channel))
(sp/close! *channel*))
(recur)))))
(finally
(on-close)))))
@@ -51,7 +55,7 @@
[f on-event]
(binding [*channel* (sp/chan :buf 32)]
(let [listener (start-listener *channel* on-event (constantly nil))]
(let [listener (start-listener on-event (constantly nil))]
(try
(f)
(finally

View File

@@ -1712,7 +1712,6 @@
[{:fill-image
{:id (:id fmedia)
:name "test"
:mtype "image/jpeg"
:width 200
:height 200}}]]

View File

@@ -449,23 +449,6 @@
(t/is (nil? res)))))
(t/deftest get-owned-teams
(let [profile1 (th/create-profile* 1 {:is-active true})
profile2 (th/create-profile* 2 {:is-active true})
team1 (th/create-team* 1 {:profile-id (:id profile1)})
team2 (th/create-team* 2 {:profile-id (:id profile2)})
params {::th/type :get-owned-teams
::rpc/profile-id (:id profile1)}
out (th/command! params)]
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:id team1) (-> result first :id)))
(t/is (not= (:default-team-id profile1) (-> result first :id))))))
(t/deftest team-deletion-1
(let [profile1 (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)})

View File

@@ -12,14 +12,14 @@
org.apache.logging.log4j/log4j-web {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
org.slf4j/slf4j-api {:mvn/version "2.0.16"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
selmer/selmer {:mvn/version "1.12.62"}
selmer/selmer {:mvn/version "1.12.61"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.13"}
metosin/malli {:mvn/version "0.18.0"}
metosin/malli {:mvn/version "0.17.0"}
expound/expound {:mvn/version "0.9.0"}
com.cognitect/transit-clj {:mvn/version "1.0.333"}
@@ -30,7 +30,7 @@
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2023.11.09-407"}
funcool/promesa
{:git/sha "f52f58cfacf62f59eab717e2637f37729d0cc383"
{:git/sha "0c5ed6ad033515a2df4b55addea044f60e9653d0"
:git/url "https://github.com/funcool/promesa"}
funcool/datoteka
@@ -59,7 +59,7 @@
{:dev
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "3.0.5"}
thheller/shadow-cljs {:mvn/version "3.0.3"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
@@ -68,7 +68,7 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
{io.github.clojure/tools.build {:git/tag "v0.10.6" :git/sha "52cf7d6"}}
:ns-default build}
:test
@@ -76,9 +76,9 @@
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"]}
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
:outdated
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
:main-opts ["-m" "antq.core"]}}}

View File

@@ -4,19 +4,20 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538",
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
},
"dependencies": {
"luxon": "^3.4.4"
"luxon": "^3.4.4",
"sax": "^1.4.1"
},
"devDependencies": {
"concurrently": "^9.0.1",
"nodemon": "^3.1.7",
"shadow-cljs": "3.0.5",
"shadow-cljs": "3.0.3",
"source-map-support": "^0.5.21",
"ws": "^8.17.0"
},

View File

@@ -13,12 +13,17 @@
[app.common.features :as cfeat]
[app.common.files.changes :as ch]
[app.common.files.migrations :as fmig]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.schema :as sm]
[app.common.svg :as csvg]
[app.common.types.color :as types.color]
[app.common.types.component :as types.component]
[app.common.types.components-list :as types.components-list]
[app.common.types.container :as types.container]
[app.common.types.file :as types.file]
[app.common.types.page :as types.page]
[app.common.types.pages-list :as types.pages-list]
[app.common.types.shape :as types.shape]
[app.common.types.typography :as types.typography]
[app.common.uuid :as uuid]
@@ -32,36 +37,41 @@
(def ^:private conjv (fnil conj []))
(def ^:private conjs (fnil conj #{}))
(defn- default-uuid
(defn default-uuid
[v]
(or v (uuid/next)))
(defn- track-used-name
[state name]
(let [container-id (::current-page-id state)]
(update-in state [::unames container-id] conjs name)))
[file name]
(let [container-id (::current-page-id file)]
(update-in file [::unames container-id] conjs name)))
(defn- commit-change
[state change & {:keys [add-container]}]
(let [file-id (get state ::current-file-id)]
(assert (uuid? file-id) "no current file id")
[file change & {:keys [add-container]
:or {add-container false}}]
(let [change (cond-> change
add-container
(assoc :page-id (::current-page-id state)
:frame-id (::current-frame-id state)))]
(update-in state [::files file-id :data] ch/process-changes [change] false))))
(let [change (cond-> change
add-container
(assoc :page-id (::current-page-id file)
:frame-id (::current-frame-id file)))]
(-> file
(update ::changes conjv change)
(update :data ch/process-changes [change] false))))
(defn- lookup-objects
[file]
(dm/get-in file [:data :pages-index (::current-page-id file) :objects]))
(defn- commit-shape
[state shape]
[file shape]
(let [parent-id
(-> state ::parent-stack peek)
(-> file ::parent-stack peek)
frame-id
(get state ::current-frame-id)
(::current-frame-id file)
page-id
(get state ::current-page-id)
(::current-page-id file)
change
{:type :add-obj
@@ -72,31 +82,39 @@
:frame-id frame-id
:page-id page-id}]
(-> state
(-> file
(commit-change change)
(track-used-name (:name shape)))))
(defn- generate-name
[type data]
(if (= type :svg-raw)
(let [tag (dm/get-in data [:content :tag])]
(str "svg-" (cond (string? tag) tag
(keyword? tag) (d/name tag)
(nil? tag) "node"
:else (str tag))))
(str/capital (d/name type))))
(defn- unique-name
[name state]
(let [container-id (::current-page-id state)
unames (dm/get-in state [:unames container-id])]
[name file]
(let [container-id (::current-page-id file)
unames (dm/get-in file [:unames container-id])]
(d/unique-name name (or unames #{}))))
(defn- clear-names [file]
(dissoc file ::unames))
(defn- assign-shape-name
(defn- assign-name
"Given a tag returns its layer name"
[shape state]
(cond-> shape
(nil? (:name shape))
(assoc :name (let [type (get shape :type)]
(case type
:frame "Board"
(str/capital (d/name type)))))
[data file type]
(cond-> data
(nil? (:name data))
(assoc :name (generate-name type data))
:always
(update :name unique-name state)))
(update :name unique-name file)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS
@@ -117,32 +135,17 @@
(def decode-library-typography
(sm/decode-fn types.typography/schema:typography sm/json-transformer))
(def schema:add-component
[:map
[:component-id ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:name {:optional true} ::sm/text]
[:path {:optional true} ::sm/text]
[:frame-id {:optional true} ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]])
(def ^:private check-add-component
(sm/check-fn schema:add-component
:hint "invalid arguments passed for add-component"))
(def decode-add-component
(sm/decode-fn schema:add-component sm/json-transformer))
(def decode-component
(sm/decode-fn types.component/schema:component sm/json-transformer))
(def schema:add-component-instance
[:map
[:component-id ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:frame-id {:optional true} ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]])
[:x ::sm/safe-number]
[:y ::sm/safe-number]])
(def ^:private check-add-component-instance
(sm/check-fn schema:add-component-instance
:hint "invalid arguments passed for add-component-instance"))
(def check-add-component-instance
(sm/check-fn schema:add-component-instance))
(def decode-add-component-instance
(sm/decode-fn schema:add-component-instance sm/json-transformer))
@@ -155,77 +158,37 @@
(def decode-add-bool
(sm/decode-fn schema:add-bool sm/json-transformer))
(def ^:private check-add-bool
(def check-add-bool
(sm/check-fn schema:add-bool))
(def schema:add-file-media
[:map
[:id {:optional true} ::sm/uuid]
[:name ::sm/text]
[:width ::sm/int]
[:height ::sm/int]])
(def decode-add-file-media
(sm/decode-fn schema:add-file-media sm/json-transformer))
(def check-add-file-media
(sm/check-fn schema:add-file-media))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn create-state
[]
{})
(defn get-current-page
[state]
(let [file-id (get state ::current-file-id)
page-id (get state ::current-page-id)]
(assert (uuid? file-id) "expected current-file-id to be assigned")
(assert (uuid? page-id) "expected current-page-id to be assigned")
(dm/get-in state [::files file-id :data :pages-index page-id])))
(defn get-current-objects
[state]
(-> (get-current-page state)
(get :objects)))
(defn get-shape
[state shape-id]
(-> (get-current-objects state)
(defn lookup-shape [file shape-id]
(-> (lookup-objects file)
(get shape-id)))
(defn add-file
[state params]
(defn get-current-page
[file]
(let [page-id (::current-page-id file)]
(dm/get-in file [:data :pages-index page-id])))
(defn create-file
[params]
(let [params (-> params
(assoc :features cfeat/default-features)
(assoc :migrations fmig/available-migrations)
(update :id default-uuid))
file (types.file/make-file params :create-page false)]
(-> state
(update ::files assoc (:id file) file)
(assoc ::current-file-id (:id file)))))
(declare close-page)
(defn close-file
[state]
(let [state (-> state
(close-page)
(dissoc ::current-file-id))]
state))
(assoc :migrations fmig/available-migrations))]
(types.file/make-file params :create-page false)))
(defn add-page
[state params]
[file params]
(let [page (-> (types.page/make-empty-page params)
(types.page/check-page))
change {:type :add-page
:page page}]
(-> state
(-> file
(commit-change change)
;; Current page being edited
@@ -240,96 +203,96 @@
;; Last object id added
(assoc ::last-id nil))))
(defn close-page [state]
(-> state
(defn close-page [file]
(-> file
(dissoc ::current-page-id)
(dissoc ::parent-stack)
(dissoc ::last-id)
(clear-names)))
(defn add-board
[state params]
(defn add-artboard
[file data]
(let [{:keys [id] :as shape}
(-> params
(-> data
(update :id default-uuid)
(assoc :type :frame)
(assign-shape-name state)
(assign-name file :frame)
(types.shape/setup-shape)
(types.shape/check-shape))]
(-> state
(-> file
(commit-shape shape)
(update ::parent-stack conjv id)
(assoc ::current-frame-id id)
(assoc ::last-id id))))
(defn close-board
[state]
(let [parent-id (-> state ::parent-stack peek)
parent (get-shape state parent-id)]
(-> state
(defn close-artboard
[file]
(let [parent-id (-> file ::parent-stack peek)
parent (lookup-shape file parent-id)]
(-> file
(assoc ::current-frame-id (or (:frame-id parent) root-id))
(update ::parent-stack pop))))
(defn add-group
[state params]
[file params]
(let [{:keys [id] :as shape}
(-> params
(update :id default-uuid)
(assoc :type :group)
(assign-shape-name state)
(assign-name file :group)
(types.shape/setup-shape)
(types.shape/check-shape))]
(-> state
(-> file
(commit-shape shape)
(assoc ::last-id id)
(update ::parent-stack conjv id))))
(defn close-group
[state]
(let [group-id (-> state :parent-stack peek)
group (get-shape state group-id)
[file]
(let [group-id (-> file :parent-stack peek)
group (lookup-shape file group-id)
children (->> (get group :shapes)
(into [] (keep (partial get-shape state)))
(into [] (keep (partial lookup-shape file)))
(not-empty))]
(assert (some? children) "group expect to have at least 1 children")
(let [state (if (:masked-group group)
(let [mask (first children)
change {:type :mod-obj
:id group-id
:operations
[{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true}
{:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true}
{:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true}
{:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true}
{:type :set :attr :flip-y :val (-> mask :flip-y) :ignore-touched true}
{:type :set :attr :selrect :val (-> mask :selrect) :ignore-touched true}
{:type :set :attr :points :val (-> mask :points) :ignore-touched true}]}]
(commit-change state change :add-container true))
(let [group (gsh/update-group-selrect group children)
change {:type :mod-obj
:id group-id
:operations
[{:type :set :attr :selrect :val (:selrect group) :ignore-touched true}
{:type :set :attr :points :val (:points group) :ignore-touched true}
{:type :set :attr :x :val (-> group :selrect :x) :ignore-touched true}
{:type :set :attr :y :val (-> group :selrect :y) :ignore-touched true}
{:type :set :attr :width :val (-> group :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> group :selrect :height) :ignore-touched true}]}]
(let [file (if (:masked-group group)
(let [mask (first children)
change {:type :mod-obj
:id group-id
:operations
[{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true}
{:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true}
{:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true}
{:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true}
{:type :set :attr :flip-y :val (-> mask :flip-y) :ignore-touched true}
{:type :set :attr :selrect :val (-> mask :selrect) :ignore-touched true}
{:type :set :attr :points :val (-> mask :points) :ignore-touched true}]}]
(commit-change file change :add-container true))
(let [group (gsh/update-group-selrect group children)
change {:type :mod-obj
:id group-id
:operations
[{:type :set :attr :selrect :val (:selrect group) :ignore-touched true}
{:type :set :attr :points :val (:points group) :ignore-touched true}
{:type :set :attr :x :val (-> group :selrect :x) :ignore-touched true}
{:type :set :attr :y :val (-> group :selrect :y) :ignore-touched true}
{:type :set :attr :width :val (-> group :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> group :selrect :height) :ignore-touched true}]}]
(commit-change state change :add-container true)))]
(update state ::parent-stack pop))))
(commit-change file change :add-container true)))]
(update file ::parent-stack pop))))
(defn add-bool
[state params]
[file params]
(let [{:keys [group-id type]}
(check-add-bool params)
group
(get-shape state group-id)
(lookup-shape file group-id)
children
(->> (get group :shapes)
@@ -337,7 +300,7 @@
(assert (some? children) "expect group to have at least 1 element")
(let [objects (get-current-objects state)
(let [objects (lookup-objects file)
bool (-> group
(assoc :type :bool)
(gsh/update-bool objects))
@@ -354,110 +317,101 @@
{:type :set :attr :width :val (-> bool :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> bool :selrect :height) :ignore-touched true}]}]
(-> state
(-> file
(commit-change change :add-container true)
(assoc ::last-id group-id)))))
(defn add-shape
[state params]
[file params]
(let [obj (-> params
(d/update-when :svg-attrs csvg/attrs->props)
(types.shape/setup-shape)
(assign-shape-name state))]
(-> state
(assign-name file :type))]
(-> file
(commit-shape obj)
(assoc ::last-id (:id obj)))))
(defn add-library-color
[state color]
[file color]
(let [color (-> color
(update :opacity d/nilv 1)
(update :id default-uuid)
(types.color/check-library-color color))
change {:type :add-color
:color color}]
(-> state
(-> file
(commit-change change)
(assoc ::last-id (:id color)))))
(defn add-library-typography
[state typography]
[file typography]
(let [typography (-> typography
(update :id default-uuid)
(d/without-nils))
change {:type :add-typography
:id (:id typography)
:typography typography}]
(-> state
(-> file
(commit-change change)
(assoc ::last-id (:id typography)))))
(defn add-component
[state params]
(let [{:keys [component-id file-id page-id frame-id name path]}
(-> (check-add-component params)
(update :component-id default-uuid))
[file params]
(let [change1 {:type :add-component
:id (or (:id params) (uuid/next))
:name (:name params)
:path (:path params)
:main-instance-id (:main-instance-id params)
:main-instance-page (:main-instance-page params)}
file-id
(or file-id (::current-file-id state))
comp-id (get change1 :id)
page-id
(or page-id (get state ::current-page-id))
frame-id
(or frame-id (get state ::current-frame-id))
change1
(d/without-nils
{:type :add-component
:id component-id
:name (or name "anonmous")
:path path
:main-instance-id frame-id
:main-instance-page page-id})
change2
{:type :mod-obj
:id frame-id
:page-id page-id
:operations
[{:type :set :attr :component-root :val true}
{:type :set :attr :main-instance :val true}
{:type :set :attr :component-id :val component-id}
{:type :set :attr :component-file :val file-id}]}]
(-> state
change2 {:type :mod-obj
:id (:main-instance-id params)
:operations
[{:type :set :attr :component-root :val true}
{:type :set :attr :component-id :val comp-id}
{:type :set :attr :component-file :val (:id file)}]}]
(-> file
(commit-change change1)
(commit-change change2))))
(commit-change change2)
(assoc ::last-id comp-id)
(assoc ::current-frame-id comp-id))))
(defn add-component-instance
[state params]
[{:keys [id data] :as file} params]
(let [{:keys [component-id file-id frame-id page-id]}
(let [{:keys [component-id x y]}
(check-add-component-instance params)
file-id
(or file-id (get state ::current-file-id))
frame-id
(or frame-id (get state ::current-frame-id))
component
(types.components-list/get-component data component-id)
page-id
(or page-id (get state ::current-page-id))
(get file ::current-page-id)]
change
{:type :mod-obj
:id frame-id
:page-id page-id
:operations
[{:type :set :attr :component-root :val true}
{:type :set :attr :component-id :val component-id}
{:type :set :attr :component-file :val file-id}]}]
(assert (uuid? page-id) "page-id is expected to be set")
(assert (uuid? component) "component is expected to exist")
(commit-change state change)))
;; FIXME: this should be on files and not in pages-list
(let [page (types.pages-list/get-page (:data file) page-id)
pos (gpt/point x y)
[shape shapes]
(types.container/make-component-instance page component id pos)
file
(reduce #(commit-change %1
{:type :add-obj
:id (:id %2)
:page-id (:id page)
:parent-id (:parent-id %2)
:frame-id (:frame-id %2)
:ignore-touched true
:obj %2})
file
shapes)]
(assoc file ::last-id (:id shape)))))
(defn delete-shape
[file id]
@@ -469,12 +423,10 @@
:id id}))
(defn update-shape
[state shape-id f]
(let [page-id (get state ::current-page-id)
objects (get-current-objects state)
[file shape-id f]
(let [page-id (::current-page-id file)
objects (lookup-objects file)
old-shape (get objects shape-id)
new-shape (f old-shape)
attrs (d/concat-set
(keys old-shape)
@@ -488,7 +440,7 @@
changes
(conj changes {:type :set :attr attr :val new-val :ignore-touched true}))))]
(-> state
(-> file
(commit-change
{:type :mod-obj
:operations (reduce generate-operation [] attrs)
@@ -497,12 +449,12 @@
(assoc ::last-id shape-id))))
(defn add-guide
[state guide]
[file guide]
(let [guide (cond-> guide
(nil? (:id guide))
(update :id default-uuid))
page-id (::current-page-id state)]
(-> state
(assoc :id (uuid/next)))
page-id (::current-page-id file)]
(-> file
(commit-change
{:type :set-guide
:page-id page-id
@@ -511,54 +463,24 @@
(assoc ::last-id (:id guide)))))
(defn delete-guide
[state id]
(let [page-id (::current-page-id state)]
(commit-change state
[file id]
(let [page-id (::current-page-id file)]
(commit-change file
{:type :set-guide
:page-id page-id
:id id
:params nil})))
(defn update-guide
[state guide]
(let [page-id (::current-page-id state)]
(commit-change state
[file guide]
(let [page-id (::current-page-id file)]
(commit-change file
{:type :set-guide
:page-id page-id
:id (:id guide)
:params guide})))
(defrecord BlobWrapper [mtype size blob])
(defn add-file-media
[state params blob]
(assert (instance? BlobWrapper blob) "expect blob to be wrapped")
(let [media-id
(uuid/next)
file-id
(get state ::current-file-id)
{:keys [id width height name]}
(-> params
(update :id default-uuid)
(check-add-file-media params))]
(-> state
(update ::blobs assoc media-id blob)
(update ::media assoc media-id
{:id media-id
:bucket "file-media-object"
:content-type (get blob :mtype)
:size (get blob :size)})
(update ::file-media assoc id
{:id id
:name name
:width width
:height height
:file-id file-id
:media-id media-id
:mtype (get blob :mtype)})
(assoc ::last-id id))))
(defn strip-image-extension [filename]
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
(str/replace filename image-extensions-re "")))

View File

@@ -47,14 +47,14 @@
[:type [:= :assign]]
;; NOTE: the full decoding is happening on the handler because it
;; needs a proper context of the current shape and its type
[:value [:map-of :keyword ::sm/any]]
[:value [:map-of :keyword :any]]
[:ignore-touched {:optional true} :boolean]
[:ignore-geometry {:optional true} :boolean]]]
[:set
[:map {:title "SetOperation"}
[:type [:= :set]]
[:attr :keyword]
[:val ::sm/any]
[:val :any]
[:ignore-touched {:optional true} :boolean]
[:ignore-geometry {:optional true} :boolean]]]
[:set-touched
@@ -238,9 +238,9 @@
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]
[:parent-id ::sm/uuid]
[:shapes ::sm/any]
[:shapes :any]
[:index {:optional true} [:maybe :int]]
[:after-shape {:optional true} ::sm/any]
[:after-shape {:optional true} :any]
[:component-swap {:optional true} :boolean]]]
[:reorder-children
@@ -250,14 +250,14 @@
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]
[:parent-id ::sm/uuid]
[:shapes ::sm/any]]]
[:shapes :any]]]
[:add-page
[:map {:title "AddPageChange"}
[:type [:= :add-page]]
[:id {:optional true} ::sm/uuid]
[:name {:optional true} :string]
[:page {:optional true} ::sm/any]]]
[:page {:optional true} :any]]]
[:mod-page
[:map {:title "ModPageChange"}
@@ -310,12 +310,12 @@
[:add-media
[:map {:title "AddMediaChange"}
[:type [:= :add-media]]
[:object ctf/schema:media]]]
[:object ::ctf/media-object]]]
[:mod-media
[:map {:title "ModMediaChange"}
[:type [:= :mod-media]]
[:object ctf/schema:media]]]
[:object ::ctf/media-object]]]
[:del-media
[:map {:title "DelMediaChange"}
@@ -327,14 +327,14 @@
[:type [:= :add-component]]
[:id ::sm/uuid]
[:name :string]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
[:path {:optional true} :string]]]
[:mod-component
[:map {:title "ModCompoenentChange"}
[:type [:= :mod-component]]
[:id ::sm/uuid]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
[:name {:optional true} :string]
[:variant-id {:optional true} ::sm/uuid]
[:variant-properties {:optional true} [:vector ::ctv/variant-property]]]]
@@ -411,7 +411,7 @@
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib ::sm/any]]]
[:tokens-lib :any]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}
@@ -425,12 +425,7 @@
[:type [:= :set-token]]
[:set-name :string]
[:token-name :string]
[:token [:maybe ctob/schema:token-attrs]]]]
[:set-base-font-size
[:map {:title "ModBaseFontSize"}
[:type [:= :set-base-font-size]]
[:base-font-size :string]]]]])
[:token [:maybe ctob/schema:token-attrs]]]]]])
(def schema:changes
[:sequential {:gen/max 5 :gen/min 1} schema:change])
@@ -1073,13 +1068,6 @@
(ctob/ensure-tokens-lib)
(ctob/move-set-group from-path to-path before-path before-group))))
;; === Base font size
(defmethod process-change :set-base-font-size
[data {:keys [base-font-size]}]
(ctf/set-base-font-size data base-font-size))
;; === Operations
(def ^:private decode-shape

View File

@@ -24,19 +24,18 @@
;; Auxiliary functions to help create a set of changes (undo + redo)
(def schema:changes
(sm/register!
^{::sm/type ::changes}
[:map {:title "changes"}
[:redo-changes vector?]
[:undo-changes seq?]
[:origin {:optional true} ::sm/any]
[:save-undo? {:optional true} boolean?]
[:stack-undo? {:optional true} boolean?]
[:undo-group {:optional true} ::sm/any]]))
(sm/register!
^{::sm/type ::changes}
[:map {:title "changes"}
[:redo-changes vector?]
[:undo-changes seq?]
[:origin {:optional true} any?]
[:save-undo? {:optional true} boolean?]
[:stack-undo? {:optional true} boolean?]
[:undo-group {:optional true} any?]])
(def check-changes!
(sm/check-fn schema:changes))
(sm/check-fn ::changes))
(defn empty-changes
([origin page-id]
@@ -125,41 +124,28 @@
; TODO: remove this when not needed
(defn- assert-page-id!
[changes]
(assert
(contains? (meta changes) ::page-id)
"Give a page-id or call (with-page) before using this function"))
(defn- assert-page!
[changes]
(assert
(contains? (meta changes) ::page)
"Give a page or call (with-page) before using this function"))
(dm/assert!
"Give a page-id or call (with-page) before using this function"
(contains? (meta changes) ::page-id)))
(defn- assert-container-id!
[changes]
(assert
(dm/assert!
"Give a page-id or call (with-container) before using this function"
(or (contains? (meta changes) ::page-id)
(contains? (meta changes) ::component-id))
"Give a page-id or call (with-container) before using this function"))
(contains? (meta changes) ::component-id))))
(defn- assert-objects!
[changes]
(assert
(contains? (meta changes) ::file-data)
"Call (with-objects) before using this function"))
(dm/assert!
"Call (with-objects) before using this function"
(contains? (meta changes) ::file-data)))
(defn- assert-library!
[changes]
(assert
(contains? (meta changes) ::library-data)
"Call (with-library-data) before using this function"))
(defn- assert-file-data!
[changes]
(assert
(contains? (meta changes) ::file-data)
"Call (with-file-data) before using this function"))
(dm/assert!
"Call (with-library-data) before using this function"
(contains? (meta changes) ::library-data)))
(defn- lookup-objects
[changes]
@@ -168,9 +154,9 @@
(defn apply-changes-local
[changes & {:keys [apply-to-library?]}]
(assert
(check-changes! changes)
"expected valid changes")
(dm/assert!
"expected valid changes"
(check-changes! changes))
(if-let [file-data (::file-data (meta changes))]
(let [library-data (::library-data (meta changes))
@@ -209,7 +195,6 @@
(defn mod-page
([changes options]
(assert-page! changes)
(let [page (::page (meta changes))]
(mod-page changes page options)))
@@ -240,7 +225,6 @@
([changes type id namespace key value]
(set-plugin-data changes type id nil namespace key value))
([changes type id page-id namespace key value]
(assert-file-data! changes)
(let [data (::file-data (meta changes))
old-val
(case type
@@ -307,8 +291,6 @@
(defn set-guide
[changes id guide]
(assert-page-id! changes)
(assert-page! changes)
(let [page-id (::page-id (meta changes))
page (::page (meta changes))
old-val (dm/get-in page [:guides id])]
@@ -322,11 +304,8 @@
:page-id page-id
:id id
:params old-val}))))
(defn set-flow
[changes id flow]
(assert-page-id! changes)
(assert-page! changes)
(let [page-id (::page-id (meta changes))
page (::page (meta changes))
old-val (dm/get-in page [:flows id])
@@ -345,8 +324,6 @@
(defn set-comment-thread-position
[changes {:keys [id frame-id position] :as thread}]
(assert-page-id! changes)
(assert-page! changes)
(let [page-id (::page-id (meta changes))
page (::page (meta changes))
@@ -368,8 +345,6 @@
(defn set-default-grid
[changes type params]
(assert-page-id! changes)
(assert-page! changes)
(let [page-id (::page-id (meta changes))
page (::page (meta changes))
old-val (dm/get-in page [:grids type])
@@ -523,7 +498,6 @@
:or {ignore-geometry? false ignore-touched false with-objects? false}}]
(assert-container-id! changes)
(assert-objects! changes)
(assert-page-id! changes)
(let [page-id (::page-id (meta changes))
component-id (::component-id (meta changes))
objects (lookup-objects changes)
@@ -872,7 +846,6 @@
(defn set-tokens-lib
[changes tokens-lib]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
(-> changes
@@ -1162,16 +1135,3 @@
(defn get-page-id
[changes]
(::page-id (meta changes)))
(defn set-base-font-size
[changes new-base-font-size]
(assert-file-data! changes)
(let [file-data (::file-data (meta changes))
previous-font-size (ctf/get-base-font-size file-data)]
(-> changes
(update :redo-changes conj {:type :set-base-font-size
:base-font-size new-base-font-size})
(update :undo-changes conj {:type :set-base-font-size
:base-font-size previous-font-size})
(apply-changes-local))))

View File

@@ -195,7 +195,6 @@
;; Rect
(dm/export grc/rect->points)
(dm/export grc/center->rect)
;;
(dm/export gsff/fit-frame-modifiers)

View File

@@ -596,7 +596,7 @@
(generate-sync-shape-direct changes file libraries container shape-id false)))
(defmethod generate-sync-shape :colors
[_ changes library-id _ shape libraries _]
[_ changes library-id _ shape _ libraries _]
(shape-log :debug (:id shape) nil :msg "Sync colors of shape" :shape (:name shape))
;; Synchronize a shape that uses some colors of the library. The value of the
@@ -607,7 +607,7 @@
#(ctc/sync-shape-colors % library-id library-colors))))
(defmethod generate-sync-shape :typographies
[_ changes library-id container shape libraries _]
[_ changes library-id container shape _ libraries _]
(shape-log :debug (:id shape) nil :msg "Sync typographies of shape" :shape (:name shape))
;; Synchronize a shape that uses some typographies of the library. The attributes
@@ -2055,8 +2055,7 @@
(pcb/with-objects objects)
(pcb/resize-parents new-objects-ids)
;; Fix the order of the children inside the parent
(cond-> (ctl/any-layout? objects parent-id)
(pcb/reorder-children parent-id (get-in objects [parent-id :shapes]))))]
(pcb/reorder-children parent-id (get-in objects [parent-id :shapes])))]
(assoc changes :file-id library-id)))
(defn generate-detach-component
@@ -2191,9 +2190,7 @@
:starting-frame frame-id}]
(vswap! unames conj name)
(-> changes
(pcb/with-page page)
(pcb/set-flow flow-id new-flow))))
(pcb/set-flow changes flow-id new-flow)))
changes
(->> shapes

View File

@@ -151,9 +151,7 @@
changes
(reduce (fn [changes {:keys [id] :as flow}]
(if (contains? ids-to-delete (:starting-frame flow))
(-> changes
(pcb/with-page page)
(pcb/set-flow id nil))
(pcb/set-flow changes id nil)
changes))
changes
(:flows page))
@@ -215,9 +213,7 @@
(map :id))
changes (reduce (fn [changes guide-id]
(-> changes
(pcb/with-page page)
(pcb/set-flow guide-id nil)))
(pcb/set-flow changes guide-id nil))
changes
guides-to-delete)

View File

@@ -60,17 +60,6 @@
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
(defn generate-set-variant-error
[changes component-id value]
(let [data (pcb/get-library-data changes)
component (ctcl/get-component data component-id true)
main-id (:main-instance-id component)]
(-> changes
(pcb/update-shapes [main-id] (if (str/blank? value)
#(dissoc % :variant-error)
#(assoc % :variant-error value))))))
(defn generate-add-new-property
[changes variant-id & {:keys [fill-values? property-name]}]
(let [data (pcb/get-library-data changes)

View File

@@ -9,6 +9,21 @@
[app.common.types.file :as ctf]
[app.common.types.variant :as ctv]))
(defn- generate-path
[path objects base-id shape]
(let [get-type #(case %
:frame :container
:group :container
:rect :shape
:circle :shape
:bool :shape
:path :shape
%)]
(if (= base-id (:id shape))
path
(generate-path (str path " " (:name shape) (get-type (:type shape))) objects base-id (get objects (:parent-id shape))))))
(defn generate-add-new-variant
[changes shape variant-id new-component-id new-shape-id prop-num]
(let [data (pcb/get-library-data changes)
@@ -31,56 +46,20 @@
(clvp/generate-update-property-value new-component-id prop-num value)
(pcb/change-parent (:parent-id shape) [new-shape] 0))))
(defn- generate-path
[path objects base-id shape]
(let [get-type #(case %
:frame :container
:group :container
:rect :shape
:circle :shape
:bool :shape
:path :shape
%)]
(if (= base-id (:id shape))
path
(generate-path (str path " " (:name shape) (get-type (:type shape))) objects base-id (get objects (:parent-id shape))))))
(defn- add-unique-path
"Adds a new property :shape-path to the shape, with the path of the shape.
Suffixes like -1, -2, etc. are added to ensure uniqueness."
[shapes objects base-id]
(letfn [(unique-path [shape counts]
(let [path (generate-path "" objects base-id shape)
num (get counts path 1)]
[(str path "-" num) (update counts path (fnil inc 1))]))]
(first
(reduce
(fn [[result counts] shape]
(let [[shape-path counts'] (unique-path shape counts)]
[(conj result (assoc shape :shape-path shape-path)) counts']))
[[] {}]
shapes))))
(defn generate-keep-touched
[changes new-shape original-shape original-shapes page libraries]
(let [objects (pcb/get-objects changes)
orig-objects (into {} (map (juxt :id identity) original-shapes))
orig-shapes-w-path (add-unique-path
(reverse original-shapes)
orig-objects
(:id original-shape))
new-shapes-w-path (add-unique-path
(reverse (cfh/get-children-with-self objects (:id new-shape)))
objects
(:id new-shape))
new-shapes-map (into {} (map (juxt :shape-path identity) new-shapes-w-path))
orig-touched (filter (comp seq :touched) orig-shapes-w-path)
(let [objects (pcb/get-objects changes)
new-path-map (into {}
(map (fn [shape] {(generate-path "" objects (:id new-shape) shape) shape}))
(cfh/get-children-with-self objects (:id new-shape)))
container (ctn/make-container page :page)]
orig-touched (filter (comp seq :touched) original-shapes)
orig-objects (into {} (map (juxt :id identity) original-shapes))
container (ctn/make-container page :page)]
(reduce
(fn [changes touched-shape]
(let [related-shape (get new-shapes-map (:shape-path touched-shape))
(let [path (generate-path "" orig-objects (:id original-shape) touched-shape)
related-shape (get new-path-map path)
orig-ref-shape (ctf/find-ref-shape nil container libraries touched-shape)]
(if related-shape
(cll/update-attrs-on-switch

View File

@@ -5,8 +5,8 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.media
"Media assets helpers (images, fonts, etc)"
(:require
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
;; We have added ".ttf" as string to solve a problem with chrome input selector
@@ -48,28 +48,38 @@
(defn mtype->extension [mtype]
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
(case mtype
"image/apng" ".apng"
"image/avif" ".avif"
"image/gif" ".gif"
"image/jpeg" ".jpg"
"image/png" ".png"
"image/svg+xml" ".svg"
"image/webp" ".webp"
"application/zip" ".zip"
"application/penpot" ".penpot"
"application/pdf" ".pdf"
"text/plain" ".txt"
"font/woff" ".woff"
"font/woff2" ".woff2"
"font/ttf" ".ttf"
"font/otf" ".otf"
"application/octet-stream" ".bin"
"image/apng" ".apng"
"image/avif" ".avif"
"image/gif" ".gif"
"image/jpeg" ".jpg"
"image/png" ".png"
"image/svg+xml" ".svg"
"image/webp" ".webp"
"application/zip" ".zip"
"application/penpot" ".penpot"
"application/pdf" ".pdf"
"text/plain" ".txt"
nil))
(defn strip-image-extension
[filename]
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
(str/replace filename image-extensions-re "")))
(s/def ::id uuid?)
(s/def ::name string?)
(s/def ::width number?)
(s/def ::height number?)
(s/def ::created-at inst?)
(s/def ::modified-at inst?)
(s/def ::mtype string?)
(s/def ::uri string?)
(s/def ::media-object
(s/keys :req-un [::id
::name
::width
::height
::mtype
::created-at
::modified-at
::uri]))
(defn parse-font-weight
[variant]

View File

@@ -211,7 +211,8 @@
(defn lazy-validator
[s]
(let [vfn (delay (validator s))]
(let [s (schema s)
vfn (delay (validator s))]
(fn [v] (@vfn v))))
(defn lazy-explainer
@@ -997,8 +998,6 @@
{:title "agent"
:description "instance of clojure agent"}}))
(register! ::any (mu/update-properties :any assoc :gen/gen sg/any))
;; ---- PREDICATES
(def valid-safe-number?

View File

@@ -7,7 +7,6 @@
(ns app.common.schema.desc-js-like
(:require
[app.common.data :as d]
[app.common.schema :as-alias sm]
[cuerdas.core :as str]
[malli.core :as m]
[malli.util :as mu]))
@@ -91,7 +90,7 @@
(defmethod visit :int [_ schema _ _] (str "integer" (-titled schema) (-min-max-suffix-number schema)))
(defmethod visit :double [_ schema _ _] (str "double" (-titled schema) (-min-max-suffix-number schema)))
(defmethod visit :select-keys [_ schema _ options] (describe* (m/deref schema) options))
(defmethod visit :and [_ s children _] (str (str/join " && " children) (-titled s)))
(defmethod visit :and [_ s children _] (str (str/join ", and " children) (-titled s)))
(defmethod visit :enum [_ s children _options] (str "enum" (-titled s) " of " (str/join ", " children)))
(defmethod visit :maybe [_ _ children _] (str (first children) " nullable"))
(defmethod visit :tuple [_ _ children _] (str "(" (str/join ", " children) ")"))
@@ -107,8 +106,7 @@
(defmethod visit :qualified-symbol [_ _ _ _] "qualified symbol")
(defmethod visit :uuid [_ _ _ _] "uuid")
(defmethod visit :boolean [_ _ _ _] "boolean")
(defmethod visit :keyword [_ _ _ _] "string")
(defmethod visit :fn [_ _ _ _] "FN")
(defmethod visit :keyword [_ _ _ _] "keyword")
(defmethod visit :vector [_ _ children _]
(str "[" (last children) "]"))
@@ -125,10 +123,8 @@
(defmethod visit :repeat [_ schema children _]
(str "repeat " (-diamond (first children)) (-repeat-suffix schema)))
(defmethod visit :set [_ schema children _]
(str "set[" (first children) "]" (minmax-suffix schema)))
(defmethod visit ::sm/set [_ schema children _]
(defmethod visit :set [_ schema children _]
(str "set[" (first children) "]" (minmax-suffix schema)))
(defmethod visit ::m/val [_ schema children _]
@@ -156,6 +152,7 @@
(or (:title props)
"*")))
(defmethod visit :map
[_ schema children {:keys [::level ::max-level] :as options}]
(let [props (m/properties schema)
@@ -175,11 +172,13 @@
": " s)))
(str/join ",\n"))
header (cond-> (str "type " title)
header (cond-> (if (zero? level)
(str "type " title)
(str title))
closed? (str "!")
(some? title) (str " "))]
(str (pad header level) "{\n" entries "\n" (pad "}\n" level))))))
(str header "{\n" entries "\n" (pad "}" level))))))
(defmethod visit :multi
[_ s children {:keys [::level ::max-level] :as options}]
@@ -206,18 +205,18 @@
(defmethod visit :merge
[_ schema children _]
(let [entries (str/join ",\n" children)
(let [entries (str/join " , " children)
props (m/properties schema)
title (or (some-> (:title props) str/camel str/capital)
"<untitled>")]
(str "merge type " title " { \n" entries "\n}\n")))
(str "merge object " title " { " entries " }")))
(defmethod visit ::sm/one-of
[_ _ children _]
(defmethod visit :app.common.schema/one-of
[_ _ children _]
(let [elems (last children)]
(str "string oneOf (" (->> elems
(map d/name)
(str/join "|")) ")")))
(str "OneOf[" (->> elems
(map d/name)
(str/join ",")) "]")))
(defmethod visit :schema [_ schema children options]
(visit ::m/schema schema children options))

View File

@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.schema.generators
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double])
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector])
#?(:cljs (:require-macros [app.common.schema.generators]))
(:require
[app.common.schema.registry :as sr]
@@ -38,6 +38,10 @@
([s opts]
(mg/generator s (assoc opts :registry sr/default-registry))))
(defn filter
[pred gen]
(tg/such-that pred gen 100))
(defn small-double
[& {:keys [min max] :or {min -100 max 100}}]
(tg/double* {:min min, :max max, :infinite? false, :NaN? false}))
@@ -57,7 +61,7 @@
(defn word-keyword
[]
(->> (word-string)
(tg/fmap c/keyword)))
(tg/fmap keyword)))
(defn email
[]
@@ -96,11 +100,12 @@
(c/map second))
(c/map list bools elements)))))))
(defn map-of
([kg vg]
(tg/map kg vg {:min-elements 1 :max-elements 3}))
([kg vg opts]
(tg/map kg vg opts)))
(def any tg/any)
(def boolean tg/boolean)
(defn set
[g]
(tg/set g))
(defn elements
[s]
@@ -114,10 +119,6 @@
[f g]
(tg/fmap f g))
(defn filter
[pred gen]
(tg/such-that pred gen 100))
(defn mcat
[f g]
(tg/bind g f))
@@ -129,18 +130,3 @@
(defn vector
[& opts]
(apply tg/vector opts))
(defn set
[g]
(tg/set g))
;; Static Generators
(def boolean tg/boolean)
(def text (word-string))
(def double (small-double))
(def int (small-int))
(def keyword (word-keyword))
(def any
(tg/one-of [text boolean double int keyword]))

View File

@@ -97,8 +97,7 @@
(defmethod visit :enum [_ _ children options] (merge (some-> (m/-infer children) (transform* options)) {:enum children}))
(defmethod visit :maybe [_ _ children _] {:oneOf (conj children {:type "null"})})
(defmethod visit :tuple [_ _ children _] {:type "array", :items children, :additionalItems false})
(defmethod visit :re [_ schema _ options]
{:type "string", :pattern (str (first (m/children schema options)))})
(defmethod visit :re [_ schema _ options] {:type "string", :pattern (first (m/children schema options))})
(defmethod visit :nil [_ _ _ _] {:type "null"})
(defmethod visit :string [_ schema _ _]

View File

@@ -6,6 +6,8 @@
(ns app.common.svg
(:require
#?(:clj [clojure.xml :as xml]
:cljs [tubax.core :as tubax])
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
@@ -13,7 +15,15 @@
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
[cuerdas.core :as str])
#?(:clj
(:import
clojure.lang.XMLHandler
java.io.InputStream
javax.xml.XMLConstants
javax.xml.parsers.SAXParserFactory
org.apache.commons.io.IOUtils)))
;; Regex for XML ids per Spec
;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn
@@ -1020,3 +1030,24 @@
:height (d/parse-integer (:height attrs) 0)})))]
(reduce-nodes redfn [] svg-data)))
#?(:clj
(defn- secure-parser-factory
[^InputStream input ^XMLHandler handler]
(.. (doto (SAXParserFactory/newInstance)
(.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
(.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
(newSAXParser)
(parse input handler))))
(defn strip-doctype
[data]
(cond-> data
(str/includes? data "<!DOCTYPE")
(str/replace #"<\!DOCTYPE[^>]*>" "")))
(defn parse
[text]
#?(:cljs (tubax/xml->clj text)
:clj (let [text (strip-doctype text)]
(dm/with-open [istream (IOUtils/toInputStream text "UTF-8")]
(xml/parse istream secure-parser-factory)))))

View File

@@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.files.shapes-builder
(ns app.common.svg.shapes-builder
"A SVG to Shapes builder."
(:require
[app.common.colors :as clr]
@@ -21,7 +21,7 @@
[app.common.math :as mth]
[app.common.schema :as sm :refer [max-safe-int min-safe-int]]
[app.common.svg :as csvg]
[app.common.types.path :as path]
[app.common.svg.path :as path]
[app.common.types.path.segment :as path.segm]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
@@ -219,7 +219,7 @@
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(when (and (contains? attrs :d) (seq (:d attrs)))
(let [transform (csvg/parse-transform (:transform attrs))
content (cond-> (path/from-string (:d attrs))
content (cond-> (path/parse (:d attrs))
(some? transform)
(path.segm/transform-content transform))

View File

@@ -35,7 +35,7 @@
(.. r (toString 16) (padStart 2 "0"))
(.. g (toString 16) (padStart 2 "0"))
(.. b (toString 16) (padStart 2 "0"))))))
sg/int))
sg/any))
(defn rgb-color-string?
[o]
@@ -54,13 +54,13 @@
::oapi/type "integer"
::oapi/format "int64"}}))
(def schema:image
(def schema:image-color
[:map {:title "ImageColor"}
[:name {:optional true} :string]
[:width ::sm/int]
[:height ::sm/int]
[:mtype ::sm/text]
[:mtype {:optional true} [:maybe :string]]
[:id ::sm/uuid]
[:name {:optional true} ::sm/text]
[:keep-aspect-ratio {:optional true} :boolean]])
(def gradient-types
@@ -93,7 +93,7 @@
[:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image]]
[:image {:optional true} [:maybe schema:image-color]]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:color
@@ -106,7 +106,7 @@
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:color {:optional true} [:maybe schema:rgb-color]]
[:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image]]]
[:image {:optional true} [:maybe schema:image-color]]]
[::sm/contains-any {:strict true} [:color :gradient :image]]])
;; Same as color but with :id prop required
@@ -115,10 +115,9 @@
(sm/required-keys schema:color-attrs [:id])
[::sm/contains-any {:strict true} [:color :gradient :image]]])
;; FIXME: revisit if we really need this all registers
(sm/register! ::color schema:color)
(sm/register! ::gradient schema:gradient)
(sm/register! ::image-color schema:image)
(sm/register! ::image-color schema:image-color)
(sm/register! ::recent-color schema:recent-color)
(sm/register! ::color-attrs schema:color-attrs)

View File

@@ -32,31 +32,24 @@
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CONSTANTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defonce BASE-FONT-SIZE "16px")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:media
"A schema that represents the file media object"
[:map {:title "FileMedia"}
[:map {:title "FileMediaObject"}
[:id ::sm/uuid]
[:created-at {:optional true} ::sm/inst]
[:created-at ::sm/inst]
[:deleted-at {:optional true} ::sm/inst]
[:name :string]
[:width ::sm/safe-int]
[:height ::sm/safe-int]
[:mtype :string]
[:media-id ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:media-id ::sm/uuid]
[:thumbnail-id {:optional true} ::sm/uuid]
[:is-local {:optional true} :boolean]])
[:is-local :boolean]])
(def schema:colors
[:map-of {:gen/max 5} ::sm/uuid ::ctc/color])
@@ -72,8 +65,7 @@
(def schema:options
[:map {:title "FileOptions"}
[:components-v2 {:optional true} ::sm/boolean]
[:base-font-size {:optional true} :string]])
[:components-v2 {:optional true} ::sm/boolean]])
(def schema:data
[:map {:title "FileData"}
@@ -110,6 +102,7 @@
(sm/register! ::media schema:media)
(sm/register! ::colors schema:colors)
(sm/register! ::typographies schema:typographies)
(sm/register! ::media-object schema:media)
(def check-file
(sm/check-fn schema:file :hint "check error on validating file"))
@@ -117,7 +110,7 @@
(def check-file-data
(sm/check-fn schema:data))
(def check-file-media
(def check-media-object
(sm/check-fn schema:media))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -141,8 +134,7 @@
(ctpl/add-page page)
:always
(update :options merge {:components-v2 true
:base-font-size BASE-FONT-SIZE})))))
(update :options assoc :components-v2 true)))))
(defn make-file
[{:keys [id project-id name revn is-shared features migrations
@@ -1037,14 +1029,3 @@
(-> file
(update-in [:data :pages-index] detach-pages))))
;; Base font size
(defn get-base-font-size
"Retrieve the base font size value or token reference."
[file-data]
(get-in file-data [:options :base-font-size] BASE-FONT-SIZE))
(defn set-base-font-size
[file-data base-font-size]
(assoc-in file-data [:options :base-font-size] base-font-size))

View File

@@ -36,10 +36,6 @@
[data]
(impl/from-bytes data))
(defn from-string
[data]
(impl/from-string data))
(defn check-path-content
[content]
(impl/check-content-like content))

View File

@@ -182,11 +182,11 @@
;; FIXME: move to helpers?, this function need performance review, it
;; is executed so many times on path edition
(defn- curve-closest-point
[position start end h1 h2 precision]
[position start end h1 h2]
(let [d (memoize (fn [t] (gpt/distance position (helpers/curve-values start end h1 h2 t))))]
(loop [t1 0.0
t2 1.0]
(if (<= (mth/abs (- t1 t2)) precision)
(if (<= (mth/abs (- t1 t2)) path-closest-point-accuracy)
(-> (helpers/curve-values start end h1 h2 t1)
;; store the segment info
(with-meta {:t t1 :from-p start :to-p end}))
@@ -214,7 +214,7 @@
(double t2)))))))
(defn- line-closest-point
"Finds the closest point in the line segment defined by from-p and to-p"
"Point on line"
[position from-p to-p]
(let [e1 (gpt/to-vec from-p to-p)
@@ -235,13 +235,15 @@
from-p
to-p))))
(defn closest-point
"Returns the closest point in the path to the position, at a given precision"
[content position precision]
;; FIXME: incorrect API, complete shape is not necessary here
(defn path-closest-point
"Given a path and a position"
[shape position]
(let [point+distance
(fn [[cur-segment prev-segment]]
(let [from-p (helpers/segment->point prev-segment)
to-p (helpers/segment->point cur-segment)
to-p (helpers/segment->point cur-segment)
h1 (gpt/point (get-in cur-segment [:params :c1x])
(get-in cur-segment [:params :c1y]))
h2 (gpt/point (get-in cur-segment [:params :c2x])
@@ -252,7 +254,44 @@
(line-closest-point position from-p to-p)
:curve-to
(curve-closest-point position from-p to-p h1 h2 precision)
(curve-closest-point position from-p to-p h1 h2)
nil)]
(when point
[point (gpt/distance point position)])))
find-min-point
(fn [[min-p min-dist :as acc] [cur-p cur-dist :as cur]]
(if (and (some? acc) (or (not cur) (<= min-dist cur-dist)))
[min-p min-dist]
[cur-p cur-dist]))]
(->> (:content shape)
(d/with-prev)
(map point+distance)
(reduce find-min-point)
(first))))
(defn closest-point
"Given a path and a position"
[content position]
(let [point+distance
(fn [[cur-segment prev-segment]]
(let [from-p (helpers/segment->point prev-segment)
to-p (helpers/segment->point cur-segment)
h1 (gpt/point (get-in cur-segment [:params :c1x])
(get-in cur-segment [:params :c1y]))
h2 (gpt/point (get-in cur-segment [:params :c2x])
(get-in cur-segment [:params :c2y]))
point
(case (:command cur-segment)
:line-to
(line-closest-point position from-p to-p)
:curve-to
(curve-closest-point position from-p to-p h1 h2)
nil)]
(when point

View File

@@ -761,5 +761,3 @@
(d/patch-object (select-keys props basic-extract-props))
(cond-> (cfh/text-shape? shape) (patch-text-props props))
(cond-> (cfh/frame-shape? shape) (patch-layout-props props)))))
(def MAX-GRADIENT-STOPS 16)

View File

@@ -16,56 +16,54 @@
(def node-types #{"root" "paragraph-set" "paragraph"})
(def schema:content
[:map
[:type [:= "root"]]
[:key {:optional true} :string]
[:children
{:optional true}
[:maybe
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:type [:= "paragraph-set"]]
[:key {:optional true} :string]
[:children
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:type [:= "paragraph"]]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} ::shape/fill]]]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:direction {:optional true} :string]
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]
[:children
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:text :string]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} ::shape/fill]]]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:direction {:optional true} :string]
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]]]]]]]]]]]])
(sm/register!
^{::sm/type ::content}
[:map
[:type [:= "root"]]
[:key {:optional true} :string]
[:children
{:optional true}
[:maybe
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:type [:= "paragraph-set"]]
[:key {:optional true} :string]
[:children
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:type [:= "paragraph"]]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} ::shape/fill]]]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:direction {:optional true} :string]
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]
[:children
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:text :string]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} ::shape/fill]]]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:direction {:optional true} :string]
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]]]]]]]]]]]])
(sm/register! ::content schema:content)
(def valid-content?
(sm/lazy-validator schema:content))
(sm/register!
^{::sm/type ::position-data}

View File

@@ -7,7 +7,6 @@
(ns app.common.types.tokens-lib
(:require
#?(:clj [app.common.fressian :as fres])
#?(:clj [clojure.data.json :as json])
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
@@ -119,7 +118,7 @@
[:map {:title "Token"}
[:name cto/token-name-ref]
[:type [::sm/one-of cto/token-types]]
[:value ::sm/any]
[:value :any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::sm/inst]])
@@ -390,8 +389,7 @@
[:description {:optional true} :string]
[:modified-at {:optional true} ::sm/inst]
[:tokens {:optional true
:gen/gen (->> (sg/map-of (sg/generator ::sm/text)
(sg/generator schema:token))
:gen/gen (->> (sg/generator [:map-of ::sm/text schema:token])
(sg/fmap #(into (d/ordered-map) %)))}
[:and
[:map-of {:gen/max 5
@@ -662,6 +660,63 @@
(def valid-active-token-themes?
(sm/validator schema:active-themes))
;; === Import / Export from DTCG format
(def ^:private legacy-node?
(sm/validator
[:or
[:map
["value" :string]
["type" :string]]
[:map
["value" [:sequential [:map ["type" :string]]]]
["type" :string]]
[:map
["value" :map]
["type" :string]]]))
(def ^:private dtcg-node?
(sm/validator
[:or
[:map
["$value" :string]
["$type" :string]]
[:map
["$value" [:sequential [:map ["$type" :string]]]]
["$type" :string]]
[:map
["$value" :map]
["$type" :string]]]))
(defn get-json-format
"Searches through parsed token file and returns:
- `:json-format/legacy` when first node satisfies `legacy-node?` predicate
- `:json-format/dtcg` when first node satisfies `dtcg-node?` predicate
- `nil` if neither combination is found"
([data]
(get-json-format data legacy-node? dtcg-node?))
([data legacy-node? dtcg-node?]
(let [branch? map?
children (fn [node] (vals node))
check-node (fn [node]
(cond
(legacy-node? node) :json-format/legacy
(dtcg-node? node) :json-format/dtcg
:else nil))
walk (fn walk [node]
(lazy-seq
(cons
(check-node node)
(when (branch? node)
(mapcat walk (children node))))))]
(->> (walk data)
(filter some?)
first))))
(defn single-set? [data]
(and (not (contains? data "$metadata"))
(not (contains? data "$themes"))))
;; DEPRECATED
(defn walk-sets-tree-seq
"Walk sets tree as a flat list.
@@ -771,10 +826,51 @@
(map-indexed (fn [index item]
(assoc item :index index))))))
(defn flatten-nested-tokens-json
"Recursively flatten the dtcg token structure, joining keys with '.'."
[tokens token-path]
(reduce-kv
(fn [acc k v]
(let [child-path (if (empty? token-path)
(name k)
(str token-path "." k))]
(if (and (map? v)
(not (contains? v "$type")))
(merge acc (flatten-nested-tokens-json v child-path))
(let [token-type (cto/dtcg-token-type->token-type (get v "$type"))]
(if token-type
(assoc acc child-path (make-token
:name child-path
:type token-type
:value (get v "$value")
:description (get v "$description")))
;; Discard unknown tokens
acc)))))
{}
tokens))
;; === Tokens Lib
(declare make-tokens-lib)
(defn legacy-nodes->dtcg-nodes [sets-data]
(walk/postwalk
(fn [node]
(cond-> node
(and (map? node)
(contains? node "value")
(sequential? (get node "value")))
(update "value"
(fn [seq-value]
(map #(set/rename-keys % {"type" "$type"}) seq-value)))
(and (map? node)
(and (contains? node "type")
(contains? node "value")))
(set/rename-keys {"value" "$value"
"type" "$type"})))
sets-data))
(defprotocol ITokensLib
"A library of tokens, sets and themes."
(set-path-exists? [_ path] "if a set at `path` exists")
@@ -791,11 +887,12 @@ Will return a value that matches this schema:
`:all` All of the nested sets are active
`:partial` Mixed active state of nested sets")
(get-active-themes-set-tokens [_] "set of set names that are active in the the active themes")
(encode-dtcg [_] "Encodes library to a dtcg compatible json string")
(decode-dtcg-json [_ parsed-json] "Decodes parsed json containing tokens and converts to library")
(decode-legacy-json [_ parsed-json] "Decodes parsed legacy json containing tokens and converts to library")
(get-all-tokens [_] "all tokens in the lib")
(validate [_]))
(declare parse-multi-set-dtcg-json)
(declare export-dtcg-json)
(deftype TokensLib [sets themes active-themes]
;; NOTE: This is only for debug purposes, pending to properly
;; implement the toString and alternative printing.
@@ -812,9 +909,6 @@ Will return a value that matches this schema:
(-clj->js [_] (js-obj "sets" (clj->js sets)
"themes" (clj->js themes)
"active-themes" (clj->js active-themes)))])
#?@(:clj
[json/JSONWriter
(-write [this writter options] (json/-write (export-dtcg-json this) writter options))])
ITokenSets
(add-set [_ token-set]
@@ -1189,6 +1283,142 @@ Will return a value that matches this schema:
active-set-names)]
tokens))
(encode-dtcg [this]
(let [themes-xform
(comp
(filter #(and (instance? TokenTheme %)
(not (hidden-temporary-theme? %))))
(map (fn [token-theme]
(let [theme-map (->> token-theme
(into {})
walk/stringify-keys)]
(-> theme-map
(set/rename-keys {"sets" "selectedTokenSets"})
(update "selectedTokenSets" (fn [sets]
(->> (for [s sets] [s "enabled"])
(into {})))))))))
themes
(->> (tree-seq d/ordered-map? vals themes)
(into [] themes-xform))
;; Active themes without exposing hidden penpot theme
active-themes-clear
(disj active-themes hidden-token-theme-path)
update-token-fn
(fn [token]
(cond-> {"$value" (:value token)
"$type" (cto/token-type->dtcg-token-type (:type token))}
(:description token) (assoc "$description" (:description token))))
name-set-tuples
(->> sets
(tree-seq d/ordered-map? vals)
(filter (partial instance? TokenSet))
(map (fn [{:keys [name tokens]}]
[name (tokens-tree tokens :update-token-fn update-token-fn)])))
ordered-set-names
(mapv first name-set-tuples)
sets
(into {} name-set-tuples)
active-sets
(get-active-themes-set-names this)]
(-> sets
(assoc "$themes" themes)
(assoc-in ["$metadata" "tokenSetOrder"] ordered-set-names)
(assoc-in ["$metadata" "activeThemes"] active-themes-clear)
(assoc-in ["$metadata" "activeSets"] active-sets))))
(decode-dtcg-json [_ data]
(assert (map? data) "expected a map data structure for `data`")
(let [metadata (get data "$metadata")
xf-normalize-set-name
(map normalize-set-name)
sets
(dissoc data "$themes" "$metadata")
ordered-sets
(-> (d/ordered-set)
(into xf-normalize-set-name (get metadata "tokenSetOrder"))
(into xf-normalize-set-name (keys sets)))
active-sets
(or (->> (get metadata "activeSets")
(into #{} xf-normalize-set-name)
(not-empty))
#{})
active-themes
(or (->> (get metadata "activeThemes")
(into #{})
(not-empty))
#{hidden-token-theme-path})
themes
(->> (get data "$themes")
(map (fn [theme]
(make-token-theme
:name (get theme "name")
:group (get theme "group")
:is-source (get theme "is-source")
:id (get theme "id")
:modified-at (some-> (get theme "modified-at")
(dt/parse-instant))
:sets (into #{}
(comp (map key)
xf-normalize-set-name
(filter #(contains? ordered-sets %)))
(get theme "selectedTokenSets")))))
(not-empty))
library
(make-tokens-lib)
sets
(reduce-kv (fn [result name tokens]
(assoc result
(normalize-set-name name)
(flatten-nested-tokens-json tokens "")))
{}
sets)
library
(reduce (fn [library name]
(if-let [tokens (get sets name)]
(add-set library (make-token-set :name name :tokens tokens))
library))
library
ordered-sets)
library
(update-theme library hidden-token-theme-group hidden-token-theme-name
#(assoc % :sets active-sets))
library
(reduce add-theme library themes)
library
(reduce (fn [library theme-path]
(let [[group name] (split-token-theme-path theme-path)]
(activate-theme library group name)))
library
active-themes)]
library))
(decode-legacy-json [this parsed-legacy-json]
(let [other-data (select-keys parsed-legacy-json ["$themes" "$metadata"])
sets-data (dissoc parsed-legacy-json "$themes" "$metadata")
dtcg-sets-data (legacy-nodes->dtcg-nodes sets-data)]
(decode-dtcg-json this (merge other-data
dtcg-sets-data))))
(get-all-tokens [this]
(reduce
(fn [tokens' set]
@@ -1250,13 +1480,17 @@ Will return a value that matches this schema:
[tokens-lib]
(or tokens-lib (make-tokens-lib)))
(def schema:tokens-lib
(sm/register!
{:type ::tokens-lib
:pred valid-tokens-lib?
:type-properties
{:encode/json export-dtcg-json
:decode/json parse-multi-set-dtcg-json}}))
(defn decode-dtcg
[encoded-json]
(-> (make-tokens-lib)
(decode-dtcg-json encoded-json)))
(def type:tokens-lib
{:type ::tokens-lib
:pred valid-tokens-lib?
:type-properties
{:encode/json encode-dtcg
:decode/json decode-dtcg}})
(defn duplicate-set [set-name lib & {:keys [suffix]}]
(let [sets (get-sets lib)
@@ -1266,335 +1500,7 @@ Will return a value that matches this schema:
(assoc :name copy-name)
(assoc :modified-at (dt/now)))))
;; === Import / Export from JSON format
;; Supported formats:
;; - Legacy: for tokens files prior to DTCG second draft
;; - DTCG: for tokens files conforming to the DTCG second draft (current for now)
;; https://www.w3.org/community/design-tokens/2022/06/14/call-to-implement-the-second-editors-draft-and-share-feedback/
;;
;; - Single set: for files that comply with the base DTCG format, that contain a single tree of tokens.
;; - Multi sets: for files with the Tokens Studio extension, that may contain several sets, and also themes and other $metadata.
;;
;; Small glossary:
;; * json data: a json-encoded string
;; * decode: convert a json string into a plain clojure nested map
;; * parse: build a TokensLib (or a fragment) from a decoded json data
;; * export: generate from a TokensLib a plain clojure nested map, suitable to be encoded as a json string
(def ^:private legacy-node?
(sm/validator
[:or
[:map
["value" :string]
["type" :string]]
[:map
["value" [:sequential [:map ["type" :string]]]]
["type" :string]]
[:map
["value" :map]
["type" :string]]]))
(def ^:private dtcg-node?
(sm/validator
[:or
[:map
["$value" :string]
["$type" :string]]
[:map
["$value" [:sequential [:map ["$type" :string]]]]
["$type" :string]]
[:map
["$value" :map]
["$type" :string]]]))
(defn- get-json-format
"Searches through decoded token file and returns:
- `:json-format/legacy` when first node satisfies `legacy-node?` predicate
- `:json-format/dtcg` when first node satisfies `dtcg-node?` predicate
- `nil` if neither combination is found"
([decoded-json]
(get-json-format decoded-json legacy-node? dtcg-node?))
([decoded-json legacy-node? dtcg-node?]
(assert (map? decoded-json) "expected a plain clojure map for `decoded-json`")
(let [branch? map?
children (fn [node] (vals node))
check-node (fn [node]
(cond
(legacy-node? node) :json-format/legacy
(dtcg-node? node) :json-format/dtcg
:else nil))
walk (fn walk [node]
(lazy-seq
(cons
(check-node node)
(when (branch? node)
(mapcat walk (children node))))))]
(->> (walk decoded-json)
(filter some?)
first)))) ;; TODO: throw error if format cannot be determined
(defn- legacy-json->dtcg-json
"Converts a decoded json file in legacy format into DTCG format."
[decoded-json]
(assert (map? decoded-json) "expected a plain clojure map for `decoded-json`")
(walk/postwalk
(fn [node]
(cond-> node
(and (map? node)
(contains? node "value")
(sequential? (get node "value")))
(update "value"
(fn [seq-value]
(map #(set/rename-keys % {"type" "$type"}) seq-value)))
(and (map? node)
(and (contains? node "type")
(contains? node "value")))
(set/rename-keys {"value" "$value"
"type" "$type"})))
decoded-json))
(defn- single-set?
"Check if the decoded json file conforms to basic DTCG format with a single set."
[decoded-json]
(assert (map? decoded-json) "expected a plain clojure map for `decoded-json`")
(and (not (contains? decoded-json "$metadata"))
(not (contains? decoded-json "$themes"))))
(defn- flatten-nested-tokens-json
"Convert a tokens tree in the decoded json fragment into a flat map,
being the keys the token paths after joining the keys with '.'."
[decoded-json-tokens parent-path]
(reduce-kv
(fn [tokens k v]
(let [child-path (if (empty? parent-path)
(name k)
(str parent-path "." k))]
(if (and (map? v)
(not (contains? v "$type")))
(merge tokens (flatten-nested-tokens-json v child-path))
(let [token-type (cto/dtcg-token-type->token-type (get v "$type"))]
(if token-type
(assoc tokens child-path (make-token
:name child-path
:type token-type
:value (get v "$value")
:description (get v "$description")))
;; Discard unknown type tokens
tokens)))))
{}
decoded-json-tokens))
(defn- parse-single-set-dtcg-json
"Parse a decoded json file with a single set of tokens in DTCG format into a TokensLib."
[set-name decoded-json-tokens]
(assert (map? decoded-json-tokens) "expected a plain clojure map for `decoded-json-tokens`")
(assert (= (get-json-format decoded-json-tokens) :json-format/dtcg) "expected a dtcg format for `decoded-json-tokens`")
(-> (make-tokens-lib)
(add-set (make-token-set :name (normalize-set-name set-name)
:tokens (flatten-nested-tokens-json decoded-json-tokens "")))))
(defn- parse-single-set-legacy-json
"Parse a decoded json file with a single set of tokens in legacy format into a TokensLib."
[set-name decoded-json-tokens]
(assert (map? decoded-json-tokens) "expected a plain clojure map for `decoded-json-tokens`")
(assert (= (get-json-format decoded-json-tokens) :json-format/legacy) "expected a legacy format for `decoded-json-tokens`")
(parse-single-set-dtcg-json set-name (legacy-json->dtcg-json decoded-json-tokens)))
(defn- parse-multi-set-dtcg-json
"Parse a decoded json file with multi sets in DTCG format into a TokensLib."
[decoded-json]
(assert (map? decoded-json) "expected a plain clojure map for `decoded-json`")
(assert (= (get-json-format decoded-json) :json-format/dtcg) "expected a dtcg format for `decoded-json`")
(let [metadata (get decoded-json "$metadata")
xf-normalize-set-name
(map normalize-set-name)
sets
(dissoc decoded-json "$themes" "$metadata")
ordered-set-names
(-> (d/ordered-set)
(into xf-normalize-set-name (get metadata "tokenSetOrder"))
(into xf-normalize-set-name (keys sets)))
active-set-names
(or (->> (get metadata "activeSets")
(into #{} xf-normalize-set-name)
(not-empty))
#{})
active-theme-names
(or (->> (get metadata "activeThemes")
(into #{})
(not-empty))
#{hidden-token-theme-path})
themes
(->> (get decoded-json "$themes")
(map (fn [theme]
(make-token-theme
:name (get theme "name")
:group (get theme "group")
:is-source (get theme "is-source")
:id (get theme "id")
:modified-at (some-> (get theme "modified-at")
(dt/parse-instant))
:sets (into #{}
(comp (map key)
xf-normalize-set-name
(filter #(contains? ordered-set-names %)))
(get theme "selectedTokenSets")))))
(not-empty))
library
(make-tokens-lib)
sets
(reduce-kv (fn [result name tokens]
(assoc result
(normalize-set-name name)
(flatten-nested-tokens-json tokens "")))
{}
sets)
library
(reduce (fn [library name]
(if-let [tokens (get sets name)]
(add-set library (make-token-set :name name :tokens tokens))
library))
library
ordered-set-names)
library
(update-theme library hidden-token-theme-group hidden-token-theme-name
#(assoc % :sets active-set-names))
library
(reduce add-theme library themes)
library
(reduce (fn [library theme-path]
(let [[group name] (split-token-theme-path theme-path)]
(activate-theme library group name)))
library
active-theme-names)]
library))
(defn- parse-multi-set-legacy-json
"Parse a decoded json file with multi sets in legacy format into a TokensLib."
[decoded-json]
(assert (map? decoded-json) "expected a plain clojure map for `decoded-json`")
(assert (= (get-json-format decoded-json) :json-format/legacy) "expected a legacy format for `decoded-json`")
(let [sets-data (dissoc decoded-json "$themes" "$metadata")
other-data (select-keys decoded-json ["$themes" "$metadata"])
dtcg-sets-data (legacy-json->dtcg-json sets-data)]
(parse-multi-set-dtcg-json (merge other-data
dtcg-sets-data))))
(defn parse-decoded-json
"Guess the format and content type of the decoded json file and parse it into a TokensLib.
The `file-name` is used to determine the set name when the json file contains a single set."
[decoded-json file-name]
(let [single-set? (single-set? decoded-json)
json-format (get-json-format decoded-json)]
(cond
(and single-set?
(= :json-format/legacy json-format))
(parse-single-set-legacy-json file-name decoded-json)
(and single-set?
(= :json-format/dtcg json-format))
(parse-single-set-dtcg-json file-name decoded-json)
(= :json-format/legacy json-format)
(parse-multi-set-legacy-json decoded-json)
:else
(parse-multi-set-dtcg-json decoded-json))))
(defn export-dtcg-json
"Convert a TokensLib into a plain clojure map, suitable to be encoded as a multi sets json string in DTCG format."
[tokens-lib]
(let [themes-xform
(comp
(filter #(and (instance? TokenTheme %)
(not (hidden-temporary-theme? %))))
(map (fn [token-theme]
(let [theme-map (->> token-theme
(into {})
walk/stringify-keys)]
(-> theme-map
(set/rename-keys {"sets" "selectedTokenSets"})
(update "selectedTokenSets" (fn [sets]
(->> (for [s sets] [s "enabled"])
(into {})))))))))
themes
(->> (get-theme-tree tokens-lib)
(tree-seq d/ordered-map? vals)
(into [] themes-xform))
;; Active themes without exposing hidden penpot theme
active-themes-clear
(-> (get-active-theme-paths tokens-lib)
(disj hidden-token-theme-path))
update-token-fn
(fn [token]
(cond-> {"$value" (:value token)
"$type" (cto/token-type->dtcg-token-type (:type token))}
(:description token) (assoc "$description" (:description token))))
name-set-tuples
(->> (get-set-tree tokens-lib)
(tree-seq d/ordered-map? vals)
(filter (partial instance? TokenSet))
(map (fn [{:keys [name tokens]}]
[name (tokens-tree tokens :update-token-fn update-token-fn)])))
ordered-set-names
(mapv first name-set-tuples)
sets
(into {} name-set-tuples)
active-set-names
(get-active-themes-set-names tokens-lib)]
(-> sets
(assoc "$themes" themes)
(assoc-in ["$metadata" "tokenSetOrder"] ordered-set-names)
(assoc-in ["$metadata" "activeThemes"] active-themes-clear)
(assoc-in ["$metadata" "activeSets"] active-set-names))))
(defn get-tokens-of-unknown-type
"Search for all tokens in the decoded json file that have a type that is not currently
supported by Penpot. Returns a map token-path -> token type."
([decoded-json]
(get-tokens-of-unknown-type decoded-json "" (get-json-format decoded-json)))
([decoded-json parent-path json-format]
(let [type-key (if (= json-format :json-format/dtcg) "$type" "type")]
(reduce-kv
(fn [unknown-tokens k v]
(let [child-path (if (empty? parent-path)
(name k)
(str parent-path "." k))]
(if (and (map? v)
(not (contains? v type-key)))
(let [nested-unknown-tokens (get-tokens-of-unknown-type v child-path json-format)]
(merge unknown-tokens nested-unknown-tokens))
(let [token-type-str (get v type-key)
token-type (cto/dtcg-token-type->token-type token-type-str)]
(if (and (not (some? token-type)) (some? token-type-str))
(assoc unknown-tokens child-path token-type-str)
unknown-tokens)))))
nil
decoded-json))))
(sm/register! type:tokens-lib)
;; === Serialization handlers for RPC API (transit) and database (fressian)

View File

@@ -33,8 +33,7 @@
;; The root shape of the main instance of a variant component.
[:map
[:variant-id {:optional true} ::sm/uuid]
[:variant-name {:optional true} :string]
[:variant-error {:optional true} :string]])
[:variant-name {:optional true} :string]])
(def schema:variant-container
;; is a board that contains all variant components of a variant set,
@@ -107,8 +106,8 @@
(add-new-props assigned remaining))))
(defn properties-map->formula
"Transforms a map of properties to a formula of properties omitting the empty ones"
(defn properties-map-to-string
"Transforms a map of properties to a string of properties omitting the empty ones"
[properties]
(->> properties
(keep (fn [{:keys [name value]}]
@@ -117,24 +116,21 @@
(str/join ", ")))
(defn properties-formula->map
"Transforms a formula of properties to a map of properties"
(defn properties-string-to-map
"Transforms a string of properties to a map of properties"
[s]
(->> (str/split s ",")
(mapv #(str/split % "=" 2))
(filter (fn [[_ v]] (not (str/blank? v))))
(mapv #(str/split % "="))
(mapv (fn [[k v]]
{:name (str/trim k)
:value (str/trim v)}))))
(defn valid-properties-formula?
"Checks if a formula is valid"
(defn valid-properties-string?
"Checks if a string of properties has a processable format or not"
[s]
(->> (str/split s ",")
(mapv #(str/split % "=" 2))
(every? #(and (= 2 (count %))
(not (str/blank? (first %)))))))
(let [pattern #"^([a-zA-Z0-9\s]+=[a-zA-Z0-9\s]+)(,\s*[a-zA-Z0-9\s]+=[a-zA-Z0-9\s]+)*$"]
(not (nil? (re-matches pattern s)))))
(defn find-properties-to-remove

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) KALEIDOS INC
(ns common-tests.files-builder-test
(:require
[app.common.files.builder :as builder]
[clojure.test :as t]))
(t/deftest test-strip-image-extension
(t/testing "removes extension from supported image files"
(t/is (= (builder/strip-image-extension "foo.png") "foo"))
(t/is (= (builder/strip-image-extension "foo.webp") "foo"))
(t/is (= (builder/strip-image-extension "foo.jpg") "foo"))
(t/is (= (builder/strip-image-extension "foo.jpeg") "foo"))
(t/is (= (builder/strip-image-extension "foo.svg") "foo"))
(t/is (= (builder/strip-image-extension "foo.gif") "foo")))
(t/testing "does not remove extension for unsupported files"
(t/is (= (builder/strip-image-extension "foo.txt") "foo.txt"))
(t/is (= (builder/strip-image-extension "foo.bmp") "foo.bmp")))
(t/testing "leaves filename intact when it has no extension"
(t/is (= (builder/strip-image-extension "README") "README"))))

View File

@@ -1,26 +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) KALEIDOS INC
(ns common-tests.media-test
(:require
[app.common.media :as media]
[clojure.test :as t]))
(t/deftest test-strip-image-extension
(t/testing "removes extension from supported image files"
(t/is (= (media/strip-image-extension "foo.png") "foo"))
(t/is (= (media/strip-image-extension "foo.webp") "foo"))
(t/is (= (media/strip-image-extension "foo.jpg") "foo"))
(t/is (= (media/strip-image-extension "foo.jpeg") "foo"))
(t/is (= (media/strip-image-extension "foo.svg") "foo"))
(t/is (= (media/strip-image-extension "foo.gif") "foo")))
(t/testing "does not remove extension for unsupported files"
(t/is (= (media/strip-image-extension "foo.txt") "foo.txt"))
(t/is (= (media/strip-image-extension "foo.bmp") "foo.bmp")))
(t/testing "leaves filename intact when it has no extension"
(t/is (= (media/strip-image-extension "README") "README"))))

View File

@@ -9,6 +9,7 @@
[clojure.test :as t]
[common-tests.colors-test]
[common-tests.data-test]
[common-tests.files-builder-test]
[common-tests.files-changes-test]
[common-tests.files-migrations-test]
[common-tests.geom-point-test]
@@ -28,7 +29,6 @@
[common-tests.logic.swap-and-reset-test]
[common-tests.logic.swap-as-override-test]
[common-tests.logic.token-test]
[common-tests.media-test]
[common-tests.pages-helpers-test]
[common-tests.record-test]
[common-tests.schema-test]
@@ -58,6 +58,7 @@
(t/run-tests
'common-tests.colors-test
'common-tests.data-test
'common-tests.files-builder-test
'common-tests.files-changes-test
'common-tests.files-migrations-test
'common-tests.geom-point-test
@@ -77,7 +78,6 @@
'common-tests.logic.swap-and-reset-test
'common-tests.logic.swap-as-override-test
'common-tests.logic.token-test
'common-tests.media-test
'common-tests.pages-helpers-test
'common-tests.record-test
'common-tests.schema-test
@@ -85,11 +85,11 @@
'common-tests.svg-test
'common-tests.text-test
'common-tests.time-test
'common-tests.types.absorb-assets-test
'common-tests.types.components-test
'common-tests.types.modifiers-test
'common-tests.types.path-data-test
'common-tests.types.shape-decode-encode-test
'common-tests.types.shape-interactions-test
'common-tests.types.shape-decode-encode-test
'common-tests.types.tokens-lib-test
'common-tests.types.components-test
'common-tests.types.absorb-assets-test
'common-tests.types.path-data-test
'common-tests.uuid-test))

View File

@@ -1,11 +0,0 @@
{
"color": {
"red": {
"100": {
"$value": "red",
"$type": "color",
"$description": ""
}
}
}
}

View File

@@ -1,11 +0,0 @@
{
"color": {
"red": {
"100": {
"value": "red",
"type": "color",
"description": ""
}
}
}
}

View File

@@ -7,9 +7,8 @@
(ns common-tests.types.tokens-lib-test
(:require
#?(:clj [app.common.fressian :as fres])
#?(:clj [app.common.json :as json])
#?(:clj [app.common.test-helpers.tokens :as tht])
[app.common.data :as d]
[app.common.test-helpers.tokens :as tht]
[app.common.time :as dt]
[app.common.transit :as tr]
[app.common.types.tokens-lib :as ctob]
@@ -1388,28 +1387,14 @@
(t/is (nil? token-theme'))))
#?(:clj
(t/deftest parse-single-set-legacy-json
(let [json (-> (slurp "test/common_tests/types/data/tokens-single-set-legacy-example.json")
(json/decode {:key-fn identity}))
lib (ctob/parse-decoded-json json "single_set")]
(t/is (= '("single_set") (ctob/get-ordered-set-names lib)))
(t/testing "token added"
(t/is (some? (ctob/get-token-in-set lib "single_set" "color.red.100")))))))
#?(:clj
(t/deftest parse-single-set-dtcg-json
(let [json (-> (slurp "test/common_tests/types/data/tokens-single-set-dtcg-example.json")
(json/decode {:key-fn identity}))
lib (ctob/parse-decoded-json json "single_set")]
(t/is (= '("single_set") (ctob/get-ordered-set-names lib)))
(t/testing "token added"
(t/is (some? (ctob/get-token-in-set lib "single_set" "color.red.100")))))))
#?(:clj
(t/deftest parse-multi-set-legacy-json
(t/deftest legacy-json-decoding
(let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-legacy-example.json")
(json/decode {:key-fn identity}))
lib (ctob/parse-decoded-json json "")
(tr/decode-str))
lib (ctob/decode-legacy-json (ctob/ensure-tokens-lib nil) json)
get-set-token (fn [set-name token-name]
(some-> (ctob/get-set lib set-name)
(ctob/get-token token-name)
(dissoc :modified-at)))
token-theme (ctob/get-theme lib "group-1" "theme-1")]
(t/is (= '("core" "light" "dark" "theme") (ctob/get-ordered-set-names lib)))
(t/testing "set exists in theme"
@@ -1417,29 +1402,32 @@
(t/is (= (:name token-theme) "theme-1"))
(t/is (= (:sets token-theme) #{"light"})))
(t/testing "tokens exist in core set"
(t/is (tht/token-data-eq? (ctob/get-token-in-set lib "core" "colors.red.600")
{:name "colors.red.600"
:type :color
:value "#e53e3e"
:description ""}))
(t/is (tht/token-data-eq? (ctob/get-token-in-set lib "core" "spacing.multi-value")
{:name "spacing.multi-value"
:type :spacing
:value "{dimension.sm} {dimension.xl}"
:description "You can have multiple values in a single spacing token"}))
(t/is (tht/token-data-eq? (ctob/get-token-in-set lib "theme" "button.primary.background")
{:name "button.primary.background"
:type :color
:value "{accent.default}"
:description ""})))
(t/is (= (get-set-token "core" "colors.red.600")
{:name "colors.red.600"
:type :color
:value "#e53e3e"
:description ""}))
(t/is (= (get-set-token "core" "spacing.multi-value")
{:name "spacing.multi-value"
:type :spacing
:value "{dimension.sm} {dimension.xl}"
:description "You can have multiple values in a single spacing token"}))
(t/is (= (get-set-token "theme" "button.primary.background")
{:name "button.primary.background"
:type :color
:value "{accent.default}"
:description ""})))
(t/testing "invalid tokens got discarded"
(t/is (nil? (ctob/get-token-in-set lib "typography" "H1.Bold")))))))
(t/is (nil? (get-set-token "typography" "H1.Bold")))))))
#?(:clj
(t/deftest parse-multi-set-dtcg-json
(t/deftest dtcg-encoding-decoding-json
(let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-example.json")
(json/decode {:key-fn identity}))
lib (ctob/parse-decoded-json json "")
(tr/decode-str))
lib (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json)
get-set-token (fn [set-name token-name]
(some-> (ctob/get-set lib set-name)
(ctob/get-token token-name)))
token-theme (ctob/get-theme lib "group-1" "theme-1")]
(t/is (= '("core" "light" "dark" "theme") (ctob/get-ordered-set-names lib)))
(t/testing "set exists in theme"
@@ -1447,29 +1435,32 @@
(t/is (= (:name token-theme) "theme-1"))
(t/is (= (:sets token-theme) #{"light"})))
(t/testing "tokens exist in core set"
(t/is (tht/token-data-eq? (ctob/get-token-in-set lib "core" "colors.red.600")
(t/is (tht/token-data-eq? (get-set-token "core" "colors.red.600")
{:name "colors.red.600"
:type :color
:value "#e53e3e"
:description ""}))
(t/is (tht/token-data-eq? (ctob/get-token-in-set lib "core" "spacing.multi-value")
(t/is (tht/token-data-eq? (get-set-token "core" "spacing.multi-value")
{:name "spacing.multi-value"
:type :spacing
:value "{dimension.sm} {dimension.xl}"
:description "You can have multiple values in a single spacing token"}))
(t/is (tht/token-data-eq? (ctob/get-token-in-set lib "theme" "button.primary.background")
(t/is (tht/token-data-eq? (get-set-token "theme" "button.primary.background")
{:name "button.primary.background"
:type :color
:value "{accent.default}"
:description ""})))
(t/testing "invalid tokens got discarded"
(t/is (nil? (ctob/get-token-in-set lib "typography" "H1.Bold")))))))
(t/is (nil? (get-set-token "typography" "H1.Bold")))))))
#?(:clj
(t/deftest parse-multi-set-dtcg-json-default-team
(t/deftest decode-dtcg-json-default-team
(let [json (-> (slurp "test/common_tests/types/data/tokens-default-team-only.json")
(json/decode {:key-fn identity}))
lib (ctob/parse-decoded-json json "")
(tr/decode-str))
lib (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json)
get-set-token (fn [set-name token-name]
(some-> (ctob/get-set lib set-name)
(ctob/get-token token-name)))
themes (ctob/get-themes lib)
first-theme (first themes)]
(t/is (= '("dark") (ctob/get-ordered-set-names lib)))
@@ -1478,14 +1469,15 @@
(t/is (= (:group first-theme) ""))
(t/is (= (:name first-theme) ctob/hidden-token-theme-name)))
(t/testing "token exist in dark set"
(t/is (tht/token-data-eq? (ctob/get-token-in-set lib "dark" "small")
(t/is (tht/token-data-eq? (get-set-token "dark" "small")
{:name "small"
:value "8"
:type :border-radius
:description ""}))))))
#?(:clj
(t/deftest export-dtcg-json
(t/deftest encode-dtcg-json
(let [now (dt/now)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "core"
@@ -1510,7 +1502,7 @@
:id "test-id-00"
:modified-at now
:sets #{"core"})))
result (ctob/export-dtcg-json tokens-lib)
result (ctob/encode-dtcg tokens-lib)
expected {"$themes" [{"description" ""
"group" "group-1"
"is-source" false
@@ -1536,7 +1528,7 @@
(t/is (= expected result)))))
#?(:clj
(t/deftest export-parse-dtcg-json
(t/deftest encode-decode-dtcg-json
(with-redefs [dt/now (constantly #inst "2024-10-16T12:01:20.257840055-00:00")]
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "core"
@@ -1557,14 +1549,17 @@
:type :color
:value "{accent.default}"})})))
encoded (ctob/export-dtcg-json tokens-lib)
tokens-lib' (ctob/parse-decoded-json encoded "")]
encoded (ctob/encode-dtcg tokens-lib)
with-prev-tokens-lib (ctob/decode-dtcg-json tokens-lib encoded)
with-empty-tokens-lib (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) encoded)]
(t/testing "library got updated but data is equal"
(t/is (not= tokens-lib' tokens-lib))
(t/is (= @tokens-lib' @tokens-lib)))))))
(t/is (not= with-prev-tokens-lib tokens-lib))
(t/is (= @with-prev-tokens-lib @tokens-lib)))
(t/testing "fresh tokens library is also equal"
(= @with-empty-tokens-lib @tokens-lib))))))
#?(:clj
(t/deftest export-dtcg-json-with-default-theme
(t/deftest encode-default-theme-json
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "core"
:tokens {"colors.red.600"
@@ -1583,7 +1578,7 @@
{:name "button.primary.background"
:type :color
:value "{accent.default}"})})))
result (ctob/export-dtcg-json tokens-lib)
result (ctob/encode-dtcg tokens-lib)
expected {"$themes" []
"$metadata" {"tokenSetOrder" ["core"]
"activeSets" #{}, "activeThemes" #{}}
@@ -1604,7 +1599,7 @@
(t/is (= expected result)))))
#?(:clj
(t/deftest export-dtcg-json-with-active-theme-and-set
(t/deftest encode-dtcg-json-with-active-theme-and-set
(let [now (dt/now)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "core"
@@ -1630,7 +1625,7 @@
:modified-at now
:sets #{"core"}))
(ctob/toggle-theme-active? "group-1" "theme-1"))
result (ctob/export-dtcg-json tokens-lib)
result (ctob/encode-dtcg tokens-lib)
expected {"$themes" [{"description" ""
"group" "group-1"
"is-source" false

View File

@@ -9,52 +9,32 @@
[app.common.types.variant :as ctv]
[clojure.test :as t]))
(t/deftest convert-between-variant-properties-maps-and-formulas
(t/deftest convert-between-variant-properties-maps-and-strings
(let [map-with-two-props [{:name "border" :value "yes"} {:name "color" :value "gray"}]
map-with-two-props-one-blank [{:name "border" :value "no"} {:name "color" :value ""}]
map-with-two-props-dashes [{:name "border" :value "no"} {:name "color" :value "--"}]
map-with-one-prop [{:name "border" :value "no"}]
map-with-equal [{:name "border" :value "yes color=yes"}]
map-with-spaces [{:name "border 1" :value "of course"}
{:name "color 2" :value "dark gray"}
{:name "background 3" :value "anoth€r co-lor"}]
map-with-spaces [{:name "border 1" :value "of course"} {:name "color 2" :value "dark gray"}]
string-valid-with-two-props "border=yes, color=gray"
string-valid-with-one-prop "border=no"
string-valid-with-spaces "border 1=of course, color 2=dark gray, background 3=anoth€r co-lor"
string-valid-with-no-value "border=no, color="
string-valid-with-dashes "border=no, color=--"
string-valid-with-equal "border=yes color=yes"
string-invalid-1 ""
string-invalid-2 "=yes"
string-invalid-3 "border"
string-invalid-4 "border=yes, =gray"
string-invalid-5 "border=yes, color"]
string-valid-with-spaces "border 1=of course, color 2=dark gray"
string-invalid "border=yes, color="]
(t/testing "convert map to formula"
(t/is (= (ctv/properties-map->formula map-with-two-props) string-valid-with-two-props))
(t/is (= (ctv/properties-map->formula map-with-two-props-one-blank) string-valid-with-one-prop))
(t/is (= (ctv/properties-map->formula map-with-spaces) string-valid-with-spaces)))
(t/testing "convert map to string"
(t/is (= (ctv/properties-map-to-string map-with-two-props) string-valid-with-two-props))
(t/is (= (ctv/properties-map-to-string map-with-two-props-one-blank) string-valid-with-one-prop))
(t/is (= (ctv/properties-map-to-string map-with-spaces) string-valid-with-spaces)))
(t/testing "convert formula to map"
(t/is (= (ctv/properties-formula->map string-valid-with-two-props) map-with-two-props))
(t/is (= (ctv/properties-formula->map string-valid-with-one-prop) map-with-one-prop))
(t/is (= (ctv/properties-formula->map string-valid-with-no-value) map-with-one-prop))
(t/is (= (ctv/properties-formula->map string-valid-with-dashes) map-with-two-props-dashes))
(t/is (= (ctv/properties-formula->map string-valid-with-equal) map-with-equal))
(t/is (= (ctv/properties-formula->map string-valid-with-spaces) map-with-spaces)))
(t/testing "convert string to map"
(t/is (= (ctv/properties-string-to-map string-valid-with-two-props) map-with-two-props))
(t/is (= (ctv/properties-string-to-map string-valid-with-one-prop) map-with-one-prop))
(t/is (= (ctv/properties-string-to-map string-valid-with-spaces) map-with-spaces)))
(t/testing "check if a formula is valid"
(t/is (= (ctv/valid-properties-formula? string-valid-with-two-props) true))
(t/is (= (ctv/valid-properties-formula? string-valid-with-one-prop) true))
(t/is (= (ctv/valid-properties-formula? string-valid-with-spaces) true))
(t/is (= (ctv/valid-properties-formula? string-valid-with-no-value) true))
(t/is (= (ctv/valid-properties-formula? string-valid-with-dashes) true))
(t/is (= (ctv/valid-properties-formula? string-invalid-1) false))
(t/is (= (ctv/valid-properties-formula? string-invalid-2) false))
(t/is (= (ctv/valid-properties-formula? string-invalid-3) false))
(t/is (= (ctv/valid-properties-formula? string-invalid-4) false))
(t/is (= (ctv/valid-properties-formula? string-invalid-5) false)))))
(t/testing "check if a string is valid"
(t/is (= (ctv/valid-properties-string? string-valid-with-two-props) true))
(t/is (= (ctv/valid-properties-string? string-valid-with-one-prop) true))
(t/is (= (ctv/valid-properties-string? string-valid-with-spaces) true))
(t/is (= (ctv/valid-properties-string? string-invalid) false)))))
(t/deftest find-properties

4
common/vendor/beicon/impl/rxjs.cljs vendored Normal file
View File

@@ -0,0 +1,4 @@
(ns beicon.impl.rxjs
(:require ["rxjs" :as rx]))
(goog/exportSymbol "rxjsMain" rx)

View File

@@ -0,0 +1,4 @@
(ns beicon.impl.rxjs-operators
(:require ["rxjs/operators" :as rxop]))
(goog/exportSymbol "rxjsOperators" rxop)

4
common/vendor/tubax/saxjs.cljs vendored Normal file
View File

@@ -0,0 +1,4 @@
(ns tubax.saxjs
(:require ["sax" :as sax]))
(goog/exportSymbol "sax" sax)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
FROM ubuntu:24.04
FROM ubuntu:22.04
LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG='en_US.UTF-8' \

View File

@@ -1,4 +1,4 @@
FROM ubuntu:24.04
FROM ubuntu:22.04
LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG=en_US.UTF-8 \
@@ -32,8 +32,8 @@ RUN set -ex; \
netpbm \
poppler-utils \
potrace \
dconf-service \
libasound2t64 \
gconf-service \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatomic1 \
@@ -43,6 +43,7 @@ RUN set -ex; \
libexpat1 \
libfontconfig1 \
libgcc1 \
libgconf-2-4 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \

View File

@@ -1,4 +1,4 @@
FROM nginxinc/nginx-unprivileged:1.28.0
FROM nginxinc/nginx-unprivileged:1.27.1
LABEL maintainer="Penpot <docker@penpot.app>"
USER root

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ description or metadata.description }}">
<link rel="stylesheet" href="{{ '/css/index.css' | url }}">
<link rel="stylesheet" href="{{ '/css/prism.css' | url }}">
<link rel="shortcut icon" href="/img/favicon.png">
@@ -14,7 +15,7 @@
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
{% metagen
title=title or metadata.title,
desc=desc or metadata.desc or description or metadata.description,
desc=desc or metadata.desc,
url="https://help.penpot.app" + page.url,
img="https://help.penpot.app/img/th-help-center.jpg",
img_alt=alt,

View File

@@ -1,6 +1,5 @@
---
title: 04· Code of Conduct
desc: Learn about contributing to the Penpot project! This page outlines the Code of Conduct, reporting bugs, translations, core code contributions, & more.
---
<h1 id="coc">Code of conduct</h1>

View File

@@ -1,6 +1,5 @@
---
title: 03· Core code contributions
desc: Learn how to contribute to Penpot's open-source design collaboration platform. Find guidelines for bug reporting, code contributions & more.
---
<h1 id="code-contributions">Core code contributions</h1>

View File

@@ -1,6 +1,5 @@
---
title: Contributing
desc: Learn how to contribute to Penpot, the open-source design collaboration platform! Find guides on bug reporting, translations, code contributions, and more.
eleventyNavigation:
key: Contributing
order: 3

View File

@@ -1,6 +1,5 @@
---
title: 05· Libraries & Templates
desc: Contribute to Penpot's libraries & templates! Learn how to share your files and access resources. Try Penpot - It's free!
---
<h1 id="libraries">Libraries & templates</h1>

View File

@@ -1,6 +1,5 @@
---
title: 01· Reporting bugs
desc: Learn how to contribute to Penpot, the open-source design and prototyping platform! Find guidelines for reporting bugs, translations, & code contributions.
---
<h1 id="reporting-bugs">Reporting bugs</h1>

View File

@@ -1,6 +1,5 @@
---
title: 02· Translations
desc: Contribute to Penpot! Learn how to translate Penpot into your language using Weblate. Add new translations, languages, or edit existing ones today.
---
<h1 id="translations">Translations</h1>

View File

@@ -1,6 +1,5 @@
---
title: Help center
desc: Find user guides, technical documentation, plugin info, FAQs, and contributing guidelines in Penpot's help center. Join the open-source community!
layout: layouts/home.njk
twitter: "@penpotapp"
image: img/placeholder.png

View File

@@ -1,7 +1,6 @@
---
layout: layouts/plugins.njk
title: 4. API
desc: Create, deploy, and use the Penpot plugin API with our comprehensive documentation. Get started today and expand Penpot's capabilities.
---
# Penpot plugins API

View File

@@ -1,7 +1,5 @@
---
layout: layouts/plugins-no-sidebar.njk
title: Beta changelog
desc: See the Penpot plugin API changelog for version 1.0! Find breaking changes, deprecations, new features, and updated documentation. Try Penpot for free.
---
# Beta changelog

View File

@@ -1,7 +1,6 @@
---
layout: layouts/plugins.njk
title: 2. Create a Plugin
desc: Dive into Penpot plugin development! This guide covers creating plugins from scratch or using templates, libraries, API communication, & deployment.
---
# Create a Plugin

View File

@@ -1,7 +1,6 @@
---
layout: layouts/plugins.njk
title: 3. Deployment
desc: Deploy your free Penpot plugins! Learn about Netlify, Cloudflare, Surge & Penpot submission in this guide. Build and share your creations.
---
# Deployment

View File

@@ -1,7 +1,6 @@
---
layout: layouts/plugins.njk
title: 5. Examples and templates
desc: Learn to create shapes, text, layouts, components, themes, and interactive prototypes. Start building now! See Penpot plugins with examples & templates!
---
# Examples and templates
@@ -118,6 +117,7 @@ Just a friendly reminder that it's important to have the <b>comment permissions<
<a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/create-comments">Comments example</a>
## 5.2. Templates
As we mentioned in the <a target="_blank" href="/plugins/create-a-plugin/">Create a plugin</a> section, we've got two great options for you to get started with your plugin.

View File

@@ -1,7 +1,6 @@
---
layout: layouts/plugins.njk
title: 6. FAQ
desc: Find answers to common questions about plugin development, from choosing the right Node version to creating components. See Penpot plugins!
---
# FAQ

View File

@@ -1,7 +1,6 @@
---
layout: layouts/plugins.njk
title: 1. Getting started
desc: Dive into Penpot plugins! Extend Penpot's functionality by automating tasks and adding new features using JavaScript, HTML, & CSS. Get started now!
---
# Getting started

View File

@@ -1,7 +1,6 @@
---
layout: layouts/plugins-home.njk
title: Plugins
desc: "Get started with Penpot Plugins: Installation, development, and deployment. Access API documentation, examples, templates, and FAQs."
eleventyNavigation:
key: Plugins
order: 5

View File

@@ -1,6 +1,5 @@
---
title: 2. Penpot Configuration
desc: Learn about self-hosting, configuration via environment variables, and authentication providers. Try Penpot - It's free! See Penpot's technical guide.
---
# Penpot Configuration

View File

@@ -1,6 +1,5 @@
---
title: Backend app
desc: Dive into self-hosting, configuration, developer insights (architecture, data model), integration, and troubleshooting. See Penpot's Technical Guide.
---
# Backend app

View File

@@ -1,6 +1,5 @@
---
title: Common code
desc: Learn about architecture, data models, and development environments. See Penpot's technical guide for developers. Dive into common code.
---
# Common code

View File

@@ -1,6 +1,5 @@
---
title: Exporter app
desc: Learn about self-hosting, configuration, architecture (backend, frontend), data model, and development environment. See Penpot's technical guide.
---
# Exporter app

View File

@@ -1,6 +1,5 @@
---
title: Frontend app
desc: Dive into the UI, data namespaces, ClojureScript, React, and worker app functionalities. View Penpot's frontend app architecture. Free to try!
---
### Frontend app

View File

@@ -1,6 +1,5 @@
---
title: 3.01. Architecture
desc: Dive into architecture, backend, frontend, data models, and development environments. Contribute and self-host for free! See Penpot's technical guide.
---
# Architecture

View File

@@ -1,6 +1,5 @@
---
title: 3.04. Common Guide
desc: "View Penpot's technical guide: self-hosting, configuration, developer insights, architecture, data model, integration, and troubleshooting."
---
# Common guide

View File

@@ -1,6 +1,5 @@
---
title: 3.08. Data Guide
desc: Learn about data structures, code organization, file operations, migrations, shape editing, and component syncing. See Penpot's technical guide. Try it free!
---
# Data Guide

View File

@@ -1,6 +1,5 @@
---
title: 3.02. Data model
desc: Learn about self-hosting, configuration, developer tools, data models, architecture, and integrations. View Penpot's technical guide. Free to use!
---
# Penpot Data Model

View File

@@ -1,6 +1,5 @@
---
title: 3.03. Dev environment
desc: Dive into Penpot's development environment. Learn about self-hosting, configuration, developer tools, architecture, and more. See the Penpot Technical Guide!
---
# Development environment
@@ -79,36 +78,24 @@ connect to penpot by browsing to http://localhost:3449 .
### Frontend
The frontend build process is located on the tmux **window 0** and
**window 1**. On **window 0** we have the gulp process responsible
for watching and building styles, fonts, icon-spreads and templates.
**window 1**. On the **window 0** we have the gulp process responsible
of watching and building styles, fonts, icon-spreads and templates.
On **window 1** we can find the **shadow-cljs** process that is
responsible for watching and building frontend clojurescript code.
On the **window 1** we can found the **shadow-cljs** process that is
responsible on watch and build frontend clojurescript code.
In addition to the watch process you probably want to be able to open a REPL
process on the frontend application. In order to do this you can split the
window (`Ctrl+b "`) and execute:
Additionally to the watch process you probably want to be able open a REPL
process on the frontend application, for this case you can split the window
and execute this:
```bash
cd penpot/frontend
npx shadow-cljs cljs-repl main
```
In order to have the REPL working you need to have an active browser session
with the penpot application opened (otherwise, you will get the error
`No application has connected to the REPL server.`).
Finally, in case you want to connect to the REPL from your IDE, you can set it
up to use nREPL with the port `3447` and the host `localhost` (you can see the
port in the startup message of the shadow-cljs process in **window 1**). You
will also need to call `(shadow/repl :main)` in the REPL to start the connection,
as explained [here](https://shadow-cljs.github.io/docs/UsersGuide.html#_server_options).
### Storybook
The storybook local server is started on tmux **window 2** and will listen
for changes in the styles, components or stories defined in the folders
for changes in the styles, components or stories defined in the folders
under the design system namespace: `app.main.ui.ds`.
You can open the broser on http://localhost:6006/ to see it.

View File

@@ -1,6 +1,5 @@
---
title: 3.05. Frontend Guide
desc: "See Penpot's technical guide: self-hosting, configuration, developer insights (architecture, data model), frontend, backend, and integrations & more!"
---
# Frontend Guide

View File

@@ -1,6 +1,5 @@
---
title: 3. Developer Guide
desc: Dive into architecture, data models, and more. Start building today! See Penpot's technical guide for self-hosting, configuration, and developer insights.
---
# Developer Guide

View File

@@ -1,6 +1,5 @@
---
title: Assets storage
desc: Learn about assets storage, API, object buckets, sharing, and garbage collection. See Penpot's technical guide for developers. Try Penpot - It's free.
---
# Assets storage

View File

@@ -1,6 +1,5 @@
---
title: Authentication
desc: Dive into Penpot today! Learn about self-hosting, configuration, developer insights, authentication, and more. View Penpot's technical guide. Try it free.
---
# User authentication

View File

@@ -1,6 +1,5 @@
---
title: 3.09. Penpot subsystems
desc: Learn about architecture, data models, and subsystems. View Penpot's technical guide for self-hosting, configuration, and development insights. Free!
---
# Penpot subsystems
@@ -13,3 +12,4 @@ implemented, over the whole app (backend, frontend or exporter), and points to
the most relevant source files to look at to start exploring it. When some
special considerations are needed (performance questions, limits, common
"gotchas", historic reasons of some decisions, etc.) they are also noted.

View File

@@ -1,6 +1,5 @@
---
title: 3.10. UI Guide
desc: Learn UI development with React & Rumext, design system implementation, and performance considerations. See Penpot's technical guide. Free to use!
---
# UI Guide
@@ -36,6 +35,7 @@ We want to hold our UI code to the same quality standards of the rest of the cod
```clojure
(mf/defc primary-button*
{::mf/props :obj}
[{:keys [children] :rest props}]
[:> "button" props children])
```
@@ -68,6 +68,7 @@ For instance, here the user would be in total control of the <code class="langua
```clojure
(mf/defc button*
{::mf/props :obj}
[{:keys [icon children] :rest props}]
[:> "button" props
icon
@@ -78,6 +79,7 @@ However, we might want to control the aspect of the icons, or limit which icons
```clojure
(mf/defc button*
{::mf/props :obj}
[{:keys [icon children] :rest props}]
(assert (or (nil? icon) (contains? valid-icon-list icon) "expected valid icon id"))
[:> "button" props
@@ -113,6 +115,7 @@ This is achieved by accepting a <code class="language-clojure">class</code> prop
```clojure
(mf/defc button*
{::mf/props :obj}
[{:keys [children class] :rest props}]
(let [class (dm/str class " " (stl/css :primary-button))
props (mf/spread-props props {:class class})]
@@ -127,6 +130,7 @@ Nested styles for DOM elements that are not instantiated by our component should
```clojure
(mf/defc button*
{::mf/props :obj}
[{:keys [children] :rest props}]
(let [props (mf/spread-props props {:class (stl/css :primary-button)})]
;; note that we are NOT instantiating a <svg> here.
@@ -151,10 +155,11 @@ Nested styles for DOM elements that are not instantiated by our component should
```clojure
(mf/defc button*
{::mf/props :obj}
[{:keys [icon children class] :rest props}]
(let [props (mf/spread-props props {:class (stl/css :button)})]
[:> "button" props
(when icon [:> icon* {:icon-id icon :size "m" :class (stl/css :icon)}])
(when icon [:> icon* {:icon-id icon :size "m"}])
[:span {:class (stl/css :label-wrapper)} children]]))
;; later in code
@@ -199,167 +204,6 @@ Remember that nesting selector increases specificity, and it's usually not neede
fill: var(--icon-color);
}
```
Note: Thanks to CSS Modules, identical class names defined in different files are scoped locally and do not cause naming collisions.
### Use CSS logical properties
The [logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values) define styles relative to the contents writing mode (e.g., inline, block) instead of physical directions (left, right, etc). This improves support for right-to-left (RTL) languages and enhances layout flexibility.
**AVOID: Physical properties**
```scss
.btn {
padding-left: var(--sp-xs);
}
```
**DO: Use directionrelative equivalents**
```scss
.btn {
padding-inline-start: var(--sp-xs);
}
```
Note: Although `width` and `height` are physical properties, their use is allowed in CSS files. They remain more readable and intuitive than their logical counterparts (`inline-size`, `block-size`) in many contexts. Since our layouts are not vertically-sensitive, we don't gain practical benefits from using logical properties here.
### Use named DS variables
Avoid hardcoded values like `px`, `rem`, or raw SASS variables `($s-*)`. Use semantic, named variables provided by the Design System to ensure consistency and scalability.
#### Spacing (margins, paddings, gaps...)
Use variables from `frontend/src/app/main/ui/ds/spacing.scss`. These are predefined and approved by the design team — **do not add or modify values without design approval**.
#### Fixed dimensions
For fixed dimensions (e.g., modals' widths) defined by design and not layout-driven, use or define variables in `frontend/src/app/main/ui/ds/_sizes.scss`. To use them:
```scss
@use "../_sizes.scss" as *;
```
Note: Since these values haven't been semantically defined yet, were temporarily using SASS variables instead of named CSS custom properties.
#### Border Widths
Use border thickness variables from `frontend/src/app/main/ui/ds/_borders.scss`. To import:
```scss
@use "../_borders.scss" as *;
```
Avoid using sass variables defined on `frontend/resources/styles/common/refactor/spacing.scss` that are deprecated.
**AVOID: Using sass unnamed variables or hardcoded values**
```scss
.btn {
padding: $s-24;
}
.icon {
width: 16px;
}
```
**DO: Use DS variables**
```scss
.btn {
padding: var(--sp-xl);
}
.icon {
width: var(--sp-l);
}
```
### Use Proper Typography Components
Replace plain text tags with `text*` or `heading*` components from the Design System to ensure visual consistency and accessibility.
**AVOID: Using text wrappers**
```clojure
[:h2 {:class (stl/css :modal-title)} title]
[:div {:class (stl/css :modal-content)}
"Content"]
```
**DO: Use spacing named variables**
```clojure
...
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
...
[:> heading* {:level 2
:typography t/headline-medium
:class (stl/css :modal-title)}
title]
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :modal-content)}
"Content"]
```
When applying typography in SCSS, use the proper mixin from the Design System.
**AVOID: Deprecated mixins**
```scss
.class {
@include headlineLargeTypography;
}
```
**DO: Use the DS mixin**
```scss
@use "../ds/typography.scss" as t;
.class {
@include t.use-typography("body-small");
}
```
You can find the full list of available typography tokens in [Storybook](https://design.penpot.app/storybook/?path=/docs/foundations-typography--docs).
If the design you are implementing doesn't match any of them, ask a designer.
### Use custom properties within components
Reduce the need for one-off SASS variables by leveraging [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) in your component styles. This keeps component theming flexible and composable.
For instance, this is how we handle the styles of <code class="language-clojure">\<Toast></code>, which have a different style depending on the level of the message (default, info, error, etc.)
```scss
.toast {
// common styles for all toasts
// ...
--toast-bg-color: var(--color-background-primary);
--toast-icon-color: var(--color-foreground-secondary);
// ... more variables here
background-color: var(--toast-bg-color);
}
.toast-icon {
color: var(--toast-bg-color);
}
.toast-info {
--toast-bg-color: var(--color-background-info);
--toast-icon-color: var(--color-accent-info);
// ... override more variables here
}
.toast-error {
--toast-bg-color: var(--color-background-error);
--toast-icon-color: var(--color-accent-error);
// ... override more variables here
}
// ... more variants here
```
## Semantics and accessibility
@@ -398,7 +242,7 @@ Please refer to the [Rumext User Guide](https://funcool.github.io/rumext/latest/
Some things to have in mind:
- Avoid using <code class="language-clojure">?</code> for boolean props, since they don't get a clean translation to JavaScript.
- When you want to use JavaScript props, use the meta <code class="language-clojure">{::mf/props :obj}</code>. In this case, avoid using <code class="language-clojure">?</code> for boolean props, since they don't get a clean translation to JavaScript.
- You can use type hints such as <code class="language-clojure">^boolean</code> to get JS semantics.
- Split big components into smaller ones. You can mark components as private with the <code class="language-clojure">::mf/private true</code> meta.
@@ -416,6 +260,7 @@ We just need to use `:rest ` when declaring the component props.
```clojure
(mf/defc button*
{::mf/props :obj}
[{:keys [children] :rest other}]
[:> "button" other children])
```
@@ -424,6 +269,7 @@ If we need to augment this props object, we can use <code class="language-clojur
```clojure
(mf/defc button*
{::mf/props :obj}
[{:keys [children class] :rest props}]
(let [class (dm/str class " " (stl/css :button))
props (mf/spread-props props {:class class})]
@@ -448,6 +294,7 @@ It's faster to use a JS Object for props instead of a native Clojure map, becaus
```clojure
(mf/defc icon*
{::mf/props :obj}
[props]
;; ...
)
@@ -479,6 +326,7 @@ This creates a brand new function every render. Instead, create the function on
)
(mf/defc login-button
{::mf/props :obj}
[]
[:button {:on-click login} "Login"])
@@ -492,6 +340,7 @@ When we do this inside of a component, a brand new function is created in every
```clojure
(mf/defc login-button
{::mf/props :obj}
[]
(let [click-handler (fn []
;; ...
@@ -507,6 +356,7 @@ When we do this inside of a component, a brand new function is created in every
)
(mf/defc login-button
{::mf/props :obj}
[]
[:button {:on-click login} "Login"])
```
@@ -573,6 +423,7 @@ Often we need to access values from props. It's best if we destructure them (bec
```clojure
(defc icon
{::mf/props :obj}
[{:keys [size img] :as props]
[:svg {:width size
:height size
@@ -585,6 +436,7 @@ Often we need to access values from props. It's best if we destructure them (bec
```clojure
(defc icon
{::mf/props :obj}
[props]
[:svg {:width (unchecked-get props "size")
:height (unchecked-get props "size")
@@ -601,6 +453,7 @@ We can avoid multiple calls to <code class="language-clojure">(deref)</code> if
```clojure
(defc accordion
{::mf/props :obj}
[{:keys [^boolean default-open title children] :as props]
(let [
@@ -662,8 +515,40 @@ We use three **levels of tokens**:
### Implementing variants
We can leverage component tokens to easily implement variants as explained [here](/technical-guide/developer/ui/#use-custom-properties-within-components).
We can leverage component tokens to easily implement variants, by overriding their values in each component variant.
For instance, this is how we handle the styles of <code class="language-clojure">\<Toast></code>, which have a different style depending on the level of the message (default, info, error, etc.)
```scss
.toast {
// common styles for all toasts
// ...
--toast-bg-color: var(--color-background-primary);
--toast-icon-color: var(--color-foreground-secondary);
// ... more variables here
background-color: var(--toast-bg-color);
}
.toast-icon {
color: var(--toast-bg-color);
}
.toast-info {
--toast-bg-color: var(--color-background-info);
--toast-icon-color: var(--color-accent-info);
// ... override more variables here
}
.toast-error {
--toast-bg-color: var(--color-background-error);
--toast-icon-color: var(--color-accent-error);
// ... override more variables here
}
// ... more variants here
```
### Using icons and SVG assets

View File

@@ -1,6 +1,5 @@
---
title: 1.3 Install with Docker
desc: This Penpot technical guide covers self-hosting, Docker installation, configuration, updates, backups, and proxy setup with NGINX and Caddy. Try Penpot!
---
<p class="advice">

View File

@@ -1,6 +1,5 @@
---
title: 1. Self-hosting Guide
desc: Customize your Penpot instance today. Learn how to install with Elestio, Docker, or Kubernetes from the technical guide for self-hosting options.
---
# Self-hosting Guide

Some files were not shown because too many files have changed in this diff Show More