├── 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 |
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 |
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
--------------------------------------------------------------------------------