tp/README.md

243 lines
13 KiB
Markdown

# TP
TP 2.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).
Complete public API reference: [API-REFERENCE.md](docs/API-REFERENCE.md) (中文).
It is the symbol-level usage index for the current TP 2.0 implementation; this
README remains the conceptual quick start.
## 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. Normal constructors defensively copy caller-owned strings and property data; candidate-local producers may use the explicit `-owned` plan/result constructors when they transfer every nested value and stop exposing it. An owned result must be created for the active prepare context and is consumed once.
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.
TP 2.0 drives the single live publication from exact transaction-scoped batch
entries, one frozen participant vector, and the candidate-bound final accept.
The same journals, surface snapshots, and change group are shared rather than
copied. Transaction participants register through
`tp-transaction-participate-v2`; TP has no alternate transaction writer or
runtime route switch.
Generic opaque authority markers are bounded and whitelist-validated before
final accept, then reverse-restored before ordinary rollback on partial apply or
accept failure. `tp-with-transaction` still returns its body value, and
internal outcomes remain observational side-channel evidence.
ETAF integration uses the same boundary through one opaque transaction
participant. ETAF prepares its immutable generation and Ebox candidate before
TP accepts the transaction; participant publish, TP final accept, and
post-accept cleanup are ordered explicitly. A participant failure restores the
previous generation and client state, while a post-accept observer failure is
contained as a diagnostic. ETAF's runtime fixed-point guard records each
effect's input/version tuple for one flush and stops repeated or over-bound
cycles instead of spinning.
## 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 |
| --- | --- |
| Core inspection and debug | `tp-debug-*`, `tp-intervals`, `tp-intervals-map`, `tp-plist`, `tp-text-snapshot`, `tp-empty-p` |
| 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` |
| Static recipes | `define-tp`/`tp-define-layer`, `define-tps`/`define-tp-group`/`tp-define-group`, layer/group queries, undefine/reset/describe |
| Signals and bindings | signal create/read/peek/set/dispose, binding install/read/dispose, `tp-variable-signal`, `tp-with-transaction`, `tp-transaction-participate-v2`, read-only `tp-transaction-active-p`, `tp-runtime-manifest`, counters/reset |
| Objects and plans | plan/result constructors, `tp-object-ensure`, retain/reuse, fragment/content-range attachment, resolve, mounted/mounts |
| Host ranges | `tp-range-anchor-create`, `tp-range-anchor-live-p`, `tp-object-attach-range`, `tp-range-rebase` |
| Surfaces | mount/update/scoped update, materialize, live/revision/client-state, at-point, report/report-summary/inspect, unmount |
| 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` |
| Search and navigation | `tp-match-*`, `tp-regexp-*`, `tp-search`, `tp-search-map`, `tp-forward*`, `tp-backward*` |
| Native query and mutation | `tp-lookup`, `tp-property-change`, `tp-property-any`, `tp-property-not-all`, `tp-with-mutation-policy` |
| Palette and display helpers | palette definition/lookups, built-in recipes, `tp-palette-show`, `tp-pop-to-buffer`, `tp-switch-to-buffer`, `tp-display-buffer-mode` |
See the [complete API reference](docs/API-REFERENCE.md) for signatures, return
shapes, examples, and the full module index. 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.
## TP 2.0 transaction migration
TP 2.0 removes the v1 transaction participant facade and the legacy execution
route. Replace `(tp-transaction-participate KEY PUBLISH ROLLBACK)` exactly with
`(tp-transaction-participate-v2 :key KEY :stage PUBLISH :rollback ROLLBACK)`.
Callers that inspect `tp-runtime-manifest` must require
`tp-transaction-protocol-v2`; route-selection and v1-adapter fields are no
longer published.
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).