Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
957a3e8
First sketching on templates
sirreal Dec 23, 2025
afc63eb
DROPME: Collect examples
sirreal Feb 2, 2026
75ecbe7
Introduce test suite based on existing examples
sirreal Feb 2, 2026
52836b9
yield + nowdoc
sirreal Feb 2, 2026
07a42d5
lints
sirreal Feb 2, 2026
336524f
test tweaks
sirreal Feb 2, 2026
c1a9050
Fix numeric array indexes
sirreal Feb 3, 2026
978d835
Add test for HTML that could produce a tag after modification
sirreal Feb 3, 2026
81f731e
Add test to prevent attribute mis-interpretation after replacement
sirreal Feb 3, 2026
cdedfb7
Move serialize_token to tag processor class (for normalization of tex…
sirreal Feb 3, 2026
e8f420c
Add attribute behavior testing
sirreal Feb 3, 2026
75cf458
working proof-of-concept
sirreal Feb 3, 2026
bf7f038
class cleanup and lints
sirreal Feb 4, 2026
df9deab
Add message on disallowed type
sirreal Feb 4, 2026
5a9cb43
Cleanup test + lints
sirreal Feb 4, 2026
f30d721
Add multiple template tests
sirreal Feb 4, 2026
63fa332
Improve test names and specificity
sirreal Feb 4, 2026
a451ed4
Improve test structure and names
sirreal Feb 4, 2026
48ff319
Refactor WP_HTML_Template to use composition over inheritance
sirreal Feb 4, 2026
e04d7c0
Remove unused preg_attribute_replace_callback method from WP_HTML_Tem…
sirreal Feb 4, 2026
65f62a9
Add more tests
sirreal Feb 4, 2026
5d58abf
lints
sirreal Feb 4, 2026
d57d0c4
Rework interface
sirreal Feb 4, 2026
bd8740e
Update tests for new API
sirreal Feb 4, 2026
4eaf11d
Add placeholder compilation skeleton to WP_HTML_Template
sirreal Feb 4, 2026
8229efa
Implement text placeholder extraction in compile()
sirreal Feb 4, 2026
ca24391
Add attribute placeholder extraction with context promotion
sirreal Feb 4, 2026
49978d4
Add validation warnings to bind()
sirreal Feb 4, 2026
d56435e
Refactor render() to use compiled placeholder data
sirreal Feb 4, 2026
2b4bf1a
Fix attribute text escaping and add trailing segment handling
sirreal Feb 4, 2026
f5f272d
Revert "Move serialize_token to tag processor class (for normalizatio…
sirreal Feb 5, 2026
11ab281
Switch to use HTML Processor
sirreal Feb 5, 2026
06a28c6
Use assertEqualHTML in tests
sirreal Feb 6, 2026
5862ee4
Add newline in PRE tests
sirreal Feb 6, 2026
0bda31b
Update PRE leading newline test
sirreal Feb 6, 2026
d99bb22
Add more test cases
sirreal Feb 6, 2026
b89a3ff
Remove special handling for PRE, LISTING tags
sirreal Feb 6, 2026
2fcc211
Test tweaks and notes
sirreal Feb 6, 2026
3e2757f
HTML API: Add $edits and $placeholder_names properties to WP_HTML_Tem…
sirreal Feb 6, 2026
45c01dd
HTML API: Populate $edits with text normalizations during compile
sirreal Feb 6, 2026
b316285
HTML API: Populate $edits with text placeholders during compile
sirreal Feb 6, 2026
9d15dfa
HTML API: Pre-compute attribute escapes at compile time
sirreal Feb 6, 2026
75f0d95
HTML API: Populate $edits with attribute placeholders during compile
sirreal Feb 6, 2026
18c925c
HTML API: Refactor render() to use unified $edits array
sirreal Feb 6, 2026
13323b3
HTML API: Refactor bind() to use $placeholder_names for validation
sirreal Feb 6, 2026
7747f6d
HTML API: Remove legacy $text_normalizations and $attr_escapes arrays
sirreal Feb 6, 2026
fc75174
HTML API: Add TODO for future $compiled removal
sirreal Feb 6, 2026
12b1f63
HTML API: Fix code style issues in WP_HTML_Template
sirreal Feb 6, 2026
8160d60
HTML API: Remove redundant $compiled variable from WP_HTML_Template
sirreal Feb 6, 2026
03e6104
lints
sirreal Feb 9, 2026
c275c86
Document $replacements
sirreal Feb 9, 2026
d6c1c24
Use WP_HTML_Text_Replacement class
sirreal Feb 9, 2026
14d5df9
Rely on HTML API text replacement
sirreal Feb 9, 2026
fa9d84e
Tests: Refactor static text escaping test to use data provider
sirreal Feb 9, 2026
26a4532
Remove dangling docblock
sirreal Feb 9, 2026
6ec99ea
Remove is_compiled (derive)
sirreal Feb 9, 2026
73ff19e
Add replacement warning test
sirreal Feb 9, 2026
51640e6
remove get_placeholders
sirreal Feb 9, 2026
ee24adc
Drop numeric placeholders / list replacements
sirreal Feb 10, 2026
7187586
Tests: Add coverage for invalid replacement type rejection
sirreal Feb 10, 2026
a8ca49d
Add status assessment
sirreal Feb 10, 2026
7cb1f74
Add multiple placeholder tests
sirreal Feb 10, 2026
623e938
Add more attribute tests, multiple attributes
sirreal Feb 10, 2026
9c3ee7d
Update status
sirreal Feb 10, 2026
0e81466
Add boolean attribute support to WP_HTML_Template
sirreal Feb 10, 2026
e8eb681
update status
sirreal Feb 10, 2026
a0208e5
Update status with table details
sirreal Feb 10, 2026
aea5e82
Add failing test for table row template support
sirreal Feb 10, 2026
cc36283
Expose create_fragment_at_current_node via anonymous class
sirreal Feb 10, 2026
a098815
Add detect_context() for table template detection
sirreal Feb 10, 2026
1879747
Add duplicate attribute issue to status
sirreal Feb 11, 2026
245f24a
Add failing tests for duplicate attribute removal bug
sirreal Feb 11, 2026
04b4c81
Fix duplicate attribute removal in WP_HTML_Template
sirreal Feb 11, 2026
4d32d0f
Update status about fixed duplicate attributes
sirreal Feb 11, 2026
2d1dd47
Status update - method visibility
sirreal Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
## Critical Analysis: WP_HTML_Template vs. Ticket #60229 Requirements

I've examined ticket #60229, the current branch implementation, and the test suite. Here's my analysis:

---

### What's Working Well

**1. Core Safety Goal Achieved**
The implementation delivers on the primary promise: context-aware auto-escaping. The code correctly escapes `&`, `<`, `>`, `"`, `'` in both text and attribute contexts. Tests confirm placeholders can't be injected via replacement values (non-recursive replacement).

**2. Funky Comment Syntax**
The `</%placeholder>` syntax is a smart choice—it's valid HTML (parsed as a funky comment), can't be nested by construction, and is visually distinctive. The parsing handles whitespace around names correctly.

**3. Nested Templates**
Template composition (`WP_HTML_Template` as replacement value) works for text context with proper escaping propagation. The rejection of templates in attribute context is the right safety call.

**4. Compile-once Design**
Lazy compilation with cached edits (`$edits` array) is efficient for template reuse.

**5. Boolean Attribute Support**
Supply `true` to create a boolean attribute (`disabled="</%d>"` + `true` → `disabled`), or `false`/`null` to remove an attribute entirely. Only works for whole-attribute placeholders—partial placeholders reject boolean values.

---

### What's Missing from the Ticket Requirements

**1. No URL Escaping (Ticket TODO)**
The ticket explicitly says "does not escape URLs differently than other attributes." The XSS test shows `javascript:alert("xss")` only escapes quotes—no `esc_url()` equivalent. This is a security gap for `href`/`src` attributes.

**2. No Attribute Spread**
Ticket comment 9 discusses "spread" attributes for making tags placeholders. Not implemented.

**3. Missing Output Format Methods**
Ticket TODO lists `->final_output_to_browser()`, `->final_output_to_plaintext()`, `->final_output_to_markdown()`, etc. None exist.

**4. Embed Replacement in Tag Processor (Ticket TODO)**
The ticket wants replacement embedded in the Tag Processor. Current implementation uses a separate class with its own parsing pass.

---

### Overlooked Issues in the Ticket

**1. RAWTEXT/RCDATA Element Handling Is Half-Baked**
Tests show placeholders inside `<script>`, `<style>` are preserved literally, while `<title>`, `<textarea>` escape them as text. Neither allows actual replacement. The ticket doesn't acknowledge this limitation clearly. If I write a template for a script tag's content, I'd expect placeholders to work.

**2. Table Context Parsing (Solvable with Private API)**
Test explicitly skipped: "IN TABLE templates are not supported yet." However, investigation reveals this **is solvable** using existing private APIs.

**Key finding:** Funky comment placeholders (`</%name>`) are explicitly handled in table contexts! From `class-wp-html-processor.php` line 3279-3283:
```php
case '#comment':
case '#funky-comment':
case '#presumptuous-tag':
$this->insert_html_element( $this->state->current_token );
return true;
```

The "foster parenting bail" only happens for:
1. Non-whitespace text directly inside table/tbody/thead/tfoot/tr
2. Non-table elements like `<div>` inside table structure

**The real blocker:** The public `create_fragment()` API artificially rejects non-body contexts (line 296):
```php
if ( '<body>' !== $context || 'UTF-8' !== $encoding ) {
return null;
}
```

**Private API path:** `create_fragment_at_current_node()` (line 477) does the right thing:
1. Takes current element as context
2. Calls `reset_insertion_mode_appropriately()` which correctly sets table insertion modes
3. Returns a fragment processor in the proper context

**How to enable table support:**
1. Use `Closure::bind` to access `create_fragment_at_current_node()` without modifying `WP_HTML_Processor`
2. Create full parser: `<!DOCTYPE html><table><tbody>`, navigate to `<tbody>`, call the private method via bound closure
3. Fragment processor will be in IN_TABLE_BODY mode where `<tr>` and placeholders are valid

**Limitations that would remain:**
- Cannot put arbitrary content (like `<div>`) inside table cells via placeholders (would trigger foster parenting)
- Must structure templates so placeholders appear where table elements are expected

**Why this matters:** WordPress admin uses tables extensively. This approach would enable table template support without waiting for full foster parenting implementation.

**3. No i18n Integration**
The ticket mentions "translation" as a sigil use case, and gziolo's comment asks about `createInterpolateElement` parity. Zero implementation of translation awareness. For WordPress core, this is a big miss.

**4. Performance Not Benchmarked**
Normalization runs twice: once in `compile()` for text detection, once in `render()` for final output. For high-frequency template rendering (e.g., list items), this could add up.

**5. Error Handling Philosophy Unclear**
`render()` returns `false` on errors, but `bind()` uses `_doing_it_wrong()` warnings and still returns a template. Mixed signals—should errors be fatal or recoverable? The test gaps document lists many untested failure cases.

**6. ~~Duplicate Attributes Bug with false/null Removal~~ FIXED**
~~When using `false` or `null` to remove an attribute, only the first occurrence is removed. If the HTML contains duplicate attributes (e.g., `<input disabled="</%d>" disabled>`), the first `disabled` will be removed but the second will remain in the output.~~ Fixed by emitting removal edits for duplicate attributes during `compile()`. All duplicate attributes are now stripped as part of template compilation.

**Implementation note:** The fix currently accesses `$attributes` and `$duplicate_attributes` on `WP_HTML_Tag_Processor` via `protected` visibility. This should be refactored to use `Closure::bind` instead, allowing the Tag Processor changes to be reverted.

---

### Gaps in Test Coverage (From Test-Gaps.md + My Observations)

**High Priority Missing:**

- ~~Non-string replacement values (integers, arrays, nulls) → should fail gracefully~~ ✅ Tests exist
- ~~Multiple placeholders in single attribute (e.g., `href="</%scheme>://</%host>"`)~~ ✅ Works, tests added
- Template immutability verification (`bind()` returns new instance)

**The PRE newline test is skipped** but the behavior matters—WordPress often outputs preformatted code.

**No tests for:**

- Very long templates/replacements (memory/performance)
- Deeply nested templates (stack depth)
- Unicode/multibyte characters in placeholder names
- SVG/MathML elements

---

### API Design Questions

**1. Why `from()` → `bind()` → `render()` instead of a single call?**
For reusable templates, the separation makes sense. But most WordPress use cases are one-shot renders. A convenience method like `::render($template, $replacements)` would be more ergonomic.

**2. `render()` Returns `string|false`—Is That Right?**
Returning `false` on error is classic WordPress, but it means every call site needs error checking. Exceptions or a Result type would be safer, though breaking with WP conventions.

**3. What About Streaming/Chunked Output?**
For large templates, building the entire string in memory before output is wasteful. No consideration of echo-as-you-go.

---

### What's the Competition Doing That WP Isn't?

- **Blade/Twig**: Rich conditionals, loops, inheritance. WP_HTML_Template is deliberately minimal.
- **htmx/alpine patterns**: Server-side templating with client-side enhancement. No thought given here.
- **Lit/React SSR**: Hydration markers. Not applicable, but WP could learn from structured output strategies.

The ticket's philosophy is "prefer trust and safety over features"—valid, but means this won't replace most template use cases. It's targeted at safe HTML generation, not a full templating language.

---

### Summary Table

| Requirement | Status | Notes |
| -------------------------- | ------ | ---------------------------------- |
| Context-aware escaping | ✅ | Works for text and attributes |
| Funky comment placeholders | ✅ | Clean implementation |
| Nested HTML via Templates | ✅ | Text context only |
| Boolean attributes | ✅ | true/false/null for whole-attr |
| Duplicate attribute removal| ✅ | Stripped during compile() |
| URL escaping | ❌ | Only generic escaping |
| Attribute spread | ❌ | Not implemented |
| Output format methods | ❌ | Not implemented |
| Tag Processor integration | ❌ | Separate class |
| Table context support | ❌ | Explicitly unsupported |
| i18n integration | ❌ | Not addressed |
| RAWTEXT/RCDATA replacement | ❌ | Placeholders don't work inside |

---

### Pending Work

1. **Revert Tag Processor visibility changes** — `class-wp-html-tag-processor.php` has `$attributes` and `$duplicate_attributes` changed from `private` to `protected`. Revert these and use `Closure::bind` in `WP_HTML_Template` instead.

4 changes: 2 additions & 2 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ class WP_HTML_Tag_Processor {
* @since 6.2.0
* @var WP_HTML_Attribute_Token[]
*/
private $attributes = array();
protected $attributes = array();

/**
* Tracks spans of duplicate attributes on a given tag, used for removing
Expand All @@ -718,7 +718,7 @@ class WP_HTML_Tag_Processor {
*
* @var (WP_HTML_Span[])[]|null
*/
private $duplicate_attributes = null;
protected $duplicate_attributes = null;

/**
* Which class names to add or remove from a tag.
Expand Down
Loading
Loading