11using System . Text ;
22using System . Text . Json ;
3+ using System . Text . RegularExpressions ;
34using AngleSharp . Dom ;
45using AngleSharp . Html . Parser ;
56using Netdocs . Abstractions ;
@@ -16,6 +17,7 @@ public sealed class SearchPlugin : IPlugin, IBuildHook
1617 private IReadOnlyList < string > _languages = [ "en" ] ;
1718 private string _separator = DefaultSeparator ;
1819 private IReadOnlyList < string > _pipeline = DefaultPipeline ;
20+ private bool _stripBloatElements = true ;
1921
2022 public string Name => "search" ;
2123
@@ -33,6 +35,11 @@ public void Configure(IPluginContext ctx)
3335 // `pipeline` overrides the lunr token pipeline; an empty list disables stemming/stopwords.
3436 if ( opts . TryGetValue ( "pipeline" , out var pipe ) && AsStringList ( pipe ) is { } p )
3537 _pipeline = p ;
38+
39+ // `strip_bloat_elements` removes SVG, img, canvas, figure, picture, and other non-semantic elements
40+ // that inflate the index without adding search value. Defaults to true.
41+ if ( opts . TryGetValue ( "strip_bloat_elements" , out var strip ) && strip is bool b )
42+ _stripBloatElements = b ;
3643 }
3744
3845 public async Task OnBuildCompleteAsync ( SiteContext site , CancellationToken ct )
@@ -43,7 +50,7 @@ public async Task OnBuildCompleteAsync(SiteContext site, CancellationToken ct)
4350 foreach ( var page in site . Pages )
4451 {
4552 var tags = ExtractTags ( page ) ;
46- var ( intro , sections ) = SplitPage ( parser , page ) ;
53+ var ( intro , sections ) = SplitPage ( parser , page , _stripBloatElements ) ;
4754 docs . Add ( new SearchDoc ( page . Url , page . Title , intro , tags ) ) ;
4855
4956 foreach ( var section in sections )
@@ -69,8 +76,9 @@ public async Task OnBuildCompleteAsync(SiteContext site, CancellationToken ct)
6976 /// The H1 is the page title (not a section), so the content beneath it — up to the first H2/H3 —
7077 /// becomes the page-level doc's text. That text is the teaser Material shows on the main result
7178 /// line for each page, so leaving it empty (as an H1-as-section split does) drops all excerpts.
72- /// Block-level HTML is preserved in the text so teasers render like the upstream index.</summary>
73- private static ( string Intro , IReadOnlyList < SearchDoc > Sections ) SplitPage ( HtmlParser parser , Page page )
79+ /// Block-level HTML is preserved to render rich teasers; bloat-inducing elements (SVG, img, canvas)
80+ /// are optionally stripped (enabled by default) to reduce index size without sacrificing presentation.</summary>
81+ private static ( string Intro , IReadOnlyList < SearchDoc > Sections ) SplitPage ( HtmlParser parser , Page page , bool stripBloat )
7482 {
7583 if ( string . IsNullOrEmpty ( page . HtmlContent ) )
7684 return ( Collapse ( page . PlainText ) , [ ] ) ;
@@ -103,11 +111,11 @@ private static (string Intro, IReadOnlyList<SearchDoc> Sections) SplitPage(HtmlP
103111 }
104112 else if ( seenSection )
105113 {
106- buffer . Append ( NodeText ( node ) ) . Append ( ' ' ) ;
114+ buffer . Append ( NodeText ( node , stripBloat ) ) . Append ( ' ' ) ;
107115 }
108116 else
109117 {
110- intro . Append ( NodeText ( node ) ) . Append ( ' ' ) ;
118+ intro . Append ( NodeText ( node , stripBloat ) ) . Append ( ' ' ) ;
111119 }
112120 }
113121 if ( currentId is not null )
@@ -119,10 +127,24 @@ private static (string Intro, IReadOnlyList<SearchDoc> Sections) SplitPage(HtmlP
119127 }
120128
121129 /// <summary>Serializes a node for the search index: block-level HTML is kept (matching the
122- /// upstream mkdocs-material index, whose text field carries markup so teasers render richly),
123- /// while bare text nodes contribute their text content.</summary>
124- private static string NodeText ( INode node ) =>
125- node is IElement el ? el . OuterHtml : node . TextContent ;
130+ /// upstream mkdocs-material index, whose text field carries markup so teasers render richly).
131+ /// Bloat-inducing elements (SVG, img, canvas, etc.) are optionally stripped to keep the index lean.</summary>
132+ private static string NodeText ( INode node , bool stripBloat ) =>
133+ node is IElement el ? StripBloatElements ( el . OuterHtml , stripBloat ) : node . TextContent ;
134+
135+ /// <summary>Strips bloat-inducing elements (svg, img, canvas, picture, figure) from HTML.
136+ /// Semantic elements (p, strong, em, code, etc.) are preserved for rich teaser rendering.</summary>
137+ private static string StripBloatElements ( string html , bool stripBloat )
138+ {
139+ if ( ! stripBloat ) return html ;
140+
141+ // Remove entire SVG, img, canvas, picture, figure elements (including content)
142+ var stripped = Regex . Replace ( html , @"<(?:svg|img|canvas|picture|figure)[^>]*>.*?</(?:svg|img|canvas|picture|figure)>" , " " , RegexOptions . IgnoreCase | RegexOptions . Singleline ) ;
143+ // Also remove self-closing variants (e.g., <img ... />)
144+ stripped = Regex . Replace ( stripped , @"<(?:svg|img|canvas|picture|figure)[^>]*/?>" , " " , RegexOptions . IgnoreCase ) ;
145+ // Collapse whitespace
146+ return Regex . Replace ( stripped , @"\s+" , " " ) . Trim ( ) ;
147+ }
126148
127149 private static string [ ] ExtractTags ( Page page )
128150 {
0 commit comments