mirror of
https://github.com/penpot/penpot.git
synced 2026-08-10 23:08:41 +00:00
The graph namespaces explained themselves by citing a separate project whose Python pipeline reads the graphs this backend writes. A reader of this repository does not have that project and should not need it, and a docstring that justifies a choice by pointing elsewhere cannot be checked here. Every claim survives; only the framing changes. Column names and types are Penpot's own decision, recorded with the reason for each divergence from the snake_case default. The transform registry describes the edges it materializes. The denormalizations in `app.graph.project.document` are justified by the walk already holding both answers. Three corrections fall out of the rewrite: - `app.graph.schema.contract` claimed a test, `graph_contract_test`, that walks a checked-in schema manifest and fails on any divergence. No such test exists. The paragraph is gone. - `app.graph.project.document` pointed at `app.graph.meta/projection-transforms`, which does not exist. - `app.graph.project.transforms/registry` claimed its entries were "in application order" while `apply-transforms!` reduced over the literal vector. The three registered transforms read disjoint columns, so the order is not load-bearing. The docstring now says so, and the one real ordering constraint is stated where it applies: `link-swap-slots!` strips `swap-slot-*` entries from `touched`, so anything reading `touched` has to run before it. `contract/pending-beadpot-columns` becomes `contract/unprojected-keys`. It is referenced nowhere else. AI-assisted-by: mixed models
211 lines
8.0 KiB
Clojure
211 lines
8.0 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 Sucursal en España SL
|
|
|
|
(ns app.graph.project.document
|
|
"Project a Penpot file-data map into Ladybug nodes and structural edges.
|
|
|
|
Projects Document, Page, Component, the full shape tree (skipping the root
|
|
frame), and `IsChildOf` edges from shapes/pages/components to their parent.
|
|
|
|
Two denormalizations happen here rather than in a later pass, because the
|
|
walk already has both answers in hand and a post-ingest statement would have
|
|
to rediscover them:
|
|
|
|
- `page-id` on every shape, from the page the walk is currently in;
|
|
- `component-id` propagated from an instance head down to its descendants,
|
|
from the head context the walk carries."
|
|
(:require
|
|
[app.common.logging :as l]
|
|
[app.common.uuid :as uuid]
|
|
[app.graph.schema.nodes :as nodes]))
|
|
|
|
(def root-frame-id
|
|
uuid/zero)
|
|
|
|
(defn- document-attrs
|
|
"The Document node's attrs: the file row, minus its data blob.
|
|
|
|
`:options` is lifted out of the blob before it goes: it is file-level
|
|
configuration a consumer wants without opening `:data`."
|
|
[file data]
|
|
(-> file
|
|
(assoc :id (or (:id data) (:id file)))
|
|
(cond-> (:options data) (assoc :options (:options data)))
|
|
(dissoc :data)))
|
|
|
|
(defn- page-attrs
|
|
[page index]
|
|
(-> page
|
|
(dissoc :objects)
|
|
(cond-> (some? index) (assoc :index (long index)))))
|
|
|
|
(defn- component-attrs
|
|
[component]
|
|
(-> component
|
|
(dissoc :objects)
|
|
;; schema:component requires :path; some legacy rows omit it
|
|
(update :path #(or % ""))))
|
|
|
|
(defn- shape-table
|
|
[shape]
|
|
(nodes/table-for-type (:type shape)))
|
|
|
|
(defn denormalized-shape
|
|
"`shape` with `page-id` set and an inherited `component-id` filled in.
|
|
|
|
A shape that carries its own `component-id` keeps it; `component-ctx` only
|
|
fills the gap for descendants (see `descend-component-ctx`)."
|
|
[shape page-id component-ctx]
|
|
(cond-> (assoc shape :page-id page-id)
|
|
(and (uuid? component-ctx) (nil? (:component-id shape)))
|
|
(assoc :component-id component-ctx)))
|
|
|
|
(defn- shape-node-attrs
|
|
[table shape page-id component-ctx]
|
|
(nodes/project-attrs table (denormalized-shape shape page-id component-ctx)))
|
|
|
|
(defn descend-component-ctx
|
|
"The component context to pass to `shape`'s children.
|
|
|
|
Inheritance stops at the nearest ancestor Frame carrying a `component-id`,
|
|
and any intermediate shape that carries one is a barrier:
|
|
|
|
- a Frame with its own `component-id` becomes the new context (it is an
|
|
instance head, and its descendants belong to *it*, not to an outer head);
|
|
- any other shape carrying a `component-id` blocks inheritance below it
|
|
without being able to supply one, since only Frames are heads;
|
|
- otherwise the context passes through unchanged."
|
|
[table shape ctx]
|
|
(let [own (:component-id shape)]
|
|
(cond
|
|
(and (some? own) (= table "Frame")) own
|
|
(some? own) ::blocked
|
|
:else ctx)))
|
|
|
|
(defn- container-table?
|
|
[table]
|
|
(contains? nodes/container-tables table))
|
|
|
|
(defn- child-shape-ids
|
|
"Child ids in Penpot z-order (reversed from the stored :shapes list)."
|
|
[parent]
|
|
(when-let [shapes (:shapes parent)]
|
|
(vec (reverse shapes))))
|
|
|
|
(defn- initial-acc
|
|
[]
|
|
{:nodes {}
|
|
:edges []
|
|
:stats {:documents 0 :pages 0 :components 0 :shapes 0}})
|
|
|
|
(declare project-shape-ids)
|
|
|
|
(defn- project-shape
|
|
[objects acc table shape parent-table parent-id position page-id component-ctx]
|
|
(let [shape-id (:id shape)
|
|
acc' (-> acc
|
|
(update-in [:nodes table] (fnil conj [])
|
|
(shape-node-attrs table shape page-id component-ctx))
|
|
(update :edges conj {:from-table table
|
|
:from-id shape-id
|
|
:to-table parent-table
|
|
:to-id parent-id
|
|
:position position})
|
|
(update-in [:stats :shapes] inc))]
|
|
(if-let [child-ids (when (container-table? table)
|
|
(child-shape-ids shape))]
|
|
(project-shape-ids objects acc' table shape-id child-ids page-id
|
|
(descend-component-ctx table shape component-ctx))
|
|
acc')))
|
|
|
|
(defn- project-shape-ids
|
|
[objects acc parent-table parent-id child-ids page-id component-ctx]
|
|
(reduce
|
|
(fn [acc [position shape-id]]
|
|
(if-let [shape (get objects shape-id)]
|
|
(if-let [table (shape-table shape)]
|
|
(project-shape objects acc table shape parent-table parent-id position
|
|
page-id component-ctx)
|
|
(do
|
|
(l/wrn :hint "unsupported shape type for graph slice"
|
|
:shape-id (str shape-id)
|
|
:type (:type shape))
|
|
acc))
|
|
(do
|
|
(l/wrn :hint "missing shape in page objects"
|
|
:shape-id (str shape-id))
|
|
acc)))
|
|
acc
|
|
(map-indexed vector child-ids)))
|
|
|
|
(defn- project-page
|
|
[acc doc-id page position]
|
|
(let [page-id (:id page)
|
|
objects (:objects page)
|
|
root (get objects root-frame-id)
|
|
page-node (nodes/project-attrs "Page" (page-attrs page position))
|
|
acc' (-> acc
|
|
(update-in [:nodes "Page"] (fnil conj []) page-node)
|
|
(update :edges conj {:from-table "Page"
|
|
:from-id page-id
|
|
:to-table "Document"
|
|
:to-id doc-id
|
|
:position position})
|
|
(update-in [:stats :pages] inc))]
|
|
(if-let [top-level-ids (child-shape-ids root)]
|
|
(project-shape-ids objects acc' "Page" page-id top-level-ids page-id nil)
|
|
acc')))
|
|
|
|
(defn- project-component
|
|
[acc doc-id component position]
|
|
(if (:deleted component)
|
|
acc
|
|
(let [comp-id (:id component)
|
|
node (nodes/project-attrs "Component" (component-attrs component))]
|
|
(-> acc
|
|
(update-in [:nodes "Component"] (fnil conj []) node)
|
|
(update :edges conj {:from-table "Component"
|
|
:from-id comp-id
|
|
:to-table "Document"
|
|
:to-id doc-id
|
|
:position position})
|
|
(update-in [:stats :components] inc)))))
|
|
|
|
(defn- project-components
|
|
[acc doc-id components]
|
|
(reduce (fn [acc [position [_id component]]]
|
|
(project-component acc doc-id component position))
|
|
acc
|
|
(map-indexed vector components)))
|
|
|
|
(defn projection-data
|
|
"Build node/edge rows for projecting `data` into Ladybug.
|
|
|
|
Returns `{:nodes {table [attrs ...]} :edges [...] :stats {...}}`."
|
|
[data file]
|
|
(let [doc-id (or (:id data) (:id file))
|
|
doc-node (nodes/project-attrs "Document" (document-attrs file data))
|
|
pages (seq (reverse (:pages data)))
|
|
comps (seq (:components data))
|
|
acc0 (-> (initial-acc)
|
|
(update-in [:nodes "Document"] (fnil conj []) doc-node)
|
|
(assoc-in [:stats :documents] 1))
|
|
acc (cond-> acc0
|
|
(seq comps)
|
|
(project-components doc-id comps))
|
|
acc (if (empty? pages)
|
|
acc
|
|
(reduce (fn [acc [position page-id]]
|
|
(if-let [page (get-in data [:pages-index page-id])]
|
|
(project-page acc doc-id page position)
|
|
(do
|
|
(l/wrn :hint "missing page in pages-index"
|
|
:page-id (str page-id))
|
|
acc)))
|
|
acc
|
|
(map-indexed vector pages)))]
|
|
(select-keys acc [:nodes :edges :stats])))
|