Update widget slot system to boolean and support multiple slots, add optimization doc

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-12-29 01:14:33 +00:00
parent 57b6eb14e3
commit fbd1e72836
3 changed files with 556 additions and 50 deletions

View File

@ -0,0 +1,399 @@
# 组件优化提案文档Widget Optimization Proposals
本文档基于当前 `tp-define-widget` 的实现,参考 Vue3 组合式 API 的设计理念,提出一系列优化和扩展方案。
## 当前实现
### 现有特性
- `:props` - 支持属性定义,包含默认值 `(prop . default)`
- `:slot` - 布尔值,`t` 表示支持 slot`nil` 表示不支持
- `:render` - 渲染函数 `(lambda (props slot) ...)`
- 支持多个 slot 值(字符串和嵌套组件)
---
## 优化提案
### 1. 生命周期钩子Lifecycle Hooks
**参考**: Vue3 的 `onMounted`, `onBeforeUpdate`, `onUpdated`
**功能描述**:
```elisp
(tp-define-widget my-widget
:props '(value)
:slot t
:on-render (lambda (props slot) ...) ; 渲染前
:on-rendered (lambda (result) ...) ; 渲染后
:render (lambda (props slot) ...))
```
**作用**:
- 在渲染前后执行特定逻辑(如日志记录、性能监控)
- 支持渲染结果的后处理
**是否必要**: ⭐⭐ 低优先级
- 目前可通过在 render 函数中处理
- 如果组件变得复杂且需要统一的渲染管道处理,则有价值
---
### 2. 命名插槽Named Slots
**参考**: Vue3 的 `<slot name="header">`, `v-slot:header`
**功能描述**:
```elisp
(tp-define-widget card
:props '(title)
:slots '(header content footer) ; 定义多个命名插槽
:render (lambda (props slots)
(concat (plist-get slots :header)
"\n"
(plist-get slots :content)
"\n"
(plist-get slots :footer))))
;; 使用
(tp-widget-parse
'(card :title "My Card"
:header-slot "Header Content"
:content-slot "Main Content"
:footer-slot "Footer"))
```
**作用**:
- 支持更灵活的内容分发
- 组件可以有多个内容区域
**是否必要**: ⭐⭐⭐ 中等优先级
- 当组件需要在不同位置插入内容时非常有用
- 类似 Vue/React 的 slot 概念
---
### 3. 作用域插槽Scoped Slots
**参考**: Vue3 的作用域插槽,允许父组件访问子组件数据
**功能描述**:
```elisp
(tp-define-widget list-item
:props '(items)
:slot t ; slot 可以是函数
:render (lambda (props slot-fn)
(mapconcat
(lambda (item)
;; slot-fn 可以访问当前 item
(funcall slot-fn item))
(plist-get props :items)
"\n")))
;; 使用
(tp-widget-parse
'(list-item :items ("apple" "banana" "orange")
(lambda (item)
(tp-set item 'face 'bold))))
```
**作用**:
- 父组件可以访问子组件的内部数据
- 更灵活的渲染控制
**是否必要**: ⭐⭐ 低优先级
- 增加复杂度
- Emacs Lisp 的闭包可以部分实现此功能
---
### 4. 组件继承/组合Component Inheritance/Composition
**参考**: Vue3 的 `mixins`, `extends`
**功能描述**:
```elisp
(tp-define-widget base-button
:props '((type . "default"))
:slot t
:render (lambda (props slot)
(tp-set slot 'face 'button)))
(tp-define-widget primary-button
:extends 'base-button
:props '((type . "primary")) ; 覆盖默认值
:render (lambda (props slot parent-render)
(let ((result (funcall parent-render props slot)))
(tp-add result 'face '(:foreground "blue")))))
```
**作用**:
- 代码复用
- 创建组件变体
**是否必要**: ⭐⭐⭐ 中等优先级
- 对于创建组件库非常有用
- 避免重复代码
---
### 5. 响应式状态Reactive State
**参考**: Vue3 的 `ref`, `reactive`
**功能描述**:
```elisp
(tp-define-widget counter
:state '((count . 0)) ; 组件内部状态
:slot t
:render (lambda (props state slot)
(let ((count (plist-get state :count)))
(format "Count: %d %s" count slot))))
;; 状态更新时自动重新渲染
(tp-widget-update 'counter :count 5)
```
**作用**:
- 组件拥有自己的内部状态
- 与现有的响应式系统(`$variable`)集成
**是否必要**: ⭐⭐⭐⭐ 高优先级
- 对于交互式组件非常重要
- 可以利用现有的 tp reactive 系统
---
### 6. 事件系统Event System
**参考**: Vue3 的 `$emit`, `v-on`
**功能描述**:
```elisp
(tp-define-widget button
:props '(label)
:emits '(click hover) ; 声明可触发的事件
:slot t
:render (lambda (props slot emit)
(tp-add slot
'mouse-1 (lambda () (funcall emit :click))
'pointer 'hand)))
;; 使用
(tp-widget-parse
'(button :label "Click Me"
:on-click (lambda () (message "Clicked!"))
"Submit"))
```
**作用**:
- 组件间通信
- 事件驱动的交互
**是否必要**: ⭐⭐⭐⭐ 高优先级
- 对于交互式 UI 必要
- 支持按钮、链接等组件的回调
---
### 7. 依赖注入Provide/Inject
**参考**: Vue3 的 `provide`, `inject`
**功能描述**:
```elisp
(tp-define-widget theme-provider
:provide '(theme) ; 向下提供
:props '((theme . "dark"))
:slot t
:render (lambda (props slot) slot))
(tp-define-widget themed-text
:inject '(theme) ; 从上层获取
:slot t
:render (lambda (props slot injected)
(let ((theme (plist-get injected :theme)))
(tp-set slot 'face
(if (equal theme "dark")
'(:foreground "white" :background "black")
'(:foreground "black" :background "white"))))))
```
**作用**:
- 跨层级的数据传递
- 主题、配置等全局状态的共享
**是否必要**: ⭐⭐ 低优先级
- Emacs 可以使用动态绑定实现
- 如果组件树很深,可能有价值
---
### 8. 条件渲染辅助Conditional Rendering Helpers
**参考**: Vue3 的 `v-if`, `v-show`, `v-for`
**功能描述**:
```elisp
;; 辅助函数
(defun tp-if (condition then &optional else)
"条件渲染"
(if condition then (or else "")))
(defun tp-for (items template)
"列表渲染"
(mapconcat template items ""))
;; 使用
(tp-define-widget user-list
:props '(users show-email)
:render (lambda (props _slot)
(tp-for (plist-get props :users)
(lambda (user)
(concat (plist-get user :name)
(tp-if (plist-get props :show-email)
(format " <%s>" (plist-get user :email))))))))
```
**作用**:
- 简化常见的渲染模式
- 提高代码可读性
**是否必要**: ⭐⭐⭐ 中等优先级
- 作为辅助函数很有用
- 可以独立于核心组件系统实现
---
### 9. 插槽类型验证Slot Type Validation
**功能描述**:
```elisp
(tp-define-widget container
:slot 'string ; 只接受字符串
;; 或
:slot '(string widget) ; 接受字符串和组件
;; 或
:slot '(widget button text) ; 只接受特定组件
:render ...)
```
**作用**:
- 类型安全
- 更好的错误提示
**是否必要**: ⭐⭐ 低优先级
- 开发时有用
- 可能影响性能
---
### 10. 异步组件Async Components
**参考**: Vue3 的 `defineAsyncComponent`
**功能描述**:
```elisp
(tp-define-async-widget remote-content
:props '(url)
:loading "Loading..."
:error "Failed to load"
:render (lambda (props slot)
(url-retrieve-synchronously (plist-get props :url))
...))
```
**作用**:
- 支持异步数据加载
- 加载和错误状态处理
**是否必要**: ⭐ 最低优先级
- Emacs 的异步模型与 Web 不同
- 可能需要使用 `url-retrieve` 和回调
---
## 优先级总结
| 优化项 | 优先级 | 复杂度 | 价值 |
|-------|-------|-------|-----|
| 响应式状态 | ⭐⭐⭐⭐ | 中 | 高 |
| 事件系统 | ⭐⭐⭐⭐ | 中 | 高 |
| 命名插槽 | ⭐⭐⭐ | 低 | 中 |
| 组件继承 | ⭐⭐⭐ | 中 | 中 |
| 条件渲染辅助 | ⭐⭐⭐ | 低 | 中 |
| 生命周期钩子 | ⭐⭐ | 低 | 低 |
| 作用域插槽 | ⭐⭐ | 高 | 中 |
| 依赖注入 | ⭐⭐ | 中 | 低 |
| 类型验证 | ⭐⭐ | 低 | 低 |
| 异步组件 | ⭐ | 高 | 低 |
---
## 建议实施顺序
1. **第一阶段**: 事件系统 + 响应式状态集成
- 这两个特性对交互式组件最重要
- 可以利用现有的 tp reactive 系统
2. **第二阶段**: 命名插槽 + 条件渲染辅助
- 提升组件的灵活性和开发体验
3. **第三阶段**: 组件继承 + 生命周期钩子
- 对于构建组件库有价值
4. **第四阶段**: 其他高级特性
- 根据实际需求决定
---
## 示例:完整的组件定义(理想状态)
```elisp
(tp-define-widget button
;; 属性定义
:props '(action
(type . "default")
(size . "medium")
(disabled . nil))
;; 状态(响应式)
:state '((loading . nil)
(focused . nil))
;; 支持插槽
:slot t
;; 可触发的事件
:emits '(click focus blur)
;; 生命周期
:on-render (lambda (props)
(unless (plist-get props :disabled)
(message "Button rendering...")))
;; 渲染函数
:render (lambda (props state slot emit)
(let* ((type (plist-get props :type))
(size (plist-get props :size))
(disabled (plist-get props :disabled))
(loading (plist-get state :loading))
(content (if loading "Loading..." slot))
(face (cond
(disabled '(:foreground "gray"))
((equal type "primary") '(:foreground "white" :background "blue"))
((equal type "danger") '(:foreground "white" :background "red"))
(t '(:foreground "black" :background "#eee")))))
(tp-add content
'face face
'mouse-1 (unless disabled
(lambda ()
(funcall emit :click)
(funcall (plist-get props :action))))
'pointer (unless disabled 'hand)))))
```
---
## 结论
当前的组件系统已经具备基本功能。上述优化提案可以根据实际使用场景和需求逐步实施。建议从**事件系统**和**响应式状态集成**开始,因为这两个特性对于构建交互式 UI 组件最为重要。

View File

@ -3436,24 +3436,24 @@ When using tp-set (direct property setting), tp-name is NOT added."
(ert-deftest tp-test-define-twidget-basic () (ert-deftest tp-test-define-twidget-basic ()
"Test basic twidget definition." "Test basic twidget definition."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(tp-twidget-reset) (tp-widget-reset)
(tp-define-twidget test-widget (tp-define-twidget test-widget
:props '(name) :props '(name)
:slot 'content :slot t
:render (lambda (props slot) :render (lambda (props slot)
(concat "Hello " (plist-get props :name) ": " slot))) (concat "Hello " (plist-get props :name) ": " slot)))
(should (assoc 'test-widget tp-twidget-alist)) (should (assoc 'test-widget tp-widget-alist))
(let ((def (cdr (assoc 'test-widget tp-twidget-alist)))) (let ((def (cdr (assoc 'test-widget tp-widget-alist))))
(should (equal (plist-get def :props) '(name))) (should (equal (plist-get def :props) '(name)))
(should (equal (plist-get def :slot) 'content))))) (should (equal (plist-get def :slot) t)))))
(ert-deftest tp-test-widget-parse-basic () (ert-deftest tp-test-widget-parse-basic ()
"Test basic widget parsing." "Test basic widget parsing."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(tp-twidget-reset) (tp-widget-reset)
(tp-define-twidget greeting (tp-define-twidget greeting
:props '(name) :props '(name)
:slot 'message :slot t
:render (lambda (props slot) :render (lambda (props slot)
(concat "Hello " (plist-get props :name) "! " slot))) (concat "Hello " (plist-get props :name) "! " slot)))
(let ((result (tp-widget-parse '(greeting :name "World" "Nice to meet you")))) (let ((result (tp-widget-parse '(greeting :name "World" "Nice to meet you"))))
@ -3462,10 +3462,10 @@ When using tp-set (direct property setting), tp-name is NOT added."
(ert-deftest tp-test-widget-parse-with-default () (ert-deftest tp-test-widget-parse-with-default ()
"Test widget parsing with default prop values." "Test widget parsing with default prop values."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(tp-twidget-reset) (tp-widget-reset)
(tp-define-twidget styled-text (tp-define-twidget styled-text
:props '((color . "blue") message) :props '((color . "blue") message)
:slot 'text :slot t
:render (lambda (props slot) :render (lambda (props slot)
(let ((color (plist-get props :color)) (let ((color (plist-get props :color))
(msg (plist-get props :message))) (msg (plist-get props :message)))
@ -3483,11 +3483,11 @@ When using tp-set (direct property setting), tp-name is NOT added."
(ert-deftest tp-test-widget-parse-with-text-properties () (ert-deftest tp-test-widget-parse-with-text-properties ()
"Test widget parsing with text properties." "Test widget parsing with text properties."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(tp-twidget-reset) (tp-widget-reset)
;; Define a simple widget that applies text properties ;; Define a simple widget that applies text properties
(tp-define-twidget bold-text (tp-define-twidget bold-text
:props '((color . "black")) :props '((color . "black"))
:slot 'content :slot t
:render (lambda (props slot) :render (lambda (props slot)
(let ((color (plist-get props :color))) (let ((color (plist-get props :color)))
(tp-set slot 'face `(:foreground ,color :weight bold))))) (tp-set slot 'face `(:foreground ,color :weight bold)))))
@ -3501,7 +3501,7 @@ When using tp-set (direct property setting), tp-name is NOT added."
(ert-deftest tp-test-widget-parse-button-example () (ert-deftest tp-test-widget-parse-button-example ()
"Test the button widget example from the problem statement." "Test the button widget example from the problem statement."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(tp-twidget-reset) (tp-widget-reset)
;; First define the tp-space layer (from problem statement) ;; First define the tp-space layer (from problem statement)
(define-tp tp-space (pixel) (define-tp tp-space (pixel)
`(display (space :width (,pixel)))) `(display (space :width (,pixel))))
@ -3509,7 +3509,7 @@ When using tp-set (direct property setting), tp-name is NOT added."
;; Note: Using tp-button as the property name to match problem statement ;; Note: Using tp-button as the property name to match problem statement
(tp-define-twidget button (tp-define-twidget button
:props '(action (bgcolor . "green")) :props '(action (bgcolor . "green"))
:slot 'label :slot t
:render (lambda (props slot) :render (lambda (props slot)
(let ((action (plist-get props :action)) (let ((action (plist-get props :action))
(bgcolor (plist-get props :bgcolor))) (bgcolor (plist-get props :bgcolor)))
@ -3532,35 +3532,35 @@ When using tp-set (direct property setting), tp-name is NOT added."
(let ((action-prop (get-text-property 2 'tp-button result))) (let ((action-prop (get-text-property 2 'tp-button result)))
(should (listp (plist-get action-prop :action))))))) (should (listp (plist-get action-prop :action)))))))
(ert-deftest tp-test-twidget-reset () (ert-deftest tp-test-widget-reset ()
"Test twidget reset clears all definitions." "Test widget reset clears all definitions."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(tp-twidget-reset) (tp-widget-reset)
(tp-define-twidget test-widget1 (tp-define-twidget test-widget1
:props '(a) :props '(a)
:slot 'b :slot t
:render (lambda (p s) "")) :render (lambda (p s) ""))
(tp-define-twidget test-widget2 (tp-define-twidget test-widget2
:props '(c) :props '(c)
:slot 'd :slot t
:render (lambda (p s) "")) :render (lambda (p s) ""))
(should (= (length tp-twidget-alist) 2)) (should (= (length tp-widget-alist) 2))
(tp-twidget-reset) (tp-widget-reset)
(should (= (length tp-twidget-alist) 0)))) (should (= (length tp-widget-alist) 0))))
(ert-deftest tp-test-widget-parse-error-undefined () (ert-deftest tp-test-widget-parse-error-undefined ()
"Test widget parse with undefined widget raises error." "Test widget parse with undefined widget raises error."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(tp-twidget-reset) (tp-widget-reset)
(should-error (tp-widget-parse '(undefined-widget "content"))))) (should-error (tp-widget-parse '(undefined-widget "content")))))
(ert-deftest tp-test-widget-multiple-props () (ert-deftest tp-test-widget-multiple-props ()
"Test widget with multiple props including defaults." "Test widget with multiple props including defaults."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(tp-twidget-reset) (tp-widget-reset)
(tp-define-twidget multi-prop (tp-define-twidget multi-prop
:props '(required (opt1 . "default1") (opt2 . "default2")) :props '(required (opt1 . "default1") (opt2 . "default2"))
:slot 'content :slot t
:render (lambda (props slot) :render (lambda (props slot)
(format "%s|%s|%s|%s" (format "%s|%s|%s|%s"
(plist-get props :required) (plist-get props :required)
@ -3577,5 +3577,74 @@ When using tp-set (direct property setting), tp-name is NOT added."
(let ((result (tp-widget-parse '(multi-prop :required "req" :opt1 "c1" :opt2 "c2" "slot")))) (let ((result (tp-widget-parse '(multi-prop :required "req" :opt1 "c1" :opt2 "c2" "slot"))))
(should (equal result "req|c1|c2|slot"))))) (should (equal result "req|c1|c2|slot")))))
(ert-deftest tp-test-widget-multiple-slots ()
"Test widget with multiple slot values."
(tp-test-with-temp-buffer
(tp-widget-reset)
(tp-define-twidget container
:slot t
:render (lambda (_props slot) (format "[%s]" slot)))
(let ((result (tp-widget-parse '(container "hello " "world"))))
(should (equal result "[hello world]")))))
(ert-deftest tp-test-widget-nested-widgets ()
"Test widget with nested widget forms in slot."
(tp-test-with-temp-buffer
(tp-widget-reset)
;; Define parent container
(tp-define-twidget p
:slot t
:render (lambda (_props slot) slot))
;; Define text wrapper
(tp-define-twidget text
:slot t
:render (lambda (_props slot)
(tp-set slot 'face 'bold)))
;; Define button widget
(tp-define-twidget button
:props '(action (bgcolor . "orange"))
:slot t
:render (lambda (props slot)
(let ((bgcolor (plist-get props :bgcolor)))
(tp-add slot 'tp-button `(:bgcolor ,bgcolor)))))
;; Test nested widgets (example from problem statement)
(let ((result (tp-widget-parse
'(p "happy hacking "
(text "emacs")
(button :action (lambda () (message "clicked!"))
"click")))))
(should (stringp result))
(should (equal (substring-no-properties result) "happy hacking emacsclick"))
;; Check that "emacs" part has bold face
(should (eq (get-text-property 14 'face result) 'bold))
;; Check that "click" part has tp-button property
(should (get-text-property 19 'tp-button result)))))
(ert-deftest tp-test-widget-no-slot ()
"Test widget without slot support."
(tp-test-with-temp-buffer
(tp-widget-reset)
;; Widget without :slot defined (defaults to nil)
(tp-define-twidget static-widget
:props '((text . "default text"))
:render (lambda (props _slot)
(plist-get props :text)))
;; Slot values should be ignored
(let ((result (tp-widget-parse '(static-widget :text "hello" "ignored"))))
(should (equal result "hello")))))
(ert-deftest tp-test-widget-slot-boolean-false ()
"Test widget with :slot nil explicitly."
(tp-test-with-temp-buffer
(tp-widget-reset)
(tp-define-twidget no-slot-widget
:props '(value)
:slot nil
:render (lambda (props slot)
(format "%s (slot: %s)" (plist-get props :value) slot)))
;; Slot should be nil even if arguments are provided
(let ((result (tp-widget-parse '(no-slot-widget :value "test" "ignored slot"))))
(should (equal result "test (slot: nil)")))))
(provide 'tp-ert-tests) (provide 'tp-ert-tests)
;;; tp-ert-tests.el ends here ;;; tp-ert-tests.el ends here

90
tp.el
View File

@ -3541,6 +3541,9 @@ Returns the modified object (string) or nil for buffer operations."
"Alist of widget definitions: (WIDGET-NAME . DEFINITION). "Alist of widget definitions: (WIDGET-NAME . DEFINITION).
Each DEFINITION is a plist with :props, :slot, and :render keys.") Each DEFINITION is a plist with :props, :slot, and :render keys.")
;; Alias for backward compatibility
(defvaralias 'tp-twidget-alist 'tp-widget-alist)
(defmacro tp-define-widget (name &rest args) (defmacro tp-define-widget (name &rest args)
"Define a text widget (widget) named NAME. "Define a text widget (widget) named NAME.
@ -3548,25 +3551,42 @@ ARGS should include:
:props - A quoted list of property definitions. Each can be: :props - A quoted list of property definitions. Each can be:
- A symbol: required property accessed via keyword - A symbol: required property accessed via keyword
- A cons cell (SYMBOL . DEFAULT): property with default value - A cons cell (SYMBOL . DEFAULT): property with default value
:slot - A quoted symbol naming the slot (last positional argument) :slot - Boolean value. nil (default) means widget does not support slot.
t means widget supports slot content.
:render - A lambda (props slot) that returns the rendered string :render - A lambda (props slot) that returns the rendered string
The render function receives: The render function receives:
- PROPS: a plist of resolved property values (with :keyword keys) - PROPS: a plist of resolved property values (with :keyword keys)
- SLOT: the slot value (last positional argument) - SLOT: the slot content. When :slot is t, this is a string containing
all slot values concatenated together. Slot values can be plain
strings or nested widget-forms (which are recursively parsed).
When :slot is nil, SLOT will be nil.
Slot values are all elements that remain after extracting the plist
(keyword-value pairs) from the widget invocation. Multiple slot values
are supported and can include both strings and nested widget-forms.
Example: Example:
(tp-define-widget button (tp-define-widget button
:props \\='(action (bgcolor . \"green\")) :props \\='(action (bgcolor . \"orange\"))
:slot \\='label :slot t
:render (lambda (props slot) :render (lambda (props slot)
(let ((action (plist-get props :action)) (let ((action (plist-get props :action))
(bgcolor (plist-get props :bgcolor))) (bgcolor (plist-get props :bgcolor)))
(tp-add (format \"%s%s%s\" (tp-add (format \"%s%s%s\"
(tp-set \" \" \\='tp-space 2) (tp-set \" \" \\='tp-space 6)
slot (tp-set \" \" \\='tp-space 2)) slot (tp-set \" \" \\='tp-space 6))
\\='face \\=`(:background ,bgcolor) \\='tp-button \\=`(:bgcolor ,bgcolor :action ,action)))))
\\='tp-button \\=`(:action ,action)))))"
(tp-define-widget p
:slot t
:render (lambda (_props slot) slot))
;; Usage with multiple slot values:
(tp-widget-parse \\='(p \"happy hacking \"
(text \"emacs\")
(button :action (lambda () (message \"clicked!\"))
\"click\")))"
(declare (indent defun)) (declare (indent defun))
(let ((props nil) (let ((props nil)
(slot nil) (slot nil)
@ -3582,11 +3602,13 @@ Example:
`(tp--define-widget-internal ',name ,props ,slot ,render))) `(tp--define-widget-internal ',name ,props ,slot ,render)))
(defalias 'define-twidget 'tp-define-widget) (defalias 'define-twidget 'tp-define-widget)
(defalias 'tp-define-twidget 'tp-define-widget)
(defalias 'tp-twidget-reset 'tp-widget-reset)
(defun tp--define-widget-internal (name props slot render) (defun tp--define-widget-internal (name props slot render)
"Internal function to define a widget NAME with PROPS, SLOT, and RENDER. "Internal function to define a widget NAME with PROPS, SLOT, and RENDER.
PROPS is a list of property definitions. PROPS is a list of property definitions.
SLOT is the slot name symbol. SLOT is a boolean indicating whether the widget supports slot content.
RENDER is the render function." RENDER is the render function."
(let ((definition (list :props props :slot slot :render render)) (let ((definition (list :props props :slot slot :render render))
(existing (assoc name tp-widget-alist))) (existing (assoc name tp-widget-alist)))
@ -3617,19 +3639,22 @@ Returns nil if no default is specified."
"Parse and render a widget invocation. "Parse and render a widget invocation.
WIDGET-FORM is a list starting with the widget name, followed by WIDGET-FORM is a list starting with the widget name, followed by
keyword-value pairs for props, and ending with a single slot value. keyword-value pairs for props, and then slot values (if the widget
supports slots).
The format is: (WIDGET-NAME :prop1 val1 :prop2 val2 ... SLOT-VALUE) The format is: (WIDGET-NAME :prop1 val1 :prop2 val2 ... SLOT-VALUES...)
Keyword arguments must come before the slot value. The slot value Keyword arguments must come before slot values. Slot values are all
is the last non-keyword argument and must be exactly one value. remaining elements after the keyword-value pairs. Each slot value can be:
- A string: used directly
- A list starting with a widget name: recursively parsed as a widget
Example: Example:
(tp-widget-parse (tp-widget-parse
\\='(button :action (lambda () \\='(p \"happy hacking \"
(interactive) (text \"emacs\")
(message \"button clicked!\")) (button :action (lambda () (message \"clicked!\"))
\"CLICK\")) \"click\")))
Returns the rendered string with text properties applied." Returns the rendered string with text properties applied."
(unless (and (listp widget-form) (symbolp (car widget-form))) (unless (and (listp widget-form) (symbolp (car widget-form)))
@ -3640,26 +3665,39 @@ Returns the rendered string with text properties applied."
(unless definition (unless definition
(error "Undefined widget: %S" widget-name)) (error "Undefined widget: %S" widget-name))
(let* ((prop-defs (plist-get definition :props)) (let* ((prop-defs (plist-get definition :props))
(slot-name (plist-get definition :slot)) (slot-supported (plist-get definition :slot))
(render-fn (plist-get definition :render)) (render-fn (plist-get definition :render))
(parsed-props nil) (parsed-props nil)
(slot-value nil)) (slot-value nil))
;; Parse the widget invocation arguments ;; Parse the widget invocation arguments
;; Extract keyword arguments and the slot (last positional argument) ;; Extract keyword arguments and collect slot values
(let ((args rest) (let ((args rest)
(collected-props nil)) (collected-props nil)
(slot-parts nil))
;; Parse keyword arguments ;; Parse keyword arguments
(while (and args (keywordp (car args))) (while (and args (keywordp (car args)))
(let ((key (car args)) (let ((key (car args))
(val (cadr args))) (val (cadr args)))
(push (cons key val) collected-props) (push (cons key val) collected-props)
(setq args (cddr args)))) (setq args (cddr args))))
;; The remaining argument(s) should be the slot value (exactly one) ;; The remaining arguments are slot values (if slot is supported)
(when args (when (and slot-supported args)
(setq slot-value (car args)) ;; Process each slot value
(when (cdr args) (dolist (slot-item args)
(warn "tp-widget-parse: Extra arguments after slot\ (cond
value ignored: %S" (cdr args)))) ;; String: use directly
((stringp slot-item)
(push slot-item slot-parts))
;; List starting with a defined widget name: recursively parse
((and (listp slot-item)
(symbolp (car slot-item))
(assoc (car slot-item) tp-widget-alist))
(push (tp-widget-parse slot-item) slot-parts))
;; Other values: convert to string
(t
(push (format "%s" slot-item) slot-parts))))
;; Combine all slot parts into one string
(setq slot-value (apply #'concat (nreverse slot-parts))))
;; Build the props plist with defaults ;; Build the props plist with defaults
(dolist (prop-def prop-defs) (dolist (prop-def prop-defs)
(let* ((prop-name (tp--widget-prop-name prop-def)) (let* ((prop-name (tp--widget-prop-name prop-def))