Skip to content

Commit 84311ae

Browse files
Support single-file component inputs
If the input starts with a <template> element, and the other root elements are all <script> or <style>, then pick the <template> contents as the actual root element and only render its contents. This allows to use SFC *.vue files as the input. Note that this requires the <template> to be the first child, rather than e.g. between the <script> and <style>. (Also, it doesn’t allow any other nodes on the root level, not even comments. Though whitespace apparently gets stripped out by libxml, so that’s fine.) This is kind of justifiable because it’s enforced by eslint-config-wikimedia via the vue/component-tags-order rule, but still probably worth fixing later. Bug: T395802
1 parent 6f99522 commit 84311ae

File tree

2 files changed

+67
-3
lines changed

2 files changed

+67
-3
lines changed

src/Component.php

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,51 @@ private function getRootNode( DOMDocument $document ) {
9999
throw new Exception( 'Empty document' );
100100
}
101101

102+
// documentElement is the <html>, its child is the <body>, its children are the actual root nodes
102103
$rootNodes = $document->documentElement->childNodes->item( 0 )->childNodes;
103104

104-
if ( $rootNodes->length > 1 ) {
105-
throw new Exception( 'Template should have only one root node' );
105+
// potentially pick just the <template> contents of a single-file component (SFC)
106+
if ( $rootNodes->item( 0 )->tagName === 'template' ) {
107+
$sfc = true;
108+
for ( $i = 1; $i < $rootNodes->length; $i++ ) {
109+
$otherNode = $rootNodes->item( $i );
110+
if ( $otherNode->nodeType !== XML_ELEMENT_NODE ) {
111+
$sfc = false;
112+
break;
113+
}
114+
if ( $otherNode->tagName !== 'script' && $otherNode->tagName !== 'style' ) {
115+
$sfc = false;
116+
break;
117+
}
118+
}
119+
if ( $sfc ) {
120+
$rootNodes = $rootNodes->item( 0 )->childNodes;
121+
}
122+
}
123+
124+
// pick the only “substantial” child element as the root (ignore whitespace, comments)
125+
$onlyRootElement = null;
126+
for ( $i = 0; $i < $rootNodes->length; $i++ ) {
127+
$node = $rootNodes->item( $i );
128+
if ( $node->nodeType === XML_COMMENT_NODE ) {
129+
// comment node, ignore
130+
continue;
131+
} elseif ( $node->nodeType === XML_TEXT_NODE && trim( $node->textContent ) === '' ) {
132+
// whitespace-only text node, ignore
133+
continue;
134+
}
135+
if ( $onlyRootElement === null ) {
136+
$onlyRootElement = $node;
137+
} else {
138+
throw new Exception( 'Template should only have one root node' );
139+
}
106140
}
107141

108-
return $rootNodes->item( 0 );
142+
if ( $onlyRootElement !== null ) {
143+
return $onlyRootElement;
144+
} else {
145+
throw new Exception( 'Template contained no root node' );
146+
}
109147
}
110148

111149
/**

tests/php/TemplatingTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,32 @@ public function testJustASingleEmptyHtmlElement() {
1717
$this->assertSame( '<div></div>', $result );
1818
}
1919

20+
public function testSingleFileComponent_OnlyTemplate(): void {
21+
$result = $this->createAndRender( '<template><div></div></template>', [] );
22+
23+
$this->assertSame( '<div></div>', $result );
24+
}
25+
26+
public function testSingleFileComponent_TemplateAndScriptAndStyle(): void {
27+
$template = <<< 'EOF'
28+
<template>
29+
<!-- eslint-disable-next-line something -->
30+
<div></div>
31+
</template>
32+
<script setup>
33+
const something = 'something';
34+
</script>
35+
<style scoped>
36+
.some-class {
37+
font-weight: bold;
38+
}
39+
</style>
40+
EOF;
41+
$result = $this->createAndRender( $template, [] );
42+
43+
$this->assertSame( '<div></div>', $result );
44+
}
45+
2046
public function testTemplateHasNoRootNodes_ThrowsAnException(): void {
2147
$this->expectException( Exception::class );
2248
$this->createAndRender( '', [] );

0 commit comments

Comments
 (0)