Compare commits

..

193 Commits

Author SHA1 Message Date
Eva Marco
3dfccdaf9b ♻️ Put token settings under config flag (#6551) 2025-05-26 12:57:32 +02:00
Marina López
e5bc369e56 Visual indicators subscription for teams and project settings (#6546)
*  Visual indicators subscription for teams and project settings

* 📎 Fixes PR feedback

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-05-26 12:56:40 +02:00
Andrés Moya
5e8929e504 🔧 Refactor token json file import/export 2025-05-26 12:02:26 +02:00
Alejandro Alonso
3ee3ee2059 Merge pull request #6553 from penpot/alotor-bug-fix-grid-editor-problem
🐛 Fix problem with grid editing
2025-05-26 11:40:24 +02:00
alonso.torres
9eacde567d 🐛 Fix problem with grid editing 2025-05-26 11:20:09 +02:00
Andrey Antukh
9638fd274f Merge pull request #6547 from penpot/eva-remove-deprecated-props
♻️ Update docs and remove deprecated props
2025-05-24 11:18:53 +02:00
Eva Marco
7c072abe28 📚 Update docs without props obj 2025-05-23 12:57:24 +02:00
Eva Marco
603e41bbfd ♻️ Remove mf/props obj from DS components 2025-05-23 12:57:02 +02:00
Pablo Alba
b561ad033c 🐛 Fix restore component when its original parent with layout is deleted 2025-05-23 12:11:35 +02:00
luisδμ
7373056037 Improve formula validating and parsing (#6527) 2025-05-23 12:08:50 +02:00
luisδμ
a9173f672d 🐛 Sanitize inputs for variant property names and values (#6532) 2025-05-23 12:08:39 +02:00
luisδμ
44829ff1ae Use different copies for different variant selection cases (#6544)
*  Use different copies for different variant selection cases

* 📎 PR changes
2025-05-23 12:08:24 +02:00
Andrey Antukh
927ee9e55e Add get-owned-teams rpc method 2025-05-23 11:20:35 +02:00
Xavier Julian
066b252522 Add composition and slots documentation to storybook 2025-05-23 10:12:20 +02:00
luisδμ
556a68a78f Select all variants with errors (#6533) 2025-05-23 09:21:57 +02:00
Belén Albeza
f9bbf2d524 Improve paths deserialization (wasm) (#6501)
* ♻️ Refactor path wasm code to its own wasm submodule

* ♻️ Use unified enum for RawSegmentData and transmute to deserialize

* ♻️ Move set_shape_path_attrs to wasm::paths module

* 💄 Unify repr declarations
2025-05-23 08:48:55 +02:00
alonso.torres
eaaca5629e 🐛 Fix problem with sidebar layout 2025-05-22 18:35:08 +02:00
Andrey Antukh
0df2a12814 Merge remote-tracking branch 'origin/staging' into develop 2025-05-22 13:34:46 +02:00
Andrey Antukh
df27db1996 Merge pull request #6531 from mdbenito/fix/choppy-closest-point
 Fix choppy behaviour of new node on path
2025-05-22 13:24:04 +02:00
Miguel de Benito Delgado
7fc0d15418 🐛 Fix cursor overlap query (#6530)
* 🐛 Fix cursor overlap query. Closes #4472

* 📎 Update CHANGES.md

---------

Signed-off-by: Miguel de Benito Delgado <m.debenito.d@gmail.com>
2025-05-22 13:22:52 +02:00
Eva Marco
99fb905070 🐛 Fix at icon (#6540) 2025-05-22 13:15:09 +02:00
Alejandro Alonso
413fc6de16 Merge pull request #6536 from penpot/niwinz-update-promesa
⬆️ Update dependencies
2025-05-22 12:49:00 +02:00
Aitor Moreno
d54a7d0401 Merge pull request #6526 from penpot/superalex-improve-zoom-performance-and-behaviour
🐛 Fix zoom performance and behaviour
2025-05-22 12:15:38 +02:00
Alejandro Alonso
ed53793d9d 🐛 Fix render shapes in multiple tiles with high dprs (#6538) 2025-05-22 12:10:51 +02:00
María Valderrama
faa68784af 💄 Add styles for external widgets on workspace (#6509)
* 💄 Add styles for Inkeep Chat at workspace

* 📎 Styles review
2025-05-22 11:56:45 +02:00
Aitor Moreno
58b1cf6b0c Merge pull request #6491 from penpot/alotor-perf-pixel-precision
 Pixel precision for new renderer
2025-05-22 11:37:11 +02:00
Andrey Antukh
f9c9e865b5 🐛 Remove unexpected modified-at on binfile-v3 import 2025-05-22 10:53:23 +02:00
Andrey Antukh
ebe321d9d3 ⬆️ Update dependencies on exporter 2025-05-22 10:53:23 +02:00
Andrey Antukh
0683fbd17c ⬆️ Update backend dependencies 2025-05-22 10:53:23 +02:00
Andrey Antukh
09a7ef3e45 ⬆️ Upgrade frontend dependendencies 2025-05-22 10:53:23 +02:00
Andrey Antukh
172b70d8a7 ⬆️ Upgrade promesa library to latest version
That fixes a workaround introduced in previous commits
2025-05-22 10:53:23 +02:00
Alejandro Alonso
3597e5bb54 🐛 Fix zoom performance and behaviour 2025-05-22 10:29:43 +02:00
Eva Marco
949b6d1205 🎉 Add missing translation (#6534) 2025-05-22 10:24:41 +02:00
Eva Marco
c0af77faf7 📚 Add css rules to the UI guide (#6521)
* 📚 Add css rules to UI guide

* 🐛 Solve comments on PR

* 🐛 Add missing class

* 🐛 Improve css modules improvement

* 🐛 Fix width

* 🐛 Fix note
2025-05-22 10:06:03 +02:00
Miguel de Benito Delgado
8f7a674000 🔥 Remove unused fn types.path.segment.path-closest-point 2025-05-21 21:32:18 +02:00
Miguel de Benito Delgado
e4f2dfaa11 Make precision closest point computation depend on zoom 2025-05-21 21:29:40 +02:00
Andrey Antukh
ec29c4f5fe Merge pull request #6528 from penpot/ladybenko-11076-fix-xywh-inputs
🐛 Fix misalignment in measure section (design tab)
2025-05-21 21:05:38 +02:00
Elena Torró
c21f5221bb Merge pull request #6453 from penpot/elenatorro-10900-add-text-fills
🎉 Add text fills
2025-05-21 18:46:04 +02:00
Elena Torro
42ef2f929a 🎉 Add text fills 2025-05-21 18:32:50 +02:00
Belén Albeza
2b21401368 🐛 Fix clip buttons size 2025-05-21 17:08:56 +02:00
Belén Albeza
a5c8063b2c 🐛 Fix presets menu alignment 2025-05-21 17:01:23 +02:00
Belén Albeza
2ad2af2aea 🐛 Fix measures inputs' alignment 2025-05-21 16:58:49 +02:00
Eva Marco
c2ce7c6cf6 🐛 Remove unnecesary icon (#6524) 2025-05-21 15:44:25 +02:00
María Valderrama
47490db4be 💄 Add styles for external widgets on workspace (#6509)
* 💄 Add styles for Inkeep Chat at workspace

* 📎 Styles review
2025-05-21 14:17:48 +02:00
andrés gonzález
a2ac2bc6c6 Change copy as SVG menu order (#6523) 2025-05-21 13:12:33 +02:00
Elena Torró
e80ca7e332 Merge pull request #6439 from penpot/elenatorro-11035-fix-overflow-x-scroll-on-sidebar
🐛 Fix default scroll visibility on layers sidebar
2025-05-21 11:51:32 +02:00
Elena Torro
e4644ff506 🔧 Use scroll only on layers and refactor layer element name 2025-05-21 11:36:24 +02:00
Andrey Antukh
662b926b4b 🌐 Rehash all translations 2025-05-21 11:20:36 +02:00
Miguel de Benito Delgado
6319ed78f9 🌐 Add missing translation strings for error messages (#6519)
* 🌐 Add i18n strings for some error messages

* 🌐 Add fr, de, es translations for some error messages
2025-05-21 11:17:53 +02:00
Eva Marco
3abc8774f6 ♻️ Change translation string from workspace.token to workspace.tokens (#6508)
* ♻️ Change string translation for tokens

* ♻️ Apply find-and-replace on translation files

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-05-21 11:17:05 +02:00
Anonymous
af1c90c252 🌐 Add translations for: Swedish
Currently translated at 89.4% (1612 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-05-21 10:44:23 +02:00
Anonymous
8019ae7840 🌐 Add translations for: Dutch
Currently translated at 95.7% (1726 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-05-21 10:44:22 +02:00
Anonymous
6bd615ff8b 🌐 Add translations for: Latvian
Currently translated at 95.7% (1726 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-05-21 10:44:22 +02:00
Anonymous
c4a793d306 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 95.7% (1726 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2025-05-21 10:44:22 +02:00
Anonymous
631b3ac062 🌐 Add translations for: Croatian
Currently translated at 89.9% (1621 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2025-05-21 10:44:22 +02:00
Anonymous
48995850fa 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 88.4% (1594 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2025-05-21 10:44:21 +02:00
Anonymous
a5c7a2c97b 🌐 Add translations for: Czech
Currently translated at 89.8% (1620 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2025-05-21 10:44:20 +02:00
Anonymous
3a8285bc69 🌐 Add translations for: Italian
Currently translated at 95.6% (1724 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-05-21 10:44:20 +02:00
Anonymous
02e3cc089e 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 90.1% (1625 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2025-05-21 10:44:20 +02:00
Anonymous
17e19afcbd 🌐 Add translations for: Hebrew
Currently translated at 95.7% (1726 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-05-21 10:44:19 +02:00
Anonymous
a2b52a6408 🌐 Add translations for: Indonesian
Currently translated at 95.7% (1726 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2025-05-21 10:44:19 +02:00
Anonymous
8cc4b69291 🌐 Add translations for: German
Currently translated at 92.1% (1661 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-05-21 10:44:19 +02:00
Anonymous
045ddf5829 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 70.9% (1279 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-05-21 10:44:18 +02:00
Anonymous
1d0335aba6 🌐 Add translations for: French
Currently translated at 95.7% (1727 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-05-21 10:44:18 +02:00
Anonymous
5412d72236 🌐 Add translations for: Spanish
Currently translated at 98.8% (1782 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2025-05-21 10:44:17 +02:00
Anonymous
896ee43212 🌐 Add translations for: English
Currently translated at 99.8% (1800 of 1803 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/en/
2025-05-21 10:44:17 +02:00
alonso.torres
5d42b9793b 🐛 Fix some problems with layouts 2025-05-21 10:42:03 +02:00
alonso.torres
6cd2c712ab Pixel precision for new renderer 2025-05-21 10:42:03 +02:00
Elena Torro
a575410a29 🐛 Fix default scroll visibility on layers sidebar 2025-05-21 10:38:27 +02:00
Hosted Weblate
6b5703c1fe 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2025-05-21 10:38:18 +02:00
Andrey Antukh
22c3d4d807 Merge remote-tracking branch 'weblate/develop' into develop 2025-05-21 10:37:42 +02:00
luisδμ
b0701f6bb4 Control malformed variant formulas (#6473)
*  Control malformed variant strings

* 📎 PR changes

* 📎 PR changes
2025-05-21 10:18:11 +02:00
Alejandro Alonso
0748ef7267 Merge pull request #6518 from penpot/niwinz-staging-tokenslib-json-encoding
🐛 Add json encoding for tokenslib type
2025-05-21 10:17:37 +02:00
Pablo Alba
9bad9b8e91 🐛 Fix restore component when its original parent is deleted (#6517) 2025-05-21 10:05:22 +02:00
Andrey Antukh
9ca4fa752c 🐛 Add json encoding for tokenslib type 2025-05-21 09:59:28 +02:00
Andrey Antukh
b6563f620b 📎 Allow merge commits on commit linter 2025-05-21 09:34:05 +02:00
Andrey Antukh
a63fa2944d Merge remote-tracking branch 'origin/staging' into develop 2025-05-21 09:23:15 +02:00
Miguel de Benito Delgado
fd89c9d82c Avoid double id lookup when calling lookup-page-objects (#6513) 2025-05-20 22:31:40 +02:00
Miguel de Benito Delgado
71bb2556f9 ♻️ Move page setup out of the data.workspace ns (#6502)
* ♻️ Split history workspace.cljs to workspace/pages.cljs - rename file to target-name

* ♻️ Split history workspace.cljs to workspace/pages.cljs - rename source-file to git-split-temp

* ♻️ Split history workspace.cljs to workspace/pages.cljs - restore name of source-file

* ♻️ Cleanup after adding new ns, add exports

* ♻️ Move set-plugin-data to main.data.plugins

* 🐛 Possible bugfix, cherry-picked from commit 8f7c63d6e2 (conflict during refactor)

---------

Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
2025-05-20 22:11:05 +02:00
Miguel de Benito Delgado
f36aa30525 Add copy-as-svg to contextual menu (#6510)
*  Add "copy as svg" to contextual menu

* 🌐 Add a few translations of the new string

* 📚 Document commit message format for translations

* 📎 Log SVG import errors to the console

* 📎 Update CHANGES.md (two PRs)

---------

Signed-off-by: Miguel de Benito Delgado <m.debenito.d@gmail.com>
2025-05-20 22:06:36 +02:00
Eva Marco
8f7c63d6e2 Add base font fallback (#6468)
*  Add base font fallback

* ♻️ Add asserts to change-builder

* 🐛 Change fn name
2025-05-20 17:27:04 +02:00
Andrey Antukh
965b22718f 📚 Update changelog 2025-05-20 15:46:56 +02:00
Miguel de Benito Delgado
48a3d38d82 Add the Shift+ctrl+drag to deselect (#6494)
*  Allow shape deselection using Ctrl+Shift+Drag

*  Allow point deselection using Ctrl+Shift+Drag

*  Properly remember previous selection during addition/removal of shapes

*  Preload point selection in path handle-area-selection

Also: prefer dm/get-in over get-in

*  Highlight path nodes in selection rectangle incrementally
2025-05-20 15:23:05 +02:00
Florian Schroedl
31f642ed25 ♻️ Use rx streams for style dictionary interface 2025-05-20 14:55:07 +02:00
andrés gonzález
9f414b6ecd 📚 Update changelog (#6511) 2025-05-20 14:14:17 +02:00
Alejandro Alonso
334d7833d5 Merge pull request #6490 from penpot/azazeln28-refactor-iteration-performance
♻️ Refactor tile iteration
2025-05-20 13:56:37 +02:00
Alejandro Alonso
ff9c8f5929 Merge pull request #6483 from penpot/niwinz-staging-bugfixes-error-report
🐛 Several bugfixes
2025-05-20 13:54:54 +02:00
Xavier Julian
f7311cbb6b ♻️ Ensure tokens feature integrates design system 2025-05-20 13:52:38 +02:00
Alejandro Alonso
e4c563f917 Merge pull request #6479 from penpot/niwinz-develop-json-encoding-fix
🐛 Fix exception on rendering openapi.json
2025-05-20 13:46:16 +02:00
Andrey Antukh
2d3ad5a88f 📎 Update changelog 2025-05-20 13:30:04 +02:00
Andrey Antukh
1334d733cd 🐛 Fix openapi json generation for :re schemas 2025-05-20 13:29:44 +02:00
Andrey Antukh
004a9f17d3 Add minor js-like type schema formatting improvements 2025-05-20 13:29:44 +02:00
Andrey Antukh
c87fa4f723 Make the rpc doc generation lazy 2025-05-20 13:29:44 +02:00
Andrey Antukh
9378a5786f Replace json library used for generate openapi json 2025-05-20 13:29:44 +02:00
Andrey Antukh
3224ba26f1 ♻️ Replace :any schema with own ::sm/any
That a more specific, json friendly generator
2025-05-20 13:29:44 +02:00
Andrey Antukh
d33a5e6df1 Backport from develop partial improvements to sm/register! helper 2025-05-20 13:29:44 +02:00
Alejandro Alonso
0d60e3d997 Merge pull request #6486 from penpot/niwinz-library-export
 Add .penpot export support for penpot library
2025-05-20 13:27:11 +02:00
Andrey Antukh
645c4a57db Add playground file with example of how to create a component
This also fixes several internal issues related to component
creaton.
2025-05-20 13:06:07 +02:00
Andrey Antukh
778de6adaf Add minimal testing structure 2025-05-20 13:06:07 +02:00
Andrey Antukh
29d23577d2 🎉 Add .penpot (binfile-v3) support for library 2025-05-20 13:06:07 +02:00
Andrey Antukh
1fea1e8f5b 🔥 Remove unused svg parsing code from common 2025-05-20 13:06:07 +02:00
Andrey Antukh
c8a211742a 🔥 Remove already broken and unused internal components-v2 migration code 2025-05-20 13:06:06 +02:00
Andrey Antukh
2da8747485 ♻️ Move library to its own directory 2025-05-20 13:05:52 +02:00
Andrey Antukh
6803c78e80 Change naming and schema registation on tokens lib 2025-05-20 13:05:52 +02:00
Andrey Antukh
d8daea72de ⬆️ Update promesa library 2025-05-20 13:05:52 +02:00
Andrey Antukh
36b162b4fa ♻️ Replace jszip usage with zip.js library 2025-05-20 13:05:52 +02:00
Andrey Antukh
4c487834f0 Add the ability to deref internal state on library file instance 2025-05-20 13:05:52 +02:00
Andrey Antukh
dc7e53881a 🔥 Remove legacy-zip exportation support 2025-05-20 13:05:52 +02:00
Alejandro Alonso
1a01c9ee4a Merge pull request #6500 from penpot/niwinz-develop-svg-fixes
🐛 Fix svg path parsing on uploading svg image
2025-05-20 12:58:48 +02:00
Yamila Moreno
b6be416c7b 📎 Add wasm envvar to manage script 2025-05-20 12:15:14 +02:00
Florian Schroedl
4a27e8d1dd 🐛 Prevent unkown tokens hint always showing 2025-05-20 10:53:04 +02:00
Aitor Moreno
1bc97f9ad0 Merge pull request #6505 from penpot/supearlex-fix-avoid-unncesary-clone-for-new-render
🐛 Avoid unnecesary clone call
2025-05-20 10:07:23 +02:00
Aitor Moreno
aaa57cb17f 🐛 Fix inline styles in code tab (#6428) 2025-05-20 10:05:35 +02:00
Alejandro Alonso
b2d6342422 🐛 Avoid unnecesary clone call 2025-05-20 09:45:19 +02:00
Andrés Moya
ba1e16b55b 🐛 Fix token directory import 2025-05-20 09:42:38 +02:00
Aitor Moreno
ef95e3ecb0 ♻️ Refactor tile iteration 2025-05-19 16:24:52 +02:00
Eva Marco
55d21761fc Add multi file import on tokens (#6444)
*  Implement token multi-file import

* ♻️ Refactor import modal UI

* 🐛 Fix comments

---------

Co-authored-by: Florian Schroedl <flo.schroedl@gmail.com>
2025-05-19 16:12:46 +02:00
Andrey Antukh
0b4a367e9e 🐛 Fix svg path parsing on uploading svg image 2025-05-19 15:35:58 +02:00
Andrey Antukh
8f2ca15ec0 Merge pull request #6495 from mdbenito/refactor/frontend-app-main-data-workspace-clipboard
♻️ Factor clipboard code out of frontend/src/app/main/data/workspace.cljs
2025-05-19 15:20:52 +02:00
Aitor Moreno
21041eb925 Merge pull request #6496 from penpot/superalex-fix-path-performance
🐛 Fix paths performance in new render
2025-05-19 13:57:20 +02:00
Pablo Alba
53cfc29a1f Merge pull request #6425 from penpot/palba-variants-overrides-same-name
 Manage layers with the same name on variants overrides
2025-05-19 13:51:16 +02:00
Alejandro Alonso
96d44e0631 🐛 Fix paths performance in new render 2025-05-19 12:22:42 +02:00
Belén Albeza
8afd217a80 🔧 Enable back clippy rules (#6492)
* 🔧 Fix lint script (rust)

* 🔧 Temporarily add clippy rules to ignore so lint script passes

* 💄 Fix clippy rule crate_in_macro_def

* 💄 Fix clippy rule redundant-static-lifetimes

* 💄 Fix clippy rule unnecessary_cast

* 💄 Fix clippy rule nonminimal_bool

* 💄 Fix clippy rule redundant_pattern_matching

* 💄 Fix clippy rule assign_op_pattern

* 💄 Fix clippy rule needless_lifetimes

* 💄 Fix clippy rule for_kv_map

* 💄 Fix clippy rule ptr_arg

* 💄 Fix clippy rule match_like_matches_macro

* 💄 Fix clippy rule macro_metavars_in_unsafe

* 💄 Fix clippy rule map_clone

* 💄 Fix clippy rule wrong_self_convention

* 💄 Fix clippy rule vec_box

* 💄 Fix clippy rule useless_format

* 💄 Fix clippy rule unwrap_or_default

* 💄 Fix clippy rule unused_unit

* 💄 Fix clippy rule unnecessary_to_owned

* 💄 Fix clippy rule too_many_arguments

* 💄 Fix clippy rule slow_vector_initialization

* 💄 Fix clippy rule single_match

* 💄 Fix clippy rule redundant_field_names

* 💄 Fix clippy rule rendudant_closure

* 💄 Fix clippy rule needless_return

* 💄 Fix clippy rule needless_range_loop

* 💄 Fix clippy rule needless_borrows_for_generic_args

* 💄 Fix clippy rule needless-borrow

* 💄 Fix clippy rule missing_transmute_annotations

* 💄 Fix clippy rule map_entry

* 💄 Fix clippy rule manual_map

* 💄 Fix clippy rule len_zero

* 💄 Fix clippy rule from_over_into

* 💄 Fix clippy rule field_reassign_with_default

* 💄 Fix clippy rule enum_variant_names

* 💄 Fix clippy rule derivable_impls

* 💄 Fix clippy rule clone_on_copy

* 💄 Fix clippy rule box_collection

* 🔧 Make lint script also check test config target

* 🔧 Remove cargo-watch as a lib dependency

* 💄 Fix clippy rule for join_bounds

* 🔧 Fix lint script return code

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2025-05-19 11:14:55 +02:00
Miguel de Benito Delgado
330e49db56 ♻️ Cleanup after adding new ns, add exports 2025-05-18 18:49:59 +02:00
Miguel de Benito Delgado
aa39170d47 ♻️ Split history workspace.cljs to workspace/clipboard.cljs - restore name of source-file 2025-05-18 18:49:07 +02:00
Miguel de Benito Delgado
8fa7a69094 ♻️ Split history workspace.cljs to workspace/clipboard.cljs - resolve conflict and keep both files 2025-05-18 18:49:07 +02:00
Miguel de Benito Delgado
33d989feb2 ♻️ Split history workspace.cljs to workspace/clipboard.cljs - rename source-file to git-split-temp 2025-05-18 18:49:07 +02:00
Miguel de Benito Delgado
e309a57223 ♻️ Split history workspace.cljs to workspace/clipboard.cljs - rename file to target-name 2025-05-18 18:49:07 +02:00
Andrey Antukh
0b289153cb Add the ability to disable wasm on build script 2025-05-18 17:30:41 +02:00
Andrey Antukh
cf274099c4 Improve events/sse internal API
For make code cleaner and more evident for a quick view
2025-05-18 17:30:41 +02:00
Andrey Antukh
6524e75770 💄 Fix check-fn naming on types.container 2025-05-18 17:30:41 +02:00
Andrey Antukh
9b80f7c9b3 💄 Don't return unnecesary object from db query
the return value is already ignored
2025-05-18 17:30:41 +02:00
Andrey Antukh
bf76f328c8 Remove duplicate error logging on sse response 2025-05-18 17:30:41 +02:00
Xavier Julian
051c2a7e99 🐛 Fix sloppy behaviour on tokens value inputs 2025-05-16 15:42:25 +02:00
Xavier Julian
887fa6b77b Add slots feature to DS input component 2025-05-16 15:42:25 +02:00
Andrey Fedorov
d9f98008f4 Add unknown token type reporting 2025-05-16 15:09:36 +02:00
Alejandro Alonso
0cb6e0dee2 🐛 Fix new render zoom (#6488)
* 🐛 Fix new render zoom

* 🐛 Use scale instead of just zoom in get_tiles_for_viewbox

---------

Co-authored-by: Belén Albeza <belen@hey.com>
2025-05-16 10:49:03 +02:00
Andrey Antukh
ad87e9842d Merge pull request #6429 from penpot/yms-update-ubuntu-in-docker-images
🐳 Update docker images and dependencies
2025-05-16 10:38:56 +02:00
Miguel de Benito Delgado
e22a55334e 💄 Rename some namespace aliases for consistency (#6485) 2025-05-15 17:43:02 +02:00
Elena Torró
f5e81debbc Merge pull request #6478 from penpot/ladybenko-11030-fix-dpr-fills
🐛 Fix fills & strokes when dpr > 1
2025-05-15 16:04:30 +02:00
Belén Albeza
300e24b403 🐛 Fix drawing shapes when dpr > 1 2025-05-15 11:01:14 +02:00
Andrey Antukh
a00e7c1061 Merge remote-tracking branch 'origin/staging' into develop 2025-05-15 09:52:31 +02:00
Alonso Torres
ba25ce3098 🐛 Fix share button being displayed with no permissions (#6476)
* 🐛 Fix share button being displayed with no permissions

*  Simplify impl by accessing perms from teams directly

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-05-15 09:49:29 +02:00
Miguel de Benito Delgado
968ea56197 ♻️ Reorganize index management on worker code (#6477)
* ♻️ Factor index management out of app.worker.impl

* 💄 Fix silly spacing

* 💄 Lint
2025-05-15 09:46:49 +02:00
Miguel de Benito Delgado
2635873b9a 📚 Update CONTRIBUTING.md with formatting and linting (#6480) 2025-05-15 09:41:33 +02:00
Alejandro Alonso
f5f1316f0b Merge pull request #6474 from penpot/superalex-develop-paste-html-fix
🐛 Fix exception on paste invalid html
2025-05-14 16:16:53 +02:00
Andrey Antukh
79a164be6d 🐛 Fix exception on paste invalid html 2025-05-14 16:07:01 +02:00
alonso.torres
ecb85778bc 🐛 Fix problem with path edition of shapes 2025-05-14 14:45:29 +02:00
Elena Torró
676c4d2dfe Merge pull request #6472 from penpot/alotor-perf-selrect-modifiers
 Set selrect for new render modifiers
2025-05-14 14:19:38 +02:00
Andrés Moya
2cdc241e68 Merge branch 'hiru-update-tech-guide' into staging 2025-05-14 12:00:03 +02:00
Andrés Moya
057bf9bf25 📚 Update code samples 2025-05-14 11:38:55 +02:00
Andrés Moya
2ddcd0ce15 📚 Update Tech Guide about abstraction levels 2025-05-14 11:37:28 +02:00
alonso.torres
fef08dfa18 Set selrect for new render modifiers 2025-05-14 11:21:43 +02:00
Andrey Antukh
831422feaf ⬆️ Update several npm dependencies on frontend module 2025-05-14 10:39:34 +02:00
Andrey Antukh
d01e3085f4 ⬆️ Update yarn to 4.9.1 2025-05-14 10:39:34 +02:00
Andrey Antukh
d9ca82dc15 ⬆️ Update dependencies 2025-05-14 10:39:34 +02:00
Yamila Moreno
1e7127d98a 🐳 Update frontend image to nginx:1.28.0 2025-05-14 10:39:34 +02:00
Yamila Moreno
002ae8b91a 🐳 Update docker images to ubuntu 24.04 2025-05-14 10:39:34 +02:00
Aitor Moreno
6831acb71d Merge pull request #6465 from penpot/superalex-fix-render-wasm-maks
🐛 Fix new render masks
2025-05-14 10:33:52 +02:00
Alejandro Alonso
1f44d53f6b 🐛 Fix new render masks 2025-05-13 15:41:41 +02:00
Alonso Torres
ca2891d441 🐛 Fix problem syncing library colors and typographies (#6467) 2025-05-13 13:28:16 +02:00
Belén Albeza
91fbe8f8ef 🎉 Cap stop amount in UI for wasm (#6438)
* 🎉 Cap in the colorpicker the amount of stops a gradient can have

* 🎉 Cap the stops amount in gradient handlers

* 🎉 Disable add stop in gradient handlers (viewport + colorpicker)

*  Add integration test for gradient limits

* 💄 Address PR suggestion
2025-05-13 10:37:05 +02:00
Miguel de Benito Delgado
69cc4fb4c2 📚 Add missing command to open a repl on frontend process (#6458)
* 📚 Add missing command to open a repl on frontend process

* 📚 Add further information on starting a REPL on the frontend process
2025-05-13 08:10:52 +02:00
Elenzakaleidos
37abb7b237 💄 Update video in readme page (#6461)
Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>
2025-05-13 08:10:19 +02:00
Ramiro Andres Sanchez Balo
5fc2208c16 📚 Improve metadata descriptions (#6457) 2025-05-13 08:09:59 +02:00
Alejandro Alonso
c2b67d7c67 Merge pull request #6459 from penpot/superalex-fix-wasm-playground-fills-size
🐛 Fix wasm playground fills size
2025-05-13 06:23:48 +02:00
Alejandro Alonso
eb76d16b3b 🐛 Fix wasm playground fills size 2025-05-12 12:05:10 +02:00
Unreal Vision
58e0b26493 🌐 Add translations for: French
Currently translated at 100.0% (1730 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-04-17 15:01:43 +02:00
Corentin Noël
c75380e063 🌐 Add translations for: French
Currently translated at 100.0% (1730 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-04-17 15:01:42 +02:00
TheScientistPT
3d67c7930c 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 92.3% (1598 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2025-04-13 20:18:26 +02:00
TheScientistPT
b55ec38c35 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 92.2% (1596 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2025-04-11 23:02:00 +02:00
Stas Haas
02a1cfb457 🌐 Add translations for: German
Currently translated at 96.1% (1663 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-04-11 23:01:59 +02:00
Corentin Noël
b2ba38b5de 🌐 Add translations for: French
Currently translated at 98.7% (1708 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-04-11 23:01:57 +02:00
Denys Kisil
68ce13368e 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 100.0% (1730 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2025-04-09 14:01:41 +02:00
Denys Kisil
a55db1d52b 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 100.0% (1730 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2025-04-07 16:01:42 +00:00
Rick Benetti
ee96c5599c 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 74.0% (1281 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-04-05 14:07:11 +00:00
Rick Benetti
21702c090d 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 73.9% (1280 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-03-31 11:06:36 +00:00
Edgars Andersons
c4254106e8 🌐 Add translations for: Latvian
Currently translated at 100.0% (1730 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-03-29 13:01:53 +01:00
Edgars Andersons
981336ed5e 🌐 Add translations for: Latvian
Currently translated at 98.4% (1704 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-03-28 11:01:58 +00:00
Linerly
3864ce6855 🌐 Add translations for: Indonesian
Currently translated at 100.0% (1730 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2025-03-28 11:01:57 +00:00
Edgars Andersons
ec0183ce94 🌐 Add translations for: Latvian
Currently translated at 97.6% (1690 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-03-27 11:01:53 +01:00
Edgars Andersons
f587ed4ade 🌐 Add translations for: Latvian
Currently translated at 97.1% (1680 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-03-25 11:01:54 +00:00
Nicola Bortoletto
bb5a103944 🌐 Add translations for: Italian
Currently translated at 99.8% (1728 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-03-23 19:01:53 +00:00
Rick Benetti
34b3520fb2 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 70.9% (1228 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-03-23 19:01:52 +00:00
Stephan Paternotte
3217ba5a77 🌐 Add translations for: Dutch
Currently translated at 100.0% (1730 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-03-22 18:01:53 +01:00
Nicola Bortoletto
a91caded9e 🌐 Add translations for: Italian
Currently translated at 96.4% (1669 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-03-22 18:01:52 +01:00
Stephan Paternotte
05ba1c3e64 🌐 Add translations for: Dutch
Currently translated at 99.0% (1714 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-03-21 15:02:01 +00:00
Edgars Andersons
77f025eb8d 🌐 Add translations for: Latvian
Currently translated at 96.0% (1662 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-03-21 15:02:00 +00:00
Yaron Shahrabani
aacec1809b 🌐 Add translations for: Hebrew
Currently translated at 100.0% (1730 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-03-21 15:01:59 +00:00
Linerly
0435f560a4 🌐 Add translations for: Indonesian
Currently translated at 95.4% (1652 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2025-03-21 15:01:58 +00:00
Stas Haas
766f034e5e 🌐 Add translations for: German
Currently translated at 94.1% (1628 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-03-21 15:01:56 +00:00
Ally Tiago
8502d9d21b 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 70.4% (1218 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-03-21 15:01:55 +00:00
Rick Benetti
6c874b2bb7 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 70.4% (1218 of 1730 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-03-21 15:01:55 +00:00
421 changed files with 37476 additions and 32199 deletions

View File

@@ -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
View File

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

View File

@@ -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

View File

@@ -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).

View File

@@ -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 />

View File

@@ -6,7 +6,7 @@
org.clojure/clojure {:mvn/version "1.12.0"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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))))))}))

View File

@@ -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

View 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"}

View File

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

View File

@@ -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"}

View File

@@ -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

View 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]]])

View File

@@ -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)

View File

@@ -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}}]

View File

@@ -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))))))

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

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

View File

@@ -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)})

View File

@@ -12,14 +12,14 @@
org.apache.logging.log4j/log4j-web {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}
org.slf4j/slf4j-api {:mvn/version "2.0.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"]}}}

View File

@@ -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"
},

View File

@@ -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))))

View File

@@ -47,14 +47,14 @@
[:type [:= :assign]]
;; NOTE: the full decoding is happening on the handler because it
;; needs a proper context of the current shape and its type
[:value [:map-of :keyword :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

View File

@@ -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))))

View File

@@ -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))

View File

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

View File

@@ -596,7 +596,7 @@
(generate-sync-shape-direct changes file libraries container shape-id false)))
(defmethod generate-sync-shape :colors
[_ changes library-id _ shape _ libraries _]
[_ changes library-id _ shape libraries _]
(shape-log :debug (:id shape) nil :msg "Sync colors of shape" :shape (:name shape))
;; Synchronize a shape that uses some colors of the library. The value of the
@@ -607,7 +607,7 @@
#(ctc/sync-shape-colors % library-id library-colors))))
(defmethod generate-sync-shape :typographies
[_ changes library-id container shape _ libraries _]
[_ changes library-id container shape libraries _]
(shape-log :debug (:id shape) nil :msg "Sync typographies of shape" :shape (:name shape))
;; Synchronize a shape that uses some typographies of the library. The attributes
@@ -2055,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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -5,8 +5,8 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.media
"Media assets helpers (images, fonts, etc)"
(:require
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
;; We have added ".ttf" as string to solve a problem with chrome input selector
@@ -48,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]

View File

@@ -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?

View File

@@ -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))

View File

@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.schema.generators
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector])
(: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]))

View File

@@ -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 _ _]

View File

@@ -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)))))

View File

@@ -35,7 +35,7 @@
(.. r (toString 16) (padStart 2 "0"))
(.. g (toString 16) (padStart 2 "0"))
(.. b (toString 16) (padStart 2 "0"))))))
sg/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)

View File

@@ -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))

View File

@@ -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))

View File

@@ -182,11 +182,11 @@
;; FIXME: move to helpers?, this function need performance review, it
;; is executed so many times on path edition
(defn- curve-closest-point
[position start end h1 h2]
[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

View File

@@ -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)

View File

@@ -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}

View File

@@ -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)

View File

@@ -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

View File

@@ -1,26 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.files-builder-test
(:require
[app.common.files.builder :as builder]
[clojure.test :as t]))
(t/deftest test-strip-image-extension
(t/testing "removes extension from supported image files"
(t/is (= (builder/strip-image-extension "foo.png") "foo"))
(t/is (= (builder/strip-image-extension "foo.webp") "foo"))
(t/is (= (builder/strip-image-extension "foo.jpg") "foo"))
(t/is (= (builder/strip-image-extension "foo.jpeg") "foo"))
(t/is (= (builder/strip-image-extension "foo.svg") "foo"))
(t/is (= (builder/strip-image-extension "foo.gif") "foo")))
(t/testing "does not remove extension for unsupported files"
(t/is (= (builder/strip-image-extension "foo.txt") "foo.txt"))
(t/is (= (builder/strip-image-extension "foo.bmp") "foo.bmp")))
(t/testing "leaves filename intact when it has no extension"
(t/is (= (builder/strip-image-extension "README") "README"))))

View File

@@ -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"))))

View File

@@ -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))

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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 \

View File

@@ -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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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 contents writing mode (e.g., inline, block) instead of physical directions (left, right, etc). This improves support for right-to-left (RTL) languages and enhances layout flexibility.
**AVOID: Physical properties**
```scss
.btn {
padding-left: var(--sp-xs);
}
```
**DO: Use directionrelative equivalents**
```scss
.btn {
padding-inline-start: var(--sp-xs);
}
```
Note: Although `width` and `height` are physical properties, their use is allowed in CSS files. They remain more readable and intuitive than their logical counterparts (`inline-size`, `block-size`) in many contexts. Since our layouts are not vertically-sensitive, we don't gain practical benefits from using logical properties here.
### Use named DS variables
Avoid hardcoded values like `px`, `rem`, or raw SASS variables `($s-*)`. Use semantic, named variables provided by the Design System to ensure consistency and scalability.
#### Spacing (margins, paddings, gaps...)
Use variables from `frontend/src/app/main/ui/ds/spacing.scss`. These are predefined and approved by the design team — **do not add or modify values without design approval**.
#### Fixed dimensions
For fixed dimensions (e.g., modals' widths) defined by design and not layout-driven, use or define variables in `frontend/src/app/main/ui/ds/_sizes.scss`. To use them:
```scss
@use "../_sizes.scss" as *;
```
Note: Since these values haven't been semantically defined yet, were temporarily using SASS variables instead of named CSS custom properties.
#### Border Widths
Use border thickness variables from `frontend/src/app/main/ui/ds/_borders.scss`. To import:
```scss
@use "../_borders.scss" as *;
```
Avoid using sass variables defined on `frontend/resources/styles/common/refactor/spacing.scss` that are deprecated.
**AVOID: Using sass unnamed variables or hardcoded values**
```scss
.btn {
padding: $s-24;
}
.icon {
width: 16px;
}
```
**DO: Use DS variables**
```scss
.btn {
padding: var(--sp-xl);
}
.icon {
width: var(--sp-l);
}
```
### Use Proper Typography Components
Replace plain text tags with `text*` or `heading*` components from the Design System to ensure visual consistency and accessibility.
**AVOID: Using text wrappers**
```clojure
[:h2 {:class (stl/css :modal-title)} title]
[:div {:class (stl/css :modal-content)}
"Content"]
```
**DO: Use spacing named variables**
```clojure
...
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
...
[:> heading* {:level 2
:typography t/headline-medium
:class (stl/css :modal-title)}
title]
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :modal-content)}
"Content"]
```
When applying typography in SCSS, use the proper mixin from the Design System.
**AVOID: Deprecated mixins**
```scss
.class {
@include headlineLargeTypography;
}
```
**DO: Use the DS mixin**
```scss
@use "../ds/typography.scss" as t;
.class {
@include t.use-typography("body-small");
}
```
You can find the full list of available typography tokens in [Storybook](https://design.penpot.app/storybook/?path=/docs/foundations-typography--docs).
If the design you are implementing doesn't match any of them, ask a designer.
### Use custom properties within components
Reduce the need for one-off SASS variables by leveraging [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) in your component styles. This keeps component theming flexible and composable.
For instance, this is how we handle the styles of <code class="language-clojure">\<Toast></code>, which have a different style depending on the level of the message (default, info, error, etc.)
```scss
.toast {
// common styles for all toasts
// ...
--toast-bg-color: var(--color-background-primary);
--toast-icon-color: var(--color-foreground-secondary);
// ... more variables here
background-color: var(--toast-bg-color);
}
.toast-icon {
color: var(--toast-bg-color);
}
.toast-info {
--toast-bg-color: var(--color-background-info);
--toast-icon-color: var(--color-accent-info);
// ... override more variables here
}
.toast-error {
--toast-bg-color: var(--color-background-error);
--toast-icon-color: var(--color-accent-error);
// ... override more variables here
}
// ... more variants here
```
## Semantics and accessibility
@@ -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

View File

@@ -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">

View File

@@ -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