concept-labs / phtmal
(C)oncept-Labs Abstract HTML
Requires
- php: >=8.2
README
Tiny, fluent HTML node tree with a minimal CSS-like selector engine — built to be lightweight, readable, and extensible. Designed for future integration with a layout
package (name TBD) and for use in server-side rendering scenarios.
Highlights
- Fluent builder via
__call()
($ul->li('A')->end()
), with optional subtree callbacks- Pretty vs minified rendering
- Safe text nodes and explicit
raw()
nodes- Normalized attributes (boolean attrs supported)
- Minimal CSS-like querying (tag,
*
,#id
,.class
,[attr]
, combinators
>
+
~
,:first-child
,:last-child
,:nth-child
)- Extensible: subclasses can override behavior and constants; interfaces define the contract
- NEW: Parse raw HTML into a Phtmal tree via
HtmlParserInterface
+DomHtmlParser
Installation
Once published to Packagist:
composer require concept-labs/phtmal
For local/VCS use meanwhile:
Path repository (local dev):
{ "repositories": [ { "type": "path", "url": "../phtmal" } ], "require": { "concept-labs/phtmal": "*" } }
VCS repository (GitHub):
{ "repositories": [ { "type": "vcs", "url": "https://github.com/Concept-Labs/phtmal" } ], "require": { "concept-labs/phtmal": "dev-main" } }
Quick start
use Concept\Phtmal\Phtmal; $html = (new Phtmal('ul')) ->li(['class' => 'item'], function (Phtmal $li) { $li->span('A'); }) ->li('B')->end() ->top(); echo $html->render(); // pretty echo (string)$html; // minified
Attributes & boolean attributes:
$btn = (new Phtmal('button', 'Save')) ->class('btn', 'btn-primary') ->attr('disabled', ['disabled']); // short boolean form → <button disabled>…</button>
Text vs raw HTML:
$div = (new Phtmal('div'))->text('Safe <b>text</b>'); // escaped $div->raw('<b>UNSAFE</b>'); // unescaped (use with care)
Querying:
$items = $html->query('li.item:first-child, li.item:last-child'); $second = $html->queryOne('#main > .card:nth-child(2)');
HTML parsing (NEW)
You can parse raw HTML (documents or fragments) into a Phtmal
tree using the parser interface.
Interfaces
HtmlParserInterface
— contract:parseDocument(string $html, array $options = []): PhtmalNodeInterface
parseFragment(string $html, string $containerTag = 'div', array $options = []): PhtmalNodeInterface
DomHtmlParser
— DOMDocument-based implementation (tolerant to malformed HTML).
Usage
Parse a full document:
use Concept\Phtmal\DomHtmlParser; $parser = new DomHtmlParser(); $root = $parser->parseDocument('<!doctype html><html><body><div id="x">t</div></body></html>'); // $root is the <html> node echo $root->render(); // pretty echo (string)$root; // minified
Parse a fragment (no implied <html>/<body>
):
$parser = new DomHtmlParser(); $list = $parser->parseFragment('<li>A</li><li class="x">B</li>', 'ul'); echo (string)$list; // <ul><li>A</li><li class="x">B</li></ul>
Scripts/styles are imported as RAW nodes (not escaped):
$parser = new DomHtmlParser(); $div = $parser->parseFragment('<script>if (a < b) { alert("x"); }</script>', 'div'); echo (string)$div; // <div><script>if (a < b) { alert("x"); }</script></div>
Custom factory (use your subclass of Phtmal):
class MyNode extends Concept\Phtmal\Phtmal {} $parser = new DomHtmlParser(fn(string $tag, ?string $text, array $attr) => new MyNode($tag, $text, $attr)); $tree = $parser->parseFragment('<span>Hello</span>', 'div');
Options
parseDocument()
and parseFragment()
accept the same $options
array:
Option | Type | Default | Description |
---|---|---|---|
dropComments |
bool |
true |
Drop HTML comments. |
preserveWhitespace |
bool |
false |
Keep whitespace-only text nodes outside <pre>/<textarea> . |
preservePreWhitespace |
bool |
true |
Preserve whitespace in <pre> / <textarea> . |
encoding |
string |
'UTF-8' |
Input encoding hint for DOMDocument. |
rawTextTags |
string[] |
['script','style'] |
Treat content of these tags as RAW (unescaped). |
Interfaces (core)
The library is interface-first. Documentation primarily lives on interfaces; implementations use {@inheritDoc}
.
PhtmalNodeInterface
— node contract (fluent API, rendering, navigation, query integration).SelectorInterface
— static querying:select(PhtmalNodeInterface $root, string $selector): array
.HtmlParserInterface
— parse raw HTML into a Phtmal tree.
Key guarantees:
- Implementations escape text on render (except explicit
#raw
nodes). - Attributes are normalized to lists of strings, enabling predictable rendering and boolean-shortcuts.
- Children order is stable.
Core API (from PhtmalNodeInterface
)
// Builder & structure __call(string $tag, array $args): PhtmalNodeInterface end(): PhtmalNodeInterface top(): PhtmalNodeInterface append(PhtmalNodeInterface|string $nodeOrText): static raw(string $html): static // Content & attributes text(?string $text): static attr(string $name, string|array|null $value = null): static id(string $id): static class(string ...$class): static data(string $key, string $value): static aria(string $key, string $value): static // Navigation & mutation parent(): ?PhtmalNodeInterface firstChild(): ?PhtmalNodeInterface nextSibling(): ?PhtmalNodeInterface cloneDeep(): PhtmalNodeInterface detach(): static replaceWith(PhtmalNodeInterface $node): PhtmalNodeInterface // Rendering render(bool $minify = false, int $indentLevel = 0): string // Querying query(string $selector): array queryOne(string $selector): ?PhtmalNodeInterface // Meta getTag(): string
Notes:
__call('li', [...])
supports either(text, attrs)
or(attrs, callback)
— if a callback is the last argument, the method returns the parent (auto-jump back). Otherwise, it returns the new child (you can->end()
manually).- Boolean attributes are rendered in short form if normalized as
['disabled' => ['disabled']]
.
Extensibility recommendations
- Overridable constants (protected):
VOID_ELEMENTS
,INDENT
,NL
. - Overridable hooks (protected):
newNode()
,escape()
,renderAttributes()
,_childrenRef()
. - Open state (protected):
parent
,children
,tag
,attributes
,text
.
Testing & QA
Install dev tools:
composer require --dev phpunit/phpunit:^10 phpstan/phpstan:^1.11
Run tests and static analysis:
vendor/bin/phpunit vendor/bin/phpstan analyse
License
MIT