Skip to content

Commit ccb9d7e

Browse files
committed
improve attribute formatting with better argument parsing
1 parent 88859c8 commit ccb9d7e

6 files changed

Lines changed: 347 additions & 32 deletions

File tree

phpdotnet/phd/Package/Generic/XHTML.php

Lines changed: 136 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,23 +1493,150 @@ public function format_modifier_text($value, $tag) {
14931493
}
14941494

14951495
private function format_attribute_modifier_text(string $value): string {
1496-
// Anything that is not a leading "#[\Attribute(" / "#[\Attribute]" chunk
1497-
// e.g. "|" separator between arguments passes through.
1498-
if (!preg_match('/^(#\[)(.+?)([](])$/', $value, $match)) {
1499-
if (trim($value) === '|') {
1500-
return ' | ';
1496+
$trimmed = trim($value);
1497+
$namePattern = '\\\\?[A-Za-z_][A-Za-z0-9_\\\\]*';
1498+
1499+
// Full attribute with literal arguments: #[\Name(args)]
1500+
if (preg_match('/^(#\[)(' . $namePattern . ')\((.+)\)]$/s', $trimmed, $match)) {
1501+
[, $prefix, $name, $args] = $match;
1502+
return $prefix . $this->renderAttributeName($name) . '(' . $this->renderAttributeArgs($args) . ')]';
1503+
}
1504+
1505+
// Simple attribute: #[\Name]
1506+
if (preg_match('/^#\[(' . $namePattern . ')]$/', $trimmed, $match)) {
1507+
$name = $match[1];
1508+
$attribute = strtolower(ltrim($name, "\\"));
1509+
$href = $this->getFilename('class.' . $attribute);
1510+
$token = '#[' . $name . ']';
1511+
if (!$href) {
1512+
return $token;
15011513
}
1502-
return $value;
1514+
1515+
return '<a href="' . $href . $this->getExt() . '">' . $token . '</a> ';
15031516
}
15041517

1505-
[, $prefix, $name, $suffix] = $match;
1518+
// Opening of an attribute followed by child elements: #[\Name(
1519+
if (preg_match('/^(#\[)(' . $namePattern . ')\($/', $trimmed, $match)) {
1520+
[, $prefix, $name] = $match;
1521+
return $prefix . $this->renderAttributeName($name) . '(';
1522+
}
1523+
1524+
// Separator between attribute arguments
1525+
if ($trimmed === '|') {
1526+
return ' | ';
1527+
}
1528+
1529+
return $trimmed === '' ? '' : $value;
1530+
}
1531+
1532+
private function renderAttributeName(string $name): string {
15061533
$attribute = strtolower(ltrim($name, "\\"));
15071534
$href = $this->getFilename('class.' . $attribute);
15081535
if (!$href) {
1509-
return $value;
1536+
return $name;
1537+
}
1538+
1539+
return '<a href="' . $href . $this->getExt() . '">' . $name . '</a>';
1540+
}
1541+
1542+
private function renderAttributeArgs(string $args): string {
1543+
$args = trim(preg_replace('/\s+/', ' ', $args));
1544+
$parts = $this->splitAttributeArgs($args);
1545+
1546+
if (count($parts) <= 1) {
1547+
return $this->link_constants_in_text($parts[0]['value'] ?? '');
1548+
}
1549+
1550+
$lines = [];
1551+
$prefix = '';
1552+
foreach ($parts as $part) {
1553+
$rendered = $this->link_constants_in_text($part['value']);
1554+
$line = '&nbsp;&nbsp;&nbsp;&nbsp;' . $prefix . $rendered;
1555+
if ($part['separator'] === ',') {
1556+
$line .= ',';
1557+
$prefix = '';
1558+
} elseif ($part['separator'] === '|') {
1559+
$prefix = '| ';
1560+
} else {
1561+
$prefix = '';
1562+
}
1563+
$lines[] = $line;
1564+
}
1565+
1566+
return '<br>' . implode('<br>', $lines) . '<br>';
1567+
}
1568+
1569+
/**
1570+
* @return list<array{value: string, separator: string|null}>
1571+
*/
1572+
private function splitAttributeArgs(string $args): array {
1573+
$parts = [];
1574+
$current = '';
1575+
$depth = 0;
1576+
$inString = false;
1577+
$stringChar = '';
1578+
$length = strlen($args);
1579+
1580+
for ($i = 0; $i < $length; $i++) {
1581+
$ch = $args[$i];
1582+
1583+
if ($inString) {
1584+
$current .= $ch;
1585+
if ($ch === '\\' && $i + 1 < $length) {
1586+
$current .= $args[++$i];
1587+
continue;
1588+
}
1589+
if ($ch === $stringChar) {
1590+
$inString = false;
1591+
}
1592+
continue;
1593+
}
1594+
1595+
if ($ch === '"' || $ch === "'") {
1596+
$inString = true;
1597+
$stringChar = $ch;
1598+
$current .= $ch;
1599+
continue;
1600+
}
1601+
1602+
if ($ch === '(' || $ch === '[') {
1603+
$depth++;
1604+
} elseif ($ch === ')' || $ch === ']') {
1605+
$depth--;
1606+
}
1607+
1608+
if ($depth === 0 && ($ch === '|' || $ch === ',')) {
1609+
$parts[] = ['value' => trim($current), 'separator' => $ch];
1610+
$current = '';
1611+
continue;
1612+
}
1613+
1614+
$current .= $ch;
1615+
}
1616+
1617+
if (trim($current) !== '') {
1618+
$parts[] = ['value' => trim($current), 'separator' => null];
15101619
}
15111620

1512-
return $prefix . '<a href="' . $href . $this->getExt() . '">' . $name . '</a>' . $suffix;
1621+
return $parts;
1622+
}
1623+
1624+
private function link_constants_in_text(string $text): string {
1625+
$escaped = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
1626+
1627+
// Link Class::CONST references that appear as literal text
1628+
return preg_replace_callback(
1629+
'/\\\\?[A-Za-z_][\w\\\\]*::[A-Za-z_][\w]*/',
1630+
function (array $match): string {
1631+
$constant = $match[0];
1632+
$link = $this->createLink($this->convertConstantNameToId($constant));
1633+
if ($link === null) {
1634+
return $constant;
1635+
}
1636+
return '<a href="' . $link . '">' . $constant . '</a>';
1637+
},
1638+
$escaped,
1639+
);
15131640
}
15141641

15151642
public function format_methodsynopsis($open, $name, $attrs, $props) {

tests/package/generic/attribute_formatting_001.phpt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Content:
4242

4343
<div class="section">
4444
<p class="para">2. Class methodparameter with known attribute</p>
45-
<div class="constructorsynopsis dc-description"><span class="modifier">public</span> <span class="methodname">mysqli::__construct</span>(<span class="methodparam"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><span class="type"><span class="type">string</span><span class="type">null</span></span> <code class="parameter">$password</code><span class="initializer"> = <span class="type">null</span></span></span>)</div>
45+
<div class="constructorsynopsis dc-description"><span class="modifier">public</span> <span class="methodname">mysqli::__construct</span>(<span class="methodparam"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><span class="type"><span class="type">string</span><span class="type">null</span></span> <code class="parameter">$password</code><span class="initializer"> = <span class="type">null</span></span></span>)</div>
4646

4747
</div>
4848

@@ -54,7 +54,7 @@ Content:
5454

5555
<div class="section">
5656
<p class="para">4. Function parameter with known attribute</p>
57-
<div class="methodsynopsis dc-description"><span class="type">bool</span> <span class="methodname">password_verify</span>(<span class="methodparam"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><span class="type">string</span> <code class="parameter">$password</code></span>, <span class="methodparam"><span class="type">string</span> <code class="parameter">$hash</code></span>)</div>
57+
<div class="methodsynopsis dc-description"><span class="type">bool</span> <span class="methodname">password_verify</span>(<span class="methodparam"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><span class="type">string</span> <code class="parameter">$password</code></span>, <span class="methodparam"><span class="type">string</span> <code class="parameter">$hash</code></span>)</div>
5858

5959
</div>
60-
</div>
60+
</div>

tests/package/generic/attribute_formatting_002.phpt

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ Content:
6262
<p class="para">2. Class with known attributes</p>
6363
<div class="classsynopsis"><div class="classsynopsisinfo">
6464

65-
<span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
66-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
65+
<span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
66+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
6767
<span class="modifier">class</span> <strong class="classname">DateTime</strong>
6868
{</div>
6969
}</div>
@@ -80,8 +80,8 @@ Content:
8080

8181
<div class="section">
8282
<p class="para">4. Method with known attributes</p>
83-
<div class="methodsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
84-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
83+
<div class="methodsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
84+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
8585
<span class="modifier">public</span> <span class="methodname">ClassName::methodName</span>()</div>
8686

8787
</div>
@@ -96,8 +96,8 @@ Content:
9696

9797
<div class="section">
9898
<p class="para">6. Constructor with known attributes</p>
99-
<div class="constructorsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
100-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
99+
<div class="constructorsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
100+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
101101
<span class="modifier">public</span> <span class="methodname">ClassName::__construct</span>()</div>
102102

103103
</div>
@@ -130,21 +130,21 @@ Content:
130130
<p class="para">8. Class, constructor and methods with known attributes</p>
131131
<div class="classsynopsis"><div class="classsynopsisinfo">
132132

133-
<span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
134-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
133+
<span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
134+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
135135
<span class="modifier">class</span> <strong class="classname">DateTime</strong>
136136
{</div>
137137
<div class="constructorsynopsis dc-description">
138-
<span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
139-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
138+
<span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
139+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
140140
<span class="modifier">public</span> <span class="methodname">ClassName::__construct</span>()</div>
141141

142-
<div class="methodsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
143-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
142+
<div class="methodsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
143+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
144144
<span class="modifier">public</span> <span class="methodname">ClassName::methodName1</span>()</div>
145145

146-
<div class="methodsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
147-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
146+
<div class="methodsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
147+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
148148
<span class="modifier">public</span> <span class="methodname">ClassName::methodName2</span>()</div>
149149

150150
}</div>
@@ -161,10 +161,10 @@ Content:
161161

162162
<div class="section">
163163
<p class="para">10. Function with known attributes</p>
164-
<div class="methodsynopsis dc-description"><span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
165-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
164+
<div class="methodsynopsis dc-description"><span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
165+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
166166
<span class="type">void</span> <span class="methodname">function_name</span>)</div>
167167

168168
</div>
169169

170-
</div>
170+
</div>

tests/package/generic/attribute_formatting_003.phpt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ Content:
7575
{</div>
7676
<div class="classsynopsisinfo classsynopsisinfo_comment">/* Properties/Constants */</div>
7777
<div class="fieldsynopsis">
78-
<span class="attribute">#[<a href="file.knownattribute.is.in.html">\KnownAttribute</a>]</span><br>
79-
<span class="attribute">#[<a href="file.anotherknownattribute.is.in.html">\AnotherKnownAttribute</a>]</span><br>
78+
<span class="attribute"><a href="file.knownattribute.is.in.html">#[\KnownAttribute]</a> </span><br>
79+
<span class="attribute"><a href="file.anotherknownattribute.is.in.html">#[\AnotherKnownAttribute]</a> </span><br>
8080
<span class="modifier">public</span>
8181
<span class="modifier">readonly</span>
8282
<span class="type">string</span>
@@ -102,4 +102,4 @@ Content:
102102
}</div>
103103
</div>
104104

105-
</div>
105+
</div>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
--TEST--
2+
Attribute formatting 005 - Attribute with literal parameter arguments
3+
--FILE--
4+
<?php
5+
namespace phpdotnet\phd;
6+
7+
require_once __DIR__ . "/../../setup.php";
8+
9+
$xmlFile = __DIR__ . "/data/attribute_formatting_005.xml";
10+
11+
$config->xmlFile = $xmlFile;
12+
13+
$format = new TestGenericChunkedXHTML($config, $outputHandler);
14+
15+
$format->SQLiteIndex(
16+
null, null,
17+
"class.deprecated",
18+
"class.deprecated",
19+
"", "", "", "", "", "", 0,
20+
);
21+
$format->SQLiteIndex(
22+
null, null,
23+
"class.attribute",
24+
"class.attribute",
25+
"", "", "", "", "", "", 0,
26+
);
27+
$format->SQLiteIndex(
28+
null, null,
29+
"attribute.constants.target-function",
30+
"class.attribute",
31+
"", "", "", "", "", "", 0,
32+
);
33+
$format->SQLiteIndex(
34+
null, null,
35+
"attribute.constants.target-method",
36+
"class.attribute",
37+
"", "", "", "", "", "", 0,
38+
);
39+
$format->SQLiteIndex(
40+
null, null,
41+
"attribute.constants.target-class",
42+
"class.attribute",
43+
"", "", "", "", "", "", 0,
44+
);
45+
46+
$render = new TestRender(new Reader($outputHandler), $config, $format);
47+
48+
$render->run();
49+
?>
50+
--EXPECT--
51+
Filename: attribute-formatting-005.html
52+
Content:
53+
<div id="attribute-formatting-005" class="chapter">
54+
<div class="section">
55+
<p class="para">1. Attribute with literal named arguments</p>
56+
<div class="classsynopsis"><div class="classsynopsisinfo">
57+
58+
<span class="attribute">#[<a href="class.deprecated.html">\Deprecated</a>(<br>&nbsp;&nbsp;&nbsp;&nbsp;since: '8.5',<br>&nbsp;&nbsp;&nbsp;&nbsp;message: 'Deprecated since PHP 8.4'<br>)]</span><br>
59+
<span class="modifier">final</span>
60+
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
61+
{</div>
62+
}</div>
63+
</div>
64+
65+
<div class="section">
66+
<p class="para">2. Attribute with single literal positional argument</p>
67+
<div class="classsynopsis"><div class="classsynopsisinfo">
68+
69+
<span class="attribute">#[<a href="class.deprecated.html">\Deprecated</a>('Deprecated since PHP 8.4')]</span><br>
70+
<span class="modifier">final</span>
71+
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
72+
{</div>
73+
}</div>
74+
</div>
75+
76+
<div class="section">
77+
<p class="para">3. Unknown attribute with literal argument</p>
78+
<div class="classsynopsis"><div class="classsynopsisinfo">
79+
80+
<span class="attribute">#[\UnknownAttribute(foo: 'bar')]</span><br>
81+
<span class="modifier">final</span>
82+
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
83+
{</div>
84+
}</div>
85+
</div>
86+
87+
<div class="section">
88+
<p class="para">4. Namespaced attribute with literal argument</p>
89+
<div class="classsynopsis"><div class="classsynopsisinfo">
90+
91+
<span class="attribute">#[\Some\Namespaced\Attribute(value: 42)]</span><br>
92+
<span class="modifier">final</span>
93+
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
94+
{</div>
95+
}</div>
96+
</div>
97+
98+
<div class="section">
99+
<p class="para">5. Multi-line attribute with literal class constant arguments</p>
100+
<div class="classsynopsis"><div class="classsynopsisinfo">
101+
102+
<span class="attribute">#[<a href="class.attribute.html">\Attribute</a>(<br>&nbsp;&nbsp;&nbsp;&nbsp;<a href="class.attribute.html#attribute.constants.target-function">Attribute::TARGET_FUNCTION</a><br>&nbsp;&nbsp;&nbsp;&nbsp;| <a href="class.attribute.html#attribute.constants.target-method">Attribute::TARGET_METHOD</a><br>&nbsp;&nbsp;&nbsp;&nbsp;| <a href="class.attribute.html#attribute.constants.target-class">Attribute::TARGET_CLASS</a><br>)]</span><br>
103+
<span class="modifier">final</span>
104+
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
105+
{</div>
106+
}</div>
107+
</div>
108+
109+
<div class="section">
110+
<p class="para">6. Attribute with mix of known and unknown class constants</p>
111+
<div class="classsynopsis"><div class="classsynopsisinfo">
112+
113+
<span class="attribute">#[<a href="class.attribute.html">\Attribute</a>(<br>&nbsp;&nbsp;&nbsp;&nbsp;<a href="class.attribute.html#attribute.constants.target-class">Attribute::TARGET_CLASS</a><br>&nbsp;&nbsp;&nbsp;&nbsp;| Unknown::CONST<br>)]</span><br>
114+
<span class="modifier">final</span>
115+
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
116+
{</div>
117+
}</div>
118+
</div>
119+
</div>

0 commit comments

Comments
 (0)