@@ -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 = ' ' . $ 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 ) {
0 commit comments