Jelajahi Sumber

feat: add 'style()' to add custom css on output

wangweimin 4 tahun lalu
induk
melakukan
724c7171c9
6 mengubah file dengan 137 tambahan dan 47 penghapusan
  1. 4 3
      docs/spec.rst
  2. 23 7
      pywebio/io_ctrl.py
  3. 82 32
      pywebio/output.py
  4. 19 1
      test/template.py
  5. 3 3
      webiojs/src/handlers/output.ts
  6. 6 1
      webiojs/src/models/output.ts

+ 4 - 3
docs/spec.rst

@@ -168,9 +168,10 @@ output:
 
 
 命令 spec 字段:
 命令 spec 字段:
 
 
-* type
-* scope
-* position
+* type: 内容类型
+* style: 自定义样式
+* scope: 内容输出的域
+* position: 在输出域中输出的位置
 * 不同type时的特有字段
 * 不同type时的特有字段
 
 
 不同 ``type`` 时的特有字段:
 不同 ``type`` 时的特有字段:

+ 23 - 7
pywebio/io_ctrl.py

@@ -5,13 +5,13 @@ import inspect
 import json
 import json
 import logging
 import logging
 from functools import partial, wraps
 from functools import partial, wraps
-
+from collections import UserList
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-class OutputReturn:
+class Output:
     """ ``put_xxx()`` 类函数的返回值
     """ ``put_xxx()`` 类函数的返回值
 
 
     若 ``put_xxx()`` 调用的返回值没有被变量接收,则直接将消息发送到会话;
     若 ``put_xxx()`` 调用的返回值没有被变量接收,则直接将消息发送到会话;
@@ -24,7 +24,7 @@ class OutputReturn:
 
 
     @staticmethod
     @staticmethod
     def safely_destruct(obj):
     def safely_destruct(obj):
-        """安全销毁 OutputReturn 对象, 使 OutputReturn.__del__ 不进行任何操作"""
+        """安全销毁 OutputReturn 对象/包含OutputReturn对象的dict/list, 使 OutputReturn.__del__ 不进行任何操作"""
         try:
         try:
             json.dumps(obj, default=partial(output_json_encoder, ignore_error=True))
             json.dumps(obj, default=partial(output_json_encoder, ignore_error=True))
         except Exception:
         except Exception:
@@ -46,16 +46,32 @@ class OutputReturn:
         self.processed = True
         self.processed = True
         return self.on_embed(self.spec)
         return self.on_embed(self.spec)
 
 
-    def __del__(self):
-        """返回值没有被变量接收时的操作:直接输出消息"""
+    def send(self):
+        """发送输出内容到Client"""
         if not self.processed:
         if not self.processed:
             send_msg('output', self.spec)
             send_msg('output', self.spec)
+            self.processed = True
+
+    def __del__(self):
+        """返回值没有被变量接收时的操作:直接输出消息"""
+        self.send()
+
+
+class OutputList(UserList):
+
+    def __del__(self):
+        """返回值没有被变量接收时的操作:直接输出消息"""
+        for o in self.data:
+            o.send()
 
 
 
 
 def output_json_encoder(obj, ignore_error=False):
 def output_json_encoder(obj, ignore_error=False):
     """json序列化与输出相关消息的Encoder函数 """
     """json序列化与输出相关消息的Encoder函数 """
-    if isinstance(obj, OutputReturn):
+    if isinstance(obj, Output):
         return obj.embed_data()
         return obj.embed_data()
+    elif isinstance(obj, OutputList):
+        return obj.data
+
     if not ignore_error:
     if not ignore_error:
         raise TypeError('Object of type  %s is not JSON serializable' % obj.__class__.__name__)
         raise TypeError('Object of type  %s is not JSON serializable' % obj.__class__.__name__)
 
 
@@ -81,7 +97,7 @@ def safely_destruct_output_when_exp(content_param):
                 bound = sig.bind(*args, **kwargs).arguments
                 bound = sig.bind(*args, **kwargs).arguments
                 for param in params:
                 for param in params:
                     if bound.get(param):
                     if bound.get(param):
-                        OutputReturn.safely_destruct(bound.get(param))
+                        Output.safely_destruct(bound.get(param))
 
 
                 raise
                 raise
 
 

+ 82 - 32
pywebio/output.py

@@ -37,14 +37,20 @@ r"""输出内容到用户浏览器
 .. autofunction:: put_link
 .. autofunction:: put_link
 .. autofunction:: put_scrollable
 .. autofunction:: put_scrollable
 .. autofunction:: put_widget
 .. autofunction:: put_widget
+
+布局与样式
+--------------
+.. autofunction:: style
+
 """
 """
 import io
 import io
 import logging
 import logging
 from base64 import b64encode
 from base64 import b64encode
 from collections.abc import Mapping, Sequence
 from collections.abc import Mapping, Sequence
 from functools import wraps
 from functools import wraps
+from typing import Union
 
 
-from .io_ctrl import output_register_callback, send_msg, OutputReturn, safely_destruct_output_when_exp
+from .io_ctrl import output_register_callback, send_msg, Output, safely_destruct_output_when_exp, OutputList
 from .session import get_current_session
 from .session import get_current_session
 from .utils import random_str, iscoroutinefunction
 from .utils import random_str, iscoroutinefunction
 
 
@@ -58,7 +64,7 @@ logger = logging.getLogger(__name__)
 __all__ = ['Position', 'set_title', 'set_output_fixed_height', 'set_auto_scroll_bottom', 'remove', 'scroll_to',
 __all__ = ['Position', 'set_title', 'set_output_fixed_height', 'set_auto_scroll_bottom', 'remove', 'scroll_to',
            'put_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove',
            'put_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove',
            'put_table', 'table_cell_buttons', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup',
            'put_table', 'table_cell_buttons', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup',
-           'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable']
+           'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable', 'style']
 
 
 
 
 # popup尺寸
 # popup尺寸
@@ -187,7 +193,7 @@ def _get_output_spec(type, scope, position, **other_spec):
     return spec
     return spec
 
 
 
 
-def put_text(text, inline=False, scope=Scope.Current, position=OutputPosition.BOTTOM) -> OutputReturn:
+def put_text(text, inline=False, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """
     """
     输出文本内容
     输出文本内容
 
 
@@ -201,10 +207,10 @@ def put_text(text, inline=False, scope=Scope.Current, position=OutputPosition.BO
     参数 `scope` 和 `position` 的更多使用说明参见 :ref:`用户手册 <scope_param>`
     参数 `scope` 和 `position` 的更多使用说明参见 :ref:`用户手册 <scope_param>`
     """
     """
     spec = _get_output_spec('text', content=str(text), inline=inline, scope=scope, position=position)
     spec = _get_output_spec('text', content=str(text), inline=inline, scope=scope, position=position)
-    return OutputReturn(spec)
+    return Output(spec)
 
 
 
 
-def put_html(html, scope=Scope.Current, position=OutputPosition.BOTTOM) -> OutputReturn:
+def put_html(html, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """
     """
     输出Html内容。
     输出Html内容。
 
 
@@ -217,10 +223,10 @@ def put_html(html, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Outpu
         html = html.__html__()
         html = html.__html__()
 
 
     spec = _get_output_spec('html', content=html, scope=scope, position=position)
     spec = _get_output_spec('html', content=html, scope=scope, position=position)
-    return OutputReturn(spec)
+    return Output(spec)
 
 
 
 
-def put_code(content, langage='', scope=Scope.Current, position=OutputPosition.BOTTOM) -> OutputReturn:
+def put_code(content, langage='', scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """
     """
     输出代码块
     输出代码块
 
 
@@ -233,7 +239,7 @@ def put_code(content, langage='', scope=Scope.Current, position=OutputPosition.B
 
 
 
 
 def put_markdown(mdcontent, strip_indent=0, lstrip=False, scope=Scope.Current,
 def put_markdown(mdcontent, strip_indent=0, lstrip=False, scope=Scope.Current,
-                 position=OutputPosition.BOTTOM) -> OutputReturn:
+                 position=OutputPosition.BOTTOM) -> Output:
     """
     """
     输出Markdown内容。
     输出Markdown内容。
 
 
@@ -274,11 +280,11 @@ def put_markdown(mdcontent, strip_indent=0, lstrip=False, scope=Scope.Current,
         mdcontent = '\n'.join(lines)
         mdcontent = '\n'.join(lines)
 
 
     spec = _get_output_spec('markdown', content=mdcontent, scope=scope, position=position)
     spec = _get_output_spec('markdown', content=mdcontent, scope=scope, position=position)
-    return OutputReturn(spec)
+    return Output(spec)
 
 
 
 
 @safely_destruct_output_when_exp('tdata')
 @safely_destruct_output_when_exp('tdata')
-def put_table(tdata, header=None, span=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> OutputReturn:
+def put_table(tdata, header=None, span=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """
     """
     输出表格
     输出表格
 
 
@@ -351,7 +357,7 @@ def put_table(tdata, header=None, span=None, scope=Scope.Current, position=Outpu
     span = {('%s,%s' % row_col): val for row_col, val in span.items()}
     span = {('%s,%s' % row_col): val for row_col, val in span.items()}
 
 
     spec = _get_output_spec('table', data=tdata, span=span, scope=scope, position=position)
     spec = _get_output_spec('table', data=tdata, span=span, scope=scope, position=position)
-    return OutputReturn(spec)
+    return Output(spec)
 
 
 
 
 def _format_button(buttons):
 def _format_button(buttons):
@@ -414,7 +420,7 @@ def table_cell_buttons(buttons, onclick, **callback_options) -> str:
 
 
 
 
 def put_buttons(buttons, onclick, small=None, scope=Scope.Current, position=OutputPosition.BOTTOM,
 def put_buttons(buttons, onclick, small=None, scope=Scope.Current, position=OutputPosition.BOTTOM,
-                **callback_options) -> OutputReturn:
+                **callback_options) -> Output:
     """
     """
     输出一组按钮
     输出一组按钮
 
 
@@ -477,11 +483,11 @@ def put_buttons(buttons, onclick, small=None, scope=Scope.Current, position=Outp
     spec = _get_output_spec('buttons', callback_id=callback_id, buttons=btns, small=small,
     spec = _get_output_spec('buttons', callback_id=callback_id, buttons=btns, small=small,
                             scope=scope, position=position)
                             scope=scope, position=position)
 
 
-    return OutputReturn(spec)
+    return Output(spec)
 
 
 
 
 def put_image(src, format=None, title='', width=None, height=None,
 def put_image(src, format=None, title='', width=None, height=None,
-              scope=Scope.Current, position=OutputPosition.BOTTOM) -> OutputReturn:
+              scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """输出图片。
     """输出图片。
 
 
     :param src: 图片内容. 类型可以为字符串类型的URL或者是 bytes-like object 或者为 ``PIL.Image.Image`` 实例
     :param src: 图片内容. 类型可以为字符串类型的URL或者是 bytes-like object 或者为 ``PIL.Image.Image`` 实例
@@ -509,7 +515,7 @@ def put_image(src, format=None, title='', width=None, height=None,
     return put_html(html, scope=scope, position=position)
     return put_html(html, scope=scope, position=position)
 
 
 
 
-def put_file(name, content, scope=Scope.Current, position=OutputPosition.BOTTOM) -> OutputReturn:
+def put_file(name, content, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """输出文件。
     """输出文件。
     在浏览器上的显示为一个以文件名为名的链接,点击链接后浏览器自动下载文件。
     在浏览器上的显示为一个以文件名为名的链接,点击链接后浏览器自动下载文件。
 
 
@@ -519,11 +525,11 @@ def put_file(name, content, scope=Scope.Current, position=OutputPosition.BOTTOM)
     """
     """
     content = b64encode(content).decode('ascii')
     content = b64encode(content).decode('ascii')
     spec = _get_output_spec('file', name=name, content=content, scope=scope, position=position)
     spec = _get_output_spec('file', name=name, content=content, scope=scope, position=position)
-    return OutputReturn(spec)
+    return Output(spec)
 
 
 
 
 def put_link(name, url=None, app=None, new_window=False, scope=Scope.Current,
 def put_link(name, url=None, app=None, new_window=False, scope=Scope.Current,
-             position=OutputPosition.BOTTOM) -> OutputReturn:
+             position=OutputPosition.BOTTOM) -> Output:
     """输出链接到其他页面或PyWebIO App的超链接
     """输出链接到其他页面或PyWebIO App的超链接
 
 
     :param str name: 链接名称
     :param str name: 链接名称
@@ -543,7 +549,7 @@ def put_link(name, url=None, app=None, new_window=False, scope=Scope.Current,
 
 
 
 
 @safely_destruct_output_when_exp('content')
 @safely_destruct_output_when_exp('content')
-def put_collapse(title, content, open=False, scope=Scope.Current, position=OutputPosition.BOTTOM) -> OutputReturn:
+def put_collapse(title, content, open=False, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """输出可折叠的内容
     """输出可折叠的内容
 
 
     :param str title: 内容标题
     :param str title: 内容标题
@@ -552,11 +558,11 @@ def put_collapse(title, content, open=False, scope=Scope.Current, position=Outpu
     :param bool open: 是否默认展开折叠内容。默认不展开内容
     :param bool open: 是否默认展开折叠内容。默认不展开内容
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     """
     """
-    if not isinstance(content, (list, tuple)):
+    if not isinstance(content, (list, tuple, OutputList)):
         content = [content]
         content = [content]
 
 
     for item in content:
     for item in content:
-        assert isinstance(item, (str, OutputReturn)), "put_collapse() content must be list of str/put_xxx()"
+        assert isinstance(item, (str, Output)), "put_collapse() content must be list of str/put_xxx()"
 
 
     tpl = """
     tpl = """
     <details {{#open}}open{{/open}}>
     <details {{#open}}open{{/open}}>
@@ -571,7 +577,7 @@ def put_collapse(title, content, open=False, scope=Scope.Current, position=Outpu
 
 
 @safely_destruct_output_when_exp('content')
 @safely_destruct_output_when_exp('content')
 def put_scrollable(content, max_height=400, horizon_scroll=False, border=True, scope=Scope.Current,
 def put_scrollable(content, max_height=400, horizon_scroll=False, border=True, scope=Scope.Current,
-                   position=OutputPosition.BOTTOM) -> OutputReturn:
+                   position=OutputPosition.BOTTOM) -> Output:
     """宽高限制的内容输出区域,内容超出限制则显示滚动条
     """宽高限制的内容输出区域,内容超出限制则显示滚动条
 
 
     :type content: list/str/put_xxx()
     :type content: list/str/put_xxx()
@@ -581,14 +587,13 @@ def put_scrollable(content, max_height=400, horizon_scroll=False, border=True, s
     :param bool border: 是否显示边框
     :param bool border: 是否显示边框
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     """
     """
-    if not isinstance(content, (list, tuple)):
+    if not isinstance(content, (list, tuple, OutputList)):
         content = [content]
         content = [content]
 
 
     for item in content:
     for item in content:
-        assert isinstance(item, (str, OutputReturn)), "put_collapse() content must be list of str/put_xxx()"
+        assert isinstance(item, (str, Output)), "put_collapse() content must be list of str/put_xxx()"
 
 
-    tpl = """
-    <div style="max-height: {{max_height}}px;
+    tpl = """<div style="max-height: {{max_height}}px;
             overflow-y: scroll;
             overflow-y: scroll;
             {{#horizon_scroll}}overflow-x: scroll;{{/horizon_scroll}}
             {{#horizon_scroll}}overflow-x: scroll;{{/horizon_scroll}}
             {{#border}} 
             {{#border}} 
@@ -601,15 +606,14 @@ def put_scrollable(content, max_height=400, horizon_scroll=False, border=True, s
         {{#contents}}
         {{#contents}}
             {{& pywebio_output_parse}}
             {{& pywebio_output_parse}}
         {{/contents}}
         {{/contents}}
-    </div>
-    """
+    </div>"""
     return put_widget(template=tpl,
     return put_widget(template=tpl,
                       data=dict(contents=content, max_height=max_height, horizon_scroll=horizon_scroll, border=border),
                       data=dict(contents=content, max_height=max_height, horizon_scroll=horizon_scroll, border=border),
                       scope=scope, position=position)
                       scope=scope, position=position)
 
 
 
 
 @safely_destruct_output_when_exp('data')
 @safely_destruct_output_when_exp('data')
-def put_widget(template, data, scope=Scope.Current, position=OutputPosition.BOTTOM) -> OutputReturn:
+def put_widget(template, data, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """输出自定义的控件
     """输出自定义的控件
 
 
     :param template: html模版,使用 `mustache.js <https://github.com/janl/mustache.js>`_ 语法
     :param template: html模版,使用 `mustache.js <https://github.com/janl/mustache.js>`_ 语法
@@ -647,7 +651,53 @@ def put_widget(template, data, scope=Scope.Current, position=OutputPosition.BOTT
         })
         })
     """
     """
     spec = _get_output_spec('custom_widget', template=template, data=data, scope=scope, position=position)
     spec = _get_output_spec('custom_widget', template=template, data=data, scope=scope, position=position)
-    return OutputReturn(spec)
+    return Output(spec)
+
+
+@safely_destruct_output_when_exp('outputs')
+def style(outputs, css_style) -> Union[Output, OutputList]:
+    """自定义输出内容的css样式
+
+    :param outputs: 输出内容,可以为 ``put_xxx()`` 调用或其列表。outputs为列表时将为每个列表项都添加自定义的css样式。
+    :type outputs: list/put_xxx()
+    :param css_style: css样式字符串
+    :return: 添加了css样式的输出内容。
+       若 ``outputs`` 为 ``put_xxx()`` 调用,返回值为添加了css样式的输出。
+       若 ``outputs`` 为list,返回值为 ``outputs`` 中每一项都添加了css样式的list。
+
+    :Example:
+
+    ::
+
+        style(put_text('Red'), 'color:red')
+
+        style([
+            put_text('Red'),
+            put_markdown('~~del~~')
+        ], 'color:red')
+
+        put_table([
+            ['A', 'B'],
+            ['C', style(put_text('Red'), 'color:red')],
+        ])
+
+        put_collapse('title', style([
+            put_text('text'),
+            put_markdown('~~del~~'),
+        ], 'margin-left:20px'))
+
+    """
+    if not isinstance(outputs, (list, tuple, OutputList)):
+        ol = [outputs]
+    else:
+        ol = outputs
+        outputs = OutputList(outputs)
+
+    for o in ol:
+        o.spec.setdefault('style', '')
+        o.spec['style'] += ';%s' % css_style
+
+    return outputs
 
 
 
 
 @safely_destruct_output_when_exp('content')
 @safely_destruct_output_when_exp('content')
@@ -681,13 +731,13 @@ def popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=T
         ])
         ])
 
 
     """
     """
-    if not isinstance(content, (list, tuple)):
+    if not isinstance(content, (list, tuple, OutputList)):
         content = [content]
         content = [content]
 
 
     for item in content:
     for item in content:
-        assert isinstance(item, (str, OutputReturn)), "popup() content must be list of str/put_xxx()"
+        assert isinstance(item, (str, Output)), "popup() content must be list of str/put_xxx()"
 
 
-    send_msg(cmd='popup', spec=dict(content=OutputReturn.jsonify(content), title=title, size=size,
+    send_msg(cmd='popup', spec=dict(content=Output.jsonify(content), title=title, size=size,
                                     implicit_close=implicit_close, closable=closable))
                                     implicit_close=implicit_close, closable=closable))
 
 
 
 

+ 19 - 1
test/template.py

@@ -56,6 +56,24 @@ def basic_output():
     put_text('<hr/>:')
     put_text('<hr/>:')
     put_html("<hr/>")
     put_html("<hr/>")
 
 
+    put_text('style:')
+    style(put_text('Red'), 'color:red')
+
+    style([
+        put_text('Red'),
+        put_markdown('~~del~~')
+    ], 'color:red')
+
+    put_table([
+        ['A', 'B'],
+        ['C', style(put_text('Red'), 'color:red')],
+    ])
+
+    put_collapse('title', style([
+        put_text('text'),
+        put_markdown('~~del~~'),
+    ], 'margin-left:20px'), open=True)
+
     put_text('table:')
     put_text('table:')
     put_table([
     put_table([
         ['Name', 'Gender', 'Address'],
         ['Name', 'Gender', 'Address'],
@@ -134,7 +152,7 @@ def basic_output():
 
 
     put_image(img_data)
     put_image(img_data)
     put_image(img_data, width="30px")
     put_image(img_data, width="30px")
-    put_image(img_data, height="50px")
+    put_image('https://cdn.jsdelivr.net/gh/wang0618/pywebio/test/assets/img.png', height="50px")
 
 
     put_file('hello_word.txt', b'hello word!')
     put_file('hello_word.txt', b'hello word!')
 
 

+ 3 - 3
webiojs/src/handlers/output.ts

@@ -29,7 +29,7 @@ export class OutputHandler implements CommandHandler {
     };
     };
 
 
     handle_message(msg: Command) {
     handle_message(msg: Command) {
-        let scroll_bottom = false;
+        let output_to_root = false;
         if (msg.command === 'output') {
         if (msg.command === 'output') {
             let elem;
             let elem;
             try {
             try {
@@ -44,7 +44,7 @@ export class OutputHandler implements CommandHandler {
             if (container_elem.length === 0)
             if (container_elem.length === 0)
                 return console.error(`Scope '${msg.spec.scope}' not found`);
                 return console.error(`Scope '${msg.spec.scope}' not found`);
 
 
-            if (!msg.spec.scope || msg.spec.scope === 'pywebio-scope-ROOT') scroll_bottom = true;
+            if (!msg.spec.scope || msg.spec.scope === 'pywebio-scope-ROOT') output_to_root = true;
 
 
             if (msg.spec.position === 0)
             if (msg.spec.position === 0)
                 container_elem.prepend(elem);
                 container_elem.prepend(elem);
@@ -63,7 +63,7 @@ export class OutputHandler implements CommandHandler {
             this.handle_output_ctl(msg);
             this.handle_output_ctl(msg);
         }
         }
         // 当设置了AutoScrollBottom、并且当前输出输出到页面末尾时,滚动到底部
         // 当设置了AutoScrollBottom、并且当前输出输出到页面末尾时,滚动到底部
-        if (state.AutoScrollBottom && scroll_bottom)
+        if (state.AutoScrollBottom && output_to_root)
             this.scroll_bottom();
             this.scroll_bottom();
     };
     };
 
 

+ 6 - 1
webiojs/src/models/output.ts

@@ -181,7 +181,12 @@ export function getWidgetElement(spec: any) {
     if (!(spec.type in type2widget))
     if (!(spec.type in type2widget))
         throw Error("Unknown type in getWidgetElement() :" + spec.type);
         throw Error("Unknown type in getWidgetElement() :" + spec.type);
 
 
-    return type2widget[spec.type].get_element(spec);
+    let elem = type2widget[spec.type].get_element(spec);
+    if (spec.style) {
+        let old_style = elem.attr('style') || '';
+        elem.attr({"style": old_style + spec.style});
+    }
+    return elem;
 }
 }