-
-
Notifications
You must be signed in to change notification settings - Fork 31
feat: Termwind-facing DOM HTML subset for HtmlRenderer::parse #896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nahime0
wants to merge
7
commits into
main
Choose a base branch
from
cursor/termwind-dom-html-199b
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
b0056a7
feat: add Termwind-facing LIBXML_* constants
cursoragent b915b03
feat: inject a Termwind-scoped DOM HTML prelude
cursoragent b633a1e
test: cover loadHTML body walk and attributes
cursoragent 1f3854c
docs: document the Termwind DOM HTML subset
cursoragent 10b1985
fix: keep Termwind HTML attributes through mixed tree slots
cursoragent 4d9745b
fix: wire DOM siblings through typed node handles
cursoragent 45a2fe5
fix: inject DOM HTML prelude in EIR examples corpus
cursoragent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| --- | ||
| title: "DOM HTML (Termwind subset)" | ||
| description: "A pay-for-use DOMDocument HTML fragment walker for Termwind-style tree walks, not full PHP DOM." | ||
| sidebar: | ||
| order: 24 | ||
| --- | ||
|
|
||
| elephc injects a small **HTML-only DOM subset** when a program names `DOMDocument`, | ||
| `DOMNode`, `DOMElement`, `DOMText`, `DOMComment`, `DOMCharacterData`, or | ||
| `DOMNodeList`. The surface is enough for Termwind's `HtmlRenderer::parse` to | ||
| walk a fragment such as `<div class="text-green-500">Hi</div>`: load the HTML, | ||
| take `body`, and read tag names, attributes, text, comments, and siblings. | ||
|
|
||
| This is **not** a second native libxml/DOM stack. Draft | ||
| [PR #654](https://github.com/illegalstudio/elephc/pull/654) (issue | ||
| [#622](https://github.com/illegalstudio/elephc/issues/622)) remains the path to | ||
| full PHP 8.5 DOM, libxml, and SimpleXML on a statically linked `elephc-dom` | ||
| bridge. This prelude uses the same class names and `LIBXML_*` integers so that | ||
| work can replace it without changing Termwind call sites. | ||
|
|
||
| ```php | ||
| <?php | ||
| $dom = new DOMDocument(); | ||
| $dom->loadHTML( | ||
| '<div class="text-green-500">Hi</div>', | ||
| LIBXML_NOERROR | LIBXML_COMPACT | LIBXML_HTML_NODEFDTD | LIBXML_NOBLANKS | LIBXML_NOXMLDECL | ||
| ); | ||
| $body = $dom->getElementsByTagName('body')->item(0); | ||
| foreach ($body->childNodes as $node) { | ||
| echo $node->nodeName, ' ', $node->getAttribute('class'), ' ', $node->nodeValue; | ||
| } | ||
| ``` | ||
|
|
||
| ## Supported surface | ||
|
|
||
| | Piece | Behavior | | ||
| |---|---| | ||
| | `new DOMDocument()` | Empty document. Optional constructor args are accepted and ignored. | | ||
| | `loadHTML(string $source, int $options = 0): bool` | Forgiving HTML fragment parse. Wraps content in `html`/`body` the way PHP's HTML parser does. | | ||
| | `getElementsByTagName(string $name): DOMNodeList` | Document-order descendant elements. `*` matches every element. | | ||
| | `DOMNodeList::item(int $index)` / `$length` / `foreach` | Indexed access and `Iterator` traversal. | | ||
| | `nodeName`, `nodeValue` | HTML tag names are lowercased. Element `nodeValue` is concatenated descendant text. | | ||
| | `childNodes`, `previousSibling`, `nextSibling`, `parentNode`, `ownerDocument` | Wired after parse. The tree is treated as immutable. | | ||
| | `DOMElement::getAttribute(string $name): string` | Attribute names are lowercased on parse. Missing attributes return `""`. | | ||
| | `instanceof DOMElement` / `DOMText` / `DOMComment` / `DOMDocument` | Class hierarchy matches PHP (`DOMText`/`DOMComment` extend `DOMCharacterData` extend `DOMNode`). | | ||
| | `saveXML(?DOMNode $node = null): string` | Serializes a node or the whole document. No XML declaration (Termwind passes `LIBXML_NOXMLDECL`). | | ||
|
|
||
| ### `LIBXML_*` flags Termwind uses | ||
|
|
||
| These constants are always available (including inside a namespace, and in | ||
| `eval()`), with php-src's integer values: | ||
|
|
||
| | Constant | Value | Effect here | | ||
| |---|---|---| | ||
| | `LIBXML_NOXMLDECL` | 2 | Save flag; parse ignores it. `saveXML()` never emits `<?xml …?>`. | | ||
| | `LIBXML_HTML_NODEFDTD` | 4 | No default doctype is added (none is added anyway). | | ||
| | `LIBXML_NOERROR` | 32 | Parse is silent and forgiving. | | ||
| | `LIBXML_NOBLANKS` | 256 | Whitespace-only text nodes are dropped. | | ||
| | `LIBXML_COMPACT` | 65536 | Accepted and ignored (libxml compaction is an optimizer hint). | | ||
|
|
||
| ## Remaining gaps versus full PHP DOM | ||
|
|
||
| Tracked against PHP's `ext/dom` / `ext/libxml` and against #654: | ||
|
|
||
| - No libxml2 or Lexbor engine, no `elephc-dom` crate, no XML `load()` / `loadXML()`. | ||
| - No modern `Dom\` HTML API (`Dom\HTMLDocument`, `Dom\Element`, …). | ||
| - No XPath, CSS selectors, DTD/schema validation, XInclude, C14N, or token lists. | ||
| - No tree mutation (`appendChild`, `removeChild`, `setAttribute`, `createElement`, …). | ||
| - No `DOMAttr` / `DOMNamedNodeMap`, processing instructions, CDATA, entities, or notations. | ||
| - No `LIBXML_*` constants beyond the five Termwind flags. | ||
| - No SimpleXML and no DOM ↔ SimpleXML import. | ||
| - `loadHTMLFile()`, encoding detection, error collection (`libxml_use_internal_errors`), and default `<head>` insertion are unimplemented. | ||
| - Specialized Termwind renderers that need richer markup (`<table>`, `<code>`, `<pre>` via `getHtml()` / `saveXML` of mixed subtrees) are not the goal of this subset; `saveXML` covers a simple element so those paths can be added later. | ||
| - The class surface is AOT-prelude only. `eval()` sees the `LIBXML_*` constants but not the DOM classes until #654 or a Magician binding lands. | ||
|
|
||
| Do not vendor Termwind itself. Compile programs that already depend on Termwind | ||
| against this subset; keep Termwind's sources in the application tree. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| *.s | ||
| *.o | ||
| main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?php | ||
| $dom = new DOMDocument(); | ||
| $html = '<div class="text-green-500">Hi</div>'; | ||
| $dom->loadHTML( | ||
| $html, | ||
| LIBXML_NOERROR | LIBXML_COMPACT | LIBXML_HTML_NODEFDTD | LIBXML_NOBLANKS | LIBXML_NOXMLDECL | ||
| ); | ||
|
|
||
| $body = $dom->getElementsByTagName("body")->item(0); | ||
| foreach ($body->childNodes as $node) { | ||
| echo $node->nodeName; | ||
| echo " class="; | ||
| echo $node->getAttribute("class"); | ||
| echo " text="; | ||
| echo $node->nodeValue; | ||
| echo "\n"; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| //! Purpose: | ||
| //! Termwind-facing DOM HTML subset: `DOMDocument::loadHTML()` plus the node | ||
| //! types and properties `HtmlRenderer` / `ValueObjects\Node` walk | ||
| //! (`getElementsByTagName`, `childNodes`, `nodeName`, `nodeValue`, | ||
| //! `getAttribute`, sibling pointers, `saveXML`). | ||
| //! | ||
| //! Called from: | ||
| //! - `crate::pipeline::compile()` and the codegen test harness via | ||
| //! `inject_if_used`, after include resolution and before name resolution. | ||
| //! | ||
| //! Key details: | ||
| //! - Choice (B) versus draft PR #654: that PR is a 178k-line PHP 8.5 | ||
| //! libxml2+Lexbor bridge (`crates/elephc-dom`) still in progress. This | ||
| //! prelude is a pay-for-use HTML fragment walker that does not add a | ||
| //! second native DOM engine. Same PHP class names and `LIBXML_*` | ||
| //! integers so #654 can replace this surface when it lands. | ||
| //! - Injected only when the program names a DOM class. No `--with-dom` | ||
| //! flag and no `elephc-dom` crate. | ||
| //! - Delivered as parsed PHP (like mysqli), not a native builtin, so every | ||
| //! supported target gets the same walk with no new assembly. | ||
|
|
||
| mod detect; | ||
| mod surface; | ||
|
|
||
| use std::sync::OnceLock; | ||
|
|
||
| use crate::parser::ast::Program; | ||
|
|
||
| /// Parsed prelude cache. The fragment is declaration-only, so one parse is | ||
| /// reused for every injecting compile. | ||
| static PARSED_PRELUDE: OnceLock<Program> = OnceLock::new(); | ||
|
|
||
| /// Tokenizes and parses the DOM HTML prelude exactly once. | ||
| fn parsed_prelude() -> Program { | ||
| PARSED_PRELUDE | ||
| .get_or_init(|| { | ||
| let source = format!("<?php\n{}", surface::SRC); | ||
| let tokens = crate::lexer::tokenize(&source).expect("dom html prelude must tokenize"); | ||
| crate::parser::parse_internal(&tokens).expect("dom html prelude must parse") | ||
| }) | ||
| .clone() | ||
| } | ||
|
|
||
| /// Prepends the Termwind DOM HTML prelude when the program names a DOM class. | ||
| /// | ||
| /// `force` exists for the codegen harness and future opt-in; ordinary compiles | ||
| /// pass `false` and rely on `program_uses_dom_html`. | ||
| pub fn inject_if_used( | ||
| program: Program, | ||
| force: bool, | ||
| inventory: &mut crate::optimize::reachability::PreludeInventory, | ||
| ) -> Program { | ||
| if !force && !detect::program_uses_dom_html(&program) { | ||
| return program; | ||
| } | ||
| let mut combined = parsed_prelude(); | ||
| inventory.record_program("dom-html", &combined); | ||
| combined.extend(program); | ||
| combined | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use crate::parser::ast::StmtKind; | ||
|
|
||
| /// The prelude must declare the Termwind node types and no others. | ||
| #[test] | ||
| fn declares_termwind_dom_classes() { | ||
| let declared: Vec<String> = parsed_prelude() | ||
| .iter() | ||
| .filter_map(|stmt| match &stmt.kind { | ||
| StmtKind::ClassDecl { name, .. } => Some(name.clone()), | ||
| _ => None, | ||
| }) | ||
| .collect(); | ||
| assert_eq!( | ||
| declared, | ||
| vec![ | ||
| "DOMNode", | ||
| "DOMNodeList", | ||
| "DOMDocument", | ||
| "DOMElement", | ||
| "DOMCharacterData", | ||
| "DOMText", | ||
| "DOMComment", | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| /// `loadHTML` keeps the two-parameter PHP signature Termwind calls. | ||
| #[test] | ||
| fn load_html_takes_source_and_options() { | ||
| let load = parsed_prelude() | ||
| .into_iter() | ||
| .find(|stmt| matches!(&stmt.kind, StmtKind::ClassDecl { name, .. } if name == "DOMDocument")) | ||
| .expect("DOMDocument must be declared"); | ||
| let StmtKind::ClassDecl { methods, .. } = &load.kind else { | ||
| unreachable!("filtered above"); | ||
| }; | ||
| let method = methods | ||
| .iter() | ||
| .find(|method| method.name == "loadHTML") | ||
| .expect("loadHTML must be declared"); | ||
| assert_eq!(method.params.len(), 2); | ||
| assert_eq!(method.params[0].0, "source"); | ||
| assert_eq!(method.params[1].0, "options"); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a program declares and uses its own global
DOMDocumentor another supported DOM class,inject_if_usedprepends the same class declarations without checking the program’s declarations, causing type checking to fail with a duplicate-class error.Context Used: AGENTS.md (source)
Knowledge Base Used: Frontend parsing and symbol resolution
Prompt To Fix With AI