tp/README.md
copilot-swe-agent[bot] 932585da3f Fix tp-get to return intervals and tp-match/tp-regexp string parsing
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
2025-12-14 13:50:45 +00:00

1101 lines
23 KiB
Markdown

# tp.el - Text Properties Library for Emacs
<p align="center">
<strong>A powerful text properties manipulation library with an innovative layer system</strong>
</p>
<p align="center">
<a href="#features">Features</a>
<a href="#installation">Installation</a>
<a href="#quick-start">Quick Start</a>
<a href="#api-reference">API Reference</a>
<a href="#the-layer-system">Layer System</a>
<a href="README_CN.md">中文文档</a>
</p>
---
**tp.el** provides a convenient and unified API for manipulating Emacs text properties. Inspired by [ov.el](https://github.com/emacsorphanage/ov) for overlays, tp.el offers:
- **Unified API**: All property-setting functions work on both **strings** and **buffers**
- **Layer System**: Stack multiple property sets on the same text region
- **Pattern Matching**: Apply properties to text matching strings or regexps
## Features
-**Unified Object Support**: Functions like `tp-set`, `tp-match`, `tp-regexp` work on both strings and buffers
-**Clear Semantics**: `tp-reset` (replace all), `tp-set` (replace specified), `tp-add` (deep merge)
-**Nested Property Access**: Get/set/remove nested sub-properties with path syntax
-**Innovative Layer System**: Stack, rotate, and manage multiple layers of properties
-**Layer Groups**: Define reusable sets of related layers
-**Search & Navigation**: Find and navigate through propertized text
-**Pattern Matching**: Apply properties to string/regexp matches with reset/add variants
-**Clean API**: Consistent naming and calling conventions
## Requirements
- **Emacs 28.1+** (uses `object-intervals` function)
- **dash.el** (list manipulation utilities)
## Installation
```elisp
;; Add to your load-path
(add-to-list 'load-path "/path/to/tp")
(require 'tp)
```
Or with `use-package`:
```elisp
(use-package tp
:load-path "/path/to/tp")
```
---
## Quick Start
### Setting Properties
tp.el provides three main functions for setting properties, each with different semantics:
```elisp
;; tp-set: Replace only specified properties, preserve others
(tp-set 1 10 '(face bold help-echo "Hello!"))
;; tp-reset: Completely replace ALL properties
(tp-reset 1 10 '(face bold)) ; Any other properties are removed
;; tp-add: Deep merge nested properties
(tp-add 1 10 '(face (:underline t))) ; Merges with existing face
```
All three functions support four calling conventions:
```elisp
;; On current buffer (properties as a list)
(tp-set 1 10 '(face bold help-echo "Hello!"))
;; On a specific buffer
(tp-set 1 10 '(face bold) some-buffer)
;; On a string with range (0-indexed)
(tp-set 0 5 '(face bold) "Hello World")
;; => #("Hello World" 0 5 (face bold))
;; On entire string (flat properties)
(tp-set "Hello World" 'face 'bold 'help-echo "test")
;; => #("Hello World" 0 11 (face bold help-echo "test"))
```
### Single-Property Setters
```elisp
;; Set only face property
(tp-set-face 1 10 'bold)
(tp-set-face "Hello" 'italic) ; entire string
;; Set only display property
(tp-set-display 1 10 '(space :width 10))
```
### Getting Properties
```elisp
;; Get specific property at position
(tp-get 5 'face) ; => bold
;; Get nested sub-property
(tp-get 5 'face :foreground) ; => "red"
(tp-get 5 'face :box :color) ; => "blue" (deeply nested)
;; Get specific property from range
(tp-get 1 10 'face) ; => bold
;; Get all properties from range
(tp-get 1 10) ; => (face bold help-echo "Hello!")
;; Get all properties at point
(tp-at 5) ; => (face bold help-echo "Hello!")
```
### Removing Properties
```elisp
;; Remove entire property
(tp-remove 1 10 'face)
;; Remove sub-property
(tp-remove 1 10 '(face :underline))
;; Remove nested sub-properties (keep others)
(tp-remove 1 10 '(face :underline (:style :position)))
;; Removes :style and :position from :underline, keeps :color if present
```
### Pattern Matching
```elisp
;; Apply properties to all occurrences of "TODO" in buffer
(tp-match "TODO" '(face warning))
;; Apply to string
(tp-match "world" "Hello world world" '(face bold))
;; => #("Hello world world" 6 11 (face bold) 12 17 (face bold))
;; Match with (PATTERN STRING) format
(tp-match '("world" "Hello world") '(face bold))
;; => #("Hello world" 6 11 (face bold))
;; Using regexp
(tp-regexp "\\b[0-9]+\\b" '(face font-lock-number-face))
;; Reset variants (replace ALL properties on matches)
(tp-match-reset "TODO" '(face warning))
(tp-regexp-reset "[0-9]+" '(face bold))
;; Add variants (deep merge properties on matches)
(tp-match-add "TODO" '(face (:underline t)))
(tp-regexp-add "[0-9]+" '(face (:weight bold)))
```
---
## API Reference
### Core Property Functions
#### `tp-set` - Set Text Properties
Set text properties on a string or buffer region. Replaces only the specified properties, preserving others.
```elisp
;; Current buffer (properties as a list)
(tp-set START END '(PROPERTY VALUE ...))
;; Specific buffer or string
(tp-set START END '(PROPERTY VALUE ...) OBJECT)
;; Entire string (flat properties)
(tp-set STRING PROPERTY VALUE ...)
```
**Examples:**
```elisp
;; Set face on buffer region
(tp-set 1 10 '(face bold)) ; => (1 . 10)
;; Set multiple properties
(tp-set 1 10 '(face bold help-echo "Click me"))
;; Set on specific buffer
(tp-set 1 10 '(face italic) my-buffer)
;; Set properties on a string (0-indexed)
(setq my-string (tp-set 0 5 '(face italic) "Hello World"))
;; => #("Hello World" 0 5 (face italic))
;; Set properties on entire string
(tp-set "Hello" 'face 'bold 'mouse-face 'highlight)
;; => #("Hello" 0 5 (face bold mouse-face highlight))
```
---
#### `tp-reset` - Replace All Properties
Completely replace ALL text properties with the specified ones.
```elisp
(tp-reset START END '(PROPERTY VALUE ...) &optional OBJECT)
(tp-reset STRING PROPERTY VALUE ...)
```
**Examples:**
```elisp
;; Replace all properties in region
(tp-reset 1 10 '(face bold)) ; Any existing properties are removed
;; On string
(tp-reset "Hello" 'face 'italic)
```
---
#### `tp-add` - Add/Merge Properties
Add or update properties with deep merge support for nested plists.
```elisp
(tp-add START END '(PROPERTY VALUE ...) &optional OBJECT)
(tp-add STRING PROPERTY VALUE ...)
```
**Examples:**
```elisp
;; Add properties (preserves existing, merges nested)
(tp-add 1 10 '(help-echo "tooltip"))
;; Deep merge face properties
(tp-set 1 10 '(face (:foreground "red")))
(tp-add 1 10 '(face (:background "blue")))
;; Result: face is (:foreground "red" :background "blue")
;; Face prepending - symbol faces are prepended to face list
(tp-set "Hello" 'face 'bold)
(tp-add "Hello" 'face 'shadow)
;; Result: face is (shadow bold)
```
---
#### `tp-set-face` - Set Face Property
Set only the face property, preserving other properties.
```elisp
(tp-set-face START END FACE &optional OBJECT)
(tp-set-face STRING FACE)
```
**Examples:**
```elisp
(tp-set-face 1 10 'bold)
(tp-set-face 1 10 '(:foreground "red" :weight bold))
(tp-set-face "Hello" 'italic)
```
---
#### `tp-set-display` - Set Display Property
Set only the display property, preserving other properties.
```elisp
(tp-set-display START END DISPLAY &optional OBJECT)
(tp-set-display STRING DISPLAY)
```
**Examples:**
```elisp
(tp-set-display 1 10 '(space :width 10))
(tp-set-display " " '(space :width 20))
```
---
#### `tp-get` - Get Property Value
Get property value(s) from position or range, with support for nested sub-properties.
For range queries, returns a list of `(START END VALUE)` intervals, allowing you to see all property values across the range.
```elisp
;; Single position
(tp-get POSITION PROPERTY)
(tp-get POSITION PROPERTY OBJECT)
;; Nested sub-property access
(tp-get POSITION PROPERTY SUB-KEY ...)
;; Range - specific property (returns list of intervals)
(tp-get START END PROPERTY)
(tp-get START END PROPERTY OBJECT)
;; Range with property path as list
(tp-get START END '(PROPERTY) OBJECT)
(tp-get START END '(PROPERTY SUB-KEY ...) OBJECT)
;; Range - all properties (returns list of intervals)
(tp-get START END)
(tp-get START END OBJECT)
;; Entire string
(tp-get STRING)
(tp-get STRING PROPERTY)
(tp-get STRING PROPERTY SUB-KEY ...)
```
**Examples:**
```elisp
;; Get from current buffer
(tp-get 5 'face) ; => bold
;; Get nested sub-property
(tp-get 5 'face :foreground) ; => "red"
(tp-get 5 'face :box :color) ; => "blue"
(tp-get 5 'display :width) ; => 10
;; Get from string (0-indexed)
(tp-get 0 'face my-string) ; => italic
;; Get from range - returns list of (START END VALUE) intervals
(tp-get 1 10 'face) ; => ((1 6 bold))
;; Get with multiple intervals
(tp-set 0 5 '(face bold) str)
(tp-set 12 17 '(face italic) str)
(tp-get 0 17 'face str) ; => ((0 5 bold) (12 17 italic))
;; Get with property path as list
(tp-get 5 20 '(face :underline :style) my-string) ; => ((5 20 wave))
;; Get all properties from range
(tp-get 1 10) ; => ((1 6 (face bold help-echo "test")))
;; Get from entire string
(tp-get "Hello World") ; => all properties at position 0
(tp-get "Hello World" 'face) ; => face value at position 0
(tp-get "Hello World" 'face :foreground) ; => foreground color
```
---
#### Fine-grained Property Functions
For manipulating sub-properties within complex properties like `face` or `display`:
```elisp
;; Get sub-property
(tp-get-sub POSITION PROPERTY SUB-PROPERTY &optional OBJECT)
;; Set sub-property
(tp-put-sub START END PROPERTY SUB-PROPERTY VALUE &optional OBJECT)
;; Remove sub-property
(tp-remove-sub START END PROPERTY SUB-PROPERTY &optional OBJECT)
```
**Examples:**
```elisp
;; Get :foreground from face
(tp-get-sub 1 'face :foreground) ; => "red"
;; Set :weight on face
(tp-put-sub 1 6 'face :weight 'bold)
;; Remove :background from face
(tp-remove-sub 1 6 'face :background)
```
---
#### `tp-at` - Get All Properties
```elisp
(tp-at &optional POINT OBJECT)
```
Get all text properties at POINT as a plist.
**Examples:**
```elisp
(tp-at 5) ; => (face bold help-echo "test")
(tp-at 0 my-string) ; Get from string
```
---
#### `tp-remove` - Remove Property
Remove a property or nested sub-property from a region or entire string.
```elisp
;; Remove entire property (buffer)
(tp-remove START END PROPERTY &optional OBJECT)
;; Remove sub-property (buffer)
(tp-remove START END '(PROPERTY SUB-KEY) &optional OBJECT)
;; Remove nested sub-properties (buffer)
(tp-remove START END '(PROPERTY SUB-KEY (NESTED-KEYS...)) &optional OBJECT)
;; Remove from entire string
(tp-remove STRING PROP1 PROP2 ...)
(tp-remove STRING PROPERTY SUB-KEY)
(tp-remove STRING PROPERTY SUB-KEY '(NESTED-KEYS...))
```
**Examples:**
```elisp
;; Remove entire property
(tp-remove 1 10 'face)
;; Remove sub-property from face
(tp-remove 1 10 '(face :underline))
;; Remove specific nested keys, keep others
(tp-remove 1 10 '(face :underline (:style :position)))
;; Removes :style and :position from :underline
;; If :color exists in :underline, it's preserved
;; Remove from entire string
(tp-remove "Hello World" 'face 'help-echo) ; Remove multiple properties
(tp-remove "Hello World" 'face :underline) ; Remove sub-property
(tp-remove "Hello World" 'face :underline '(:style :position)) ; Remove nested
```
---
#### `tp-remove-list` - Remove Multiple Properties
```elisp
(tp-remove-list START END PROPERTIES &optional OBJECT)
```
Remove multiple properties at once.
**Examples:**
```elisp
(tp-remove-list 1 10 '(face help-echo mouse-face))
```
---
#### `tp-clear` - Clear All Properties
```elisp
(tp-clear &optional START END OBJECT)
```
Clear all text properties from a region.
**Examples:**
```elisp
(tp-clear 1 10) ; Clear region
(tp-clear) ; Clear entire buffer
```
---
### Pattern Matching Functions
#### `tp-match` - Match String
```elisp
;; Buffer
(tp-match PATTERN '(PROPERTY VALUE ...))
;; String or Buffer object
(tp-match PATTERN OBJECT '(PROPERTY VALUE ...))
;; Pattern as (PATTERN STRING) format
(tp-match '(PATTERN STRING) '(PROPERTY VALUE ...))
```
Set properties on all occurrences of a string pattern.
**Examples:**
```elisp
;; In buffer - returns list of (START . END) pairs
(tp-match "TODO" '(face warning))
;; => ((10 . 14) (50 . 54) ...)
;; On string - returns modified string
(tp-match "o" "Hello World" '(face bold))
;; => #("Hello World" 4 5 (face bold) 7 8 (face bold))
;; Using (PATTERN STRING) format
(tp-match '("world" "Hello world") '(face bold))
;; => #("Hello world" 6 11 (face bold))
```
---
#### `tp-match-reset` - Match and Reset
Reset (completely replace) all properties on matches.
```elisp
(tp-match-reset PATTERN '(PROPERTY VALUE ...) &optional OBJECT)
```
**Examples:**
```elisp
(tp-match-reset "TODO" '(face warning))
;; Replaces ALL properties on matched text
```
---
#### `tp-match-add` - Match and Add
Add/merge properties on matches with deep merge support.
```elisp
(tp-match-add PATTERN '(PROPERTY VALUE ...) &optional OBJECT)
```
**Examples:**
```elisp
(tp-match-add "TODO" '(face (:underline t)))
;; Merges with existing properties
```
---
#### `tp-regexp` - Match Regexp
```elisp
;; Buffer
(tp-regexp PATTERN '(PROPERTY VALUE ...))
;; String or Buffer object
(tp-regexp PATTERN OBJECT '(PROPERTY VALUE ...))
```
Set properties on all matches of a regular expression.
**Examples:**
```elisp
;; Highlight all numbers in buffer
(tp-regexp "[0-9]+" '(face font-lock-number-face))
;; On string
(tp-regexp "[A-Z]+" "Hello WORLD" '(face bold))
;; => #("Hello WORLD" 6 11 (face bold))
```
---
#### `tp-regexp-reset` - Regexp and Reset
Reset (completely replace) all properties on regexp matches.
```elisp
(tp-regexp-reset PATTERN '(PROPERTY VALUE ...) &optional OBJECT)
```
---
#### `tp-regexp-add` - Regexp and Add
Add/merge properties on regexp matches with deep merge support.
```elisp
(tp-regexp-add PATTERN '(PROPERTY VALUE ...) &optional OBJECT)
```
---
### Propertize Functions
#### `tp-layer-propertize` - Apply Layer to Object
```elisp
(tp-layer-propertize OBJECT LAYER &optional START END)
```
Apply a predefined layer's properties to an object.
**Examples:**
```elisp
;; Define a layer first
(tp-layer-define highlight '(face (:background "yellow")))
;; Apply to string
(tp-layer-propertize "Important" 'highlight)
;; Apply to substring
(tp-layer-propertize "Hello World" 'highlight 0 5)
;; Apply to buffer region
(tp-layer-propertize (current-buffer) 'highlight 1 10)
```
---
#### `tp-group-propertize` - Apply Layer Group
```elisp
(tp-group-propertize OBJECT LAYER-GROUP &optional START END)
```
Apply all layers from a layer group to an object.
---
### Search & Navigation Functions
#### `tp-forward` / `tp-backward`
```elisp
(tp-forward PROPERTY &optional VALUE PREDICATE NOT-CURRENT)
(tp-backward PROPERTY &optional VALUE PREDICATE NOT-CURRENT)
```
Search forward/backward for text with PROPERTY.
**Examples:**
```elisp
;; Find next text with 'marker property
(tp-forward 'marker)
;; Find next text where 'type equals 'heading
(tp-forward 'type 'heading)
```
---
#### `tp-next` / `tp-prev`
```elisp
(tp-next &optional POINT PROPERTY VALUE)
(tp-prev &optional POINT PROPERTY VALUE)
```
Get the next/previous position with text properties.
---
#### `tp-goto-next` / `tp-goto-prev`
```elisp
(tp-goto-next &optional PROPERTY VALUE)
(tp-goto-prev &optional PROPERTY VALUE)
```
Move point to next/previous text with PROPERTY.
---
#### `tp-regions-map` / `tp-strings-map`
```elisp
(tp-regions-map FUNCTION PROPERTY &optional VALUE PREDICATE COLLECT)
(tp-strings-map FUNCTION PROPERTY &optional VALUE PREDICATE COLLECT)
```
Apply a function to all regions/strings with PROPERTY.
**Examples:**
```elisp
;; Upcase all marked text
(tp-strings-map
(lambda (str idx)
(message "Found: %s at index %d" str idx))
'marker)
```
---
### Query Functions
#### `tp-in` - Find Regions with Property
```elisp
(tp-in PROPERTY &optional VALUE START END)
```
Get all regions with PROPERTY in current buffer.
**Examples:**
```elisp
;; Get all regions with 'marker property
(tp-in 'marker)
;; => ((1 5 (marker t ...)) (10 15 (marker t ...)))
;; Filter by value
(tp-in 'type 'heading)
```
---
#### `tp-all` - Get All Propertized Regions
```elisp
(tp-all &optional START END)
```
Get all regions with any text properties.
---
#### `tp-intervals` - Get Property Intervals
```elisp
(tp-intervals START END &optional OBJECT)
```
Get all text property intervals in a region.
---
#### `tp-empty-p` - Check for Properties
```elisp
(tp-empty-p OBJECT)
```
Return t if OBJECT has no text properties.
---
#### `tp-plist` - Get Merged Properties
```elisp
(tp-plist START END &optional OBJECT)
```
Get a merged plist of all properties in a region.
---
## The Layer System
The **layer system** is tp.el's innovative feature that allows stacking multiple sets of properties on the same text region. Only the **top layer** is visible, but lower layers are preserved and can be revealed through rotation or pinning.
### Layer Concept
```
┌─────────────────────────────┐
│ TOP LAYER (visible) │ ← What you see
├─────────────────────────────┤
│ Middle Layer (hidden) │ ← Preserved
├─────────────────────────────┤
│ Bottom Layer (hidden) │ ← Preserved
└─────────────────────────────┘
```
### Layer Definition Functions
#### `tp-layer-define` - Define a Layer
```elisp
(tp-layer-define NAME PROPERTIES)
```
Define a named layer with properties.
**Examples:**
```elisp
(tp-layer-define highlight
'(face (:background "yellow" :foreground "black")))
(tp-layer-define error
'(face (:background "red" :foreground "white")
help-echo "Error!"))
(tp-layer-define info
'(face (:background "blue" :foreground "white")))
```
---
#### `tp-group-define` - Define Layer Group
```elisp
(tp-group-define NAME
LAYER1 PROPERTIES1
LAYER2 PROPERTIES2
...)
```
Define a group of related layers.
**Examples:**
```elisp
(tp-group-define status-colors
status-ok '(face (:foreground "green"))
status-warning '(face (:foreground "orange"))
status-error '(face (:foreground "red")))
```
---
#### `tp-layer-props` / `tp-group-props`
```elisp
(tp-layer-props LAYER-NAME)
(tp-group-props GROUP-NAME)
```
Get properties for a layer or all layers in a group.
---
#### `tp-layer-undefine` / `tp-group-undefine`
```elisp
(tp-layer-undefine NAME)
(tp-group-undefine NAME)
```
Remove layer or group definition.
---
#### `tp-layer-reset`
```elisp
(tp-layer-reset)
```
Clear all layer and group definitions.
---
### Layer Manipulation Functions
#### `tp-layer-push` - Add Layer
```elisp
(tp-layer-push START END NAME &optional OBJECT)
```
Push a layer to the top of the stack.
**Examples:**
```elisp
(tp-layer-define base '(face default))
(tp-layer-define highlight '(face (:background "yellow")))
;; Push base layer first
(tp-layer-push 1 10 'base)
;; Push highlight on top (now visible)
(tp-layer-push 1 10 'highlight)
```
---
#### `tp-layer-delete` - Remove Layer
```elisp
(tp-layer-delete START END NAME &optional OBJECT)
```
Delete a layer from anywhere in the stack.
**Examples:**
```elisp
;; Remove the highlight layer
(tp-layer-delete 1 10 'highlight)
;; base layer is now visible
```
---
#### `tp-layer-rotate` - Cycle Layers
```elisp
(tp-layer-rotate START END &optional OBJECT)
```
Rotate layers - top goes to bottom, next becomes visible.
**Examples:**
```elisp
;; Stack: highlight (top) -> base (bottom)
(tp-layer-rotate 1 10)
;; Stack: base (top) -> highlight (bottom)
```
---
#### `tp-layer-pin` - Bring Layer to Top
```elisp
(tp-layer-pin START END NAME &optional OBJECT)
```
Move a specific layer to the top.
**Examples:**
```elisp
;; Make 'base the top layer
(tp-layer-pin 1 10 'base)
```
---
#### `tp-layer-hide` / `tp-layer-show`
```elisp
(tp-layer-hide START END NAME &optional OBJECT)
(tp-layer-show START END NAME &optional OBJECT)
```
Hide layer (move to bottom) or show layer (move to top).
---
#### `tp-layer-merge`
```elisp
(tp-layer-merge START END LAYER1 LAYER2 NEW-NAME &optional OBJECT)
```
Merge two layers into one new layer.
---
### Layer Query Functions
#### `tp-layer-list` - List All Layers
```elisp
(tp-layer-list START END &optional OBJECT)
```
Get list of all layer names in region.
**Examples:**
```elisp
(tp-layer-list 1 10) ; => (highlight base)
```
---
#### `tp-layer-count`
```elisp
(tp-layer-count START END &optional OBJECT)
```
Count layers in region.
---
#### `tp-layer-exists-p`
```elisp
(tp-layer-exists-p START END NAME &optional OBJECT)
```
Check if layer exists in region.
---
#### `tp-layer-top`
```elisp
(tp-layer-top START END &optional OBJECT)
```
Get name of the top (visible) layer.
---
## Practical Examples
### Syntax Highlighting with Multiple Layers
```elisp
;; Define layers for different highlighting purposes
(tp-layer-define code-base
'(face font-lock-keyword-face))
(tp-layer-define code-error
'(face (:underline (:color "red" :style wave))
help-echo "Syntax error"))
(tp-layer-define code-debug
'(face (:background "dark blue")))
;; Apply base highlighting
(tp-layer-push 1 100 'code-base)
;; Add error highlight on problematic code
(tp-layer-push 50 60 'code-error)
;; Toggle between error and normal view
(defun toggle-error-view ()
(interactive)
(tp-layer-rotate 50 60))
```
### Status Indicator
```elisp
(tp-group-define task-status
status-todo '(face (:foreground "gray"))
status-progress '(face (:foreground "yellow"))
status-done '(face (:foreground "green")))
;; Cycle through statuses
(defun cycle-task-status ()
(interactive)
(tp-layer-rotate (line-beginning-position) (line-end-position)))
```
### Temporary Highlights
```elisp
(tp-layer-define temp-highlight
'(face (:background "yellow")))
(defun flash-region (start end)
"Flash a region temporarily."
(tp-layer-push start end 'temp-highlight)
(run-with-timer 0.5 nil
(lambda ()
(tp-layer-delete start end 'temp-highlight))))
```
---
## Aliases
For convenience, tp.el provides these aliases:
| Alias | Original Function |
|-------|-------------------|
| `tp-put` | `tp-set` |
| `tp-layer-properties` | `tp-layer-props` |
| `tp-layer-group-define` | `tp-group-define` |
| `tp-layer-group-properties` | `tp-group-props` |
| `tp-layer-group-propertize` | `tp-group-propertize` |
| `tp-layer-group-undefine` | `tp-group-undefine` |
### Deprecated Functions
| Function | Replacement | Notes |
|----------|-------------|-------|
| `tp-propertize` | `tp-set` | Use `tp-set` for new code |
---
## License
GNU General Public License v2 or later.
---
## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
---
<p align="center">
<em>tp.el - Making text properties powerful and easy to use</em>
</p>