mirror of
https://github.com/penpot/penpot.git
synced 2026-01-03 11:58:46 -05:00
Compare commits
193 Commits
revert-646
...
add-numeri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dfccdaf9b | ||
|
|
e5bc369e56 | ||
|
|
5e8929e504 | ||
|
|
3ee3ee2059 | ||
|
|
9eacde567d | ||
|
|
9638fd274f | ||
|
|
7c072abe28 | ||
|
|
603e41bbfd | ||
|
|
b561ad033c | ||
|
|
7373056037 | ||
|
|
a9173f672d | ||
|
|
44829ff1ae | ||
|
|
927ee9e55e | ||
|
|
066b252522 | ||
|
|
556a68a78f | ||
|
|
f9bbf2d524 | ||
|
|
eaaca5629e | ||
|
|
0df2a12814 | ||
|
|
df27db1996 | ||
|
|
7fc0d15418 | ||
|
|
99fb905070 | ||
|
|
413fc6de16 | ||
|
|
d54a7d0401 | ||
|
|
ed53793d9d | ||
|
|
faa68784af | ||
|
|
58b1cf6b0c | ||
|
|
f9c9e865b5 | ||
|
|
ebe321d9d3 | ||
|
|
0683fbd17c | ||
|
|
09a7ef3e45 | ||
|
|
172b70d8a7 | ||
|
|
3597e5bb54 | ||
|
|
949b6d1205 | ||
|
|
c0af77faf7 | ||
|
|
8f7a674000 | ||
|
|
e4f2dfaa11 | ||
|
|
ec29c4f5fe | ||
|
|
c21f5221bb | ||
|
|
42ef2f929a | ||
|
|
2b21401368 | ||
|
|
a5c8063b2c | ||
|
|
2ad2af2aea | ||
|
|
c2ce7c6cf6 | ||
|
|
47490db4be | ||
|
|
a2ac2bc6c6 | ||
|
|
e80ca7e332 | ||
|
|
e4644ff506 | ||
|
|
662b926b4b | ||
|
|
6319ed78f9 | ||
|
|
3abc8774f6 | ||
|
|
af1c90c252 | ||
|
|
8019ae7840 | ||
|
|
6bd615ff8b | ||
|
|
c4a793d306 | ||
|
|
631b3ac062 | ||
|
|
48995850fa | ||
|
|
a5c7a2c97b | ||
|
|
3a8285bc69 | ||
|
|
02e3cc089e | ||
|
|
17e19afcbd | ||
|
|
a2b52a6408 | ||
|
|
8cc4b69291 | ||
|
|
045ddf5829 | ||
|
|
1d0335aba6 | ||
|
|
5412d72236 | ||
|
|
896ee43212 | ||
|
|
5d42b9793b | ||
|
|
6cd2c712ab | ||
|
|
a575410a29 | ||
|
|
6b5703c1fe | ||
|
|
22c3d4d807 | ||
|
|
b0701f6bb4 | ||
|
|
0748ef7267 | ||
|
|
9bad9b8e91 | ||
|
|
9ca4fa752c | ||
|
|
b6563f620b | ||
|
|
a63fa2944d | ||
|
|
fd89c9d82c | ||
|
|
71bb2556f9 | ||
|
|
f36aa30525 | ||
|
|
8f7c63d6e2 | ||
|
|
965b22718f | ||
|
|
48a3d38d82 | ||
|
|
31f642ed25 | ||
|
|
9f414b6ecd | ||
|
|
334d7833d5 | ||
|
|
ff9c8f5929 | ||
|
|
f7311cbb6b | ||
|
|
e4c563f917 | ||
|
|
2d3ad5a88f | ||
|
|
1334d733cd | ||
|
|
004a9f17d3 | ||
|
|
c87fa4f723 | ||
|
|
9378a5786f | ||
|
|
3224ba26f1 | ||
|
|
d33a5e6df1 | ||
|
|
0d60e3d997 | ||
|
|
645c4a57db | ||
|
|
778de6adaf | ||
|
|
29d23577d2 | ||
|
|
1fea1e8f5b | ||
|
|
c8a211742a | ||
|
|
2da8747485 | ||
|
|
6803c78e80 | ||
|
|
d8daea72de | ||
|
|
36b162b4fa | ||
|
|
4c487834f0 | ||
|
|
dc7e53881a | ||
|
|
1a01c9ee4a | ||
|
|
b6be416c7b | ||
|
|
4a27e8d1dd | ||
|
|
1bc97f9ad0 | ||
|
|
aaa57cb17f | ||
|
|
b2d6342422 | ||
|
|
ba1e16b55b | ||
|
|
ef95e3ecb0 | ||
|
|
55d21761fc | ||
|
|
0b4a367e9e | ||
|
|
8f2ca15ec0 | ||
|
|
21041eb925 | ||
|
|
53cfc29a1f | ||
|
|
96d44e0631 | ||
|
|
8afd217a80 | ||
|
|
330e49db56 | ||
|
|
aa39170d47 | ||
|
|
8fa7a69094 | ||
|
|
33d989feb2 | ||
|
|
e309a57223 | ||
|
|
0b289153cb | ||
|
|
cf274099c4 | ||
|
|
6524e75770 | ||
|
|
9b80f7c9b3 | ||
|
|
bf76f328c8 | ||
|
|
051c2a7e99 | ||
|
|
887fa6b77b | ||
|
|
d9f98008f4 | ||
|
|
0cb6e0dee2 | ||
|
|
ad87e9842d | ||
|
|
e22a55334e | ||
|
|
f5e81debbc | ||
|
|
300e24b403 | ||
|
|
a00e7c1061 | ||
|
|
ba25ce3098 | ||
|
|
968ea56197 | ||
|
|
2635873b9a | ||
|
|
f5f1316f0b | ||
|
|
79a164be6d | ||
|
|
ecb85778bc | ||
|
|
676c4d2dfe | ||
|
|
2cdc241e68 | ||
|
|
057bf9bf25 | ||
|
|
2ddcd0ce15 | ||
|
|
fef08dfa18 | ||
|
|
831422feaf | ||
|
|
d01e3085f4 | ||
|
|
d9ca82dc15 | ||
|
|
1e7127d98a | ||
|
|
002ae8b91a | ||
|
|
6831acb71d | ||
|
|
1f44d53f6b | ||
|
|
ca2891d441 | ||
|
|
91fbe8f8ef | ||
|
|
69cc4fb4c2 | ||
|
|
37abb7b237 | ||
|
|
5fc2208c16 | ||
|
|
c2b67d7c67 | ||
|
|
eb76d16b3b | ||
|
|
58e0b26493 | ||
|
|
c75380e063 | ||
|
|
3d67c7930c | ||
|
|
b55ec38c35 | ||
|
|
02a1cfb457 | ||
|
|
b2ba38b5de | ||
|
|
68ce13368e | ||
|
|
a55db1d52b | ||
|
|
ee96c5599c | ||
|
|
21702c090d | ||
|
|
c4254106e8 | ||
|
|
981336ed5e | ||
|
|
3864ce6855 | ||
|
|
ec0183ce94 | ||
|
|
f587ed4ade | ||
|
|
bb5a103944 | ||
|
|
34b3520fb2 | ||
|
|
3217ba5a77 | ||
|
|
a91caded9e | ||
|
|
05ba1c3e64 | ||
|
|
77f025eb8d | ||
|
|
aacec1809b | ||
|
|
0435f560a4 | ||
|
|
766f034e5e | ||
|
|
8502d9d21b | ||
|
|
6c874b2bb7 |
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check Commit Type
|
||||
uses: gsactions/commit-message-checker@v2
|
||||
with:
|
||||
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].*[^.]$'
|
||||
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].*[^.]$'
|
||||
flags: 'gm'
|
||||
error: 'Commit should match CONTRIBUTING.md guideline'
|
||||
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -68,6 +68,8 @@
|
||||
/vendor/**/target
|
||||
/vendor/svgclean/bundle*.js
|
||||
/web
|
||||
/library/target/
|
||||
|
||||
clj-profiler/
|
||||
node_modules
|
||||
/test-results/
|
||||
|
||||
99
CHANGES.md
99
CHANGES.md
@@ -8,42 +8,84 @@
|
||||
|
||||
**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 `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
|
||||
- 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)
|
||||
|
||||
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.
|
||||
- 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
|
||||
|
||||
### :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)
|
||||
|
||||
@@ -63,10 +105,10 @@ root.
|
||||
- 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)
|
||||
@@ -93,6 +135,13 @@ root.
|
||||
- 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
|
||||
|
||||
|
||||
133
CONTRIBUTING.md
133
CONTRIBUTING.md
@@ -1,62 +1,59 @@
|
||||
# Contributing Guide #
|
||||
|
||||
Thank you for your interest in contributing to Penpot. This is a
|
||||
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.
|
||||
|
||||
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/).
|
||||
|
||||
## Reporting Bugs ##
|
||||
|
||||
We are using [GitHub Issues](https://github.com/penpot/penpot/issues)
|
||||
for our public bugs. We keep a close eye on this and try to make it
|
||||
for our public bugs. We keep a close eye on them 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
|
||||
- a browser and the browser version used
|
||||
- a dev tools console exception stack trace (if it is available)
|
||||
- the browser and browser version used
|
||||
- a dev tools console exception stack trace (if available)
|
||||
|
||||
If you found a bug that you consider better discuss in private (for
|
||||
example: security bugs), consider first send an email to
|
||||
If you found a bug which you think is better to discuss in private (for
|
||||
example, security bugs), consider first sending an email to
|
||||
`support@penpot.app`.
|
||||
|
||||
**We don't have formal bug bounty program for security reports; this
|
||||
is an open source application and your contribution will be recognized
|
||||
**We don't have a 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 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 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 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
|
||||
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
|
||||
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 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 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 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.
|
||||
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)
|
||||
|
||||
We use the `easy fix` tag to indicate issues that are appropriate for beginners.
|
||||
|
||||
## Commit Guidelines ##
|
||||
|
||||
We have very precise rules over how our git commit messages can be formatted.
|
||||
We have very precise rules on how our git commit messages must be formatted.
|
||||
|
||||
The commit message format is:
|
||||
|
||||
@@ -71,34 +68,37 @@ The commit message format is:
|
||||
Where type is:
|
||||
|
||||
- :bug: `:bug:` a commit that fixes a bug
|
||||
- :sparkles: `:sparkles:` a commit that an improvement
|
||||
- :tada: `:tada:` a commit with new feature
|
||||
- :sparkles: `:sparkles:` a commit that adds an improvement
|
||||
- :tada: `:tada:` a commit with a new feature
|
||||
- :recycle: `:recycle:` a commit that introduces a refactor
|
||||
- :lipstick: `:lipstick:` a commit with cosmetic changes
|
||||
- :ambulance: `:ambulance:` a commit that fixes critical bug
|
||||
- :ambulance: `:ambulance:` a commit that fixes a 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 not relevant changes
|
||||
- :arrow_up: `:arrow_up:` a commit with dependencies updates
|
||||
- :arrow_down: `:arrow_down:` a commit with dependencies downgrades
|
||||
- :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
|
||||
- :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 imperative mood.
|
||||
- The subject should have capitalized the first letter, without period
|
||||
at the end and no larger than 65 characters.
|
||||
- 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 blank line between the subject line and the body.
|
||||
- An entry on the CHANGES.md file if applicable, referencing the
|
||||
github or taiga issue/user-story using the these same rules.
|
||||
- An entry in the CHANGES.md file if applicable, referencing the
|
||||
GitHub or Taiga issue/user story using these same rules.
|
||||
|
||||
Examples of good commit messages:
|
||||
|
||||
@@ -111,8 +111,30 @@ Examples of good commit messages:
|
||||
- `:ambulance: Fix critical bug on user registration process`
|
||||
- `:tada: Add new approach for user registration`
|
||||
|
||||
## Formatting and Linting ##
|
||||
|
||||
## Code of conduct ##
|
||||
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 ##
|
||||
|
||||
As contributors and maintainers of this project, we pledge to respect
|
||||
all people who contribute through reporting issues, posting feature
|
||||
@@ -132,11 +154,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 to this Code of Conduct. Project
|
||||
contributions that are not aligned with 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.
|
||||
|
||||
@@ -145,12 +167,11 @@ 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/
|
||||
1.1.0, available from [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
|
||||
|
||||
## Developer's Certificate of Origin (DCO)
|
||||
|
||||
## Developer's Certificate of Origin (DCO) ##
|
||||
|
||||
By submitting code you are agree and can certify the below:
|
||||
By submitting code you agree to and can certify the following:
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
@@ -178,13 +199,15 @@ By submitting code you are agree and can certify the below:
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
Then, all your code patches (**documentation are excluded**) should
|
||||
Then, all your code patches (**documentation is excluded**) should
|
||||
contain a sign-off at the end of the patch/commit description body. It
|
||||
can be automatically added on adding `-s` parameter to `git commit`.
|
||||
can be automatically added by adding the `-s` parameter to `git commit`.
|
||||
|
||||
This is an example of the aspect of the line:
|
||||
This is an example of what the line should look like:
|
||||
|
||||
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).
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<br />
|
||||
|
||||
[Penpot video](https://github.com/penpot/penpot/assets/5446186/b8ad0764-585e-4ddc-b098-9b4090d337cc)
|
||||
[Penpot video](https://github.com/user-attachments/assets/08b83119-c090-4a74-86ed-7bfbdda9a793)
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
@@ -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.6-9"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
|
||||
|
||||
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.5.2.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.6.0.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.994"}
|
||||
metosin/reitit-core {:mvn/version "0.7.2"}
|
||||
{:mvn/version "1.3.1002"}
|
||||
metosin/reitit-core {:mvn/version "0.8.0"}
|
||||
nrepl/nrepl {:mvn/version "1.3.1"}
|
||||
cider/cider-nrepl {:mvn/version "0.52.0"}
|
||||
cider/cider-nrepl {:mvn/version "0.55.7"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.5"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.48.0.0"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "6.2.1"}
|
||||
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
|
||||
|
||||
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.18.3"}
|
||||
org.jsoup/jsoup {:mvn/version "1.20.1"}
|
||||
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.2"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.3"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.28.26"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.31.48"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.6" :git/sha "52cf7d6"}}
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
@@ -31,8 +31,8 @@ export PENPOT_FLAGS="\
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptons \
|
||||
enable-subscriptons-old";
|
||||
enable-subscriptions \
|
||||
enable-subscriptions-old";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
|
||||
@@ -24,8 +24,8 @@ export PENPOT_FLAGS="\
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptons \
|
||||
enable-subscriptons-old ";
|
||||
enable-subscriptions \
|
||||
enable-subscriptions-old ";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[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]))
|
||||
|
||||
@@ -28,13 +27,11 @@
|
||||
|
||||
(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"
|
||||
(feat.compv2/migrate-file! cfg file-id
|
||||
:validate? (::validate cfg true)
|
||||
:skip-on-graphic-error? true)
|
||||
nil
|
||||
|
||||
"fdata/shape-data-type"
|
||||
nil
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
[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]
|
||||
@@ -73,7 +74,7 @@
|
||||
[:size ::sm/int]
|
||||
[:content-type :string]
|
||||
[:bucket [::sm/one-of {:format :string} sto/valid-buckets]]
|
||||
[:hash :string]])
|
||||
[:hash {:optional true} :string]])
|
||||
|
||||
(def ^:private schema:file-thumbnail
|
||||
[:map {:title "FileThumbnail"}
|
||||
@@ -88,13 +89,19 @@
|
||||
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/page sm/json-transformer))
|
||||
(sm/encoder ctp/schema:page sm/json-transformer))
|
||||
|
||||
(def encode-shape
|
||||
(sm/encoder ::cts/shape sm/json-transformer))
|
||||
@@ -129,7 +136,7 @@
|
||||
(sm/decoder schema:manifest sm/json-transformer))
|
||||
|
||||
(def decode-media
|
||||
(sm/decoder ::ctf/media sm/json-transformer))
|
||||
(sm/decoder ctf/schema:media sm/json-transformer))
|
||||
|
||||
(def decode-component
|
||||
(sm/decoder ::ctc/component sm/json-transformer))
|
||||
@@ -229,27 +236,13 @@
|
||||
: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 (resolve-extension (:content-type smeta))
|
||||
ext (cmedia/mtype->extension (:content-type smeta))
|
||||
path (str "objects/" id ".json")
|
||||
params (-> (meta sobject)
|
||||
(assoc :id (:id sobject))
|
||||
@@ -574,7 +567,13 @@
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-media)
|
||||
(validate-media))
|
||||
object (assoc object :file-id file-id)]
|
||||
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))]
|
||||
(if (= id (:id object))
|
||||
(conj result object)
|
||||
result)))
|
||||
@@ -800,7 +799,7 @@
|
||||
:expected-id (str id)
|
||||
:found-id (str (:id object))))
|
||||
|
||||
(let [ext (resolve-extension (:content-type object))
|
||||
(let [ext (cmedia/mtype->extension (:content-type object))
|
||||
path (str "objects/" id ext)
|
||||
content (->> path
|
||||
(get-zip-entry input)
|
||||
@@ -814,13 +813,14 @@
|
||||
:expected-size (:size object)
|
||||
:found-size (sto/get-size 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)))
|
||||
(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))))
|
||||
|
||||
(let [params (-> object
|
||||
(dissoc :id :size)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@
|
||||
(: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]
|
||||
@@ -54,18 +53,20 @@
|
||||
::yres/status 200
|
||||
::yres/body (yres/stream-body
|
||||
(fn [_ output]
|
||||
(binding [events/*channel* (sp/chan :buf buf :xf (keep encode))]
|
||||
(let [listener (events/start-listener
|
||||
(partial write! output)
|
||||
(partial pu/close! output))]
|
||||
(try
|
||||
(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]
|
||||
(let [result (handler)]
|
||||
(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)))))))}))
|
||||
(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))))))}))
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
"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]
|
||||
@@ -22,39 +21,38 @@
|
||||
[buddy.core.bytes :as bb]
|
||||
[buddy.core.codecs :as bc]
|
||||
[clojure.java.shell :as sh]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.xml :as xml]
|
||||
[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))
|
||||
|
||||
(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 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 ::upload
|
||||
(s/keys :req-un [::filename ::size ::path]
|
||||
:opt-un [::mtype ::headers]))
|
||||
(def ^:private schema:input
|
||||
[:map {:title "Input"}
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} ::sm/text]])
|
||||
|
||||
;; 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]]])
|
||||
(def ^:private check-input
|
||||
(sm/check-fn schema:input))
|
||||
|
||||
(defn validate-media-type!
|
||||
([upload] (validate-media-type! upload cm/valid-image-types))
|
||||
@@ -97,17 +95,44 @@
|
||||
(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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::width integer?)
|
||||
(s/def ::height integer?)
|
||||
(s/def ::format #{:jpeg :webp :png})
|
||||
(s/def ::quality #(< 0 % 101))
|
||||
(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 ::thumbnail-params
|
||||
(s/keys :req-un [::input ::format ::width ::height]))
|
||||
(def ^:private check-thumbnail-params
|
||||
(sm/check-fn schema:thumbnail-params))
|
||||
|
||||
;; Related info on how thumbnails generation
|
||||
;; http://www.imagemagick.org/Usage/thumbnails/
|
||||
@@ -129,30 +154,38 @@
|
||||
:data tmp)))
|
||||
|
||||
(defmethod process :generic-thumbnail
|
||||
[{: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))))
|
||||
[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))))
|
||||
|
||||
(defmethod process :profile-thumbnail
|
||||
[{: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))))
|
||||
[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))))
|
||||
|
||||
(defn get-basic-info-from-svg
|
||||
[{:keys [tag attrs] :as data}]
|
||||
@@ -184,10 +217,9 @@
|
||||
|
||||
(defmethod process :info
|
||||
[{:keys [input] :as params}]
|
||||
(us/assert ::input input)
|
||||
(let [{:keys [path mtype]} input]
|
||||
(let [{:keys [path mtype] :as input} (check-input input)]
|
||||
(if (= mtype "image/svg+xml")
|
||||
(let [info (some-> path slurp csvg/parse get-basic-info-from-svg)]
|
||||
(let [info (some-> path slurp parse-svg get-basic-info-from-svg)]
|
||||
(when-not info
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-svg-file
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
[:string {:max 250}]
|
||||
[::sm/one-of {:format "string"} valid-event-types]]]
|
||||
[:props
|
||||
[:map-of :keyword :any]]
|
||||
[:map-of :keyword ::sm/any]]
|
||||
[:context {:optional true}
|
||||
[:map-of :keyword :any]]])
|
||||
[:map-of :keyword ::sm/any]]])
|
||||
|
||||
(def schema:push-audit-events
|
||||
[:map {:title "push-audit-events"}
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
|
||||
(db/update! pool :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
{:id project-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
result))
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
[:is-shared ::sm/boolean]
|
||||
[:project-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:data {:optional true} :any]])
|
||||
[:data {:optional true} ::sm/any]])
|
||||
|
||||
(def schema:permissions-mixin
|
||||
[:map {:title "PermissionsMixin"}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[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]
|
||||
@@ -110,7 +109,7 @@
|
||||
;; --- MUTATION COMMAND: persist-temp-file
|
||||
|
||||
(defn persist-temp-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id ::rpc/profile-id] :as params}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||
(let [file (files/get-file cfg id
|
||||
:migrate? false
|
||||
:lock-for-update? true)]
|
||||
@@ -119,7 +118,6 @@
|
||||
(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]]})
|
||||
@@ -147,19 +145,6 @@
|
||||
: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
|
||||
|
||||
@@ -80,9 +80,9 @@
|
||||
(def ^:private schema:create-font-variant
|
||||
[:map {:title "create-font-variant"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:data [:map-of :string :any]]
|
||||
[:data [:map-of ::sm/text ::sm/any]]
|
||||
[:font-id ::sm/uuid]
|
||||
[:font-family :string]
|
||||
[:font-family ::sm/text]
|
||||
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
|
||||
[:font-style [::sm/one-of {:format "string"} valid-style]]])
|
||||
|
||||
|
||||
@@ -114,18 +114,6 @@
|
||||
|
||||
;; --- 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,
|
||||
@@ -191,6 +179,37 @@
|
||||
(->> (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)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(: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]
|
||||
@@ -19,7 +20,6 @@
|
||||
[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,8 +178,7 @@
|
||||
(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})))
|
||||
|
||||
@@ -209,7 +208,7 @@
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::rpc/methods] :as cfg}]
|
||||
[(let [context (prepare-doc-context methods)]
|
||||
[(let [context (delay (prepare-doc-context methods))]
|
||||
[["/_doc"
|
||||
{:handler (doc-handler context)
|
||||
:allowed-methods #{:get}}]
|
||||
@@ -217,7 +216,7 @@
|
||||
{:handler (doc-handler context)
|
||||
:allowed-methods #{:get}}]])
|
||||
|
||||
(let [context (prepare-openapi-context methods)]
|
||||
(let [context (delay (prepare-openapi-context methods))]
|
||||
[["/openapi"
|
||||
{:handler (openapi-handler)
|
||||
:allowed-methods #{:get}}]
|
||||
|
||||
@@ -1,306 +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 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))))))
|
||||
@@ -13,7 +13,6 @@
|
||||
[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]
|
||||
@@ -62,6 +61,27 @@
|
||||
{: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]
|
||||
@@ -96,7 +116,7 @@
|
||||
(defn take-team-snapshot!
|
||||
[system team-id label]
|
||||
(let [conn (db/get-connection system)]
|
||||
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
|
||||
(->> (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
|
||||
@@ -108,19 +128,16 @@
|
||||
(defn restore-team-snapshot!
|
||||
[system team-id label]
|
||||
(let [conn (db/get-connection system)
|
||||
ids (->> (feat.comp-v2/get-and-lock-team-files conn team-id)
|
||||
ids (->> (get-and-lock-team-files conn team-id)
|
||||
(into #{}))
|
||||
|
||||
snap (search-file-snapshots conn ids label)
|
||||
|
||||
ids' (into #{} (map :file-id) snap)
|
||||
team (-> (feat.comp-v2/get-team conn team-id)
|
||||
(update :features disj "components/v2"))]
|
||||
ids' (into #{} (map :file-id) snap)]
|
||||
|
||||
(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))
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
[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]
|
||||
@@ -439,7 +438,7 @@
|
||||
|
||||
(binding [h/*system* system
|
||||
db/*conn* (db/get-connection system)]
|
||||
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
|
||||
(->> (h/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)
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
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]
|
||||
@@ -18,33 +17,30 @@
|
||||
|
||||
(def ^:dynamic *channel* nil)
|
||||
|
||||
(defn channel
|
||||
[]
|
||||
(sp/chan :buf 32))
|
||||
|
||||
(defn tap
|
||||
[type data]
|
||||
(when-let [channel *channel*]
|
||||
(sp/put! channel [type data])
|
||||
nil))
|
||||
([type data]
|
||||
(when-let [channel *channel*]
|
||||
(sp/put! channel [type data])
|
||||
nil))
|
||||
([channel type data]
|
||||
(when channel
|
||||
(sp/put! channel [type data])
|
||||
nil)))
|
||||
|
||||
(defn start-listener
|
||||
[on-event on-close]
|
||||
|
||||
(dm/assert!
|
||||
"expected active events channel"
|
||||
(sp/chan? *channel*))
|
||||
[channel on-event on-close]
|
||||
(assert (sp/chan? channel) "expected active events 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)))))
|
||||
@@ -55,7 +51,7 @@
|
||||
[f on-event]
|
||||
|
||||
(binding [*channel* (sp/chan :buf 32)]
|
||||
(let [listener (start-listener on-event (constantly nil))]
|
||||
(let [listener (start-listener *channel* on-event (constantly nil))]
|
||||
(try
|
||||
(f)
|
||||
(finally
|
||||
|
||||
@@ -1712,6 +1712,7 @@
|
||||
[{:fill-image
|
||||
{:id (:id fmedia)
|
||||
:name "test"
|
||||
:mtype "image/jpeg"
|
||||
:width 200
|
||||
:height 200}}]]
|
||||
|
||||
|
||||
@@ -449,6 +449,23 @@
|
||||
(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)})
|
||||
|
||||
@@ -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.16"}
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
|
||||
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
|
||||
|
||||
selmer/selmer {:mvn/version "1.12.61"}
|
||||
selmer/selmer {:mvn/version "1.12.62"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
|
||||
metosin/jsonista {:mvn/version "0.3.13"}
|
||||
metosin/malli {:mvn/version "0.17.0"}
|
||||
metosin/malli {:mvn/version "0.18.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 "0c5ed6ad033515a2df4b55addea044f60e9653d0"
|
||||
{:git/sha "f52f58cfacf62f59eab717e2637f37729d0cc383"
|
||||
: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.3"}
|
||||
thheller/shadow-cljs {:mvn/version "3.0.5"}
|
||||
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.6" :git/sha "52cf7d6"}}
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
|
||||
: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"]}
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"]}
|
||||
|
||||
:outdated
|
||||
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
||||
:main-opts ["-m" "antq.core"]}}}
|
||||
|
||||
|
||||
@@ -4,20 +4,19 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
},
|
||||
"dependencies": {
|
||||
"luxon": "^3.4.4",
|
||||
"sax": "^1.4.1"
|
||||
"luxon": "^3.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"shadow-cljs": "3.0.3",
|
||||
"shadow-cljs": "3.0.5",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
|
||||
@@ -13,17 +13,12 @@
|
||||
[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]
|
||||
@@ -37,41 +32,36 @@
|
||||
(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
|
||||
[file name]
|
||||
(let [container-id (::current-page-id file)]
|
||||
(update-in file [::unames container-id] conjs name)))
|
||||
[state name]
|
||||
(let [container-id (::current-page-id state)]
|
||||
(update-in state [::unames container-id] conjs name)))
|
||||
|
||||
(defn- commit-change
|
||||
[file change & {:keys [add-container]
|
||||
:or {add-container false}}]
|
||||
[state change & {:keys [add-container]}]
|
||||
(let [file-id (get state ::current-file-id)]
|
||||
(assert (uuid? file-id) "no current file id")
|
||||
|
||||
(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]))
|
||||
(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))))
|
||||
|
||||
(defn- commit-shape
|
||||
[file shape]
|
||||
[state shape]
|
||||
(let [parent-id
|
||||
(-> file ::parent-stack peek)
|
||||
(-> state ::parent-stack peek)
|
||||
|
||||
frame-id
|
||||
(::current-frame-id file)
|
||||
(get state ::current-frame-id)
|
||||
|
||||
page-id
|
||||
(::current-page-id file)
|
||||
(get state ::current-page-id)
|
||||
|
||||
change
|
||||
{:type :add-obj
|
||||
@@ -82,39 +72,31 @@
|
||||
:frame-id frame-id
|
||||
:page-id page-id}]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(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 file]
|
||||
(let [container-id (::current-page-id file)
|
||||
unames (dm/get-in file [:unames container-id])]
|
||||
[name state]
|
||||
(let [container-id (::current-page-id state)
|
||||
unames (dm/get-in state [:unames container-id])]
|
||||
(d/unique-name name (or unames #{}))))
|
||||
|
||||
(defn- clear-names [file]
|
||||
(dissoc file ::unames))
|
||||
|
||||
(defn- assign-name
|
||||
(defn- assign-shape-name
|
||||
"Given a tag returns its layer name"
|
||||
[data file type]
|
||||
|
||||
(cond-> data
|
||||
(nil? (:name data))
|
||||
(assoc :name (generate-name type data))
|
||||
[shape state]
|
||||
(cond-> shape
|
||||
(nil? (:name shape))
|
||||
(assoc :name (let [type (get shape :type)]
|
||||
(case type
|
||||
:frame "Board"
|
||||
(str/capital (d/name type)))))
|
||||
|
||||
:always
|
||||
(update :name unique-name file)))
|
||||
(update :name unique-name state)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMAS
|
||||
@@ -135,17 +117,32 @@
|
||||
(def decode-library-typography
|
||||
(sm/decode-fn types.typography/schema:typography sm/json-transformer))
|
||||
|
||||
(def decode-component
|
||||
(sm/decode-fn types.component/schema:component 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 schema:add-component-instance
|
||||
[:map
|
||||
[:component-id ::sm/uuid]
|
||||
[:x ::sm/safe-number]
|
||||
[:y ::sm/safe-number]])
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:frame-id {:optional true} ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(def check-add-component-instance
|
||||
(sm/check-fn schema:add-component-instance))
|
||||
(def ^:private check-add-component-instance
|
||||
(sm/check-fn schema:add-component-instance
|
||||
:hint "invalid arguments passed for add-component-instance"))
|
||||
|
||||
(def decode-add-component-instance
|
||||
(sm/decode-fn schema:add-component-instance sm/json-transformer))
|
||||
@@ -158,37 +155,77 @@
|
||||
(def decode-add-bool
|
||||
(sm/decode-fn schema:add-bool sm/json-transformer))
|
||||
|
||||
(def check-add-bool
|
||||
(def ^:private 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 lookup-shape [file shape-id]
|
||||
(-> (lookup-objects file)
|
||||
(get shape-id)))
|
||||
(defn create-state
|
||||
[]
|
||||
{})
|
||||
|
||||
(defn get-current-page
|
||||
[file]
|
||||
(let [page-id (::current-page-id file)]
|
||||
(dm/get-in file [:data :pages-index page-id])))
|
||||
[state]
|
||||
(let [file-id (get state ::current-file-id)
|
||||
page-id (get state ::current-page-id)]
|
||||
|
||||
(defn create-file
|
||||
[params]
|
||||
(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)
|
||||
(get shape-id)))
|
||||
|
||||
(defn add-file
|
||||
[state params]
|
||||
(let [params (-> params
|
||||
(assoc :features cfeat/default-features)
|
||||
(assoc :migrations fmig/available-migrations))]
|
||||
(types.file/make-file params :create-page false)))
|
||||
(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))
|
||||
|
||||
(defn add-page
|
||||
[file params]
|
||||
[state params]
|
||||
(let [page (-> (types.page/make-empty-page params)
|
||||
(types.page/check-page))
|
||||
change {:type :add-page
|
||||
:page page}]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change change)
|
||||
|
||||
;; Current page being edited
|
||||
@@ -203,96 +240,96 @@
|
||||
;; Last object id added
|
||||
(assoc ::last-id nil))))
|
||||
|
||||
(defn close-page [file]
|
||||
(-> file
|
||||
(defn close-page [state]
|
||||
(-> state
|
||||
(dissoc ::current-page-id)
|
||||
(dissoc ::parent-stack)
|
||||
(dissoc ::last-id)
|
||||
(clear-names)))
|
||||
|
||||
(defn add-artboard
|
||||
[file data]
|
||||
(defn add-board
|
||||
[state params]
|
||||
(let [{:keys [id] :as shape}
|
||||
(-> data
|
||||
(-> params
|
||||
(update :id default-uuid)
|
||||
(assoc :type :frame)
|
||||
(assign-name file :frame)
|
||||
(assign-shape-name state)
|
||||
(types.shape/setup-shape)
|
||||
(types.shape/check-shape))]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-shape shape)
|
||||
(update ::parent-stack conjv id)
|
||||
(assoc ::current-frame-id id)
|
||||
(assoc ::last-id id))))
|
||||
|
||||
(defn close-artboard
|
||||
[file]
|
||||
(let [parent-id (-> file ::parent-stack peek)
|
||||
parent (lookup-shape file parent-id)]
|
||||
(-> file
|
||||
(defn close-board
|
||||
[state]
|
||||
(let [parent-id (-> state ::parent-stack peek)
|
||||
parent (get-shape state parent-id)]
|
||||
(-> state
|
||||
(assoc ::current-frame-id (or (:frame-id parent) root-id))
|
||||
(update ::parent-stack pop))))
|
||||
|
||||
(defn add-group
|
||||
[file params]
|
||||
[state params]
|
||||
(let [{:keys [id] :as shape}
|
||||
(-> params
|
||||
(update :id default-uuid)
|
||||
(assoc :type :group)
|
||||
(assign-name file :group)
|
||||
(assign-shape-name state)
|
||||
(types.shape/setup-shape)
|
||||
(types.shape/check-shape))]
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-shape shape)
|
||||
(assoc ::last-id id)
|
||||
(update ::parent-stack conjv id))))
|
||||
|
||||
(defn close-group
|
||||
[file]
|
||||
(let [group-id (-> file :parent-stack peek)
|
||||
group (lookup-shape file group-id)
|
||||
[state]
|
||||
(let [group-id (-> state :parent-stack peek)
|
||||
group (get-shape state group-id)
|
||||
children (->> (get group :shapes)
|
||||
(into [] (keep (partial lookup-shape file)))
|
||||
(into [] (keep (partial get-shape state)))
|
||||
(not-empty))]
|
||||
|
||||
(assert (some? children) "group expect to have at least 1 children")
|
||||
|
||||
(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}]}]
|
||||
(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}]}]
|
||||
|
||||
(commit-change file change :add-container true)))]
|
||||
(update file ::parent-stack pop))))
|
||||
(commit-change state change :add-container true)))]
|
||||
(update state ::parent-stack pop))))
|
||||
|
||||
(defn add-bool
|
||||
[file params]
|
||||
[state params]
|
||||
(let [{:keys [group-id type]}
|
||||
(check-add-bool params)
|
||||
|
||||
group
|
||||
(lookup-shape file group-id)
|
||||
(get-shape state group-id)
|
||||
|
||||
children
|
||||
(->> (get group :shapes)
|
||||
@@ -300,7 +337,7 @@
|
||||
|
||||
(assert (some? children) "expect group to have at least 1 element")
|
||||
|
||||
(let [objects (lookup-objects file)
|
||||
(let [objects (get-current-objects state)
|
||||
bool (-> group
|
||||
(assoc :type :bool)
|
||||
(gsh/update-bool objects))
|
||||
@@ -317,101 +354,110 @@
|
||||
{:type :set :attr :width :val (-> bool :selrect :width) :ignore-touched true}
|
||||
{:type :set :attr :height :val (-> bool :selrect :height) :ignore-touched true}]}]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change change :add-container true)
|
||||
(assoc ::last-id group-id)))))
|
||||
|
||||
(defn add-shape
|
||||
[file params]
|
||||
[state params]
|
||||
(let [obj (-> params
|
||||
(d/update-when :svg-attrs csvg/attrs->props)
|
||||
(types.shape/setup-shape)
|
||||
(assign-name file :type))]
|
||||
(-> file
|
||||
(assign-shape-name state))]
|
||||
(-> state
|
||||
(commit-shape obj)
|
||||
(assoc ::last-id (:id obj)))))
|
||||
|
||||
(defn add-library-color
|
||||
[file color]
|
||||
[state 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}]
|
||||
(-> file
|
||||
|
||||
(-> state
|
||||
(commit-change change)
|
||||
(assoc ::last-id (:id color)))))
|
||||
|
||||
(defn add-library-typography
|
||||
[file typography]
|
||||
[state typography]
|
||||
(let [typography (-> typography
|
||||
(update :id default-uuid)
|
||||
(d/without-nils))
|
||||
change {:type :add-typography
|
||||
:id (:id typography)
|
||||
:typography typography}]
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change change)
|
||||
(assoc ::last-id (:id typography)))))
|
||||
|
||||
(defn add-component
|
||||
[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)}
|
||||
[state params]
|
||||
(let [{:keys [component-id file-id page-id frame-id name path]}
|
||||
(-> (check-add-component params)
|
||||
(update :component-id default-uuid))
|
||||
|
||||
comp-id (get change1 :id)
|
||||
|
||||
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)
|
||||
(assoc ::last-id comp-id)
|
||||
(assoc ::current-frame-id comp-id))))
|
||||
|
||||
(defn add-component-instance
|
||||
[{:keys [id data] :as file} params]
|
||||
|
||||
(let [{:keys [component-id x y]}
|
||||
(check-add-component-instance params)
|
||||
|
||||
component
|
||||
(types.components-list/get-component data component-id)
|
||||
file-id
|
||||
(or file-id (::current-file-id state))
|
||||
|
||||
page-id
|
||||
(get file ::current-page-id)]
|
||||
(or page-id (get state ::current-page-id))
|
||||
|
||||
(assert (uuid? page-id) "page-id is expected to be set")
|
||||
(assert (uuid? component) "component is expected to exist")
|
||||
frame-id
|
||||
(or frame-id (get state ::current-frame-id))
|
||||
|
||||
;; 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)
|
||||
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})
|
||||
|
||||
[shape shapes]
|
||||
(types.container/make-component-instance page component id pos)
|
||||
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}]}]
|
||||
|
||||
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)]
|
||||
(-> state
|
||||
(commit-change change1)
|
||||
(commit-change change2))))
|
||||
|
||||
(assoc file ::last-id (:id shape)))))
|
||||
|
||||
(defn add-component-instance
|
||||
[state params]
|
||||
|
||||
(let [{:keys [component-id file-id frame-id page-id]}
|
||||
(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))
|
||||
|
||||
page-id
|
||||
(or page-id (get state ::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}]}]
|
||||
|
||||
(commit-change state change)))
|
||||
|
||||
(defn delete-shape
|
||||
[file id]
|
||||
@@ -423,10 +469,12 @@
|
||||
:id id}))
|
||||
|
||||
(defn update-shape
|
||||
[file shape-id f]
|
||||
(let [page-id (::current-page-id file)
|
||||
objects (lookup-objects file)
|
||||
[state shape-id f]
|
||||
(let [page-id (get state ::current-page-id)
|
||||
|
||||
objects (get-current-objects state)
|
||||
old-shape (get objects shape-id)
|
||||
|
||||
new-shape (f old-shape)
|
||||
attrs (d/concat-set
|
||||
(keys old-shape)
|
||||
@@ -440,7 +488,7 @@
|
||||
changes
|
||||
(conj changes {:type :set :attr attr :val new-val :ignore-touched true}))))]
|
||||
|
||||
(-> file
|
||||
(-> state
|
||||
(commit-change
|
||||
{:type :mod-obj
|
||||
:operations (reduce generate-operation [] attrs)
|
||||
@@ -449,12 +497,12 @@
|
||||
(assoc ::last-id shape-id))))
|
||||
|
||||
(defn add-guide
|
||||
[file guide]
|
||||
[state guide]
|
||||
(let [guide (cond-> guide
|
||||
(nil? (:id guide))
|
||||
(assoc :id (uuid/next)))
|
||||
page-id (::current-page-id file)]
|
||||
(-> file
|
||||
(update :id default-uuid))
|
||||
page-id (::current-page-id state)]
|
||||
(-> state
|
||||
(commit-change
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
@@ -463,24 +511,54 @@
|
||||
(assoc ::last-id (:id guide)))))
|
||||
|
||||
(defn delete-guide
|
||||
[file id]
|
||||
|
||||
(let [page-id (::current-page-id file)]
|
||||
(commit-change file
|
||||
[state id]
|
||||
(let [page-id (::current-page-id state)]
|
||||
(commit-change state
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params nil})))
|
||||
|
||||
(defn update-guide
|
||||
[file guide]
|
||||
(let [page-id (::current-page-id file)]
|
||||
(commit-change file
|
||||
[state guide]
|
||||
(let [page-id (::current-page-id state)]
|
||||
(commit-change state
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:id (:id guide)
|
||||
:params guide})))
|
||||
|
||||
(defn strip-image-extension [filename]
|
||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||
(str/replace filename image-extensions-re "")))
|
||||
(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))))
|
||||
|
||||
@@ -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 :any]]
|
||||
[:value [:map-of :keyword ::sm/any]]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:ignore-geometry {:optional true} :boolean]]]
|
||||
[:set
|
||||
[:map {:title "SetOperation"}
|
||||
[:type [:= :set]]
|
||||
[:attr :keyword]
|
||||
[:val :any]
|
||||
[:val ::sm/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 :any]
|
||||
[:shapes ::sm/any]
|
||||
[:index {:optional true} [:maybe :int]]
|
||||
[:after-shape {:optional true} :any]
|
||||
[:after-shape {:optional true} ::sm/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 :any]]]
|
||||
[:shapes ::sm/any]]]
|
||||
|
||||
[:add-page
|
||||
[:map {:title "AddPageChange"}
|
||||
[:type [:= :add-page]]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:name {:optional true} :string]
|
||||
[:page {:optional true} :any]]]
|
||||
[:page {:optional true} ::sm/any]]]
|
||||
|
||||
[:mod-page
|
||||
[:map {:title "ModPageChange"}
|
||||
@@ -310,12 +310,12 @@
|
||||
[:add-media
|
||||
[:map {:title "AddMediaChange"}
|
||||
[:type [:= :add-media]]
|
||||
[:object ::ctf/media-object]]]
|
||||
[:object ctf/schema:media]]]
|
||||
|
||||
[:mod-media
|
||||
[:map {:title "ModMediaChange"}
|
||||
[:type [:= :mod-media]]
|
||||
[:object ::ctf/media-object]]]
|
||||
[:object ctf/schema:media]]]
|
||||
|
||||
[:del-media
|
||||
[:map {:title "DelMediaChange"}
|
||||
@@ -327,14 +327,14 @@
|
||||
[:type [:= :add-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
|
||||
[:path {:optional true} :string]]]
|
||||
|
||||
[:mod-component
|
||||
[:map {:title "ModCompoenentChange"}
|
||||
[:type [:= :mod-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/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 :any]]]
|
||||
[:tokens-lib ::sm/any]]]
|
||||
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
@@ -425,7 +425,12 @@
|
||||
[:type [:= :set-token]]
|
||||
[:set-name :string]
|
||||
[:token-name :string]
|
||||
[:token [:maybe ctob/schema:token-attrs]]]]]])
|
||||
[:token [:maybe ctob/schema:token-attrs]]]]
|
||||
|
||||
[:set-base-font-size
|
||||
[:map {:title "ModBaseFontSize"}
|
||||
[:type [:= :set-base-font-size]]
|
||||
[:base-font-size :string]]]]])
|
||||
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
@@ -1068,6 +1073,13 @@
|
||||
(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
|
||||
|
||||
@@ -24,18 +24,19 @@
|
||||
|
||||
;; Auxiliary functions to help create a set of changes (undo + redo)
|
||||
|
||||
(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 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]]))
|
||||
|
||||
(def check-changes!
|
||||
(sm/check-fn ::changes))
|
||||
(sm/check-fn schema:changes))
|
||||
|
||||
(defn empty-changes
|
||||
([origin page-id]
|
||||
@@ -124,28 +125,41 @@
|
||||
; TODO: remove this when not needed
|
||||
(defn- assert-page-id!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Give a page-id or call (with-page) before using this function"
|
||||
(contains? (meta changes) ::page-id)))
|
||||
(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"))
|
||||
|
||||
(defn- assert-container-id!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Give a page-id or call (with-container) before using this function"
|
||||
(assert
|
||||
(or (contains? (meta changes) ::page-id)
|
||||
(contains? (meta changes) ::component-id))))
|
||||
(contains? (meta changes) ::component-id))
|
||||
"Give a page-id or call (with-container) before using this function"))
|
||||
|
||||
(defn- assert-objects!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Call (with-objects) before using this function"
|
||||
(contains? (meta changes) ::file-data)))
|
||||
(assert
|
||||
(contains? (meta changes) ::file-data)
|
||||
"Call (with-objects) before using this function"))
|
||||
|
||||
(defn- assert-library!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Call (with-library-data) before using this function"
|
||||
(contains? (meta changes) ::library-data)))
|
||||
(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"))
|
||||
|
||||
|
||||
(defn- lookup-objects
|
||||
[changes]
|
||||
@@ -154,9 +168,9 @@
|
||||
|
||||
(defn apply-changes-local
|
||||
[changes & {:keys [apply-to-library?]}]
|
||||
(dm/assert!
|
||||
"expected valid changes"
|
||||
(check-changes! changes))
|
||||
(assert
|
||||
(check-changes! changes)
|
||||
"expected valid changes")
|
||||
|
||||
(if-let [file-data (::file-data (meta changes))]
|
||||
(let [library-data (::library-data (meta changes))
|
||||
@@ -195,6 +209,7 @@
|
||||
|
||||
(defn mod-page
|
||||
([changes options]
|
||||
(assert-page! changes)
|
||||
(let [page (::page (meta changes))]
|
||||
(mod-page changes page options)))
|
||||
|
||||
@@ -225,6 +240,7 @@
|
||||
([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
|
||||
@@ -291,6 +307,8 @@
|
||||
|
||||
(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])]
|
||||
@@ -304,8 +322,11 @@
|
||||
: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])
|
||||
@@ -324,6 +345,8 @@
|
||||
|
||||
(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))
|
||||
|
||||
@@ -345,6 +368,8 @@
|
||||
|
||||
(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])
|
||||
@@ -498,6 +523,7 @@
|
||||
: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)
|
||||
@@ -846,6 +872,7 @@
|
||||
|
||||
(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
|
||||
@@ -1135,3 +1162,16 @@
|
||||
(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))))
|
||||
@@ -4,7 +4,7 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.svg.shapes-builder
|
||||
(ns app.common.files.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.svg.path :as path]
|
||||
[app.common.types.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/parse (:d attrs))
|
||||
content (cond-> (path/from-string (:d attrs))
|
||||
(some? transform)
|
||||
(path.segm/transform-content transform))
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
|
||||
;; Rect
|
||||
(dm/export grc/rect->points)
|
||||
(dm/export grc/center->rect)
|
||||
|
||||
;;
|
||||
(dm/export gsff/fit-frame-modifiers)
|
||||
|
||||
@@ -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,7 +2055,8 @@
|
||||
(pcb/with-objects objects)
|
||||
(pcb/resize-parents new-objects-ids)
|
||||
;; Fix the order of the children inside the parent
|
||||
(pcb/reorder-children parent-id (get-in objects [parent-id :shapes])))]
|
||||
(cond-> (ctl/any-layout? objects parent-id)
|
||||
(pcb/reorder-children parent-id (get-in objects [parent-id :shapes]))))]
|
||||
(assoc changes :file-id library-id)))
|
||||
|
||||
(defn generate-detach-component
|
||||
@@ -2190,7 +2191,9 @@
|
||||
:starting-frame frame-id}]
|
||||
|
||||
(vswap! unames conj name)
|
||||
(pcb/set-flow changes flow-id new-flow)))
|
||||
(-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/set-flow flow-id new-flow))))
|
||||
|
||||
changes
|
||||
(->> shapes
|
||||
|
||||
@@ -151,7 +151,9 @@
|
||||
changes
|
||||
(reduce (fn [changes {:keys [id] :as flow}]
|
||||
(if (contains? ids-to-delete (:starting-frame flow))
|
||||
(pcb/set-flow changes id nil)
|
||||
(-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/set-flow id nil))
|
||||
changes))
|
||||
changes
|
||||
(:flows page))
|
||||
@@ -213,7 +215,9 @@
|
||||
(map :id))
|
||||
|
||||
changes (reduce (fn [changes guide-id]
|
||||
(pcb/set-flow changes guide-id nil))
|
||||
(-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/set-flow guide-id nil)))
|
||||
changes
|
||||
guides-to-delete)
|
||||
|
||||
|
||||
@@ -60,6 +60,17 @@
|
||||
(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)
|
||||
|
||||
@@ -9,21 +9,6 @@
|
||||
[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)
|
||||
@@ -46,20 +31,56 @@
|
||||
(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)
|
||||
new-path-map (into {}
|
||||
(map (fn [shape] {(generate-path "" objects (:id new-shape) shape) shape}))
|
||||
(cfh/get-children-with-self objects (:id new-shape)))
|
||||
(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)
|
||||
|
||||
orig-touched (filter (comp seq :touched) original-shapes)
|
||||
orig-objects (into {} (map (juxt :id identity) original-shapes))
|
||||
container (ctn/make-container page :page)]
|
||||
container (ctn/make-container page :page)]
|
||||
(reduce
|
||||
(fn [changes touched-shape]
|
||||
(let [path (generate-path "" orig-objects (:id original-shape) touched-shape)
|
||||
related-shape (get new-path-map path)
|
||||
(let [related-shape (get new-shapes-map (:shape-path touched-shape))
|
||||
orig-ref-shape (ctf/find-ref-shape nil container libraries touched-shape)]
|
||||
(if related-shape
|
||||
(cll/update-attrs-on-switch
|
||||
|
||||
@@ -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,38 +48,28 @@
|
||||
(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"
|
||||
"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"
|
||||
nil))
|
||||
|
||||
(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 strip-image-extension
|
||||
[filename]
|
||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||
(str/replace filename image-extensions-re "")))
|
||||
|
||||
(defn parse-font-weight
|
||||
[variant]
|
||||
|
||||
@@ -211,8 +211,7 @@
|
||||
|
||||
(defn lazy-validator
|
||||
[s]
|
||||
(let [s (schema s)
|
||||
vfn (delay (validator s))]
|
||||
(let [vfn (delay (validator s))]
|
||||
(fn [v] (@vfn v))))
|
||||
|
||||
(defn lazy-explainer
|
||||
@@ -998,6 +997,8 @@
|
||||
{:title "agent"
|
||||
:description "instance of clojure agent"}}))
|
||||
|
||||
(register! ::any (mu/update-properties :any assoc :gen/gen sg/any))
|
||||
|
||||
;; ---- PREDICATES
|
||||
|
||||
(def valid-safe-number?
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(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]))
|
||||
@@ -90,7 +91,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 ", and " children) (-titled s)))
|
||||
(defmethod visit :and [_ s children _] (str (str/join " && " 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) ")"))
|
||||
@@ -106,7 +107,8 @@
|
||||
(defmethod visit :qualified-symbol [_ _ _ _] "qualified symbol")
|
||||
(defmethod visit :uuid [_ _ _ _] "uuid")
|
||||
(defmethod visit :boolean [_ _ _ _] "boolean")
|
||||
(defmethod visit :keyword [_ _ _ _] "keyword")
|
||||
(defmethod visit :keyword [_ _ _ _] "string")
|
||||
(defmethod visit :fn [_ _ _ _] "FN")
|
||||
|
||||
(defmethod visit :vector [_ _ children _]
|
||||
(str "[" (last children) "]"))
|
||||
@@ -123,10 +125,12 @@
|
||||
(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 _]
|
||||
(str "set[" (first children) "]" (minmax-suffix schema)))
|
||||
|
||||
(defmethod visit ::m/val [_ schema children _]
|
||||
(let [suffix (minmax-suffix schema)]
|
||||
(cond-> (first children)
|
||||
@@ -152,7 +156,6 @@
|
||||
(or (:title props)
|
||||
"*")))
|
||||
|
||||
|
||||
(defmethod visit :map
|
||||
[_ schema children {:keys [::level ::max-level] :as options}]
|
||||
(let [props (m/properties schema)
|
||||
@@ -172,13 +175,11 @@
|
||||
": " s)))
|
||||
(str/join ",\n"))
|
||||
|
||||
header (cond-> (if (zero? level)
|
||||
(str "type " title)
|
||||
(str title))
|
||||
header (cond-> (str "type " title)
|
||||
closed? (str "!")
|
||||
(some? title) (str " "))]
|
||||
|
||||
(str header "{\n" entries "\n" (pad "}" level))))))
|
||||
(str (pad header level) "{\n" entries "\n" (pad "}\n" level))))))
|
||||
|
||||
(defmethod visit :multi
|
||||
[_ s children {:keys [::level ::max-level] :as options}]
|
||||
@@ -205,18 +206,18 @@
|
||||
|
||||
(defmethod visit :merge
|
||||
[_ schema children _]
|
||||
(let [entries (str/join " , " children)
|
||||
(let [entries (str/join ",\n" children)
|
||||
props (m/properties schema)
|
||||
title (or (some-> (:title props) str/camel str/capital)
|
||||
"<untitled>")]
|
||||
(str "merge object " title " { " entries " }")))
|
||||
(str "merge type " title " { \n" entries "\n}\n")))
|
||||
|
||||
(defmethod visit :app.common.schema/one-of
|
||||
[_ _ children _]
|
||||
(defmethod visit ::sm/one-of
|
||||
[_ _ children _]
|
||||
(let [elems (last children)]
|
||||
(str "OneOf[" (->> elems
|
||||
(map d/name)
|
||||
(str/join ",")) "]")))
|
||||
(str "string oneOf (" (->> elems
|
||||
(map d/name)
|
||||
(str/join "|")) ")")))
|
||||
|
||||
(defmethod visit :schema [_ schema children options]
|
||||
(visit ::m/schema schema children options))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema.generators
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector])
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double])
|
||||
#?(:cljs (:require-macros [app.common.schema.generators]))
|
||||
(:require
|
||||
[app.common.schema.registry :as sr]
|
||||
@@ -38,10 +38,6 @@
|
||||
([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}))
|
||||
@@ -61,7 +57,7 @@
|
||||
(defn word-keyword
|
||||
[]
|
||||
(->> (word-string)
|
||||
(tg/fmap keyword)))
|
||||
(tg/fmap c/keyword)))
|
||||
|
||||
(defn email
|
||||
[]
|
||||
@@ -100,12 +96,11 @@
|
||||
(c/map second))
|
||||
(c/map list bools elements)))))))
|
||||
|
||||
(def any tg/any)
|
||||
(def boolean tg/boolean)
|
||||
|
||||
(defn set
|
||||
[g]
|
||||
(tg/set g))
|
||||
(defn map-of
|
||||
([kg vg]
|
||||
(tg/map kg vg {:min-elements 1 :max-elements 3}))
|
||||
([kg vg opts]
|
||||
(tg/map kg vg opts)))
|
||||
|
||||
(defn elements
|
||||
[s]
|
||||
@@ -119,6 +114,10 @@
|
||||
[f g]
|
||||
(tg/fmap f g))
|
||||
|
||||
(defn filter
|
||||
[pred gen]
|
||||
(tg/such-that pred gen 100))
|
||||
|
||||
(defn mcat
|
||||
[f g]
|
||||
(tg/bind g f))
|
||||
@@ -130,3 +129,18 @@
|
||||
(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]))
|
||||
|
||||
@@ -97,7 +97,8 @@
|
||||
(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 (first (m/children schema options))})
|
||||
(defmethod visit :re [_ schema _ options]
|
||||
{:type "string", :pattern (str (first (m/children schema options)))})
|
||||
(defmethod visit :nil [_ _ _ _] {:type "null"})
|
||||
|
||||
(defmethod visit :string [_ schema _ _]
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
(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]
|
||||
@@ -15,15 +13,7 @@
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str])
|
||||
#?(:clj
|
||||
(:import
|
||||
clojure.lang.XMLHandler
|
||||
java.io.InputStream
|
||||
javax.xml.XMLConstants
|
||||
javax.xml.parsers.SAXParserFactory
|
||||
org.apache.commons.io.IOUtils)))
|
||||
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; Regex for XML ids per Spec
|
||||
;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn
|
||||
@@ -1030,24 +1020,3 @@
|
||||
: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)))))
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
(.. r (toString 16) (padStart 2 "0"))
|
||||
(.. g (toString 16) (padStart 2 "0"))
|
||||
(.. b (toString 16) (padStart 2 "0"))))))
|
||||
sg/any))
|
||||
sg/int))
|
||||
|
||||
(defn rgb-color-string?
|
||||
[o]
|
||||
@@ -54,13 +54,13 @@
|
||||
::oapi/type "integer"
|
||||
::oapi/format "int64"}}))
|
||||
|
||||
(def schema:image-color
|
||||
(def schema:image
|
||||
[:map {:title "ImageColor"}
|
||||
[:name {:optional true} :string]
|
||||
[:width ::sm/int]
|
||||
[:height ::sm/int]
|
||||
[:mtype {:optional true} [:maybe :string]]
|
||||
[:mtype ::sm/text]
|
||||
[: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-color]]
|
||||
[:image {:optional true} [:maybe schema:image]]
|
||||
[: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-color]]]
|
||||
[:image {:optional true} [:maybe schema:image]]]
|
||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||
|
||||
;; Same as color but with :id prop required
|
||||
@@ -115,9 +115,10 @@
|
||||
(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-color)
|
||||
(sm/register! ::image-color schema:image)
|
||||
(sm/register! ::recent-color schema:recent-color)
|
||||
(sm/register! ::color-attrs schema:color-attrs)
|
||||
|
||||
|
||||
@@ -32,24 +32,31 @@
|
||||
[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 "FileMediaObject"}
|
||||
[:map {:title "FileMedia"}
|
||||
[:id ::sm/uuid]
|
||||
[:created-at ::sm/inst]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:deleted-at {:optional true} ::sm/inst]
|
||||
[:name :string]
|
||||
[:width ::sm/safe-int]
|
||||
[:height ::sm/safe-int]
|
||||
[:mtype :string]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:media-id ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:thumbnail-id {:optional true} ::sm/uuid]
|
||||
[:is-local :boolean]])
|
||||
[:is-local {:optional true} :boolean]])
|
||||
|
||||
(def schema:colors
|
||||
[:map-of {:gen/max 5} ::sm/uuid ::ctc/color])
|
||||
@@ -65,7 +72,8 @@
|
||||
|
||||
(def schema:options
|
||||
[:map {:title "FileOptions"}
|
||||
[:components-v2 {:optional true} ::sm/boolean]])
|
||||
[:components-v2 {:optional true} ::sm/boolean]
|
||||
[:base-font-size {:optional true} :string]])
|
||||
|
||||
(def schema:data
|
||||
[:map {:title "FileData"}
|
||||
@@ -102,7 +110,6 @@
|
||||
(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"))
|
||||
@@ -110,7 +117,7 @@
|
||||
(def check-file-data
|
||||
(sm/check-fn schema:data))
|
||||
|
||||
(def check-media-object
|
||||
(def check-file-media
|
||||
(sm/check-fn schema:media))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -134,7 +141,8 @@
|
||||
(ctpl/add-page page)
|
||||
|
||||
:always
|
||||
(update :options assoc :components-v2 true)))))
|
||||
(update :options merge {:components-v2 true
|
||||
:base-font-size BASE-FONT-SIZE})))))
|
||||
|
||||
(defn make-file
|
||||
[{:keys [id project-id name revn is-shared features migrations
|
||||
@@ -1029,3 +1037,14 @@
|
||||
|
||||
(-> 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))
|
||||
@@ -36,6 +36,10 @@
|
||||
[data]
|
||||
(impl/from-bytes data))
|
||||
|
||||
(defn from-string
|
||||
[data]
|
||||
(impl/from-string data))
|
||||
|
||||
(defn check-path-content
|
||||
[content]
|
||||
(impl/check-content-like content))
|
||||
|
||||
@@ -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]
|
||||
[position start end h1 h2 precision]
|
||||
(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)) path-closest-point-accuracy)
|
||||
(if (<= (mth/abs (- t1 t2)) precision)
|
||||
(-> (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
|
||||
"Point on line"
|
||||
"Finds the closest point in the line segment defined by from-p and to-p"
|
||||
[position from-p to-p]
|
||||
|
||||
(let [e1 (gpt/to-vec from-p to-p)
|
||||
@@ -235,52 +235,13 @@
|
||||
from-p
|
||||
to-p))))
|
||||
|
||||
;; 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)
|
||||
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
|
||||
[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]
|
||||
|
||||
"Returns the closest point in the path to the position, at a given precision"
|
||||
[content position precision]
|
||||
(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])
|
||||
@@ -291,7 +252,7 @@
|
||||
(line-closest-point position from-p to-p)
|
||||
|
||||
:curve-to
|
||||
(curve-closest-point position from-p to-p h1 h2)
|
||||
(curve-closest-point position from-p to-p h1 h2 precision)
|
||||
|
||||
nil)]
|
||||
(when point
|
||||
|
||||
@@ -761,3 +761,5 @@
|
||||
(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)
|
||||
@@ -16,54 +16,56 @@
|
||||
|
||||
(def node-types #{"root" "paragraph-set" "paragraph"})
|
||||
|
||||
(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]]]]]]]]]]]]])
|
||||
(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! ::content schema:content)
|
||||
|
||||
(def valid-content?
|
||||
(sm/lazy-validator schema:content))
|
||||
|
||||
(sm/register!
|
||||
^{::sm/type ::position-data}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(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]
|
||||
@@ -118,7 +119,7 @@
|
||||
[:map {:title "Token"}
|
||||
[:name cto/token-name-ref]
|
||||
[:type [::sm/one-of cto/token-types]]
|
||||
[:value :any]
|
||||
[:value ::sm/any]
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::sm/inst]])
|
||||
|
||||
@@ -389,7 +390,8 @@
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:tokens {:optional true
|
||||
:gen/gen (->> (sg/generator [:map-of ::sm/text schema:token])
|
||||
:gen/gen (->> (sg/map-of (sg/generator ::sm/text)
|
||||
(sg/generator schema:token))
|
||||
(sg/fmap #(into (d/ordered-map) %)))}
|
||||
[:and
|
||||
[:map-of {:gen/max 5
|
||||
@@ -660,63 +662,6 @@
|
||||
(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.
|
||||
@@ -826,51 +771,10 @@
|
||||
(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")
|
||||
@@ -887,12 +791,11 @@ 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.
|
||||
@@ -909,6 +812,9 @@ 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]
|
||||
@@ -1283,142 +1189,6 @@ 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]
|
||||
@@ -1480,17 +1250,13 @@ Will return a value that matches this schema:
|
||||
[tokens-lib]
|
||||
(or tokens-lib (make-tokens-lib)))
|
||||
|
||||
(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}})
|
||||
(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 duplicate-set [set-name lib & {:keys [suffix]}]
|
||||
(let [sets (get-sets lib)
|
||||
@@ -1500,7 +1266,335 @@ Will return a value that matches this schema:
|
||||
(assoc :name copy-name)
|
||||
(assoc :modified-at (dt/now)))))
|
||||
|
||||
(sm/register! type:tokens-lib)
|
||||
;; === 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))))
|
||||
|
||||
;; === Serialization handlers for RPC API (transit) and database (fressian)
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
;; The root shape of the main instance of a variant component.
|
||||
[:map
|
||||
[:variant-id {:optional true} ::sm/uuid]
|
||||
[:variant-name {:optional true} :string]])
|
||||
[:variant-name {:optional true} :string]
|
||||
[:variant-error {:optional true} :string]])
|
||||
|
||||
(def schema:variant-container
|
||||
;; is a board that contains all variant components of a variant set,
|
||||
@@ -106,8 +107,8 @@
|
||||
(add-new-props assigned remaining))))
|
||||
|
||||
|
||||
(defn properties-map-to-string
|
||||
"Transforms a map of properties to a string of properties omitting the empty ones"
|
||||
(defn properties-map->formula
|
||||
"Transforms a map of properties to a formula of properties omitting the empty ones"
|
||||
[properties]
|
||||
(->> properties
|
||||
(keep (fn [{:keys [name value]}]
|
||||
@@ -116,21 +117,24 @@
|
||||
(str/join ", ")))
|
||||
|
||||
|
||||
(defn properties-string-to-map
|
||||
"Transforms a string of properties to a map of properties"
|
||||
(defn properties-formula->map
|
||||
"Transforms a formula of properties to a map of properties"
|
||||
[s]
|
||||
(->> (str/split s ",")
|
||||
(mapv #(str/split % "="))
|
||||
(mapv #(str/split % "=" 2))
|
||||
(filter (fn [[_ v]] (not (str/blank? v))))
|
||||
(mapv (fn [[k v]]
|
||||
{:name (str/trim k)
|
||||
:value (str/trim v)}))))
|
||||
|
||||
|
||||
(defn valid-properties-string?
|
||||
"Checks if a string of properties has a processable format or not"
|
||||
(defn valid-properties-formula?
|
||||
"Checks if a formula is valid"
|
||||
[s]
|
||||
(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)))))
|
||||
(->> (str/split s ",")
|
||||
(mapv #(str/split % "=" 2))
|
||||
(every? #(and (= 2 (count %))
|
||||
(not (str/blank? (first %)))))))
|
||||
|
||||
|
||||
(defn find-properties-to-remove
|
||||
|
||||
@@ -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.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"))))
|
||||
26
common/test/common_tests/media_test.cljc
Normal file
26
common/test/common_tests/media_test.cljc
Normal 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.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"))))
|
||||
@@ -9,7 +9,6 @@
|
||||
[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]
|
||||
@@ -29,6 +28,7 @@
|
||||
[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,7 +58,6 @@
|
||||
(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
|
||||
@@ -78,6 +77,7 @@
|
||||
'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.modifiers-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.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.tokens-lib-test
|
||||
'common-tests.uuid-test))
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"color": {
|
||||
"red": {
|
||||
"100": {
|
||||
"$value": "red",
|
||||
"$type": "color",
|
||||
"$description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"color": {
|
||||
"red": {
|
||||
"100": {
|
||||
"value": "red",
|
||||
"type": "color",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@
|
||||
(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]
|
||||
@@ -1387,47 +1388,28 @@
|
||||
(t/is (nil? token-theme'))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest legacy-json-decoding
|
||||
(let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-legacy-example.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"
|
||||
(t/is (= (:group token-theme) "group-1"))
|
||||
(t/is (= (:name token-theme) "theme-1"))
|
||||
(t/is (= (:sets token-theme) #{"light"})))
|
||||
(t/testing "tokens exist in core set"
|
||||
(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? (get-set-token "typography" "H1.Bold")))))))
|
||||
(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 dtcg-encoding-decoding-json
|
||||
(let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-example.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)))
|
||||
(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
|
||||
(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 "")
|
||||
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"
|
||||
@@ -1435,32 +1417,59 @@
|
||||
(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? (get-set-token "core" "colors.red.600")
|
||||
(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? (get-set-token "core" "spacing.multi-value")
|
||||
(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? (get-set-token "theme" "button.primary.background")
|
||||
(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/testing "invalid tokens got discarded"
|
||||
(t/is (nil? (get-set-token "typography" "H1.Bold")))))))
|
||||
(t/is (nil? (ctob/get-token-in-set lib "typography" "H1.Bold")))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest decode-dtcg-json-default-team
|
||||
(t/deftest parse-multi-set-dtcg-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 "")
|
||||
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"
|
||||
(t/is (= (:group token-theme) "group-1"))
|
||||
(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/testing "invalid tokens got discarded"
|
||||
(t/is (nil? (ctob/get-token-in-set lib "typography" "H1.Bold")))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest parse-multi-set-dtcg-json-default-team
|
||||
(let [json (-> (slurp "test/common_tests/types/data/tokens-default-team-only.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)))
|
||||
(json/decode {:key-fn identity}))
|
||||
lib (ctob/parse-decoded-json json "")
|
||||
themes (ctob/get-themes lib)
|
||||
first-theme (first themes)]
|
||||
(t/is (= '("dark") (ctob/get-ordered-set-names lib)))
|
||||
@@ -1469,15 +1478,14 @@
|
||||
(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? (get-set-token "dark" "small")
|
||||
(t/is (tht/token-data-eq? (ctob/get-token-in-set lib "dark" "small")
|
||||
{:name "small"
|
||||
:value "8"
|
||||
:type :border-radius
|
||||
:description ""}))))))
|
||||
|
||||
|
||||
#?(:clj
|
||||
(t/deftest encode-dtcg-json
|
||||
(t/deftest export-dtcg-json
|
||||
(let [now (dt/now)
|
||||
tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "core"
|
||||
@@ -1502,7 +1510,7 @@
|
||||
:id "test-id-00"
|
||||
:modified-at now
|
||||
:sets #{"core"})))
|
||||
result (ctob/encode-dtcg tokens-lib)
|
||||
result (ctob/export-dtcg-json tokens-lib)
|
||||
expected {"$themes" [{"description" ""
|
||||
"group" "group-1"
|
||||
"is-source" false
|
||||
@@ -1528,7 +1536,7 @@
|
||||
(t/is (= expected result)))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest encode-decode-dtcg-json
|
||||
(t/deftest export-parse-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"
|
||||
@@ -1549,17 +1557,14 @@
|
||||
:type :color
|
||||
:value "{accent.default}"})})))
|
||||
|
||||
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)]
|
||||
encoded (ctob/export-dtcg-json tokens-lib)
|
||||
tokens-lib' (ctob/parse-decoded-json encoded "")]
|
||||
(t/testing "library got updated but data is equal"
|
||||
(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))))))
|
||||
(t/is (not= tokens-lib' tokens-lib))
|
||||
(t/is (= @tokens-lib' @tokens-lib)))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest encode-default-theme-json
|
||||
(t/deftest export-dtcg-json-with-default-theme
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "core"
|
||||
:tokens {"colors.red.600"
|
||||
@@ -1578,7 +1583,7 @@
|
||||
{:name "button.primary.background"
|
||||
:type :color
|
||||
:value "{accent.default}"})})))
|
||||
result (ctob/encode-dtcg tokens-lib)
|
||||
result (ctob/export-dtcg-json tokens-lib)
|
||||
expected {"$themes" []
|
||||
"$metadata" {"tokenSetOrder" ["core"]
|
||||
"activeSets" #{}, "activeThemes" #{}}
|
||||
@@ -1599,7 +1604,7 @@
|
||||
(t/is (= expected result)))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest encode-dtcg-json-with-active-theme-and-set
|
||||
(t/deftest export-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"
|
||||
@@ -1625,7 +1630,7 @@
|
||||
:modified-at now
|
||||
:sets #{"core"}))
|
||||
(ctob/toggle-theme-active? "group-1" "theme-1"))
|
||||
result (ctob/encode-dtcg tokens-lib)
|
||||
result (ctob/export-dtcg-json tokens-lib)
|
||||
expected {"$themes" [{"description" ""
|
||||
"group" "group-1"
|
||||
"is-source" false
|
||||
|
||||
@@ -9,32 +9,52 @@
|
||||
[app.common.types.variant :as ctv]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest convert-between-variant-properties-maps-and-strings
|
||||
(t/deftest convert-between-variant-properties-maps-and-formulas
|
||||
(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-spaces [{:name "border 1" :value "of course"} {:name "color 2" :value "dark gray"}]
|
||||
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"}]
|
||||
|
||||
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"
|
||||
string-invalid "border=yes, color="]
|
||||
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"]
|
||||
|
||||
(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 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 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 "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 "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/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/deftest find-properties
|
||||
|
||||
4
common/vendor/beicon/impl/rxjs.cljs
vendored
4
common/vendor/beicon/impl/rxjs.cljs
vendored
@@ -1,4 +0,0 @@
|
||||
(ns beicon.impl.rxjs
|
||||
(:require ["rxjs" :as rx]))
|
||||
|
||||
(goog/exportSymbol "rxjsMain" rx)
|
||||
@@ -1,4 +0,0 @@
|
||||
(ns beicon.impl.rxjs-operators
|
||||
(:require ["rxjs/operators" :as rxop]))
|
||||
|
||||
(goog/exportSymbol "rxjsOperators" rxop)
|
||||
4
common/vendor/tubax/saxjs.cljs
vendored
4
common/vendor/tubax/saxjs.cljs
vendored
@@ -1,4 +0,0 @@
|
||||
(ns tubax.saxjs
|
||||
(:require ["sax" :as sax]))
|
||||
|
||||
(goog/exportSymbol "sax" sax)
|
||||
996
common/yarn.lock
996
common/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:24.04
|
||||
LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG='en_US.UTF-8' \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:24.04
|
||||
LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
@@ -32,8 +32,8 @@ RUN set -ex; \
|
||||
netpbm \
|
||||
poppler-utils \
|
||||
potrace \
|
||||
gconf-service \
|
||||
libasound2 \
|
||||
dconf-service \
|
||||
libasound2t64 \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libatomic1 \
|
||||
@@ -43,7 +43,6 @@ RUN set -ex; \
|
||||
libexpat1 \
|
||||
libfontconfig1 \
|
||||
libgcc1 \
|
||||
libgconf-2-4 \
|
||||
libgdk-pixbuf2.0-0 \
|
||||
libglib2.0-0 \
|
||||
libgtk-3-0 \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM nginxinc/nginx-unprivileged:1.27.1
|
||||
FROM nginxinc/nginx-unprivileged:1.28.0
|
||||
LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
USER root
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<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">
|
||||
@@ -15,7 +14,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,
|
||||
desc=desc or metadata.desc or description or metadata.description,
|
||||
url="https://help.penpot.app" + page.url,
|
||||
img="https://help.penpot.app/img/th-help-center.jpg",
|
||||
img_alt=alt,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
@@ -117,7 +118,6 @@ 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.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Backend app
|
||||
desc: Dive into self-hosting, configuration, developer insights (architecture, data model), integration, and troubleshooting. See Penpot's Technical Guide.
|
||||
---
|
||||
|
||||
# Backend app
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Exporter app
|
||||
desc: Learn about self-hosting, configuration, architecture (backend, frontend), data model, and development environment. See Penpot's technical guide.
|
||||
---
|
||||
|
||||
# Exporter app
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: 3.04. Common Guide
|
||||
desc: "View Penpot's technical guide: self-hosting, configuration, developer insights, architecture, data model, integration, and troubleshooting."
|
||||
---
|
||||
|
||||
# Common guide
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
@@ -78,24 +79,36 @@ 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 the **window 0** we have the gulp process responsible
|
||||
of watching and building styles, fonts, icon-spreads and templates.
|
||||
**window 1**. On **window 0** we have the gulp process responsible
|
||||
for watching and building styles, fonts, icon-spreads and templates.
|
||||
|
||||
On the **window 1** we can found the **shadow-cljs** process that is
|
||||
responsible on watch and build frontend clojurescript code.
|
||||
On **window 1** we can find the **shadow-cljs** process that is
|
||||
responsible for watching and building frontend clojurescript code.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
@@ -12,4 +13,3 @@ 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.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
|
||||
@@ -35,7 +36,6 @@ 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,7 +68,6 @@ 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
|
||||
@@ -79,7 +78,6 @@ 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
|
||||
@@ -115,7 +113,6 @@ 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})]
|
||||
@@ -130,7 +127,6 @@ 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.
|
||||
@@ -155,11 +151,10 @@ 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"}])
|
||||
(when icon [:> icon* {:icon-id icon :size "m" :class (stl/css :icon)}])
|
||||
[:span {:class (stl/css :label-wrapper)} children]]))
|
||||
|
||||
;; later in code
|
||||
@@ -204,6 +199,167 @@ 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 content’s 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 direction‐relative 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, we’re 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
|
||||
|
||||
@@ -242,7 +398,7 @@ Please refer to the [Rumext User Guide](https://funcool.github.io/rumext/latest/
|
||||
|
||||
Some things to have in mind:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -260,7 +416,6 @@ 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])
|
||||
```
|
||||
@@ -269,7 +424,6 @@ 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})]
|
||||
@@ -294,7 +448,6 @@ 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]
|
||||
;; ...
|
||||
)
|
||||
@@ -326,7 +479,6 @@ 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"])
|
||||
|
||||
@@ -340,7 +492,6 @@ 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 []
|
||||
;; ...
|
||||
@@ -356,7 +507,6 @@ 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"])
|
||||
```
|
||||
@@ -423,7 +573,6 @@ 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
|
||||
@@ -436,7 +585,6 @@ 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")
|
||||
@@ -453,7 +601,6 @@ 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 [
|
||||
@@ -515,40 +662,8 @@ We use three **levels of tokens**:
|
||||
|
||||
### Implementing variants
|
||||
|
||||
We can leverage component tokens to easily implement variants, by overriding their values in each component variant.
|
||||
We can leverage component tokens to easily implement variants as explained [here](/technical-guide/developer/ui/#use-custom-properties-within-components).
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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
Reference in New Issue
Block a user