|
const widgetJSX = (() => { |
|
const re = { |
|
html: /<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>/g, |
|
attr: /([a-zA-Z0-9]+)="(.*?)"/g, |
|
tag: /(<|<\/)([a-zA-Z][a-zA-Z0-9]*)([^>]*?)(\/>|>)/s, |
|
color: /^(#(?:[0-9a-fA-F]{3}){1,2})$/s, |
|
colors: /^(#(?:[0-9a-fA-F]{3}){1,2})\|(#(?:[0-9a-fA-F]{3}){1,2})$/s, |
|
gradient: /(#(?:[0-9a-fA-F]{3}){1,2}):([+-]?\d+(\.\d+)?)/g, |
|
url: /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/s, |
|
} |
|
|
|
const get = { |
|
color: string => { |
|
if (re.colors.test(string)) { |
|
const [c1, c2] = string.split('|') |
|
return Color.dynamic(new Color(c1), new Color(c2)) |
|
} |
|
if (re.color.test(string)) { |
|
return (new Color(string)) |
|
} |
|
return null |
|
}, |
|
} |
|
|
|
const set = { |
|
background: async (element, string, dark, light) => { |
|
if (re.gradient.test(string)) { |
|
const match = [...string.matchAll(/(#(?:[0-9a-fA-F]{3}){1,2}):([+-]?\d+(\.\d+)?)/g)] |
|
const colors = match.map(v => ({ color: v[1], location: parseFloat(v[2]) })) |
|
if (colors.length < 2) return null |
|
const gradient = new LinearGradient() |
|
gradient.locations = colors.map(v => v.location) |
|
gradient.colors = colors.map(v => new Color(v.color)) |
|
element.backgroundGradient = gradient |
|
return |
|
} |
|
if (re.url.test(string)) { |
|
const req = new Request(string) |
|
const image = await req.loadImage() |
|
element.backgroundImage = image |
|
return |
|
} |
|
const color = get.color(string) |
|
if (color) element.backgroundColor = color |
|
}, |
|
alignment: (align, parent, addElement) => { |
|
if (align) { |
|
const stack = parent.addStack() |
|
stack.layoutHorizontally() |
|
if (align !== 'left') stack.addSpacer() |
|
const el = addElement(stack) |
|
if (align !== 'right') stack.addSpacer() |
|
return el |
|
} else { |
|
const el = addElement(parent) |
|
return el |
|
} |
|
}, |
|
border: (element, string) => { |
|
if (!re.gradient.test(string)) return |
|
const match = [...string.matchAll(/(#(?:[0-9a-fA-F]{3}){1,2}):([+-]?\d+(\.\d+)?)/g)] |
|
element.borderWidth = parseFloat(match[0][2]) |
|
element.borderColor = new Color(match[0][1]) |
|
}, |
|
} |
|
|
|
const render = { |
|
Widget: async ({ attributes, children }) => { |
|
const { background = '#1b1b1b', padding, size = 'small' } = attributes |
|
|
|
const widget = new ListWidget() |
|
await set.background(widget, background) |
|
|
|
if (padding) widget.setPadding(...padding.split(',').map(v => parseFloat(v))) |
|
|
|
for (const child of children) { |
|
if (render[child.tag]) await render[child.tag]({ parent: widget, ...child }) |
|
} |
|
|
|
if (size === 'small') await widget.presentSmall() |
|
if (size === 'medium') await widget.presentMedium() |
|
if (size === 'large') await widget.presentLarge() |
|
|
|
return widget |
|
}, |
|
|
|
Stack: async ({ parent, attributes, children }) => { |
|
const { layout, align, size, background, cornerRadius, border } = attributes |
|
|
|
const stack = parent.addStack() |
|
await set.background(stack, background) |
|
|
|
if (size) stack.size = new Size(...size.split(',').map(v => parseFloat(v))) |
|
if (cornerRadius) stack.cornerRadius = parseFloat(cornerRadius) |
|
if (border) set.border(stack, border) |
|
|
|
const renderChildren = async () => { |
|
for (const child of children) { |
|
if (render[child.tag]) await render[child.tag]({ parent: stack, ...child }) |
|
} |
|
} |
|
|
|
if (layout === 'vertical') { |
|
stack.layoutVertically() |
|
if (align === 'center') stack.centerAlignContent() |
|
if (align === 'end') stack.bottomAlignContent() |
|
if (align === 'start') stack.topAlignContent() |
|
await renderChildren() |
|
} |
|
|
|
if (layout === 'horizontal') { |
|
stack.layoutHorizontally() |
|
if (align !== 'start') stack.addSpacer() |
|
await renderChildren() |
|
if (align !== 'end') stack.addSpacer() |
|
} |
|
|
|
return stack |
|
}, |
|
|
|
Text: async ({ parent, attributes }) => { |
|
const { string = '', size = 16, weight = 'regular', color, lines, align, opacity } = attributes |
|
|
|
const text = set.alignment(align, parent, element => element.addText(string)) |
|
if (align) text[`${align}AlignText`]() |
|
|
|
if (weight === 'regular') text.font = Font.regularSystemFont(parseFloat(size)) |
|
if (weight === 'bold') text.font = Font.boldSystemFont(parseFloat(size)) |
|
|
|
const clr = get.color(color) |
|
if (clr) text.textColor = clr |
|
|
|
if (opacity) text.textOpacity = parseFloat(opacity) |
|
if (lines) text.lineLimit = parseInt(lines) |
|
|
|
return text |
|
}, |
|
|
|
Spacer: async ({ parent, attributes }) => { |
|
const { length } = attributes |
|
const spacer = parent.addSpacer(length && parseFloat(length)) |
|
|
|
return spacer |
|
}, |
|
|
|
Image: async ({ parent, attributes }) => { |
|
const { src, size, cornerRadius, opacity, align } = attributes |
|
if (!src) return null |
|
|
|
const req = new Request(src) |
|
const img = await req.loadImage() |
|
const image = set.alignment(align, parent, element => element.addImage(img)) |
|
|
|
if (size) image.imageSize = new Size(...size.split(',').map(v => parseFloat(v))) |
|
if (opacity) image.imageOpacity = parseFloat(opacity) |
|
if (cornerRadius) image.cornerRadius = parseFloat(cornerRadius) |
|
|
|
return image |
|
} |
|
} |
|
|
|
const init = async string => { |
|
const getAttributes = input => { |
|
const attrubutes = [...input.matchAll(re.attr)] |
|
return attrubutes.reduce((acc, v) => ({ ...acc, [v[1]]: v[2] }), {}) |
|
} |
|
|
|
const filter = (objects, depth = 0) => { |
|
return !objects ? null : objects.map(item => { |
|
if (item.depth > depth) return null |
|
if (item.children) item.children = filter(item.children, item.depth + 1) |
|
return item |
|
}).filter(i => i !== null) |
|
} |
|
|
|
const removeDepth = objects => { |
|
return objects.map(item => { |
|
delete item.depthopearaopeop |
|
if (item.children) item.children = removeDepth(item.children) |
|
return item |
|
}) |
|
} |
|
|
|
const toArray = [...string.matchAll(re.html)].map(v => v[0]) |
|
|
|
const { content } = toArray.reduce(({ content, depth }, input) => { |
|
const [, start, tag, attrs, end] = input.match(re.tag) || [] |
|
const attributes = getAttributes(attrs) |
|
if (start === '<' && end !== '/>') { |
|
content.push({ tag, children: [], attributes, depth: depth++ }) |
|
} else if (start === '<' && end === '/>') { |
|
content.push({ tag, attributes, depth }) |
|
} else if (start === '</') { |
|
depth -= 1 |
|
} |
|
return { content, depth } |
|
}, { content: [], depth: 0 }) |
|
|
|
content.forEach((item, index) => { |
|
const sliced = content.slice(index + 1) |
|
for (let i = 0; i < sliced.length; i++) { |
|
if (sliced[i].depth > item.depth) item.children.push(sliced[i]) |
|
else break |
|
} |
|
}) |
|
|
|
const parsed = removeDepth(filter(content)) |
|
const widget = parsed[0] |
|
|
|
widget.component = await render.Widget(widget) |
|
|
|
Script.setWidget(widget.component) |
|
Script.complete() |
|
} |
|
|
|
return init |
|
})() |
|
|
|
widgetJSX(` |
|
<Widget size="medium" background="#1b1b1b:0.5,#1b1b4b:1" padding="10,10,10,10"> |
|
<Stack layout="horizontal" align="center"> |
|
<Image src="https://docs.scriptable.app/img/glyph.png" size="32,32" /> |
|
<Spacer length="15" /> |
|
<Stack layout="vertical" align="center"> |
|
<Text string="Hello World" /> |
|
<Text string="Scriptable JSX Widget" size="14" opacity="0.5" /> |
|
</Stack> |
|
</Stack> |
|
</Widget> |
|
`) |