@jag-k/scriptable-jsx
Version:
Create a Scriptable bundle from JSX
400 lines (396 loc) • 13 kB
JavaScript
// =============================
// Widget JSX Element processing
// =============================
function processJSXColor(color) {
return typeof color === "string"
? new Color(color, 1)
: typeof color === "number"
? new Color("#" + color.toString(16).padStart(6, "0"), 1)
: color;
}
function processPaddingProps(widget, props, defaultPadding) {
widget.useDefaultPadding();
const paddings = defaultPadding || [0, 0, 0, 0];
if (props["p-all"] !== undefined) {
paddings[0] = props["p-all"];
paddings[1] = props["p-all"];
paddings[2] = props["p-all"];
paddings[3] = props["p-all"];
}
if (props["p-y"] !== undefined) {
paddings[0] = props["p-y"];
paddings[2] = props["p-y"];
}
if (props["p-x"] !== undefined) {
paddings[1] = props["p-x"];
paddings[3] = props["p-x"];
}
if (props["p-top"] !== undefined) {
paddings[0] = props["p-top"];
}
if (props["p-right"] !== undefined) {
paddings[1] = props["p-right"];
}
if (props["p-bottom"] !== undefined) {
paddings[2] = props["p-bottom"];
}
if (props["p-left"] !== undefined) {
paddings[3] = props["p-left"];
}
widget.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
}
function processTextProps(text, props) {
switch (props.align) {
case "left":
text.leftAlignText();
break;
case "right":
text.rightAlignText();
break;
case "center":
text.centerAlignText();
break;
}
if (props.font) {
text.font = props.font;
}
if (props.color) {
text.textColor = processJSXColor(props.color);
}
if (props.lineLimit) {
text.lineLimit = props.lineLimit;
}
if (props.minimumScaleFactor) {
text.minimumScaleFactor = props.minimumScaleFactor;
}
if (props.opacity) {
text.textOpacity = props.opacity;
}
if (props.shadowColor) {
text.shadowColor = processJSXColor(props.shadowColor);
}
if (props.shadowOffset) {
text.shadowOffset = props.shadowOffset;
}
if (props.shadowRadius) {
text.shadowRadius = props.shadowRadius;
}
if (props.url) {
text.url = props.url;
}
}
function processStackProps(stack, props) {
switch (props.align) {
case "top":
stack.topAlignContent();
break;
case "bottom":
stack.bottomAlignContent();
break;
case "center":
stack.centerAlignContent();
break;
}
if (props.backgroundColor) {
stack.backgroundColor = processJSXColor(props.backgroundColor);
}
if (props.backgroundGradient) {
stack.backgroundGradient = props.backgroundGradient;
}
if (props.backgroundImage) {
stack.backgroundImage = props.backgroundImage;
}
if (props.borderColor) {
stack.borderColor = processJSXColor(props.borderColor);
}
if (props.borderWidth) {
stack.borderWidth = props.borderWidth;
}
if (props.cornerRadius) {
stack.cornerRadius = props.cornerRadius;
}
if (props.size) {
stack.size = props.size;
}
if (props.spacing) {
stack.spacing = props.spacing;
}
if (props.url) {
stack.url = props.url;
}
processPaddingProps(stack, props, [0, 0, 0, 0]);
}
function processImageProps(image, props) {
switch (props.align) {
case "left":
image.leftAlignImage();
break;
case "right":
image.rightAlignImage();
break;
case "center":
image.centerAlignImage();
break;
}
if (props.size) {
image.imageSize = props.size;
}
if (props.borderColor) {
image.borderColor = processJSXColor(props.borderColor);
}
if (props.borderWidth) {
image.borderWidth = props.borderWidth;
}
if (props.containerRelativeShape) {
image.containerRelativeShape = props.containerRelativeShape;
}
switch (props.contentMode) {
case "filling":
image.applyFillingContentMode();
break;
case "fitting":
image.applyFittingContentMode();
break;
}
if (props.cornerRadius) {
image.cornerRadius = props.cornerRadius;
}
if (props.opacity) {
image.imageOpacity = props.opacity;
}
if (props.resizable) {
image.resizable = props.resizable;
}
if (props.tintColor) {
image.tintColor = processJSXColor(props.tintColor);
}
if (props.url) {
image.url = props.url;
}
}
function processWidgetProps(widget, props) {
if (props.backgroundColor) {
widget.backgroundColor = processJSXColor(props.backgroundColor);
}
if (props.backgroundGradient) {
widget.backgroundGradient = props.backgroundGradient;
}
if (props.backgroundImage) {
widget.backgroundImage = props.backgroundImage;
}
if (props.refreshAfterDate) {
widget.refreshAfterDate = props.refreshAfterDate;
}
if (props.spacing !== undefined) {
widget.spacing = props.spacing;
}
if (props.url) {
widget.url = props.url;
}
processPaddingProps(widget, props, [15, 15, 15, 15]);
}
function processContainerChildren(widget, children) {
for (const child of children) {
if (child === null || child === undefined || child === false ||
typeof child === "function") {
continue;
}
else if (typeof child === "string" || typeof child === "number" ||
typeof child === "bigint" || typeof child === "symbol" || child === true) {
widget.addText(String(child));
}
else if (typeof child === "object") {
if (child instanceof Array) {
processContainerChildren(widget, child);
}
else if (child instanceof Date) {
widget.addDate(child);
}
else if (child instanceof Image) {
widget.addImage(child);
}
else if ("type" in child) {
switch (child.type) {
case "text": {
const text = widget.addText(child.text);
processTextProps(text, child.props || {});
break;
}
case "date": {
const init = child.props.date;
const date = widget.addDate(init instanceof Date ? init : new Date(init));
switch (child.props.style) {
case "date":
date.applyDateStyle();
break;
case "offset":
date.applyOffsetStyle();
break;
case "time":
date.applyTimeStyle();
break;
case "relative":
date.applyRelativeStyle();
break;
case "timer":
date.applyTimerStyle();
break;
}
processTextProps(date, child.props || {});
break;
}
case "stack": {
const stack = widget.addStack();
switch (child?.props?.layout) {
case "vertical":
stack.layoutVertically();
break;
case "horizontal":
stack.layoutHorizontally();
break;
}
processStackProps(stack, child.props || {});
processContainerChildren(stack, child.children);
break;
}
case "spacer": {
const spacer = widget.addSpacer();
if (child?.props?.size) {
spacer.length = child.props.size;
}
break;
}
case "image": {
let init;
const { data } = child.props || {};
if (typeof data === "string") {
init = Image.fromFile(child.props.fileURL);
}
else if (data.size) {
init = data;
}
else {
init = Image.fromData(data);
}
const image = widget.addImage(init);
processImageProps(image, child.props || {});
break;
}
}
}
}
}
}
function widgetCreateElement(element, props, ...children) {
if (typeof element === "string") {
// intrinsic element
switch (element) {
case "widget":
const widget = new ListWidget();
processContainerChildren(widget, children);
processWidgetProps(widget, props || {});
return widget;
case "text":
let text = "";
for (let child of children) {
if (typeof child === "string" ||
typeof child === "number" ||
typeof child === "bigint") {
text += child;
}
}
return { type: element, props, text };
case "stack":
return { type: element, props, children };
case "spacer":
return { type: element, props };
case "image":
return { type: element, props };
case "date":
return { type: element, props };
}
}
}
// =============================
// Alert JSX Element processing
// =============================
function processAlertProps(alert, props) {
if (props.title) {
alert.title = props.title;
}
if (props.message) {
alert.message = props.message;
}
}
function processActionProps(alert, props, children) {
const text = props.text || (children instanceof Array ? children.join(" ") : children) || "";
switch (props.type || "action") {
case "action": {
alert.addAction(text);
break;
}
case "cancel": {
alert.addCancelAction(text);
break;
}
case "destructive": {
alert.addDestructiveAction(text);
break;
}
}
}
function processTextFieldProps(alert, props) {
(props.secure ? alert.addSecureTextField : alert.addTextField)(props.placeholder || "", props.text || "");
}
function processAlertChildren(alert, children) {
for (const child of children) {
if (typeof child === "string") {
return child;
}
if (typeof child === "object") {
if (child instanceof Array) {
processAlertChildren(alert, child);
}
else if ("type" in child) {
switch (child.type) {
case "action": {
processActionProps(alert, child.props || {}, child.children || "");
break;
}
case "text-field": {
processTextFieldProps(alert, child.props || {});
break;
}
}
}
}
}
}
function alertCreateElement(element, props, ...children) {
if (typeof element === "string") {
// intrinsic element
switch (element) {
case "alert":
const alert = new Alert();
processAlertChildren(alert, children);
processAlertProps(alert, props || {});
return alert;
case "text-field":
return { type: element, props };
case "action":
return { type: element, props, children };
}
}
}
const createElements = [widgetCreateElement, alertCreateElement];
class ScriptableJSX {
static createElement(element, props, ...children) {
for (const creator of createElements) {
const res = creator(element, props, ...children);
if (res) {
return res;
}
}
}
}
export { ScriptableJSX };