Replace the legacy managed layer renderer with one independent retained/reactive text runtime. TP now owns exact dependencies, stable objects, marker-backed mounts, property contribution composition, atomic publication, rollback, and direct text-property facades without ECSS or Ebox dependencies.\n\nBREAKING CHANGE: remove tp-render, tp-stack, scan-driven managed layers, inline runtime metadata, TP-owned CSS cascade APIs, and dollar-variable declarations.\n\nVerified: 290/290 ERT, shuffled 290/290 (seed 20260806), 8/8 doctests, WERROR compile-all, checkdoc, package-lint, diff-check, and isolated TP-only load.
202 lines
11 KiB
Markdown
202 lines
11 KiB
Markdown
# TP
|
|
|
|
TP 1.0 is a standalone retained/reactive text runtime for Emacs. It projects declarative properties, reactive data, and stable text objects onto strings and buffers while owning text-property composition, exact dependency tracking, retained identity, marker-backed mounts, diffing, transactions, rollback, and final buffer publication.
|
|
|
|
TP does not depend on Ebox or ECSS. It does not implement CSS selectors, stylesheets, specificity, cascade winners, Box, Flex, Grid, measurement, or layout. A CSS consumer may compute final declarations with ECSS and publish them through TP, but TP itself only understands Emacs text properties and generic retained text surfaces.
|
|
|
|
Chinese documentation: [README_CN.md](README_CN.md).
|
|
|
|
## Requirements
|
|
|
|
- Emacs 28.1 or newer.
|
|
- No third-party runtime dependency.
|
|
|
|
```elisp
|
|
(add-to-list 'load-path "/path/to/tp")
|
|
(require 'tp)
|
|
```
|
|
|
|
## Choose the smallest public entry point
|
|
|
|
| Need | API | Live runtime? |
|
|
| --- | --- | --- |
|
|
| Return a propertized string | `tp-propertize` | No |
|
|
| Apply declarations once to an existing range | `tp-apply` | No |
|
|
| Reactively decorate existing host text | `tp-watch` | Yes, `properties` capability |
|
|
| Own retained text content | `tp-surface-mount` / `tp-surface-update` | Yes, `content` capability |
|
|
| Inspect or remove a retained publication | `tp-surface-report` / `tp-surface-inspect` / `tp-surface-unmount` | Yes |
|
|
|
|
The one-shot and retained APIs use the same property-policy and projection semantics. One-shot calls deliberately create no object, binding, marker, subscription, or surface state.
|
|
|
|
## Static properties
|
|
|
|
```elisp
|
|
(let* ((callback (lambda (_window _object _position) "Open"))
|
|
(text
|
|
(tp-propertize
|
|
"Hello"
|
|
(list 'face '(:foreground "white" :background "navy")
|
|
'help-echo callback
|
|
'keymap nil))))
|
|
text)
|
|
```
|
|
|
|
Explicit `nil` and an absent property are different. In the example above, `keymap` is present with value `nil`. Function values are literal data, so TP preserves `callback` instead of calling it.
|
|
|
|
To modify an existing range without changing its text:
|
|
|
|
```elisp
|
|
(with-current-buffer (get-buffer-create "*tp-demo*")
|
|
(erase-buffer)
|
|
(insert "abcdef")
|
|
(tp-apply (current-buffer) 2 5 '(face italic)))
|
|
```
|
|
|
|
The established `tp-set`, `tp-reset`, `tp-add`, `tp-remove`, `tp-clear`, `tp-get`, `tp-at`, `tp-member`, match, regexp, search, navigation, interval, and native query APIs remain the direct façade for ordinary text-property work.
|
|
|
|
## Reusable declaration recipes
|
|
|
|
`define-tp` and `define-tps` define reusable recipes that expand to direct Emacs properties. They are definition-time conveniences, not live mounted layers, and they never write identity or provenance into displayed text.
|
|
|
|
```elisp
|
|
(define-tp link-style (foreground)
|
|
`(face (:foreground ,foreground :weight bold)
|
|
mouse-face highlight
|
|
help-echo "Open item"))
|
|
|
|
(tp-set "Project" '(link-style "#58a6ff"))
|
|
```
|
|
|
|
Use `tp-computed` when a property value must be evaluated. Every ordinary function object remains literal.
|
|
|
|
```elisp
|
|
(defvar my-height 1.2)
|
|
|
|
(define-tp sized-label ()
|
|
`(face ,(tp-computed (lambda () (list :height my-height)))))
|
|
```
|
|
|
|
The computed function runs in the current binding/prepare context, so `tp-signal-read` and `tp-binding-read` establish exact dependencies. Its returned value is normalized and projected once, then treated as literal data.
|
|
|
|
## Reactive existing text
|
|
|
|
`tp-watch` decorates a marker-backed host range without owning or replacing its text:
|
|
|
|
```elisp
|
|
(let ((online (tp-signal-create nil)))
|
|
(with-current-buffer (get-buffer-create "*tp-status*")
|
|
(erase-buffer)
|
|
(insert "offline")
|
|
(tp-watch
|
|
(current-buffer) 1 8
|
|
(lambda ()
|
|
(list 'face
|
|
(list :foreground
|
|
(if (tp-signal-read online) "green" "red")))))))
|
|
```
|
|
|
|
A signal records the binding that actually reads it. Updating one signal invalidates only its subscribers; conditional computations automatically stop subscribing to sources that the new branch no longer reads. Equal writes are no-ops, and repeated writes inside `tp-with-transaction` recompute each affected binding at most once.
|
|
|
|
`tp-watch` returns the underlying surface handle. Pass it to `tp-surface-inspect`, `tp-surface-report`, or `tp-surface-unmount`.
|
|
|
|
## Retained content
|
|
|
|
A retained producer receives a prepare context, ensures stable objects before producing output, installs any object-local bindings, and returns a pure surface plan:
|
|
|
|
```elisp
|
|
(let* ((status (tp-signal-create "ready"))
|
|
(producer
|
|
(lambda (context)
|
|
(let* ((object (tp-object-ensure context nil 'status 'text))
|
|
(binding
|
|
(tp-bind object '(demo . status)
|
|
(lambda () (tp-signal-read status)))))
|
|
(tp-surface-plan-create
|
|
:key 'status
|
|
:kind 'text
|
|
:text (tp-binding-read binding)
|
|
:props '(face bold)
|
|
:capability 'content))))
|
|
(buffer (get-buffer-create "*tp-retained*"))
|
|
(surface
|
|
(tp-surface-mount buffer producer '(:capability content))))
|
|
(tp-signal-set status "done")
|
|
(tp-surface-report surface))
|
|
```
|
|
|
|
Plan fields are `key`, `kind`, `text`, `props`, `children`, `tags`, and `capability`. Plans contain no markers, buffer positions, patch operations, producer closures, or client continuations. Constructors defensively copy caller-owned strings and property data.
|
|
|
|
Stable identity is surface-local. `tp-object-ensure` reconciles by parent, sibling key, and kind; `tp-object-resolve` returns a live opaque handle by key path. `tp-surface-update-scoped` authorizes a full candidate update against one or more retained objects and rejects output changes outside their current mount ranges unless the caller explicitly selects root fallback.
|
|
|
|
`tp-surface-materialize-string` uses the same producer and plan semantics without creating a live surface. Its candidate objects, bindings, subscriptions, and anchors are released before it returns.
|
|
|
|
## Host ranges and property ownership
|
|
|
|
The `properties` capability uses opaque range anchors. A producer creates or receives a `tp-range-anchor-create` handle and attaches it to an object with `tp-object-attach-range`. Markers follow host edits; the plan itself remains position-free.
|
|
|
|
TP records the host baseline and each TP contribution per property interval. Overlapping contributions are composed through the registered property policy. If external code changes a property after TP publishes it, the next update reports `tp-property-conflict` instead of overwriting the external value. `tp-range-rebase` explicitly accepts the current host value as the new baseline. Unmount restores only values still owned by TP and preserves conflicting host edits.
|
|
|
|
## Transactions and failures
|
|
|
|
`tp-with-transaction` batches signal writes and all affected surfaces. TP prepares every candidate first, then publishes surfaces in stable order. A compute, validation, buffer write, marker/index publication, or transaction participant failure restores signal values, binding values and dependencies, text, properties, markers, indexes, plans, client state, revisions, and previous reports together.
|
|
|
|
Observers run only after a successful commit. Their failures are recorded and do not roll back an already committed transaction. A buffer killed during publication remains killed; rollback never recreates it.
|
|
|
|
## Property policies
|
|
|
|
`tp-define-property-policy` registers generic semantics for a final Emacs text property:
|
|
|
|
- normalization and validation;
|
|
- equality for no-op detection;
|
|
- contribution merging;
|
|
- projection to the final property value;
|
|
- explicit presence, including present `nil`.
|
|
|
|
`tp-register-text-property` supplies the default policy for a native property. `face` contributions use merge semantics; other properties use their registered policy. `tp-merge-declarations`, `tp-define-style`, and `tp-style-declarations` operate only on direct declarations. They do not implement CSS cascade.
|
|
|
|
## Public runtime families
|
|
|
|
| Family | Main public APIs |
|
|
| --- | --- |
|
|
| Property policy and declarations | `tp-define-property-policy`, `tp-register-text-property`, `tp-text-declarations`, `tp-computed`, `tp-resolve-value`, `tp-merge-declarations`, `tp-define-style`, `tp-style-declarations` |
|
|
| Signals and bindings | `tp-signal-create`, `tp-signal-read`, `tp-signal-peek`, `tp-signal-set`, `tp-bind`, `tp-binding-read`, `tp-with-transaction`, `tp-reactive-counters` |
|
|
| Objects and plans | `tp-object-ensure`, `tp-object-retain`, `tp-object-attach-fragment`, `tp-object-resolve`, `tp-object-mounts`, `tp-surface-plan-create`, `tp-surface-result-create` |
|
|
| Host ranges | `tp-range-anchor-create`, `tp-object-attach-range`, `tp-range-rebase` |
|
|
| Surfaces | `tp-surface-materialize-string`, `tp-surface-mount`, `tp-surface-update`, `tp-surface-update-scoped`, `tp-surface-unmount`, `tp-surface-at-point`, `tp-surface-report`, `tp-surface-inspect` |
|
|
| Direct façade | `tp-propertize`, `tp-apply`, `tp-watch`, `tp-set`, `tp-reset`, `tp-add`, `tp-remove`, `tp-clear`, `tp-get`, `tp-at`, `tp-member` |
|
|
|
|
See [API semantics](docs/API-SEMANTICS.md) for ownership, lifecycle, error, and return contracts, and [architecture](docs/ARCHITECTURE.md) for module boundaries and transaction flow.
|
|
|
|
## TP 1.0 migration
|
|
|
|
TP 1.0 removes the 0.3 managed stack/renderer runtime instead of hiding it behind compatibility branches. Removed behavior includes `tp-render.el`, `tp-stack.el`, stack mutation APIs, `tp-text`, `$variable` declarations, layer-to-buffer registries, scan-driven refresh, managed attach/detach/diagnostics, and inline `tp-name`/`tp-layers`/`tp-meta` runtime storage.
|
|
|
|
Use direct recipes for reusable static declarations, `tp-watch` for reactive properties on existing text, and retained content surfaces for reactive text or structured UI. TP does not automatically scan historical propertized text to reconstruct runtime identity.
|
|
|
|
## Examples
|
|
|
|
- [Static properties](examples/static-properties.el)
|
|
- [Reactive status](examples/reactive-status.el)
|
|
- [Retained dashboard](examples/retained-dashboard.el)
|
|
- [Diagnostic decoration](examples/diagnostic-decoration.el)
|
|
|
|
These examples use only TP public APIs and are tested without Ebox or ECSS on the load path.
|
|
|
|
## Verification
|
|
|
|
```sh
|
|
make test
|
|
make test-shuffled SHUFFLE_SEED=20260806
|
|
make doctest
|
|
make compile-all WERROR=t
|
|
make checkdoc
|
|
make package-lint
|
|
make diff-check
|
|
```
|
|
|
|
The structural tests also prove that normal signal and surface updates do not scan `buffer-list` or search displayed text for identity, equal results do not publish a new revision, and unmount/kill cleanup releases subscriptions and marker-backed runtime state.
|
|
|
|
## License
|
|
|
|
GPL-3.0-or-later. See [LICENSE](LICENSE).
|