output.ts 8.4 KB

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