Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
216 changes: 206 additions & 10 deletions phpdotnet/phd/Package/Generic/XHTML.php
Original file line number Diff line number Diff line change
Expand Up @@ -1478,7 +1478,11 @@ public function format_methodparam_modifier($open, $name, $attrs, $props) {
return '<span class="modifier">';
}
if (isset($attrs[Reader::XMLNS_DOCBOOK]["role"])) {
$role = $this->getRole();
$this->popRole();
if ($role === "attribute") {
return '</span> ';
}
}
return '</span>';
}
Expand All @@ -1491,23 +1495,215 @@ public function format_modifier_text($value, $tag) {
}

private function format_attribute_modifier_text(string $value): string {
// Anything that is not a leading "#[\Attribute(" / "#[\Attribute]" chunk
// e.g. "|" separator between arguments passes through.
if (!preg_match('/^(#\[)(.+?)([](])$/', $value, $match)) {
if (trim($value) === '|') {
return ' | ';
}
return $value;
$trimmed = trim($value);
$namePattern = '\\\\?[A-Za-z_][A-Za-z0-9_\\\\]*';

// Full attribute with literal arguments: #[\Name(args)]
if (preg_match('/^(#\[)(' . $namePattern . ')\((.+)\)]$/s', $trimmed, $match)) {
[, $prefix, $name, $args] = $match;
return $prefix . $this->link_attribute_name($name) . '(' . $this->render_attribute_args($args) . ')]';
}

[, $prefix, $name, $suffix] = $match;
// Simple attribute: #[\Name]
if (preg_match('/^#\[(' . $namePattern . ')]$/', $trimmed, $match)) {
return '#[' . $this->link_attribute_name($match[1]) . ']';
}

// Opening of an attribute followed by child elements: #[\Name(
if (preg_match('/^(#\[)(' . $namePattern . ')\($/', $trimmed, $match)) {
[, $prefix, $name] = $match;
return $prefix . $this->link_attribute_name($name) . '(';
}

// Separator between attribute arguments
if ($trimmed === '|') {
return ' | ';
}

return $trimmed === '' ? '' : $value;
}

private function link_attribute_name(string $name): string {
if ($name[0] !== '\\') {
$this->outputHandler->v('Attribute name "%s" is not fully qualified', $name, VERBOSE_MESSAGES);
}
$attribute = strtolower(ltrim($name, "\\"));
$href = $this->getFilename('class.' . $attribute);
if (!$href) {
return $value;
return $name;
}

return '<a href="' . $href . $this->getExt() . '">' . $name . '</a>';
}

private function render_attribute_args(string $args): string {
$args = trim(preg_replace('/\s+/', ' ', $args));
$parts = $this->split_attribute_args($args);

if (count($parts) <= 1) {
return $this->render_attribute_arg_value($parts[0]['value'] ?? '');
}

$lines = [];
$prefix = '';
foreach ($parts as $part) {
$rendered = $this->render_attribute_arg_value($part['value']);
$line = '&nbsp;&nbsp;&nbsp;&nbsp;' . $prefix . $rendered;
if ($part['separator'] === ',') {
$line .= ',';
$prefix = '';
} elseif ($part['separator'] === '|') {
$prefix = '| ';
} else {
$line .= ',';
$prefix = '';
}
$lines[] = $line;
}

return '<br>' . implode('<br>', $lines) . '<br>';
}

/**
* @return list<array{value: string, separator: string|null}>
*/
private function split_attribute_args(string $args): array {
$parts = [];
$current = '';
$depth = 0;
$inString = false;
$stringChar = '';
$length = strlen($args);

for ($i = 0; $i < $length; $i++) {
$ch = $args[$i];

if ($inString) {
$current .= $ch;
if ($ch === '\\' && $i + 1 < $length) {
$current .= $args[++$i];
continue;
}
if ($ch === $stringChar) {
$inString = false;
}
continue;
}

if ($ch === '"' || $ch === "'") {
$inString = true;
$stringChar = $ch;
$current .= $ch;
continue;
}

if ($ch === '(' || $ch === '[') {
$depth++;
} elseif ($ch === ')' || $ch === ']') {
$depth--;
}

if ($depth === 0 && ($ch === '|' || $ch === ',')) {
$parts[] = ['value' => trim($current), 'separator' => $ch];
$current = '';
continue;
}

$current .= $ch;
}

if (trim($current) !== '') {
$parts[] = ['value' => trim($current), 'separator' => null];
}

return $parts;
}

private function render_attribute_arg_value(string $value): string {
$prefix = '';
if (preg_match('/^([A-Za-z_][A-Za-z0-9_]*)\s*:(?!:)\s*/', $value, $m)) {
$prefix = '<code class="parameter">' . $m[1] . '</code>: ';
$value = substr($value, strlen($m[0]));
}

$out = '';
$buffer = '';
$length = strlen($value);
for ($i = 0; $i < $length; $i++) {
$ch = $value[$i];
if ($ch !== '"' && $ch !== "'") {
$buffer .= $ch;
continue;
}

if ($buffer !== '') {
$out .= $this->format_attribute_non_string_segment($buffer);
$buffer = '';
}

$stringChar = $ch;
$literal = $ch;
$i++;
for (; $i < $length; $i++) {
$sc = $value[$i];
$literal .= $sc;
if ($sc === '\\' && $i + 1 < $length) {
$literal .= $value[++$i];
continue;
}
if ($sc === $stringChar) {
break;
}
}
$out .= '<span class="type string">' . htmlspecialchars($literal, ENT_NOQUOTES, 'UTF-8') . '</span>';
}
if ($buffer !== '') {
$out .= $this->format_attribute_non_string_segment($buffer);
}

return $prefix . '<a href="' . $href . $this->getExt() . '">' . $name . '</a>' . $suffix;
return $prefix . $out;
}

private function format_attribute_non_string_segment(string $segment): string {
$linked = $this->link_constants_in_text($segment);

// Split out <a>...</a> regions so we don't touch their contents.
$parts = preg_split('/(<a [^>]*>[^<]*<\/a>)/', $linked, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as $i => $part) {
if ($i % 2 === 1) {
continue;
}
$part = preg_replace_callback(
'/\b(true|false|null)\b/i',
fn(array $m) => '<span class="type ' . strtolower($m[1]) . '">' . $m[1] . '</span>',
$part,
);
$part = preg_replace_callback(
'/(?<![\w.])-?\d+(\.\d+)?(?![\w.])/',
fn(array $m) => '<span class="type ' . (isset($m[1]) && $m[1] !== '' ? 'float' : 'int') . '">' . $m[0] . '</span>',
$part,
);
$parts[$i] = $part;
}
return implode('', $parts);
}

private function link_constants_in_text(string $text): string {
$escaped = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');

// Link Class::CONST references that appear as literal text
return preg_replace_callback(
'/\\\\?[A-Za-z_][\w\\\\]*::[A-Za-z_][\w]*/',
function (array $match): string {
$constant = $match[0];
$link = $this->createLink($this->convertConstantNameToId($constant));
if ($link === null) {
return $constant;
}
return '<a href="' . $link . '">' . $constant . '</a>';
},
$escaped,
);
}

public function format_methodsynopsis($open, $name, $attrs, $props) {
Expand Down
8 changes: 4 additions & 4 deletions tests/package/generic/attribute_formatting_001.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,25 @@ Content:
<div id="attribute-formatting" class="chapter">
<div class="section">
<p class="para">1. Class methodparameter with unknown attribute</p>
<div class="constructorsynopsis dc-description"><span class="modifier">public</span> <span class="methodname">mysqli::__construct</span>(<span class="methodparam"><span class="attribute">#[\UnknownAttribute]</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>
<div class="constructorsynopsis dc-description"><span class="modifier">public</span> <span class="methodname">mysqli::__construct</span>(<span class="methodparam"><span class="attribute">#[\UnknownAttribute]</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>

</div>

<div class="section">
<p class="para">2. Class methodparameter with known attribute</p>
<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>
<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>

</div>

<div class="section">
<p class="para">3. Function parameter with unknown attribute</p>
<div class="methodsynopsis dc-description"><span class="type">bool</span> <span class="methodname">password_verify</span>(<span class="methodparam"><span class="attribute">#[\UnknownAttribute]</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>
<div class="methodsynopsis dc-description"><span class="type">bool</span> <span class="methodname">password_verify</span>(<span class="methodparam"><span class="attribute">#[\UnknownAttribute]</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>

</div>

<div class="section">
<p class="para">4. Function parameter with known attribute</p>
<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>
<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>

</div>
</div>
130 changes: 130 additions & 0 deletions tests/package/generic/attribute_formatting_005.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
--TEST--
Attribute formatting 005 - Attribute with literal parameter arguments
--FILE--
<?php
namespace phpdotnet\phd;

require_once __DIR__ . "/../../setup.php";

$xmlFile = __DIR__ . "/data/attribute_formatting_005.xml";

$config->xmlFile = $xmlFile;

$format = new TestGenericChunkedXHTML($config, $outputHandler);

$format->SQLiteIndex(
null, null,
"class.deprecated",
"class.deprecated",
"", "", "", "", "", "", 0,
);
$format->SQLiteIndex(
null, null,
"class.attribute",
"class.attribute",
"", "", "", "", "", "", 0,
);
$format->SQLiteIndex(
null, null,
"attribute.constants.target-function",
"class.attribute",
"", "", "", "", "", "", 0,
);
$format->SQLiteIndex(
null, null,
"attribute.constants.target-method",
"class.attribute",
"", "", "", "", "", "", 0,
);
$format->SQLiteIndex(
null, null,
"attribute.constants.target-class",
"class.attribute",
"", "", "", "", "", "", 0,
);

$render = new TestRender(new Reader($outputHandler), $config, $format);

$render->run();
?>
--EXPECT--
Filename: attribute-formatting-005.html
Content:
<div id="attribute-formatting-005" class="chapter">
<div class="section">
<p class="para">1. Attribute with literal named arguments</p>
<div class="classsynopsis"><div class="classsynopsisinfo">

<span class="attribute">#[<a href="class.deprecated.html">\Deprecated</a>(<br>&nbsp;&nbsp;&nbsp;&nbsp;<code class="parameter">since</code>: <span class="type string">'8.5'</span>,<br>&nbsp;&nbsp;&nbsp;&nbsp;<code class="parameter">message</code>: <span class="type string">'Deprecated since PHP 8.4'</span>,<br>)]</span><br>
<span class="modifier">final</span>
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
{</div>
}</div>
</div>

<div class="section">
<p class="para">2. Attribute with single literal positional argument</p>
<div class="classsynopsis"><div class="classsynopsisinfo">

<span class="attribute">#[<a href="class.deprecated.html">\Deprecated</a>(<span class="type string">'Deprecated since PHP 8.4'</span>)]</span><br>
<span class="modifier">final</span>
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
{</div>
}</div>
</div>

<div class="section">
<p class="para">3. Unknown attribute with literal argument</p>
<div class="classsynopsis"><div class="classsynopsisinfo">

<span class="attribute">#[\UnknownAttribute(<code class="parameter">foo</code>: <span class="type string">'bar'</span>)]</span><br>
<span class="modifier">final</span>
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
{</div>
}</div>
</div>

<div class="section">
<p class="para">4. Namespaced attribute with literal argument</p>
<div class="classsynopsis"><div class="classsynopsisinfo">

<span class="attribute">#[\Some\Namespaced\Attribute(<code class="parameter">value</code>: <span class="type int">42</span>)]</span><br>
<span class="modifier">final</span>
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
{</div>
}</div>
</div>

<div class="section">
<p class="para">5. Multi-line attribute with literal class constant arguments</p>
<div class="classsynopsis"><div class="classsynopsisinfo">

<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>
<span class="modifier">final</span>
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
{</div>
}</div>
</div>

<div class="section">
<p class="para">6. Attribute with mix of known and unknown class constants</p>
<div class="classsynopsis"><div class="classsynopsisinfo">

<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>
<span class="modifier">final</span>
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
{</div>
}</div>
</div>

<div class="section">
<p class="para">7. Attribute with bool, null, int and float literal arguments</p>
<div class="classsynopsis"><div class="classsynopsisinfo">

<span class="attribute">#[\UnknownAttribute(<br>&nbsp;&nbsp;&nbsp;&nbsp;<code class="parameter">enabled</code>: <span class="type true">true</span>,<br>&nbsp;&nbsp;&nbsp;&nbsp;<code class="parameter">fallback</code>: <span class="type false">false</span>,<br>&nbsp;&nbsp;&nbsp;&nbsp;<code class="parameter">default</code>: <span class="type null">null</span>,<br>&nbsp;&nbsp;&nbsp;&nbsp;<code class="parameter">count</code>: <span class="type int">42</span>,<br>&nbsp;&nbsp;&nbsp;&nbsp;<code class="parameter">ratio</code>: <span class="type float">3.14</span>,<br>)]</span><br>
<span class="modifier">final</span>
<span class="modifier">class</span> <strong class="classname">Deprecated</strong>
{</div>
}</div>
</div>
</div>
Loading