diff --git a/changelog.d/unreleased/2106.added.md b/changelog.d/unreleased/2106.added.md new file mode 100644 index 0000000000..c26604dce9 --- /dev/null +++ b/changelog.d/unreleased/2106.added.md @@ -0,0 +1,16 @@ +--- +category: added +issues: + - 2106 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs +--- + +## English + +- **HTML `data-*` and `aria-*` attributes are now indexed as property symbols (#2106)** — HTML symbol extraction now records semantic attribute names such as `data-testid` and `aria-label`, including boolean and multiline attributes, without reading attribute-like text inside quoted values. + +## 日本語 + +- **HTML の `data-*` / `aria-*` 属性を property シンボルとして索引するようになりました (#2106)** — HTML のシンボル抽出が `data-testid` や `aria-label` などの意味を持つ属性名を記録するようになり、boolean 属性や複数行属性にも対応しつつ、引用符付き値の中にある属性風テキストは拾いません。 diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs index 13b8a09544..ae5b426499 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs @@ -186,6 +186,22 @@ private static List ExtractHtmlSymbols(long fileId, string[] lines } } + if (IsHtmlSemanticStateAttributeName(attrNameLower)) + { + var attrStartLine = FindHtmlLineNumber(lineStarts, attrNameStart); + var attrSignatureIndex = Math.Clamp(attrStartLine - 1, 0, lines.Length - 1); + symbols.Add(new SymbolRecord + { + FileId = fileId, + Kind = "property", + Name = attrNameLower, + Line = attrStartLine, + StartLine = attrStartLine, + EndLine = attrStartLine, + Signature = lines[attrSignatureIndex].Trim(), + }); + } + if (attrValue == null || attrValue.Length == 0) continue; @@ -256,6 +272,13 @@ private static List ExtractHtmlSymbols(long fileId, string[] lines return symbols; } + private static bool IsHtmlSemanticStateAttributeName(string attrNameLower) + { + return (attrNameLower.StartsWith("data-", StringComparison.Ordinal) || + attrNameLower.StartsWith("aria-", StringComparison.Ordinal)) && + attrNameLower.Length > 5; + } + private static List ExtractMarkdownSymbols(long fileId, string[] lines) { // Markdown headings are the closest thing to navigable symbols in docs files. diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index ec6bbd3704..bd8c9d16cf 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -22226,6 +22226,30 @@ public void Extract_Html_IgnoresDataIdAndAriaIdAndXmlIdAttributes() Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "ns"); } + [Fact] + public void Extract_Html_CapturesDataAndAriaAttributeNamesAsProperties() + { + var content = """ + +
+
+ """; + + var symbols = SymbolExtractor.Extract(1, "html", content); + + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "data-testid" && s.Line == 1); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "data-user-id" && s.Line == 1); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "aria-label" && s.Line == 1); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "aria-expanded" && s.Line == 1); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "data-panel-state" && s.Line == 3); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "aria-labelledby" && s.Line == 4); + Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "data-fake"); + Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "aria-hidden"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "real"); + } + [Fact] public void Extract_Html_CapturesExternalScriptAndLinkAsImports() {