Files
penpot/frontend/src/app/plugins/parser.cljs
2024-09-04 13:29:56 +02:00

573 lines
16 KiB
Clojure

;; 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.plugins.parser
(:require
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.util.object :as obj]
[cuerdas.core :as str]))
(defn parse-id
[id]
(when id (uuid/uuid id)))
(defn parse-keyword
[kw]
(when kw (keyword kw)))
(defn parse-hex
[color]
(if (string? color) (-> color str/lower) color))
(defn parse-point
[^js point]
(when point
{:x (obj/get point "x")
:y (obj/get point "y")}))
(defn parse-shape-type
[type]
(case type
"board" :frame
"boolean" :bool
"rectangle" :rect
"ellipse" :circle
(parse-keyword type)))
;; {
;; name?: string;
;; nameLike?: string;
;; type?:
;; | 'board'
;; | 'group'
;; | 'boolean'
;; | 'rectangle'
;; | 'path'
;; | 'text'
;; | 'ellipse'
;; | 'svg-raw'
;; | 'image';
;; }
(defn parse-criteria
[^js criteria]
(when (some? criteria)
(d/without-nils
{:name (obj/get criteria "name")
:name-like (obj/get criteria "nameLike")
:type (-> (obj/get criteria "type") parse-shape-type)})))
;;export type ImageData = {
;; name?: string;
;; width: number;
;; height: number;
;; mtype?: string;
;; id: string;
;; keepApectRatio?: boolean;
;;}
(defn parse-image-data
[^js image-data]
(when (some? image-data)
(d/without-nils
{:id (-> (obj/get image-data "id") parse-id)
:name (obj/get image-data "name")
:width (obj/get image-data "width")
:height (obj/get image-data "height")
:mtype (obj/get image-data "mtype")
:keep-aspect-ratio (obj/get image-data "keepApectRatio")})))
;; export type Gradient = {
;; type: 'linear' | 'radial';
;; startX: number;
;; startY: number;
;; endX: number;
;; endY: number;
;; width: number;
;; stops: Array<{ color: string; opacity?: number; offset: number }>;
;; }
(defn parse-gradient-stop
[^js stop]
(when (some? stop)
(d/without-nils
{:color (-> (obj/get stop "color") parse-hex)
:opacity (obj/get stop "opacity")
:offset (obj/get stop "offset")})))
(defn parse-gradient
[^js gradient]
(when (some? gradient)
(d/without-nils
{:type (-> (obj/get gradient "type") parse-keyword)
:start-x (obj/get gradient "startX")
:start-y (obj/get gradient "startY")
:end-x (obj/get gradient "endX")
:end-y (obj/get gradient "endY")
:width (obj/get gradient "width")
:stops (->> (obj/get gradient "stops")
(mapv parse-gradient-stop))})))
;; export interface Color {
;; id?: string;
;; name?: string;
;; path?: string;
;; color?: string;
;; opacity?: number;
;; refId?: string;
;; refFile?: string;
;; gradient?: Gradient;
;; image?: ImageData;
;; }
(defn parse-color
[^js color]
(when (some? color)
(d/without-nils
{:id (-> (obj/get color "id") parse-id)
:name (obj/get color "name")
:path (obj/get color "path")
:color (-> (obj/get color "color") parse-hex)
:opacity (obj/get color "opacity")
:ref-id (-> (obj/get color "refId") parse-id)
:ref-file (-> (obj/get color "refFile") parse-id)
:gradient (-> (obj/get color "gradient") parse-gradient)
:image (-> (obj/get color "image") parse-image-data)})))
;; export interface Shadow {
;; id?: string;
;; style?: 'drop-shadow' | 'inner-shadow';
;; offsetX?: number;
;; offsetY?: number;
;; blur?: number;
;; spread?: number;
;; hidden?: boolean;
;; color?: Color;
;; }
(defn parse-shadow
[^js shadow]
(when (some? shadow)
(d/without-nils
{:id (-> (obj/get shadow "id") parse-id)
:style (-> (obj/get shadow "style") parse-keyword)
:offset-x (obj/get shadow "offsetX")
:offset-y (obj/get shadow "offsetY")
:blur (obj/get shadow "blur")
:spread (obj/get shadow "spread")
:hidden (obj/get shadow "hidden")
:color (-> (obj/get shadow "color") parse-color)})))
(defn parse-shadows
[^js shadows]
(when (some? shadows)
(into [] (map parse-shadow) shadows)))
;;export interface Fill {
;; fillColor?: string;
;; fillOpacity?: number;
;; fillColorGradient?: Gradient;
;; fillColorRefFile?: string;
;; fillColorRefId?: string;
;; fillImage?: ImageData;
;;}
(defn parse-fill
[^js fill]
(when (some? fill)
(d/without-nils
{:fill-color (-> (obj/get fill "fillColor") parse-hex)
:fill-opacity (obj/get fill "fillOpacity")
:fill-color-gradient (-> (obj/get fill "fillColorGradient") parse-gradient)
:fill-color-ref-file (-> (obj/get fill "fillColorRefFile") parse-id)
:fill-color-ref-id (-> (obj/get fill "fillColorRefId") parse-id)
:fill-image (-> (obj/get fill "fillImage") parse-image-data)})))
(defn parse-fills
[^js fills]
(when (some? fills)
(into [] (map parse-fill) fills)))
;; export interface Stroke {
;; strokeColor?: string;
;; strokeColorRefFile?: string;
;; strokeColorRefId?: string;
;; strokeOpacity?: number;
;; strokeStyle?: 'solid' | 'dotted' | 'dashed' | 'mixed' | 'none' | 'svg';
;; strokeWidth?: number;
;; strokeAlignment?: 'center' | 'inner' | 'outer';
;; strokeCapStart?: StrokeCap;
;; strokeCapEnd?: StrokeCap;
;; strokeColorGradient?: Gradient;
;; }
(defn parse-stroke
[^js stroke]
(when (some? stroke)
(d/without-nils
{:stroke-color (-> (obj/get stroke "strokeColor") parse-hex)
:stroke-color-ref-file (-> (obj/get stroke "strokeColorRefFile") parse-id)
:stroke-color-ref-id (-> (obj/get stroke "strokeColorRefId") parse-id)
:stroke-opacity (obj/get stroke "strokeOpacity")
:stroke-style (-> (obj/get stroke "strokeStyle") parse-keyword)
:stroke-width (obj/get stroke "strokeWidth")
:stroke-alignment (-> (obj/get stroke "strokeAlignment") parse-keyword)
:stroke-cap-start (-> (obj/get stroke "strokeCapStart") parse-keyword)
:stroke-cap-end (-> (obj/get stroke "strokeCapEnd") parse-keyword)
:stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient)})))
(defn parse-strokes
[^js strokes]
(when (some? strokes)
(into [] (map parse-stroke) strokes)))
;; export interface Blur {
;; id?: string;
;; type?: 'layer-blur';
;; value?: number;
;; hidden?: boolean;
;; }
(defn parse-blur
[^js blur]
(when (some? blur)
(d/without-nils
{:id (-> (obj/get blur "id") parse-id)
:type (-> (obj/get blur "type") parse-keyword)
:value (obj/get blur "value")
:hidden (obj/get blur "hidden")})))
;; export interface Export {
;; type: 'png' | 'jpeg' | 'svg' | 'pdf';
;; scale: number;
;; suffix: string;
;; }
(defn parse-export
[^js export]
(when (some? export)
(d/without-nils
{:type (-> (obj/get export "type") parse-keyword)
:scale (obj/get export "scale" 1)
:suffix (obj/get export "suffix" "")})))
(defn parse-exports
[^js exports]
(when (some? exports)
(into [] (map parse-export) exports)))
;; export interface GuideColumnParams {
;; color: { color: string; opacity: number };
;; type?: 'stretch' | 'left' | 'center' | 'right';
;; size?: number;
;; margin?: number;
;; itemLength?: number;
;; gutter?: number;
;; }
(defn parse-frame-guide-column-params
[^js params]
(when params
(d/without-nils
{:color (-> (obj/get params "color") parse-color)
:type (-> (obj/get params "type") parse-keyword)
:size (obj/get params "size")
:margin (obj/get params "margin")
:item-length (obj/get params "itemLength")
:gutter (obj/get params "gutter")})))
;; export interface GuideColumn {
;; type: 'column';
;; display: boolean;
;; params: GuideColumnParams;
;; }
(defn parse-frame-guide-column
[^js guide]
(when guide
(d/without-nils
{:type (-> (obj/get guide "type") parse-keyword)
:display (obj/get guide "display")
:params (-> (obj/get guide "params") parse-frame-guide-column-params)})))
;; export interface GuideRow {
;; type: 'row';
;; display: boolean;
;; params: GuideColumnParams;
;; }
(defn parse-frame-guide-row
[^js guide]
(when guide
(d/without-nils
{:type (-> (obj/get guide "type") parse-keyword)
:display (obj/get guide "display")
:params (-> (obj/get guide "params") parse-frame-guide-column-params)})))
;;export interface GuideSquareParams {
;; color: { color: string; opacity: number };
;; size?: number;
;;}
(defn parse-frame-guide-square-params
[^js params]
(when (some? params)
(d/without-nils
{:color (-> (obj/get params "color") parse-color)
:size (obj/get params "size")})))
;; export interface GuideSquare {
;; type: 'square';
;; display: boolean;
;; params: GuideSquareParams;
;; }
(defn parse-frame-guide-square
[^js guide]
(when guide
(d/without-nils
{:type (-> (obj/get guide "type") parse-keyword)
:display (obj/get guide "display")
:params (-> (obj/get guide "params") parse-frame-guide-column-params)})))
(defn parse-frame-guide
[^js guide]
(when (some? guide)
(case (obj/get guide "type")
"column"
parse-frame-guide-column
"row"
parse-frame-guide-row
"square"
(parse-frame-guide-square guide))))
(defn parse-frame-guides
[^js guides]
(when (some? guides)
(into [] (map parse-frame-guide) guides)))
;;interface PathCommand {
;; command:
;; | 'M' | 'move-to'
;; | 'Z' | 'close-path'
;; | 'L' | 'line-to'
;; | 'H' | 'line-to-horizontal'
;; | 'V' | 'line-to-vertical'
;; | 'C' | 'curve-to'
;; | 'S' | 'smooth-curve-to'
;; | 'Q' | 'quadratic-bezier-curve-to'
;; | 'T' | 'smooth-quadratic-bezier-curve-to'
;; | 'A' | 'elliptical-arc';
;;
;; params?: {
;; x?: number;
;; y?: number;
;; c1x: number;
;; c1y: number;
;; c2x: number;
;; c2y: number;
;; rx?: number;
;; ry?: number;
;; xAxisRotation?: number;
;; largeArcFlag?: boolean;
;; sweepFlag?: boolean;
;; };
;;}
(defn parse-command-type
[^string command-type]
(case command-type
"M" :move-to
"Z" :close-path
"L" :line-to
"H" :line-to-horizontal
"V" :line-to-vertical
"C" :curve-to
"S" :smooth-curve-to
"Q" :quadratic-bezier-curve-to
"T" :smooth-quadratic-bezier-curve-to
"A" :elliptical-arc
(parse-keyword command-type)))
(defn parse-command-params
[^js params]
(when (some? params)
(d/without-nils
{:x (obj/get params "x")
:y (obj/get params "y")
:c1x (obj/get params "c1x")
:c1y (obj/get params "c1y")
:c2x (obj/get params "c2x")
:c2y (obj/get params "c2y")
:rx (obj/get params "rx")
:ry (obj/get params "ry")
:x-axis-rotation (obj/get params "xAxisRotation")
:large-arc-flag (obj/get params "largeArcFlag")
:sweep-flag (obj/get params "sweepFlag")})))
(defn parse-command
[^js command]
(when (some? command)
(d/without-nils
{:command (-> (obj/get command "command") parse-command-type)
:params (-> (obj/get command "params") parse-command-params)})))
(defn parse-path-content
[^js content]
(when (some? content)
(into [] (map parse-command) content)))
;; export interface Dissolve {
;; type: 'dissolve';
;; duration: number;
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export interface Slide {
;; type: 'slide';
;; way: 'in' | 'out';
;; direction?:
;; | 'right'
;; | 'left'
;; | 'up'
;; | 'down';
;; duration: number;
;; offsetEffect?: boolean;
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export interface Push {
;; type: 'push';
;; direction?:
;; | 'right'
;; | 'left'
;; | 'up'
;; | 'down';
;;
;; duration: number;
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export type Animation = Dissolve | Slide | Push;
(defn parse-animation
[^js animation]
(when animation
(let [animation-type (-> (obj/get animation "type") parse-keyword)]
(d/without-nils
(case animation-type
:dissolve
{:type animation-type
:duration (obj/get animation "duration")
:easing (-> (obj/get animation "easing") parse-keyword)}
:slide
{:type animation-type
:way (-> (obj/get animation "way") parse-keyword)
:direction (-> (obj/get animation "direction") parse-keyword)
:duration (obj/get animation "duration")
:easing (-> (obj/get animation "easing") parse-keyword)
:offset-effect (obj/get animation "offsetEffect")}
:push
{:type animation-type
:direction (-> (obj/get animation "direction") parse-keyword)
:duration (obj/get animation "duration")
:easing (-> (obj/get animation "easing") parse-keyword)}
nil)))))
;;export type Action =
;; | NavigateTo
;; | OpenOverlay
;; | ToggleOverlay
;; | CloseOverlay
;; | PreviousScreen
;; | OpenUrl;
;;
;;export interface NavigateTo {
;; type: 'navigate-to';
;; destination: Board;
;; preserveScrollPosition?: boolean;
;; animation: Animation;
;;}
;;
;;export interface OverlayAction {
;; destination: Board;
;; relativeTo?: Shape;
;; position?:
;; | 'manual'
;; | 'center'
;; | 'top-left'
;; | 'top-right'
;; | 'top-center'
;; | 'bottom-left'
;; | 'bottom-right'
;; | 'bottom-center';
;; manualPositionLocation?: Point;
;; closeWhenClickOutside?: boolean;
;; addBackgroundOverlay?: boolean;
;; animation: Animation;
;;}
;;
;;export interface OpenOverlay extends OverlayAction {
;; type: 'open-overlay';
;;}
;;
;;export interface ToggleOverlay extends OverlayAction {
;; type: 'toggle-overlay';
;;}
;;
;;export interface CloseOverlay {
;; type: 'close-overlay';
;; destination?: Board;
;; animation: Animation;
;;}
;;
;;export interface PreviousScreen {
;; type: 'previous-screen';
;;}
;;
;;export interface OpenUrl {
;; type: 'open-url';
;; url: string;
;;}
(defn parse-action
[action]
(when action
(let [action-type (-> (obj/get action "type") parse-keyword)]
(d/without-nils
(case action-type
:navigate-to
{:action-type :navigate
:destination (-> (obj/get action "destination") (obj/get "$id"))
:preserve-scroll (obj/get action "preserveScrollPosition")
:animation (-> (obj/get action "animation") parse-animation)}
(:open-overlay
:toggle-overlay)
{:action-type action-type
:destination (-> (obj/get action "destination") (obj/get "$id"))
:relative-to (-> (obj/get action "relativeTo") (obj/get "$id"))
:overlay-pos-type (-> (obj/get action "position") parse-keyword)
:overlay-position (-> (obj/get action "manualPositionLocation") parse-point)
:close-click-outside (obj/get action "closeWhenClickOutside")
:background-overlay (obj/get action "addBackgroundOverlay")
:animation (-> (obj/get action "animation") parse-animation)}
:close-overlay
{:action-type action-type
:destination (-> (obj/get action "destination") (obj/get "$id"))
:animation (-> (obj/get action "animation") parse-animation)}
:previous-screen
{:action-type :prev-screen}
:open-url
{:action-type action-type
:url (obj/get action "url")}
nil)))))
(defn parse-interaction
[^js interaction]
(when interaction
(let [trigger (-> (obj/get interaction "trigger") parse-keyword)
delay (obj/get interaction "trigger")
action (-> (obj/get interaction "action") parse-action)]
(d/without-nils
(d/patch-object {:event-type trigger :delay delay} action)))))