SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
3 days agolast push
HTML 98.9% Python 0.6%
9.3 KB · 110 lines javascript
Raw Blame History
1#!/usr/bin/env node2// -----------------------------------------------------------------------------3// Lou-Ka — kit Figma · test/run-mock.mjs4// Exécute plugin/code.js hors Figma avec une maquette minimale de l'API5// (frames, textes, styles, variables, pages) pour attraper les erreurs de6// logique JavaScript avant l'import dans Figma. Ne valide pas le rendu.7// Usage : node plugin/test/run-mock.mjs8// -----------------------------------------------------------------------------9import { readFileSync } from "node:fs";10import vm from "node:vm";11import path from "node:path";1213const ROOT = new URL("../..", import.meta.url).pathname;14const code = readFileSync(path.join(ROOT, "plugin/code.js"), "utf8");1516const stats = { nodes: 0, texts: 0, components: 0, sets: 0, styles: 0, variables: 0, images: 0, pages: 0, svg: 0, notes: [] };17const FONTS = { "Space Grotesk": ["Regular", "Medium", "SemiBold", "Bold"], Inter: ["Regular", "Medium", "Semi Bold", "Bold"], "JetBrains Mono": ["Regular", "Medium", "Bold"] };1819class Node {20  constructor(type) {21    this.type = type; this.id = String(++stats.nodes); this.name = type; this.children = []; this.parent = null;22    this.width = 100; this.height = 100; this.x = 0; this.y = 0; this.fills = []; this.strokes = []; this.effects = [];23    this.layoutMode = "NONE"; this.opacity = 1; this.rotation = 0; this.visible = true;24  }25  resize(w, h) { if (!(w > 0) || !(h > 0) || Number.isNaN(w) || Number.isNaN(h)) throw new Error(`resize(${w}, ${h}) invalide sur « ${this.name} »`); this.width = w; this.height = h; }26  rescale(s) { this.width *= s; this.height *= s; }27  appendChild(c) { if (!c || !(c instanceof Node)) throw new Error(`appendChild(${c}) sur « ${this.name} »`); if (c.parent) c.parent.children = c.parent.children.filter((k) => k !== c); c.parent = this; this.children.push(c); this._fit(); }28  remove() { if (this.parent) this.parent.children = this.parent.children.filter((k) => k !== this); this.parent = null; }29  findOne(fn) { for (const c of this.children) { if (fn(c)) return c; const r = c.findOne && c.findOne(fn); if (r) return r; } return null; }30  set layoutSizingHorizontal(v) { if (!this.parent || this.parent.layoutMode === "NONE") throw new Error(`layoutSizingHorizontal=${v} hors auto-layout sur « ${this.name} »`); this._lsh = v; }31  get layoutSizingHorizontal() { return this._lsh; }32  set layoutSizingVertical(v) { if (!this.parent || this.parent.layoutMode === "NONE") throw new Error(`layoutSizingVertical hors auto-layout sur « ${this.name} »`); this._lsv = v; }33  get layoutSizingVertical() { return this._lsv; }34  set layoutGrow(v) { if (!this.parent || this.parent.layoutMode === "NONE") throw new Error(`layoutGrow hors auto-layout sur « ${this.name} »`); this._lg = v; }35  get layoutGrow() { return this._lg; }36  set layoutPositioning(v) { if (v === "ABSOLUTE" && (!this.parent || this.parent.layoutMode === "NONE")) throw new Error(`layoutPositioning ABSOLUTE hors auto-layout sur « ${this.name} »`); this._lp = v; }37  get layoutPositioning() { return this._lp || "AUTO"; }38  // approximation grossière de l'auto-layout AUTO (hug) pour des tailles plausibles39  _fit() {40    if (this.layoutMode === "NONE") return;41    const kids = this.children.filter((k) => k.layoutPositioning !== "ABSOLUTE");42    const pt = this.paddingTop || 0, pr = this.paddingRight || 0, pb = this.paddingBottom || 0, pl = this.paddingLeft || 0, gap = this.itemSpacing || 0;43    const H = this.layoutMode === "HORIZONTAL";44    const main = kids.reduce((a, k) => a + (H ? k.width : k.height), 0) + gap * Math.max(0, kids.length - 1);45    const cross = kids.reduce((a, k) => Math.max(a, H ? k.height : k.width), 0);46    const w = H ? main + pl + pr : cross + pl + pr, h = H ? cross + pt + pb : main + pt + pb;47    if (H) { if (this.primaryAxisSizingMode !== "FIXED") this.width = Math.max(1, w); if (this.counterAxisSizingMode !== "FIXED") this.height = Math.max(1, h); }48    else { if (this.primaryAxisSizingMode !== "FIXED") this.height = Math.max(1, h); if (this.counterAxisSizingMode !== "FIXED") this.width = Math.max(1, w); }49  }50}51class Text extends Node {52  constructor() { super("TEXT"); stats.texts++; this._chars = ""; this.fontSize = 12; this._fontName = null; }53  set fontName(f) { if (!f || !FONTS[f.family] || !FONTS[f.family].includes(f.style)) throw new Error(`fontName non chargée : ${JSON.stringify(f)}`); this._fontName = f; this._measure(); }54  get fontName() { return this._fontName; }55  set characters(v) { if (!this._fontName) throw new Error("characters sans fontName"); this._chars = String(v); this._measure(); }56  get characters() { return this._chars; }57  set fontSize(v) { this._fs = v; this._measure(); }58  get fontSize() { return this._fs; }59  set textAutoResize(v) { this._tar = v; }60  get textAutoResize() { return this._tar; }61  _measure() { if (this._tar === "HEIGHT") return; const lines = this._chars.split("\n"); const longest = Math.max(1, ...lines.map((l) => l.length)); this.width = longest * (this._fs || 12) * 0.55; this.height = lines.length * (this._fs || 12) * 1.3; }62  resize(w, h) { super.resize(w, h); const cpl = Math.max(1, Math.floor(w / ((this._fs || 12) * 0.55))); this.height = Math.max(1, Math.ceil(this._chars.length / cpl)) * (this._fs || 12) * 1.5; }63  _check(a, b) { if (a < 0 || b > this._chars.length || a > b) throw new Error(`plage [${a},${b}) hors « ${this._chars} »`); }64  setRangeFills(a, b) { this._check(a, b); } setRangeFontName(a, b, f) { this._check(a, b); if (!FONTS[f.family] || !FONTS[f.family].includes(f.style)) throw new Error("setRangeFontName non chargée"); } setRangeFontSize(a, b) { this._check(a, b); }65}66class Page extends Node { constructor() { super("PAGE"); stats.pages++; } }67const pages = [];68const figma = {69  currentPage: null,70  root: { children: pages },71  createFrame: () => new Node("FRAME"),72  createComponent: () => { stats.components++; return new Node("COMPONENT"); },73  createRectangle: () => new Node("RECTANGLE"),74  createEllipse: () => new Node("ELLIPSE"),75  createText: () => new Text(),76  createPage: () => { const p = new Page(); pages.push(p); return p; },77  createNodeFromSvg: (markup) => { if (typeof markup !== "string" || !markup.includes("<svg")) throw new Error("SVG invalide"); if (/NaN|undefined/.test(markup)) throw new Error("SVG contient NaN/undefined : " + markup.slice(0, 120)); stats.svg++; const n = new Node("FRAME"); const w = +(markup.match(/width="([\d.]+)"/) || [])[1] || 24, h = +(markup.match(/height="([\d.]+)"/) || [])[1] || 24; n.width = w; n.height = h; return n; },78  createImage: (bytes) => { if (!(bytes instanceof Uint8Array) || !bytes.length) throw new Error("createImage sans octets"); stats.images++; return { hash: "img" + stats.images }; },79  base64Decode: (s) => Uint8Array.from(Buffer.from(s, "base64")),80  combineAsVariants: (nodes, parent) => { stats.sets++; const s = new Node("COMPONENT_SET"); for (const n of nodes) { if (n.type !== "COMPONENT") throw new Error("combineAsVariants : nœud non composant"); if (!/=/.test(n.name)) throw new Error(`variante sans « Propriété=Valeur » : ${n.name}`); s.appendChild(n); } parent.appendChild(s); return s; },81  createPaintStyle: () => { stats.styles++; return {}; }, createTextStyle: () => { stats.styles++; return {}; }, createEffectStyle: () => { stats.styles++; return {}; },82  variables: {83    createVariableCollection: (name) => ({ name, modes: [{ modeId: "m1", name: "Mode 1" }], renameMode() {} }),84    createVariable: (name, col, type) => { if (!["COLOR", "FLOAT", "STRING", "BOOLEAN"].includes(type)) throw new Error("type de variable"); stats.variables++; return { name, setValueForMode(mode, v) { if (type === "COLOR" && (v.r == null || v.a == null)) throw new Error(`valeur COLOR invalide pour ${name}`); if (type === "FLOAT" && typeof v !== "number") throw new Error(`FLOAT invalide ${name}`); } }; },85  },86  loadFontAsync: async (f) => { if (!FONTS[f.family] || !FONTS[f.family].includes(f.style)) throw new Error(`police absente ${f.family} ${f.style}`); },87  setCurrentPageAsync: async (p) => { figma.currentPage = p; },88  viewport: { scrollAndZoomIntoView() {} },89  notify: (m) => stats.notes.push(m),90  closePlugin: () => { done(); },91};92figma.currentPage = figma.createPage();9394let done; const finished = new Promise((r) => (done = r));95const sandbox = { figma, console, setTimeout, Promise, Math, JSON, Date, Number, String, Object, Array, Buffer, Uint8Array, Error, parseInt, RegExp };96vm.createContext(sandbox);97const t0 = Date.now();98try {99  vm.runInContext(code, sandbox, { filename: "code.js" });100  await Promise.race([finished, new Promise((_, rej) => setTimeout(() => rej(new Error("timeout 60 s")), 60000))]);101} catch (e) { console.error("✗ ÉCHEC :", e.stack || e); process.exit(1); }102103const err = stats.notes.find((n) => /^Erreur/.test(n));104if (err) { console.error("✗ Le plugin a signalé :", err); process.exit(1); }105const count = (p, pred) => { let n = 0; const walk = (k) => { if (pred(k)) n++; (k.children || []).forEach(walk); }; walk(p); return n; };106console.log(`✓ code.js exécuté sans erreur en ${Date.now() - t0} ms`);107console.log(`  pages : ${pages.map((p) => `${p.name} (${count(p, () => true) - 1} nœuds)`).join(" · ")}`);108console.log(`  nœuds ${stats.nodes} · textes ${stats.texts} · composants ${stats.components} · jeux de variantes ${stats.sets} · styles ${stats.styles} · variables ${stats.variables} · images ${stats.images} · svg ${stats.svg}`);109console.log(`  notifications : ${stats.notes.join(" | ")}`);110