├── README.md ├── RoundedIcon.png ├── WidgetMarkup.js ├── WidgetMarkup.min.js ├── examples ├── Calendar.js ├── WidgetMarkup_Indie_Dev_Monday.js ├── WidgetMarkup_Random_Scriptable_API.js └── WidgetMarkup_Time_Progress.js └── vscode-markup-highlighting.png /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Widget Markup 3 | 4 | Widget Markup for Scriptable 5 | Write Scriptable widgets with markup. 6 | 7 | # Installation 8 | 9 | Just import the library script to your scriptable widget file. 10 | 11 | ```jsx 12 | const { widgetMarkup, concatMarkup } = importModule('WidgetMarkup'); 13 | ``` 14 | Alternatively if you would rather have just one file for your widget, you can copy the contents of `WidgetMarkup.min.js` and paste it on top of your widget file. Then import it like so... 15 | ```jsx 16 | const { widgetMarkup, concatMarkup } = WidgetMarkup; 17 | ``` 18 | 19 | # Usage 20 | The library exposes only two template literal tags, `widgetMarkup` and `concatMarkup`. 21 | 22 | ## `widgetMarkup` 23 | 24 | This is the primary markup parser. This is where you place your markup tags and is the main body of your widget. It will return an instance of Scriptable's `ListWidget` class. 25 | 26 | ```jsx 27 | const widget = await widgetMarkup` 28 | 29 | 30 | 31 | Hi I'm a widget 👋 32 | 33 | 34 | 35 | `; 36 | if (config.runsInWidget) { 37 | // Runs inside a widget so add it to the homescreen 38 | Script.setWidget(widget); 39 | } 40 | Script.complete() 41 | ``` 42 | 43 | ## `concatMarkup` 44 | 45 | This method is used to concatenate markup elements to the main widget body. This method is used to ensure that the parser can properly set the styles of the elements being concatenated. 46 | 47 | ```jsx 48 | 49 | const textElement = concatMarkup`I'm from the outside.` 50 | 51 | const widget = await widgetMarkup` 52 | 54 | ${textElement} 55 | 56 | 57 | `; 58 | ``` 59 | 60 | # Tags Supported 61 | 62 | Currently the library only supports the following tags 63 | 64 | - `` — The parent element of the widget. There can only be one widget tag per widget. This is the container for all the elements in the widget. 65 | - `` — The equivalent of Scriptable's [.addStack()](https://docs.scriptable.app/widgetstack/) method. This tag can have other tags inside as children. It could also have other stack tags as children. 66 | 67 | ```xml 68 | 69 | Hello World 70 | 71 | 72 | ``` 73 | 74 | - `` — The equivalent of Scriptable's [.addText()](https://docs.scriptable.app/widgettext/) method. Place widget text between this tag. 75 | 76 | ```xml 77 | Hi I'm a widget text 78 | ``` 79 | 80 | - `` — The equivalent of Scriptable's [addImage()](https://docs.scriptable.app/widgetimage/) method. Used to add image to the widget. This tag requires a `src` attribute which takes in data with type [Image](https://docs.scriptable.app/image/). 81 | 82 | ```jsx 83 | 84 | const docsSymbol = SFSymbol.named("book"); 85 | const widget = await widgetMarkup` 86 | 87 | 88 | 89 | 90 | 91 | `; 92 | ``` 93 | 94 | - `` — The equivalent of Scriptable's [addSpacer()](https://docs.scriptable.app/widgetspacer/) method. This tag accepts a `value` attribute which takes in a numeric value for the length. If the value of the `value` attribute is 0 then this instructs the spacer to have a flexible length. 95 | - `` — The equivalent of Scriptable's [addDate()](https://docs.scriptable.app/widgetdate/) method. This tag requires `value` attribute which takes in an instance of the `Date` class. 96 | 97 | ## Convenience Tags 98 | - `` - Equivalent to `stack.layoutHorizontally()` 99 | - `` - Equivalent to `stack.layoutVertically()` 100 | 101 | # Styling 102 | 103 | All tags can accept a `attr` attribute. This attribute can only accept an object which is the list of styles the element will have. Here's a simple example of styling a text element. 104 | 105 | Note: The `styles` attribute is deprecated but still works. 106 | 107 | ```jsx 108 | const textStyles = { 109 | minimumScaleFactor: 0.5, 110 | textColor: Color.white(), 111 | font: Font.systemFont(18) 112 | }; 113 | 114 | const widget = await widgetMarkup` 115 | 116 | Hello World 117 | 118 | `; 119 | ``` 120 | 121 | The code above would be equivalent to this in Scriptable. 122 | 123 | ```jsx 124 | const widget = new ListWidget(); 125 | const text = widget.addText('Hello World'); 126 | text.minimumScaleFactor = 0.5; 127 | text.textColor = Color.white(); 128 | text.font = Font.systemFont(18); 129 | ``` 130 | 131 | The library encourages you to separate your styles from the structure of your widget. As the complexity of the structure of your widget grows, this way of coding will hopefully make your code more readable and maintainable. 132 | 133 | ## Methods 134 | 135 | Some styling properties in Scriptable needs to be called as a function/method. An example of this is `layoutVertically` or `layoutHorizontally` for stacks. For this type of styling, the library requires that the method name should be prefixed with a `*` . This is to signal to the parser that the styling property needs to be called as a function. Here's an example. 136 | 137 | ```jsx 138 | const stackStyles = { 139 | '*layoutVertically': null, // prefix with * and given a value of null if the method does not require a parameter. 140 | size: new Size(100, 20) 141 | }; 142 | 143 | const widget = await widgetMarkup` 144 | 145 | 146 | ... 147 | 148 | 149 | `; 150 | ``` 151 | 152 | Which would be equivalent to this... 153 | 154 | ```jsx 155 | const widget = new ListWidget(); 156 | const stackElement = widget.addStack(); 157 | stackElement.layoutVertically(); // Called as a function. 158 | stackElement.size = new Size(100, 20); 159 | ``` 160 | 161 | What if the styling method requires a parameter? A perfect example of this is the `setPadding` method which can take four parameters. For this you can pass in the parameters as an array of values. 162 | 163 | ```jsx 164 | const stackStyles = { 165 | '*setPadding': [5, 9, 5, 5], // Assign an array of values for the parameters 166 | size: new Size(100, 20) 167 | }; 168 | 169 | const widget = await widgetMarkup` 170 | 171 | 172 | ... 173 | 174 | 175 | `; 176 | ``` 177 | 178 | The code above is equivalent to... 179 | 180 | ```jsx 181 | const widget = new ListWidget(); 182 | const stackElement = widget.addStack(); 183 | stackElement.setPadding(5, 9, 5, 5); // Padding assigned. 184 | stackElement.size = new Size(100, 20); 185 | ``` 186 | 187 | # Syntax Highlighting 188 | 189 | If you are using vscode for development. I suggest installing [Matt Bierner's Comment tagged templates](https://marketplace.visualstudio.com/items?itemName=bierner.comment-tagged-templates) extension. This makes your widget markup even more readable while you code. 190 | 191 | Markup syntax highlighting 192 | 193 | # Examples 194 | You can find example widget re-builds using this library [here](https://github.com/rafaelgandi/WidgetMarkup-Scriptable/tree/main/examples) 195 | 196 | # Download 197 | Latest release is [here](https://shareable.vercel.app/script/20) 198 | 199 | # Conclusion 200 | 201 | This library is still on a very early stage of development and you may encounter bugs. Just let me know if you have any questions about it or if you found a bug. You can contact me [here](https://rafaelgandi.com/contact). This library is also open for you to edit and improve. Play around with it and make it work for your needs. Hopefully it encourages you to write more awesome Scriptable iOS widgets. 202 | 203 | If you find this library helpful, send me a screenshot of your widget or a code snippet on how you are using it. I'd love to see how you guys are using this library. 204 | 205 | 206 | -------------------------------------------------------------------------------- /RoundedIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgandi/WidgetMarkup-Scriptable/f49168713bfb9322793d659650696704d4ff7560/RoundedIcon.png -------------------------------------------------------------------------------- /WidgetMarkup.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: deep-green; icon-glyph: microscope; 4 | 5 | //////////////////////////////////////////////////////////////////////////////////// 6 | // {} WidgetMarkup - Simple implementation of markup for Scriptable iOS widgets. 7 | //////////////////////////////////////////////////////////////////////////////////// 8 | // Version 1.1.20221122 9 | // Change Log: 10 | // - Added hstack and vstack convenience tags. 11 | // - Added support for "attr" attribute and deprecated "styles" attribute 12 | 13 | 14 | const WidgetMarkup = (() => { 15 | function _getObjectClass(obj) { 16 | // See: https://stackoverflow.com/a/12730085 17 | if (obj && obj.constructor && obj.constructor.toString) { 18 | let arr = obj.constructor.toString().match(/function\s*(\w+)/); 19 | if (arr && arr.length == 2) { 20 | return arr[1]; 21 | } 22 | } 23 | return undefined; 24 | } 25 | 26 | function encodeXML(str) { 27 | // See: https://stackoverflow.com/a/7918944 28 | return str.replace(/&/g, '&') 29 | .replace(//g, '>') 31 | .replace(/"/g, '"') 32 | .replace(/'/g, '''); 33 | } 34 | 35 | function decodeXML(str) { 36 | // See: https://stackoverflow.com/a/7918944 37 | return str.replace(/'/g, "'") 38 | .replace(/"/g, '"') 39 | .replace(/>/g, '>') 40 | .replace(/</g, '<') 41 | .replace(/&/g, '&'); 42 | } 43 | 44 | function _mapMethodsAndCall(inst, options) { 45 | Object.keys(options).forEach((key) => { 46 | if (key.indexOf('*') !== -1) { 47 | key = key.replace('*', ''); 48 | if (!(key in inst)) { 49 | throw new Error(`Method "${key}()" is not applicable to instance of ${_getObjectClass(inst)}`); 50 | } 51 | if (Array.isArray(options['*' + key])) { 52 | inst[key](...options['*' + key]); 53 | } 54 | else { 55 | inst[key](options[key]); 56 | } 57 | } 58 | else { 59 | if (!(key in inst)) { 60 | throw new Error(`Property "${key}" is not applicable to instance of ${_getObjectClass(inst)}`); 61 | } 62 | inst[key] = options[key]; 63 | } 64 | }); 65 | return inst; 66 | } 67 | 68 | function _iterateChildren(widgetInstance, children) { 69 | children.forEach((child) => { 70 | if (child.tag === 'text') { 71 | const holderKeyRegExp = /(\$\$\[.+\])/ig; 72 | if (holderKeyRegExp.test(child.textContent)) { 73 | child.textContent = child.textContent.replace(holderKeyRegExp, (match, p1) => { 74 | return holder[p1].toString(); 75 | }); 76 | } 77 | let t = widgetInstance.addText(decodeXML(child.textContent)); 78 | _mapMethodsAndCall(t, _getAttrValue(child.attributes, 'styles')); 79 | _mapMethodsAndCall(t, _getAttrValue(child.attributes, 'attr')); 80 | } 81 | else if (child.tag === 'spacer') { 82 | let space = parseInt(_getAttrValue(child.attributes, 'value'), 10); 83 | if (space < 1 || isNaN(space)) { 84 | widgetInstance.addSpacer(); 85 | } 86 | else { 87 | widgetInstance.addSpacer(space); 88 | } 89 | } 90 | else if (child.tag === 'image') { 91 | let img = widgetInstance.addImage(_getAttrValue(child.attributes, 'src')); 92 | _mapMethodsAndCall(img, _getAttrValue(child.attributes, 'styles')); 93 | _mapMethodsAndCall(img, _getAttrValue(child.attributes, 'attr')); 94 | } 95 | else if (child.tag === 'date') { 96 | let date = widgetInstance.addDate(_getAttrValue(child.attributes, 'value')); 97 | _mapMethodsAndCall(date, _getAttrValue(child.attributes, 'styles')); 98 | _mapMethodsAndCall(date, _getAttrValue(child.attributes, 'attr')); 99 | } 100 | else if (child.tag === 'stack') { 101 | let stack = widgetInstance.addStack(); 102 | _mapMethodsAndCall(stack, _getAttrValue(child.attributes, 'styles')); 103 | _mapMethodsAndCall(stack, _getAttrValue(child.attributes, 'attr')); 104 | _iterateChildren(stack, child.children); 105 | } 106 | // LM: 2022-11-22 16:53:55 [Added hstack and vstack convenience tags] 107 | else if (['hstack', 'vstack'].indexOf(child.tag) !== -1) { 108 | let stack = widgetInstance.addStack(); 109 | stack[(child.tag === 'hstack') ? 'layoutHorizontally' : 'layoutVertically'](); 110 | _mapMethodsAndCall(stack, _getAttrValue(child.attributes, 'styles')); 111 | _mapMethodsAndCall(stack, _getAttrValue(child.attributes, 'attr')); 112 | _iterateChildren(stack, child.children); 113 | } 114 | }); 115 | return widgetInstance; 116 | } 117 | 118 | function _getAttrValue(attrs = [], name = 'styles') { 119 | let attr = {}; 120 | attrs.forEach((a) => { 121 | if (a.name.toLowerCase() === name.toLowerCase()) { 122 | if (typeof holder[a.value] !== 'undefined') { 123 | attr = holder[a.value]; 124 | } 125 | else { 126 | attr = a.value; 127 | } 128 | } 129 | }); 130 | return attr; 131 | } 132 | 133 | const holder = {}; 134 | function _replacer(str, eq) { 135 | let builtStr = ''; 136 | str.forEach((s, i) => { 137 | if (eq[i]) { 138 | if (Array.isArray(eq[i])) { 139 | eq[i] = eq[i].join(''); 140 | } 141 | if (typeof eq[i] === 'string') { 142 | builtStr += s + eq[i]; 143 | } 144 | else { 145 | let k = '$$[' + UUID.string() + (Math.floor(Math.random() * 20)) + ']'; 146 | holder[k] = eq[i]; 147 | builtStr += s + k; 148 | } 149 | } 150 | else { 151 | builtStr += s; 152 | } 153 | }); 154 | return builtStr; 155 | } 156 | 157 | function concatMarkup(str, ...eq) { 158 | let r = _replacer(str, eq); 159 | return r; 160 | } 161 | 162 | function _prepareMarkup(markup) { 163 | const textTagRegExp = /<\s*text[^>]*>(.*?)<\s*\/\s*text>/ig; // See: https://www.regextester.com/27540 164 | markup = markup.replace(/(\r\n|\n|\r)/gm, ''); // See: https://stackoverflow.com/a/10805198 165 | return markup.replace(textTagRegExp, (match, content) => { 166 | return match.replace(content, encodeXML(content)); 167 | }); 168 | } 169 | 170 | async function _getMappedDOM(markup) { 171 | const webview = new WebView(); 172 | await webview.loadHTML(''); 173 | //console.log(markup); 174 | // LM: 2021-09-12 11:29:33 [Escape any special chars to xml entities] 175 | markup = _prepareMarkup(markup); 176 | markup = `${markup}`; 177 | // See: https://gomakethings.com/how-to-create-a-map-of-dom-nodes-with-vanilla-js/ 178 | const js = ` 179 | var getAttributes = function (attributes) { 180 | return Array.prototype.map.call(attributes, function (attribute) { 181 | return { 182 | name: attribute.name, 183 | value: attribute.value 184 | }; 185 | }); 186 | }; 187 | 188 | var createDOMMap = function (element) { 189 | return Array.prototype.map.call(element.childNodes, (function (node) { 190 | if (node.nodeType !== 3 && node.nodeType !== 8) { 191 | var details = { 192 | tag: node.tagName.toLowerCase(), 193 | textContent: node.textContent, 194 | attributes: node.nodeType !== 1 ? [] : getAttributes(node.attributes) 195 | }; 196 | details.children = createDOMMap(node); 197 | return details; 198 | } 199 | return null; 200 | })).filter((e) => e !== null); 201 | }; 202 | 203 | function getDom() { 204 | let htmlStr = '${markup}'; 205 | const dom = new DOMParser(); 206 | let doc = dom.parseFromString(htmlStr, 'application/xml'); 207 | return JSON.stringify(createDOMMap(doc.documentElement)); 208 | } 209 | try { 210 | completion(getDom()); 211 | } 212 | catch (err) { 213 | completion([{ 214 | tag: 'error', 215 | textContent: err.message 216 | }]); 217 | } 218 | `; 219 | let response = await webview.evaluateJavaScript(js, true); 220 | const mappedArray = JSON.parse(response); 221 | if (mappedArray.length && mappedArray[0].tag.toLocaleLowerCase().indexOf('error') !== -1) { 222 | throw new Error(mappedArray[0].textContent); 223 | } 224 | return mappedArray; 225 | } 226 | 227 | async function widgetMarkup(str, ...eq) { 228 | let markup = _replacer(str, eq); 229 | let map = await _getMappedDOM(markup); 230 | const parentElementMap = map[0]; 231 | if (typeof parentElementMap === 'undefined') { 232 | throw new Error("WidgetMarkup requires that the element be the parent element of your widget."); 233 | } 234 | const childrenMap = parentElementMap.children; 235 | const widget = new ListWidget(); 236 | _mapMethodsAndCall(widget, _getAttrValue(parentElementMap.attributes, 'styles')); 237 | _mapMethodsAndCall(widget, _getAttrValue(parentElementMap.attributes, 'attr')); 238 | _iterateChildren(widget, childrenMap); 239 | return widget; 240 | } 241 | 242 | return { widgetMarkup, concatMarkup }; 243 | })(); 244 | 245 | 246 | // Expose template literal tags 247 | module.exports.widgetMarkup = WidgetMarkup.widgetMarkup; 248 | module.exports.concatMarkup = WidgetMarkup.concatMarkup; 249 | -------------------------------------------------------------------------------- /WidgetMarkup.min.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////// 2 | // {} WidgetMarkup - Simple implementation of markup for Scriptable iOS widgets. 3 | // COPY THIS SCRIPT TO YOUR WIDGET FILE. 4 | //////////////////////////////////////////////////////////////////////////////////// 5 | // Version 1.1.20221122 6 | const WidgetMarkup=(()=>{function t(t){if(t&&t.constructor&&t.constructor.toString){let e=t.constructor.toString().match(/function\s*(\w+)/);if(e&&2==e.length)return e[1]}}function e(e,n){return Object.keys(n).forEach((r=>{if(-1!==r.indexOf("*")){if(!((r=r.replace("*",""))in e))throw new Error(`Method "${r}()" is not applicable to instance of ${t(e)}`);Array.isArray(n["*"+r])?e[r](...n["*"+r]):e[r](n[r])}else{if(!(r in e))throw new Error(`Property "${r}" is not applicable to instance of ${t(e)}`);e[r]=n[r]}})),e}function n(t,o){return o.forEach((o=>{if("text"===o.tag){const n=/(\$\$\[.+\])/gi;n.test(o.textContent)&&(o.textContent=o.textContent.replace(n,((t,e)=>a[e].toString())));let i=t.addText(o.textContent.replace(/'/g,"'").replace(/"/g,'"').replace(/>/g,">").replace(/</g,"<").replace(/&/g,"&"));e(i,r(o.attributes,"styles")),e(i,r(o.attributes,"attr"))}else if("spacer"===o.tag){let e=parseInt(r(o.attributes,"value"),10);e<1||isNaN(e)?t.addSpacer():t.addSpacer(e)}else if("image"===o.tag){let n=t.addImage(r(o.attributes,"src"));e(n,r(o.attributes,"styles")),e(n,r(o.attributes,"attr"))}else if("date"===o.tag){let n=t.addDate(r(o.attributes,"value"));e(n,r(o.attributes,"styles")),e(n,r(o.attributes,"attr"))}else if("stack"===o.tag){let a=t.addStack();e(a,r(o.attributes,"styles")),e(a,r(o.attributes,"attr")),n(a,o.children)}else if(-1!==["hstack","vstack"].indexOf(o.tag)){let a=t.addStack();a["hstack"===o.tag?"layoutHorizontally":"layoutVertically"](),e(a,r(o.attributes,"styles")),e(a,r(o.attributes,"attr")),n(a,o.children)}})),t}function r(t=[],e="styles"){let n={};return t.forEach((t=>{t.name.toLowerCase()===e.toLowerCase()&&(n=void 0!==a[t.value]?a[t.value]:t.value)})),n}const a={};function o(t,e){let n="";return t.forEach(((t,r)=>{if(e[r])if(Array.isArray(e[r])&&(e[r]=e[r].join("")),"string"==typeof e[r])n+=t+e[r];else{let o="$$["+UUID.string()+Math.floor(20*Math.random())+"]";a[o]=e[r],n+=t+o}else n+=t})),n}return{widgetMarkup:async function(t,...a){let i=o(t,a),l=await async function(t){const e=new WebView;await e.loadHTML("");const n=`\n var getAttributes = function (attributes) {\n return Array.prototype.map.call(attributes, function (attribute) {\n return {\n name: attribute.name,\n value: attribute.value\n };\n });\n };\n \n var createDOMMap = function (element) {\n return Array.prototype.map.call(element.childNodes, (function (node) {\n if (node.nodeType !== 3 && node.nodeType !== 8) {\n var details = {\n tag: node.tagName.toLowerCase(),\n textContent: node.textContent,\n attributes: node.nodeType !== 1 ? [] : getAttributes(node.attributes)\n };\n details.children = createDOMMap(node);\n return details;\n }\n return null;\n })).filter((e) => e !== null);\n };\n \n function getDom() {\n let htmlStr = '${t=`${t=function(t){return(t=t.replace(/(\r\n|\n|\r)/gm,"")).replace(/<\s*text[^>]*>(.*?)<\s*\/\s*text>/gi,((t,e)=>t.replace(e,e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"))))}(t)}`}';\n const dom = new DOMParser();\n let doc = dom.parseFromString(htmlStr, 'application/xml');\n return JSON.stringify(createDOMMap(doc.documentElement));\n }\n try {\n completion(getDom());\n }\n catch (err) {\n completion([{\n tag: 'error',\n textContent: err.message\n }]);\n } \n `;let r=await e.evaluateJavaScript(n,!0);const a=JSON.parse(r);if(a.length&&-1!==a[0].tag.toLocaleLowerCase().indexOf("error"))throw new Error(a[0].textContent);return a}(i);const s=l[0];if(void 0===s)throw new Error("WidgetMarkup requires that the element be the parent element of your widget.");const c=s.children,u=new ListWidget;return e(u,r(s.attributes,"styles")),e(u,r(s.attributes,"attr")),n(u,c),u},concatMarkup:function(t,...e){return o(t,e)}}})(); 7 | -------------------------------------------------------------------------------- /examples/Calendar.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: teal; icon-glyph: calendar-alt; 4 | 5 | 6 | 7 | // Inspired by WidgetCal app 😁 8 | 9 | 10 | 11 | //////////////////////////////////////////////////////////////////////////////////// 12 | // {} WidgetMarkup - Simple implementation of markup for Scriptable iOS widgets. 13 | // COPY THIS SCRIPT TO YOUR WIDGET FILE. 14 | //////////////////////////////////////////////////////////////////////////////////// 15 | // Version 0.20210912a 16 | const WidgetMarkup=(()=>{function t(t){if(t&&t.constructor&&t.constructor.toString){let e=t.constructor.toString().match(/function\s*(\w+)/);if(e&&2==e.length)return e[1]}}function e(e,n){return Object.keys(n).forEach(r=>{if(-1!==r.indexOf("*")){if(!((r=r.replace("*",""))in e))throw new Error(`Method "${r}()" is not applicable to instance of ${t(e)}`);Array.isArray(n["*"+r])?e[r](...n["*"+r]):e[r](n[r])}else{if(!(r in e))throw new Error(`Property "${r}" is not applicable to instance of ${t(e)}`);e[r]=n[r]}}),e}function n(t=[],e="styles"){let n={};return t.forEach(t=>{t.name.toLowerCase()===e.toLowerCase()&&(n=void 0!==r[t.value]?r[t.value]:t.value)}),n}const r={};function a(t,e){let n="";return t.forEach((t,a)=>{if(e[a])if(Array.isArray(e[a])&&(e[a]=e[a].join("")),"string"==typeof e[a])n+=t+e[a];else{let o="$$["+UUID.string()+Math.floor(20*Math.random())+"]";r[o]=e[a],n+=t+o}else n+=t}),n}return{widgetMarkup:async function(t,...o){let i=a(t,o);const l=(await async function(t){const e=new WebView;await e.loadHTML(""),console.log(t);const n=`\n var getAttributes = function (attributes) {\n return Array.prototype.map.call(attributes, function (attribute) {\n return {\n name: attribute.name,\n value: attribute.value\n };\n });\n };\n \n var createDOMMap = function (element) {\n return Array.prototype.map.call(element.childNodes, (function (node) {\n if (node.nodeType !== 3 && node.nodeType !== 8) {\n var details = {\n tag: node.tagName.toLowerCase(),\n textContent: node.textContent,\n attributes: node.nodeType !== 1 ? [] : getAttributes(node.attributes)\n };\n details.children = createDOMMap(node);\n return details;\n }\n return null;\n })).filter((e) => e !== null);\n };\n \n function getDom() {\n let htmlStr = '${t=`${t=function(t){return(t=t.replace(/(\r\n|\n|\r)/gm,"")).replace(/<\s*text[^>]*>(.*?)<\s*\/\s*text>/gi,(t,e)=>t.replace(e,function(t){return t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}(e)))}(t)}`}';\n const dom = new DOMParser();\n let doc = dom.parseFromString(htmlStr, 'application/xml');\n return JSON.stringify(createDOMMap(doc.documentElement));\n }\n try {\n completion(getDom());\n }\n catch (err) {\n completion([{\n tag: 'error',\n textContent: err.message\n }]);\n } \n `;let r=await e.evaluateJavaScript(n,!0);const a=JSON.parse(r);if(a.length&&-1!==a[0].tag.toLocaleLowerCase().indexOf("error"))throw new Error(a[0].textContent);return a}(i))[0];if(void 0===l)throw new Error("WidgetMarkup requires that the element be the parent element of your widget.");const c=l.children,s=new ListWidget;return e(s,n(l.attributes,"styles")),function t(a,o){return o.forEach(o=>{if("text"===o.tag){const t=/(\$\$\[.+\])/gi;t.test(o.textContent)&&(o.textContent=o.textContent.replace(t,(t,e)=>r[e].toString())),e(a.addText(function(t){return t.replace(/'/g,"'").replace(/"/g,'"').replace(/>/g,">").replace(/</g,"<").replace(/&/g,"&")}(o.textContent)),n(o.attributes,"styles"))}else if("spacer"===o.tag){let t=parseInt(n(o.attributes,"value"),10);t<1||isNaN(t)?a.addSpacer():a.addSpacer(t)}else if("image"===o.tag)e(a.addImage(n(o.attributes,"src")),n(o.attributes,"styles"));else if("date"===o.tag)e(a.addDate(n(o.attributes,"value")),n(o.attributes,"styles"));else if("stack"===o.tag){let r=a.addStack();e(r,n(o.attributes,"styles")),t(r,o.children)}}),a}(s,c),s},concatMarkup:function(t,...e){return a(t,e)}}})(); 17 | const { widgetMarkup, concatMarkup } = WidgetMarkup; 18 | 19 | const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 20 | const today = new Date(); 21 | function generateCalendar(d = new Date()) { 22 | Date.prototype.monthDays = function () { 23 | let dd = new Date(this.getFullYear(), this.getMonth() + 1, 0); 24 | return dd.getDate(); 25 | }; 26 | var details = { 27 | totalDays: d.monthDays(), 28 | weekDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 29 | months: MONTHS, 30 | }; 31 | var start = new Date(d.getFullYear(), d.getMonth()).getDay(); 32 | var cal = []; 33 | var day = 1; 34 | for (var i = 0; i <= 5; i++) { 35 | cal[i] = []; 36 | for (var j = 0; j < 7; j++) { 37 | if (i === 0) { 38 | cal[i].push({ 39 | weekLabel: details.weekDays[j] 40 | }); 41 | } 42 | else if (day > details.totalDays) { 43 | cal[i].push(null); 44 | } 45 | else { 46 | if (i === 1 && j < start) { 47 | cal[i].push(null); 48 | } 49 | else { 50 | let dateNum = (day++); 51 | let dateInstance = new Date(d.getFullYear(), d.getMonth(), dateNum); 52 | cal[i].push({ 53 | date: dateNum, 54 | day: details.weekDays[j], 55 | dateInstance, 56 | month: details.months[dateInstance.getMonth()], 57 | year: dateInstance.getFullYear() 58 | }); 59 | } 60 | } 61 | } 62 | } 63 | return cal; 64 | } 65 | 66 | function ymd(date) { 67 | let y = date.getFullYear(); 68 | let m = date.getMonth(); 69 | let d = date.getDate(); 70 | return `${y}-${m}-${d}`; 71 | } 72 | 73 | async function getEvents() { 74 | let start = new Date(today.getFullYear(), today.getMonth(), 1); 75 | let end = new Date(today.getFullYear(), (today.getMonth() + 1), 1); 76 | let events = await CalendarEvent.between(start, end); 77 | let normalized = {}; 78 | events.forEach((e) => { 79 | let key = ymd(e.startDate); 80 | if (!normalized?.[key]) { 81 | normalized[key] = []; 82 | } 83 | normalized[key].push(e); 84 | }); 85 | //console.log(normalized); 86 | return normalized; 87 | } 88 | 89 | const events = await getEvents(); 90 | const attr = { 91 | con: { 92 | url: 'calshow://', 93 | '*setPadding': [0,0,0,0], 94 | spacing: 0, 95 | // refreshAfterDate: new Date((new Date()).getTime() + (15 * 60000)) // Refresh after 15min 96 | }, 97 | dateText: { 98 | font: new Font('PingFangSC-Light', 10), 99 | textColor: Color.white() 100 | }, 101 | row: { 102 | '*layoutHorizontally': null 103 | }, 104 | weekTextLabels: { 105 | font: new Font('ArialRoundedMTBold', 10), 106 | }, 107 | cell: { 108 | '*layoutVertically': null, 109 | borderWidth: 1, 110 | borderColor: new Color('#333333'), 111 | '*setPadding': [2,3,0,0], 112 | size: new Size(45, 55) 113 | }, 114 | todayCell: { 115 | borderColor: new Color('#46C35B'), 116 | }, 117 | sundayRed: { 118 | textColor: new Color('#FF5D4D') 119 | }, 120 | saturdayLabel: { 121 | textColor: new Color('#00A6EE') 122 | }, 123 | weekCellStack: { 124 | '*layoutHorizontally': null, 125 | size: new Size(45, 20), 126 | borderWidth: 0, 127 | }, 128 | eventsListStack: { 129 | '*setPadding': [5,0,0,0], 130 | '*layoutVertically': null, 131 | size: new Size(45, 30), 132 | }, 133 | eventText: { 134 | font: new Font('PingFangSC-Light', 10), 135 | textColor: new Color('#FCB841'), 136 | lineLimit: 1 137 | }, 138 | oldEventText: { 139 | textOpacity: 0.5 140 | }, 141 | eventStack: { 142 | '*setPadding': [0,0,0,0], 143 | '*layoutVertically': null 144 | }, 145 | monthYearLabelStack: { 146 | '*setPadding': [5,3,3,3], 147 | '*layoutHorizontally': null 148 | }, 149 | monthYearLabelText: { 150 | font: new Font('ArialRoundedMTBold', 15), 151 | } 152 | }; 153 | 154 | function createCalendarMarkUp() { 155 | let days = generateCalendar(); 156 | let body = ''; 157 | days.forEach((row, ri) => { 158 | body += concatMarkup /* xml */ ``; 159 | row.forEach((cell) => { 160 | if (ri === 0) { // print week labels 161 | let labelAttrs = attr.weekTextLabels; 162 | if (cell.weekLabel.toLowerCase() === 'sun') { 163 | labelAttrs = {...attr.weekTextLabels, ...attr.sundayRed}; 164 | } 165 | else if (cell.weekLabel.toLowerCase() === 'sat') { 166 | labelAttrs = {...attr.weekTextLabels, ...attr.saturdayLabel}; 167 | } 168 | body += concatMarkup /* xml */ ` 169 | 170 | 171 | ${cell.weekLabel} 172 | 173 | 174 | `; 175 | } 176 | else { 177 | if (cell === null) { 178 | body += concatMarkup /* xml */ ` 179 | 180 | 181 | `; 182 | } 183 | else { 184 | let dateAttrs = attr.dateText; 185 | let cellAttrs = attr.cell; 186 | if (cell.day.toLowerCase() === 'sun') { 187 | dateAttrs = {...attr.dateText, ...attr.sundayRed}; 188 | } 189 | else if (cell.day.toLowerCase() === 'sat') { 190 | dateAttrs = {...attr.dateText, ...attr.saturdayLabel}; 191 | } 192 | if (ymd(today) === ymd(cell.dateInstance)) { 193 | cellAttrs = {...attr.cell, ...attr.todayCell} 194 | } 195 | body += concatMarkup /* xml */ ` 196 | 197 | ${cell.date.toString()} 198 | 199 | ${(() => { 200 | let eventsMarkup = ''; 201 | let key = ymd(cell.dateInstance); 202 | if (events?.[key]) { 203 | events[key].forEach((e) => { 204 | let eventTextAttr = attr.eventText; 205 | if (cell.dateInstance.getTime() < today.getTime() && ymd(cell.dateInstance) !== ymd(today)) { 206 | eventTextAttr = {...attr.eventText, ...attr.oldEventText}; 207 | } 208 | eventsMarkup += concatMarkup /* xml */ ` 209 | 210 | ${e.title} 211 | 212 | `; 213 | }); 214 | return eventsMarkup; 215 | } 216 | else { 217 | return ''; 218 | } 219 | })()} 220 | 221 | `; 222 | } 223 | } 224 | }); 225 | body += concatMarkup /* xml */ ``; 226 | }); 227 | return body; 228 | } 229 | 230 | const widget = await widgetMarkup/* xml */` 231 | 232 | ${createCalendarMarkUp()} 233 | 234 | 🧑🏽‍🚀${MONTHS[today.getMonth()]} ${today.getFullYear().toString()}🪐 235 | 236 | 237 | `; 238 | 239 | 240 | 241 | // Check where the script is running 242 | if (config.runsInWidget) { 243 | // Runs inside a widget so add it to the homescreen widget 244 | Script.setWidget(widget); 245 | } 246 | else { 247 | // Show the medium widget inside the app 248 | widget.presentLarge(); 249 | } 250 | Script.complete(); -------------------------------------------------------------------------------- /examples/WidgetMarkup_Indie_Dev_Monday.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: red; icon-glyph: calendar-alt; 4 | /* 5 | * This is the offical Scriptable widget for Indie Dev Monday 6 | * 7 | * Indie Dev Monday (https://indiedevmonday.com) is a weekly newsletter 8 | * spotlighting indie developers 9 | * 10 | * This script includes: 11 | * 12 | * Latest issue widget (no parameter) 13 | * - shows latest issue 14 | * - works in small, medium, and large 15 | * - tapping opens latest issues 16 | * 17 | * Random indie dev widget ("indie" has text parameter) 18 | * - shows a random indie that has already been spotlighted 19 | * - tapping opens that indie's issue 20 | * 21 | */ 22 | 23 | /* 24 | This is a re-implementation of the Indie Dev Monday widget using the WidgetMarkup library. 25 | */ 26 | const { widgetMarkup, concatMarkup } = importModule('WidgetMarkup'); 27 | 28 | 29 | let indies = await loadIndies() 30 | let issues = (await loadIssues())["items"] 31 | 32 | let widget = null 33 | if (config.runsInWidget) { 34 | if (config.widgetFamily == "small") { 35 | widget = await createSmallWidget(indies, issues) 36 | } else { 37 | widget = await createMediumWidget(indies, issues) 38 | } 39 | Script.setWidget(widget) 40 | Script.complete() 41 | } else if (config.runsWithSiri) { 42 | let widget = await createMediumWidget(indies, issues) 43 | await widget.presentMedium() 44 | Script.complete() 45 | } else { 46 | await presentMenu(indies, issues) 47 | } 48 | 49 | async function presentMenu(indies, issues) { 50 | let alert = new Alert() 51 | alert.title = issues[0].title 52 | alert.message = 53 | alert.addAction("View Small Widget") 54 | alert.addAction("View Medium Widget") 55 | alert.addAction("View Large Widget") 56 | alert.addAction("Open Website") 57 | alert.addCancelAction("Cancel") 58 | let idx = await alert.presentSheet() 59 | if (idx == 0) { 60 | let widget = await createSmallWidget(indies, issues) 61 | await widget.presentSmall() 62 | } else if (idx == 1) { 63 | let widget = await createMediumWidget(indies, issues) 64 | await widget.presentMedium() 65 | } else if (idx == 2) { 66 | let widget = await createMediumWidget(indies, issues) 67 | await widget.presentLarge() 68 | } else if (idx == 3) { 69 | Safari.open(issues[0].url) 70 | } 71 | } 72 | 73 | async function createSmallWidget(indies, issues) { 74 | let style = args.widgetParameter 75 | if (style === "indie") { 76 | return createSmallWidgetIndie(indies, issues) 77 | } else { 78 | return createSmallWidgetIssue(indies, issues) 79 | } 80 | } 81 | 82 | async function createSmallWidgetIndie(indies, issues) { 83 | let indie = indies[Math.floor(Math.random() * indies.length)]; 84 | let urlSlug = "https://indiedevmonday.com/assets/images/indies/" + indie.slug + ".png"; 85 | let img = await loadImage(urlSlug); 86 | const styles = { 87 | widgetCon: { 88 | backgroundColor: new Color("#ac3929"), 89 | url: "https://indiedevmonday.com/issue-" + indie.issue, 90 | refreshAfterDate: new Date(Date.now() + (3600 * 4 * 1000)), // i think 4 hours 91 | '*setPadding': [10, 10, 10, 10] 92 | }, 93 | image: { 94 | '*centerAlignImage': null 95 | }, 96 | text: { 97 | font: Font.lightRoundedSystemFont(10), 98 | textColor: Color.white(), 99 | lineLimit: 1, 100 | '*centerAlignText': null, 101 | minimumScaleFactor: 0.2 102 | } 103 | }; 104 | const w = await widgetMarkup/* xml */` 105 | 106 | 107 | 108 | 109 | Indie Dev Monday 110 | 111 | `; 112 | return w 113 | } 114 | 115 | async function createSmallWidgetIssue(indies, issues) { 116 | // Indie and issue data 117 | let indie = indies[0] 118 | let issue = issues[0] 119 | 120 | let title = issue.title 121 | 122 | let issueNumber = issue.url.split('-')[1] 123 | let indiesInIssue = indies.filter(function (indie) { 124 | return indie.issue === issueNumber 125 | }) 126 | let indieNames = indiesInIssue.map(function (indie) { 127 | return indie.name 128 | }) 129 | title = indieNames.slice(0, -1).join(',') + ' and ' + indieNames.slice(-1); 130 | 131 | let imglogo = await loadLogo(); 132 | const styles = { 133 | widgetCon: { 134 | backgroundColor: new Color("#ac3929"), 135 | url: "https://indiedevmonday.com/issue-" + indie.issue, 136 | '*setPadding': [15, 15, 15, 15] 137 | }, 138 | hstack: { 139 | '*layoutHorizontally': null, 140 | '*bottomAlignContent': null 141 | }, 142 | hstackText: { 143 | font: Font.lightRoundedSystemFont(18), 144 | textColor: Color.white(), 145 | lineLimit: 2, 146 | '*centerAlignText': null, 147 | minimumScaleFactor: 0.2 148 | }, 149 | imglogo: { 150 | imageSize: new Size(30, 30), 151 | cornerRadius: 12 152 | }, 153 | wTitle: { 154 | font: Font.boldRoundedSystemFont(22), 155 | lineLimit: 2 156 | }, 157 | date: { 158 | font: Font.lightRoundedSystemFont(14) 159 | } 160 | }; 161 | const w = await widgetMarkup/* xml */` 162 | 163 | 164 | ${"#" + issueNumber} 165 | 166 | 167 | 168 | 169 | ${title} 170 | 171 | 172 | 173 | `; 174 | return w 175 | } 176 | 177 | async function createMediumWidget(indies, issues) { 178 | // Indie and issue data 179 | let indie = indies[0] 180 | let issue = issues[0] 181 | let title = issue.title 182 | let issueNumber = issue.url.split('-')[1] 183 | let indiesInIssue = indies.filter(function (indie) { 184 | return indie.issue === issueNumber 185 | }) 186 | let indieNames = indiesInIssue.map(function (indie) { 187 | return indie.name 188 | }) 189 | 190 | if (indieNames.length > 0) { 191 | title = indieNames.slice(0, -1).join(',') + ' and ' + indieNames.slice(-1); 192 | } 193 | 194 | // Widget 195 | let imglogo = await loadLogo(); 196 | const styles = { 197 | widgetCon: { 198 | backgroundColor: new Color("#ac3929"), 199 | url: "https://indiedevmonday.com/issue-" + indie.issue, 200 | '*setPadding': [15, 15, 15, 15] 201 | }, 202 | hstack: { 203 | '*layoutHorizontally': null, 204 | '*bottomAlignContent': null 205 | }, 206 | hstackText: { 207 | font: Font.lightRoundedSystemFont(18), 208 | textColor: Color.white(), 209 | lineLimit: 2, 210 | '*centerAlignText': null, 211 | minimumScaleFactor: 0.2 212 | }, 213 | imglogo: { 214 | imageSize: new Size(30, 30), 215 | cornerRadius: 12 216 | }, 217 | wTitle: { 218 | font: Font.boldRoundedSystemFont(22), 219 | lineLimit: 2 220 | }, 221 | date: { 222 | font: Font.lightRoundedSystemFont(14) 223 | }, 224 | hstack2: { 225 | '*layoutHorizontally': null, 226 | 227 | } 228 | }; 229 | 230 | const issueImages = await (async () => { 231 | let returnArr = []; 232 | for (indie of indiesInIssue) { 233 | let urlSlug = "https://indiedevmonday.com/assets/images/indies/" + indie.slug + ".png"; 234 | let img = await loadImage(urlSlug); 235 | returnArr.push(concatMarkup/* xml */` 236 | 237 | 238 | `); 239 | } 240 | return returnArr.join(''); 241 | })(); 242 | 243 | const w = await widgetMarkup/* xml */` 244 | 245 | 246 | ${"#" + issueNumber} 247 | 248 | 249 | 250 | 251 | 252 | 253 | ${title} 254 | 255 | 256 | 257 | ${issueImages} 258 | 259 | 260 | 261 | 262 | `; 263 | return w 264 | } 265 | 266 | function getIssueDate(issue) { 267 | let date = issue["date_published"] 268 | 269 | let formatter = new DateFormatter() 270 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 271 | return formatter.date(date) 272 | } 273 | 274 | 275 | async function loadIndies() { 276 | let url = "https://indiedevmonday.com/indies.json" 277 | let req = new Request(url) 278 | return req.loadJSON() 279 | } 280 | 281 | async function loadIssues() { 282 | let url = "https://indiedevmonday.com/feed.json" 283 | let req = new Request(url) 284 | return req.loadJSON() 285 | } 286 | 287 | async function loadLogo() { 288 | let url = "https://indiedevmonday.com/assets/images/logo_trans.png" 289 | return await loadImage(url) 290 | } 291 | 292 | async function loadImage(url) { 293 | let req = new Request(url) 294 | return req.loadImage() 295 | } -------------------------------------------------------------------------------- /examples/WidgetMarkup_Random_Scriptable_API.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: deep-blue; icon-glyph: book; 4 | // This script shows a random Scriptable API in a widget. The script is meant to be used with a widget configured on the Home Screen. 5 | // You can run the script in the app to preview the widget or you can go to the Home Screen, add a new Scriptable widget and configure the widget to run this script. 6 | // You can also try creating a shortcut that runs this script. Running the shortcut will show widget. 7 | 8 | /* 9 | This is a re-implementation of the Random Scriptable API widget using the WidgetMarkup library. 10 | */ 11 | const {widgetMarkup, concatMarkup} = importModule('WidgetMarkup'); 12 | 13 | async function randomAPI() { 14 | let docs = await loadDocs() 15 | let apiNames = Object.keys(docs) 16 | let num = Math.round(Math.random() * apiNames.length) 17 | let apiName = apiNames[num] 18 | let api = docs[apiName] 19 | return { 20 | name: apiName, 21 | description: api["!doc"], 22 | url: api["!url"] 23 | } 24 | } 25 | 26 | async function loadDocs() { 27 | let url = "https://docs.scriptable.app/scriptable.json" 28 | let req = new Request(url) 29 | return await req.loadJSON() 30 | } 31 | 32 | async function loadAppIcon() { 33 | let url = "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/21/1e/13/211e13de-2e74-4221-f7db-d6d2c53b4323/AppIcon-1x_U007emarketing-0-7-0-85-220.png/540x540sr.jpg" 34 | let req = new Request(url) 35 | return req.loadImage() 36 | } 37 | 38 | async function createWidget(api) { 39 | let appIcon = await loadAppIcon() 40 | let title = "Random Scriptable API" 41 | // Add background gradient 42 | let gradient = new LinearGradient() 43 | gradient.locations = [0, 1] 44 | gradient.colors = [ 45 | new Color("141414"), 46 | new Color("13233F") 47 | ] 48 | const styles = { 49 | widget: { 50 | backgroundGradient: gradient 51 | }, 52 | appIconImage: { 53 | imageSize: new Size(15, 15), 54 | cornerRadius: 4 55 | }, 56 | titleText: { 57 | textColor: Color.white(), 58 | textOpacity: 0.7, 59 | font: Font.mediumSystemFont(13) 60 | }, 61 | nameText: { 62 | textColor: Color.white(), 63 | font: Font.boldSystemFont(18) 64 | }, 65 | descriptionText: { 66 | minimumScaleFactor: 0.5, 67 | textColor: Color.white(), 68 | font: Font.systemFont(18) 69 | }, 70 | linkStack: { 71 | '*centerAlignContent': null, 72 | url: api.url 73 | }, 74 | linkText: { 75 | font: Font.mediumSystemFont(13), 76 | textColor: Color.blue() 77 | }, 78 | linkImage: { 79 | imageSize: new Size(11, 11), 80 | tintColor: Color.blue() 81 | }, 82 | docsImage: { 83 | imageSize: new Size(20, 20), 84 | tintColor: Color.white(), 85 | imageOpacity: 0.5, 86 | url: "https://docs.scriptable.app" 87 | } 88 | }; 89 | 90 | const widget = await widgetMarkup` 91 | 92 | 93 | 94 | 95 | ${title} 96 | 97 | 98 | ${api.name} 99 | 100 | ${api.description} 101 | ${(() => { 102 | if (!config.runsWithSiri) { 103 | let linkSymbol = SFSymbol.named("arrow.up.forward"); 104 | let docsSymbol = SFSymbol.named("book"); 105 | return concatMarkup` 106 | 107 | 108 | 109 | Read more 110 | 111 | 112 | 113 | 114 | 115 | 116 | `; 117 | } 118 | else { return ''; } 119 | })()} 120 | 121 | `; 122 | return widget 123 | } 124 | 125 | let api = await randomAPI() 126 | let widget = await createWidget(api) 127 | if (config.runsInWidget) { 128 | // The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen. 129 | Script.setWidget(widget) 130 | } else { 131 | // The script runs inside the app, so we preview the widget. 132 | widget.presentMedium() 133 | } 134 | // Calling Script.complete() signals to Scriptable that the script have finished running. 135 | // This can speed up the execution, in particular when running the script from Shortcuts or using Siri. 136 | Script.complete() 137 | -------------------------------------------------------------------------------- /examples/WidgetMarkup_Time_Progress.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: yellow; icon-glyph: hourglass-half; 4 | 5 | 6 | /* 7 | This is a re-implementation of the Time Progress widget using the WidgetMarkup library. 8 | */ 9 | const { widgetMarkup, concatMarkup } = importModule('WidgetMarkup'); 10 | const width = 125; 11 | const h = 5; 12 | const now = new Date(); 13 | const weekday = now.getDay() == 0 ? 6 : now.getDay() - 1; 14 | const minutes = now.getMinutes(); 15 | 16 | function createProgress(total, havegone) { 17 | const context = new DrawContext() 18 | context.size = new Size(width, h) 19 | context.opaque = false 20 | context.respectScreenScale = true 21 | context.setFillColor(new Color("#48484b")) 22 | const path = new Path() 23 | path.addRoundedRect(new Rect(0, 0, width, h), 3, 2) 24 | context.addPath(path) 25 | context.fillPath() 26 | context.setFillColor(new Color("#ffd60a")) 27 | const path1 = new Path() 28 | path1.addRoundedRect(new Rect(0, 0, width * havegone / total, h), 3, 2) 29 | context.addPath(path1) 30 | context.fillPath() 31 | return context.getImage() 32 | } 33 | 34 | function makeRow(total, haveGone, str) { 35 | const styles = { 36 | text: { 37 | textColor: new Color("#e587ce"), 38 | font: Font.boldSystemFont(13) 39 | }, 40 | image: { 41 | imageSize: new Size(width, h) 42 | } 43 | }; 44 | return concatMarkup/* xml */` 45 | ${str} 46 | 47 | 48 | 49 | `; 50 | } 51 | 52 | const w = await widgetMarkup/* xml */` 53 | 54 | ${(() => { 55 | if (Device.locale() == "zh_CN") { 56 | return [ 57 | makeRow(24 * 60, (now.getHours() + 1) * 60 + minutes, "今日"), 58 | makeRow(7, weekday + 1, "本周"), 59 | makeRow(30, now.getDate() + 1, "本月"), 60 | makeRow(12, now.getMonth() + 1, "今年") 61 | ].join(''); 62 | } else { 63 | return [ 64 | makeRow(24 * 60, (now.getHours() + 1) * 60 + minutes, "Today"), 65 | makeRow(7, weekday + 1, "This week"), 66 | makeRow(30, now.getDate() + 1, "This month"), 67 | makeRow(12, now.getMonth() + 1, "This year") 68 | ].join(''); 69 | } 70 | })()} 71 | 72 | `; 73 | 74 | Script.setWidget(w); 75 | Script.complete(); 76 | w.presentMedium(); 77 | 78 | 79 | -------------------------------------------------------------------------------- /vscode-markup-highlighting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelgandi/WidgetMarkup-Scriptable/f49168713bfb9322793d659650696704d4ff7560/vscode-markup-highlighting.png --------------------------------------------------------------------------------