Consolidate Lit repositories: figma-to-lit, figma_lit_example, lit-3d-piano, lit-calculator, lit-code-editor, lit-draggable-dom, lit-file-based-routing, lit-force-graph, lit-html-editor, lit-html-table, lit-modules, lit-native, lit-node-editor, lit-sheet-music, lit-starter-ts, lit-vscode-extension, lit-wmr, vite-lit-capacitor, vite-lit-element-starter, vite-rxdb-lit
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
import { camelCase, paramCase, pascalCase, snakeCase } from "change-case";
|
||||
// @ts-ignore
|
||||
import Mustache from 'mustache';
|
||||
|
||||
figma.codegen.on("generate", async (event) => {
|
||||
const args = await createTemplate(event.node);
|
||||
return [
|
||||
{
|
||||
language: "TYPESCRIPT",
|
||||
code: renderTemplate(args, true),
|
||||
title: "Lit (TS)",
|
||||
},
|
||||
{
|
||||
language: "JAVASCRIPT",
|
||||
code: renderTemplate(args, false),
|
||||
title: "Lit (JS)",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const names = new Map<string, SceneNode | Variable>();
|
||||
|
||||
function uniqueName(node: SceneNode | Variable) {
|
||||
for (const [key, value] of names) {
|
||||
if (value.id === node.id) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
function getName() {
|
||||
let target = node.name;
|
||||
if (target.includes('#')) {
|
||||
target = target.split('#')[0];
|
||||
}
|
||||
return pascalCase(target);
|
||||
}
|
||||
let target = getName();
|
||||
let count = 0;
|
||||
while (true) {
|
||||
if (!names.has(target)) {
|
||||
names.set(target, node);
|
||||
break;
|
||||
}
|
||||
target = `${getName()}${++count}`;
|
||||
}
|
||||
console.log(node, target);
|
||||
return target;
|
||||
}
|
||||
|
||||
function normalize(str: string) {
|
||||
const regex = /[^\w\s\d]/g;
|
||||
return str.replace(regex, '');
|
||||
}
|
||||
|
||||
function className(str: string) {
|
||||
// Convert raw name to PascalCase
|
||||
let result = normalize(str);
|
||||
if (/^\d/.test(str)) {
|
||||
result = `X ${result}`;
|
||||
}
|
||||
if (!result.endsWith("Element")) {
|
||||
result += " Element";
|
||||
}
|
||||
return pascalCase(result);
|
||||
}
|
||||
|
||||
function tagName(str: string) {
|
||||
// Convert raw name to kebab-case
|
||||
let result = normalize(str.trim());
|
||||
if (/^\d/.test(str)) {
|
||||
result = `x-${result}`;
|
||||
}
|
||||
result = paramCase(result);
|
||||
if (!result.endsWith("-element")) {
|
||||
result += "-element";
|
||||
}
|
||||
return paramCase(result);
|
||||
}
|
||||
|
||||
function propertyName(str: string) {
|
||||
let result = normalize(str);
|
||||
if (/^\d/.test(str)) {
|
||||
result = `x-${result}`;
|
||||
}
|
||||
return camelCase(result);
|
||||
}
|
||||
|
||||
const litTSTemplate = `
|
||||
import {html, css, LitElement} from 'lit';
|
||||
import {customElement, property} from 'lit/decorators.js';
|
||||
|
||||
{{#components}}
|
||||
@customElement('{{tag}}')
|
||||
export class {{name}} extends LitElement {
|
||||
static styles = css\`
|
||||
{{#styles}}
|
||||
.{{selector}} {
|
||||
{{#attributes}}
|
||||
{{key}}: {{value}};
|
||||
{{/attributes}}
|
||||
}
|
||||
{{/styles}}
|
||||
\`;
|
||||
|
||||
{{#properties}}
|
||||
@property({type: {{type}}}) {{name}} = {{value}};
|
||||
{{/properties}}
|
||||
|
||||
{{#events}}
|
||||
{{name}}(e: {{type}}) {
|
||||
console.log('{{name}} {{type}}', e);
|
||||
}
|
||||
{{/events}}
|
||||
|
||||
render() {
|
||||
return html\`
|
||||
{{#xml}}{{node}}{{/xml}}
|
||||
\`;
|
||||
}
|
||||
}
|
||||
|
||||
{{/components}}
|
||||
`.trim();
|
||||
|
||||
const litJSTemplate = `
|
||||
import {html, css, LitElement} from 'lit';
|
||||
|
||||
{{#components}}
|
||||
export class {{name}} extends LitElement {
|
||||
static styles = css\`
|
||||
{{#styles}}
|
||||
.{{selector}} {
|
||||
{{#attributes}}
|
||||
{{key}}: {{value}};
|
||||
{{/attributes}}
|
||||
}
|
||||
{{/styles}}
|
||||
\`;
|
||||
|
||||
static properties = {
|
||||
{{#properties}}
|
||||
{{name}}: {type: {{type}}},
|
||||
{{/properties}}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
{{#properties}}
|
||||
this.{{name}} = {{value}};
|
||||
{{/properties}}
|
||||
}
|
||||
|
||||
{{#events}}
|
||||
{{name}}(e: {{type}}) {
|
||||
console.log('{{name}} {{type}}', e);
|
||||
}
|
||||
{{/events}}
|
||||
|
||||
render() {
|
||||
return html\`
|
||||
{{#xml}}{{node}}{{/xml}}
|
||||
\`;
|
||||
}
|
||||
}
|
||||
customElements.define('{{tag}}', {{name}});
|
||||
{{/components}}
|
||||
`.trim();
|
||||
|
||||
interface LitTemplate {
|
||||
components: LitComponent[];
|
||||
}
|
||||
|
||||
interface LitComponent {
|
||||
tag: string;
|
||||
name: string;
|
||||
styles: LitStyle[];
|
||||
properties: LitProperty[];
|
||||
events: LitEvent[];
|
||||
node: LitNode;
|
||||
}
|
||||
|
||||
interface LitStyle {
|
||||
selector: string;
|
||||
attributes: LitStyleAttribute[];
|
||||
}
|
||||
|
||||
interface LitStyleAttribute {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface LitEvent {
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface LitProperty {
|
||||
type: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface LitNode {
|
||||
tag: string;
|
||||
attributes: LitStyleAttribute[] | null;
|
||||
children: LitNode[] | string[] | null;
|
||||
component: boolean;
|
||||
node: SceneNode,
|
||||
}
|
||||
|
||||
async function createTemplate(node: SceneNode): Promise<LitTemplate> {
|
||||
const components: LitComponent[] = [];
|
||||
|
||||
async function addComponent(target: SceneNode) {
|
||||
// const globalAttrs = getNodeProperties(target, { recursive: true });
|
||||
if (isComponent(target) || target === node) {
|
||||
const styles: LitStyle[] = [];
|
||||
const events: LitEvent[] = [];
|
||||
const properties: LitProperty[] = [];
|
||||
|
||||
async function addStyle(child: SceneNode) {
|
||||
if (isComponent(child) && target !== child) return;
|
||||
|
||||
const css = await child.getCSSAsync();
|
||||
styles.push({
|
||||
selector: tagName(uniqueName(child)),
|
||||
attributes: Object.entries(css).map((([key, value]) => ({ key, value })))
|
||||
});
|
||||
|
||||
if ('children' in child) {
|
||||
for (const item of child.children) {
|
||||
await addStyle(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await addStyle(target);
|
||||
|
||||
const attrs = getNodeProperties(target, { recursive: true });
|
||||
for (const [key, value] of Object.entries({ ...attrs })) {
|
||||
properties.push({
|
||||
type: 'String',
|
||||
name: propertyName(key),
|
||||
value: typeof value === 'string' ? `'${value}'` : value,
|
||||
});
|
||||
}
|
||||
|
||||
function addEvents(child: SceneNode) {
|
||||
if ('reactions' in child) {
|
||||
const reactions = child.reactions;
|
||||
for (const item of reactions) {
|
||||
if (item.trigger) {
|
||||
const type = item.trigger.type;
|
||||
if (type === 'ON_CLICK') {
|
||||
const eventName = propertyName(`${snakeCase(uniqueName(child))}-${type}`);
|
||||
events.push({
|
||||
name: eventName,
|
||||
type: 'Event',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('children' in child) {
|
||||
for (const item of child.children) {
|
||||
if (!isComponent(item)) {
|
||||
addEvents(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEvents(target);
|
||||
|
||||
const component: LitComponent = {
|
||||
node: nodeTree(target),
|
||||
name: className(uniqueName(target)),
|
||||
tag: tagName(uniqueName(target)),
|
||||
styles,
|
||||
events,
|
||||
properties,
|
||||
};
|
||||
components.push(component);
|
||||
|
||||
if ('children' in target) {
|
||||
for (const child of target.children) {
|
||||
await addComponent(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await addComponent(node);
|
||||
|
||||
return { components };
|
||||
}
|
||||
|
||||
function renderXml(node: LitNode) {
|
||||
const sb: string[] = [];
|
||||
if (node.component) {
|
||||
if (node.attributes) {
|
||||
sb.push(`<${node.tag} `)
|
||||
for (const attr of node.attributes) {
|
||||
sb.push(` ${propertyName(attr.key)}=\${this.${propertyName(attr.key)}} `)
|
||||
}
|
||||
sb.push(`>`)
|
||||
} else {
|
||||
sb.push(`<${node.tag}>`)
|
||||
}
|
||||
sb.push(`</${node.tag}>`)
|
||||
} else {
|
||||
let events = false;
|
||||
let element = 'div';
|
||||
if ('reactions' in node.node) {
|
||||
const reactions = node.node.reactions;
|
||||
events = reactions.length > 0;
|
||||
for (const item of reactions) {
|
||||
if (item.trigger) {
|
||||
if (
|
||||
item.trigger.type === "ON_CLICK" ||
|
||||
item.trigger.type === "ON_HOVER" ||
|
||||
item.trigger.type === "ON_PRESS" ||
|
||||
item.trigger.type === "MOUSE_DOWN"
|
||||
) {
|
||||
element = 'button';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (events) {
|
||||
sb.push(`<${element} class="${node.tag}"`)
|
||||
if ('reactions' in node.node) {
|
||||
const reactions = node.node.reactions;
|
||||
for (const item of reactions) {
|
||||
if (item.trigger) {
|
||||
const type = item.trigger.type;
|
||||
if (type === 'ON_CLICK') {
|
||||
const eventName = propertyName(`${snakeCase(uniqueName(node.node))}-${type}`);
|
||||
sb.push(` @click=\${this.${eventName}}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.push(`>`)
|
||||
} else {
|
||||
sb.push(`<${element} class="${node.tag}">`)
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (typeof child === 'string') {
|
||||
let bound = false;
|
||||
if (node.node.boundVariables?.characters) {
|
||||
const variable = figma.variables.getVariableById(node.node.boundVariables.characters.id);
|
||||
if (variable) {
|
||||
sb.push(` \${this.${propertyName(uniqueName(variable))}}`);
|
||||
bound = true;
|
||||
}
|
||||
} else if (node.node.componentPropertyReferences?.characters) {
|
||||
const label = node.node.componentPropertyReferences.characters.split('#')[0];
|
||||
sb.push(` \${this.${propertyName(label)}}`);
|
||||
bound = true;
|
||||
}
|
||||
if (!bound) sb.push(` ${child}`);
|
||||
} else {
|
||||
sb.push(` ${renderXml(child)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.push(`</${element}>`)
|
||||
}
|
||||
return sb.join('\n');
|
||||
}
|
||||
|
||||
function renderTemplate(args: LitTemplate, typescript: boolean = true) {
|
||||
const template = typescript ? litTSTemplate : litJSTemplate;
|
||||
Mustache.escape = function (text: string) { return text; };
|
||||
return Mustache.render(template, {
|
||||
...args,
|
||||
"xml": function () {
|
||||
return () => renderXml(this.node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeProperties(node: SceneNode, options?: {
|
||||
recursive?: boolean
|
||||
}) {
|
||||
const attributes: { [key: string]: string } = {};
|
||||
|
||||
if (node.type === 'COMPONENT') {
|
||||
const properties = node.componentPropertyDefinitions;
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
const label = key.split('#')[0];
|
||||
attributes[label] = `${value.defaultValue}`;
|
||||
}
|
||||
}
|
||||
if (node.type === 'INSTANCE') {
|
||||
const properties = node.componentProperties;
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
const label = key.split('#')[0];
|
||||
attributes[label] = `${value.value}`;
|
||||
}
|
||||
}
|
||||
if (node.boundVariables) {
|
||||
const variables = node.boundVariables!
|
||||
if (variables?.characters) {
|
||||
const variable = figma.variables.getVariableById(variables.characters.id);
|
||||
if (variable) {
|
||||
attributes[uniqueName(variable)] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const references = node.componentPropertyReferences;
|
||||
if (references?.characters) {
|
||||
const label = references.characters.split('#')[0];
|
||||
attributes[label] = attributes[label] ?? label;
|
||||
}
|
||||
|
||||
if (options?.recursive) {
|
||||
if ('children' in node) {
|
||||
for (const child of node.children) {
|
||||
const attrs = getNodeProperties(child, { recursive: true });
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
attributes[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function nodeTree(node: SceneNode, root: boolean = true): LitNode {
|
||||
const children: LitNode[] = [];
|
||||
const attributes = getNodeProperties(node, { recursive: true });
|
||||
if ('children' in node) {
|
||||
for (const child of node.children) {
|
||||
children.push(nodeTree(child, false));
|
||||
}
|
||||
}
|
||||
const attrs = Object.entries(attributes).map(([key, value]) => ({ key, value }));
|
||||
let str: string | null = null;
|
||||
if ('characters' in node) {
|
||||
str = node.characters;
|
||||
}
|
||||
return {
|
||||
node,
|
||||
tag: tagName(uniqueName(node)),
|
||||
children: children.length === 0 ? (str ? [str] : null) : children,
|
||||
attributes: attrs.length === 0 ? null : attrs,
|
||||
component: isComponent(node) && !root,
|
||||
};
|
||||
}
|
||||
|
||||
function isComponent(node: SceneNode) {
|
||||
return (
|
||||
node.type === "COMPONENT" ||
|
||||
node.type === "INSTANCE" ||
|
||||
node.type === 'FRAME'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user