output.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import {b64toBlob, randomid} from "../utils";
  2. import * as marked from 'marked';
  3. /*
  4. * 当前限制
  5. * 若Widget被作为其他Widget的子项时,该Widget中绑定的事件将会失效
  6. * */
  7. export interface Widget {
  8. handle_type: string;
  9. get_element(spec: any): JQuery; // The length of element must equal 1
  10. }
  11. let Text = {
  12. handle_type: 'text',
  13. get_element: function (spec: any): JQuery {
  14. let elem = spec.inline ? $('<span></span>') : $('<p></p>');
  15. spec.content = spec.content.replace(/ /g, '\u00A0');
  16. // make '\n' to <br/>
  17. let lines = (spec.content || '').split('\n');
  18. for (let idx = 0; idx < lines.length - 1; idx++)
  19. elem.append(document.createTextNode(lines[idx])).append('<br/>');
  20. elem.append(document.createTextNode(lines[lines.length - 1]));
  21. return elem;
  22. }
  23. };
  24. marked.setOptions({
  25. breaks: true, //可行尾不加两空格直接换行
  26. smartLists: true,
  27. smartypants: false,
  28. mangle: false,
  29. highlight: function (code, lang, callback) {
  30. if (Prism.languages[lang]) {
  31. try {
  32. code = Prism.highlight(code, Prism.languages[lang]);
  33. } catch (e) {
  34. console.error('Prism highlight error:' + e)
  35. }
  36. }
  37. if (callback)
  38. return callback(null, code);
  39. else
  40. return code;
  41. },
  42. });
  43. let Markdown = {
  44. handle_type: 'markdown',
  45. get_element: function (spec: any) {
  46. // spec.options, see also https://marked.js.org/using_advanced#options
  47. let html_str = marked(spec.content, spec.options);
  48. if (spec.sanitize)
  49. try {
  50. html_str = DOMPurify.sanitize(html_str);
  51. } catch (e) {
  52. console.log('Sanitize html failed: %s\nHTML: \n%s', e, html_str);
  53. }
  54. return $(html_str);
  55. }
  56. };
  57. // 将html字符串解析成jQuery对象
  58. function parseHtml(html_str: string) {
  59. let nodes = $.parseHTML(html_str, null, true);
  60. let elem;
  61. if (nodes.length != 1)
  62. elem = $(document.createElement('div')).append(nodes);
  63. else
  64. elem = $(nodes[0]);
  65. return elem;
  66. }
  67. let Html = {
  68. handle_type: 'html',
  69. get_element: function (spec: any) {
  70. let html_str = spec.content;
  71. if (spec.sanitize)
  72. try {
  73. html_str = DOMPurify.sanitize(html_str);
  74. } catch (e) {
  75. console.log('Sanitize html failed: %s\nHTML: \n%s', e, html_str);
  76. }
  77. return parseHtml(html_str);
  78. }
  79. };
  80. let Buttons = {
  81. handle_type: 'buttons',
  82. get_element: function (spec: any) {
  83. const btns_tpl = `<div>{{#buttons}}
  84. <button class="btn {{#color}}btn-{{color}}{{/color}}{{#small}} btn-sm{{/small}}">{{label}}</button>
  85. {{/buttons}}</div>`;
  86. spec.color = spec.link ? "link" : "primary";
  87. let html = Mustache.render(btns_tpl, spec);
  88. let elem = $(html);
  89. let btns = elem.find('button');
  90. for (let idx = 0; idx < spec.buttons.length; idx++) {
  91. // note: 若Buttons被作为其他Widget的子项时,Buttons中绑定的事件将会失效,所以使用onclick attr设置点击事件
  92. btns.eq(idx).attr('onclick', `WebIO.pushData(${JSON.stringify(spec.buttons[idx].value)}, "${spec.callback_id}")`);
  93. }
  94. return elem;
  95. }
  96. };
  97. // 已废弃。为了向下兼容而保留
  98. let File = {
  99. handle_type: 'file',
  100. get_element: function (spec: any) {
  101. const html = `<div><button type="button" class="btn btn-link">${spec.name}</button></div>`;
  102. let element = $(html);
  103. let blob = b64toBlob(spec.content);
  104. element.on('click', 'button', function (e) {
  105. saveAs(blob, spec.name, {}, false);
  106. });
  107. return element;
  108. }
  109. };
  110. let Table = {
  111. handle_type: 'table',
  112. get_element: function (spec: { data: any[][], span: { [i: string]: { col: number, row: number } } }) {
  113. const table_tpl = `
  114. <table>
  115. <tr>
  116. {{#header}}
  117. <th{{#col}} colspan="{{col}}"{{/col}}{{#row}} rowspan="{{row}}"{{/row}}>{{& data}}</th>
  118. {{/header}}
  119. </tr>
  120. {{#tdata}}
  121. <tr>
  122. {{# . }}
  123. <td{{#col}} colspan="{{col}}"{{/col}}{{#row}} rowspan="{{row}}"{{/row}}>{{& data}}</td>
  124. {{/ . }}
  125. </tr>
  126. {{/tdata}}
  127. </table>`;
  128. interface itemType {
  129. data: string,
  130. col?: number,
  131. row?: number
  132. }
  133. // 将spec转化成模版引擎的输入
  134. let table_data: itemType[][] = [];
  135. for (let row_id in spec.data) {
  136. table_data.push([]);
  137. let row = spec.data[row_id];
  138. for (let col_id in row) {
  139. let data = spec.data[row_id][col_id];
  140. // 处理简单类型单元格,即单元格不是output命令的spec
  141. if (typeof data !== 'object') {
  142. data = {type: 'text', content: data, inline: true};
  143. }
  144. table_data[row_id].push({
  145. data: outputSpecToHtml(data),
  146. ...(spec.span[row_id + ',' + col_id] || {})
  147. });
  148. }
  149. }
  150. let header: itemType[], data: itemType[][];
  151. [header, ...data] = table_data;
  152. let html = Mustache.render(table_tpl, {header: header, tdata: data});
  153. return $(html);
  154. }
  155. };
  156. const TABS_TPL = `<div class="webio-tabs">
  157. {{#tabs}}
  158. <input type="radio" class="toggle" name="{{#uniqueid}}name{{/uniqueid}}" id="{{#uniqueid}}name{{/uniqueid}}{{index}}" {{#checked}}checked{{/checked}}>
  159. <label for="{{#uniqueid}}name{{/uniqueid}}{{index}}">{{title}}</label>
  160. <div class="webio-tabs-content">
  161. {{#content}}
  162. {{& pywebio_output_parse}}
  163. {{/content}}
  164. </div>
  165. {{/tabs}}
  166. </div>`;
  167. let TabsWidget = {
  168. handle_type: 'tabs',
  169. get_element: function (spec: { tabs: { title: string, content: any, index: number, checked: boolean }[] }) {
  170. spec.tabs[0]['checked'] = true;
  171. for (let idx = 0; idx < spec.tabs.length; idx++) {
  172. spec.tabs[idx]['index'] = idx;
  173. }
  174. return render_tpl(TABS_TPL, spec);
  175. }
  176. };
  177. let CustomWidget = {
  178. handle_type: 'custom_widget',
  179. get_element: function (spec: { template: string, data: { [i: string]: any } }) {
  180. return render_tpl(spec.template, spec.data);
  181. }
  182. };
  183. let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget, TabsWidget];
  184. let type2widget: { [i: string]: Widget } = {};
  185. for (let w of all_widgets)
  186. type2widget[w.handle_type] = w;
  187. export function getWidgetElement(spec: any) {
  188. if (!(spec.type in type2widget))
  189. throw Error("Unknown type in getWidgetElement() :" + spec.type);
  190. let elem = type2widget[spec.type].get_element(spec);
  191. if (spec.style) {
  192. // add style attribute
  193. let old_style = elem.attr('style') || '';
  194. elem.attr({"style": old_style + spec.style});
  195. }
  196. if (spec.container_dom_id) {
  197. let dom_id = 'pywebio-scope-' + spec.container_dom_id;
  198. if (spec.container_selector)
  199. elem.find(spec.container_selector).attr('id', dom_id);
  200. else
  201. elem.attr('id', dom_id);
  202. }
  203. return elem;
  204. }
  205. // 将output指令的spec字段解析成html字符串
  206. export function outputSpecToHtml(spec: any) {
  207. let html = '';
  208. try {
  209. let nodes = getWidgetElement(spec);
  210. for (let node of nodes)
  211. html += node.outerHTML || '';
  212. } catch (e) {
  213. console.error('Get sub widget html error,', e, spec);
  214. }
  215. return html;
  216. }
  217. function render_tpl(tpl: string, data: { [i: string]: any }) {
  218. data['pywebio_output_parse'] = function () {
  219. if (this.type)
  220. return outputSpecToHtml(this);
  221. else
  222. return outputSpecToHtml({type: 'text', content: this, inline: true});
  223. };
  224. // {{#uniqueid}}name{{/uniqueid}}
  225. // {{uniqueid}}
  226. data['uniqueid'] = function () {
  227. let names2id: { [name: string]: any } = {};
  228. return function (name: string) {
  229. if (name) {
  230. if (!(name in names2id))
  231. names2id[name] = 'webio-' + randomid(10);
  232. return names2id[name];
  233. } else {
  234. return 'webio-' + randomid(10);
  235. }
  236. };
  237. }
  238. // count the function call number
  239. let cnt = 0;
  240. data['index'] = function () {
  241. cnt += 1;
  242. return cnt;
  243. };
  244. let html = Mustache.render(tpl, data);
  245. return parseHtml(html);
  246. }
  247. function gen_widget_from_tpl(name: string, tpl: string) {
  248. Mustache.parse(tpl);
  249. return {
  250. handle_type: name,
  251. get_element: function (data: { [i: string]: any }) {
  252. return render_tpl(tpl, data);
  253. }
  254. };
  255. }