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
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2106.added.md
Original file line number Diff line number Diff line change
@@ -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 属性や複数行属性にも対応しつつ、引用符付き値の中にある属性風テキストは拾いません。
23 changes: 23 additions & 0 deletions src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,22 @@ private static List<SymbolRecord> 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;

Expand Down Expand Up @@ -256,6 +272,13 @@ private static List<SymbolRecord> 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<SymbolRecord> ExtractMarkdownSymbols(long fileId, string[] lines)
{
// Markdown headings are the closest thing to navigable symbols in docs files.
Expand Down
24 changes: 24 additions & 0 deletions tests/CodeIndex.Tests/SymbolExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<button DATA-TestId="save-button" data-user-id=42 aria-label="Save" aria-expanded></button>
<section
data-panel-state="open"
aria-labelledby='panel-title'></section>
<div title="data-fake=&quot;nope&quot; aria-hidden=&quot;true&quot;" id="real"></div>
""";

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()
{
Expand Down
Loading