18using System.Collections.Generic;
21using System.Text.RegularExpressions;
35 public string Name {
get;
set; } =
"generic";
62 private static string ConfigFilePath =
"./LLMDigestRules.yaml";
70 false ==
string.IsNullOrWhiteSpace(
GPAL.Version)
72 : (System.Reflection.Assembly.GetExecutingAssembly().GetName().
Version?.ToString() ??
"0");
75 public string Version {
get;
set; } =
null;
78 public List<DigestRuleSet>
RuleSets {
get;
set; } =
new List<DigestRuleSet>();
81 public List<DigestRuleSet>
UserRuleSets {
get;
set; } =
new List<DigestRuleSet>();
83 private const string GpalBanner =
84 "##### GPAL (managed by GPAL, refreshed from defaults on every run) #####";
85 private const string UserBanner =
86 "##### NEW (yours, GPAL never touches this section) #####";
88 private static readonly
string FileHeader =
89 "# ==================================================================" + Environment.NewLine +
90 "# GPAL LLM Digest Rules" + Environment.NewLine +
91 "#" + Environment.NewLine +
92 "# ruleSets : GPAL owns these. They are regenerated from the built-in" + Environment.NewLine +
93 "# defaults whenever the GPAL version changes, so any edits" + Environment.NewLine +
94 "# made directly here are lost on the next version. Within a" + Environment.NewLine +
95 "# version this file is left as-is (it is the source of truth)." + Environment.NewLine +
96 "# userRuleSets : Yours. GPAL never changes anything in this section." + Environment.NewLine +
97 "# To take ownership of a GPAL rule set, move the whole entry" + Environment.NewLine +
98 "# out of ruleSets and into userRuleSets. GPAL will not re-add" + Environment.NewLine +
99 "# a rule set whose name it finds there, so your copy wins." + Environment.NewLine +
100 "# ==================================================================" + Environment.NewLine;
110 if (file !=
null) ConfigFilePath = file.Filename;
113 if (
false == File.Exists(ConfigFilePath))
125 .WithInput((
GPALFile)ConfigFilePath)
130 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Failed to load LLM digest rules from file. Using defaults.",
null, GPALObjectType.None, ex);
137 loaded.RuleSets = loaded.RuleSets ??
new List<DigestRuleSet>();
138 loaded.UserRuleSets = loaded.UserRuleSets ??
new List<DigestRuleSet>();
143 var userNames =
new HashSet<string>(
144 loaded.
UserRuleSets.Select(r => r?.Name).Where(n =>
false ==
string.IsNullOrWhiteSpace(n)),
145 StringComparer.OrdinalIgnoreCase);
147 loaded.RuleSets =
Default().
RuleSets.Where(r =>
false == userNames.Contains(r.Name)).ToList();
166 if (file !=
null) ConfigFilePath = file.Filename;
167 WriteConfigFile(
Default(), ConfigFilePath);
174 WriteConfigFile(cfg, ConfigFilePath);
178 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not write LLM digest rules file",
null, GPALObjectType.None, ex);
187 private static void WriteConfigFile(DigestRulesConfig cfg,
string path)
191 string tmp = path +
".gpaltmp.yaml";
192 GPAL.Converter.WithInput(cfg).SaveTo((GPALFile)tmp);
194 string yaml = File.ReadAllText(tmp);
195 try { File.Delete(tmp); }
catch { }
198 yaml = Regex.Replace(yaml,
@"(?m)^ruleSets:", GpalBanner + Environment.NewLine +
"ruleSets:");
199 yaml = Regex.Replace(yaml,
@"(?m)^userRuleSets:", Environment.NewLine + UserBanner + Environment.NewLine +
"userRuleSets:");
201 string full = FileHeader + yaml;
203 if (
false == File.Exists(path) || File.ReadAllText(path) != full)
204 File.WriteAllText(path, full);
216 var gpal =
RuleSets ??
new List<DigestRuleSet>();
217 var all = user.Concat(gpal).ToList();
219 var
generic = user.FirstOrDefault(r =>
"generic" == r.Name)
220 ?? gpal.FirstOrDefault(r =>
"generic" == r.Name)
225 if (
false ==
string.IsNullOrWhiteSpace(ruleSetName))
226 selected = all.FirstOrDefault(r =>
string.Equals(r.Name, ruleSetName, StringComparison.OrdinalIgnoreCase));
228 if (
null == selected &&
false ==
string.IsNullOrWhiteSpace(url))
229 selected = all.FirstOrDefault(r =>
230 "generic" != r.Name &&
231 null != r.DomainPatterns &&
232 r.DomainPatterns.Any(p => 0 <= url.IndexOf(p, StringComparison.OrdinalIgnoreCase)));
234 if (
null == selected || selected ==
generic)
239 Name = selected.
Name,
244 JunkSelectors =
generic.JunkSelectors.Concat(selected.
JunkSelectors).Distinct().ToList(),
247 MainContentCandidates = selected.
MainContentCandidates.Concat(
generic.MainContentCandidates).Distinct().ToList(),
248 SitemapBaseUrl =
false ==
string.IsNullOrWhiteSpace(selected.
SitemapBaseUrl) ? selected.SitemapBaseUrl :
generic.SitemapBaseUrl,
249 EnabledPasses = 0 < selected.EnabledPasses.Count ? selected.EnabledPasses : generic.EnabledPasses
262 JunkSelectors =
new List<string>
269 "//*[contains(@class,'cookie') or contains(@class,'consent') or contains(@class,'gdpr') or contains(@id,'cookie')]",
273 "//header[not(ancestor::main or contains(@class,'hero') or contains(@class,'main') or contains(@class,'content'))]",
276 "//*[contains(@role,'navigation') or contains(@role,'banner') and not(ancestor::main)]",
285 "//*[contains(concat(' ', normalize-space(@class), ' '), ' ad ') or contains(concat(' ', normalize-space(@class), ' '), ' ads ')" +
286 " or contains(concat(' ', normalize-space(@class), ' '), ' ad-') or contains(concat(' ', normalize-space(@class), ' '), ' ads-')" +
287 " or contains(@class,'advert')" +
288 " or contains(concat(' ', normalize-space(@class), ' '), ' popup ') or contains(concat(' ', normalize-space(@class), ' '), ' popup-') or contains(concat(' ', normalize-space(@class), ' '), '-popup ')" +
289 " or contains(concat(' ', normalize-space(@class), ' '), ' modal ') or contains(concat(' ', normalize-space(@class), ' '), ' modal-') or contains(concat(' ', normalize-space(@class), ' '), '-modal ')" +
290 " or contains(concat(' ', normalize-space(@class), ' '), ' skip ') or contains(concat(' ', normalize-space(@class), ' '), ' skip-') or contains(concat(' ', normalize-space(@class), ' '), '-skip ')" +
291 " or contains(concat(' ', normalize-space(@id), ' '), ' skip-') or contains(concat(' ', normalize-space(@id), ' '), '-skip ') or normalize-space(@id) = 'skip'" +
292 " or contains(text(),'Skip to content')]",
295 "//*[contains(@class,'social') or contains(@class,'share') or contains(@class,'follow')]",
299 "//*[contains(@class,'vjs-control-bar') or contains(@class,'vjs-control-text') or contains(@class,'vjs-text-track-display') or contains(@class,'vjs-title-bar') or contains(@class,'vjs-loading-spinner') or contains(@class,'vjs-modal-dialog') or contains(@class,'vjs-menu') or contains(@class,'vjs-playback-rate')]",
302 "//*[contains(@class,'accessibility') or contains(@class,'legal') or contains(text(),'©') and count(.//a) > 10]",
310 "//*[contains(@class, 'sidebar')" +
311 " or contains(concat(' ', normalize-space(@class), ' '), ' widget ')" +
312 " or contains(concat(' ', normalize-space(@class), ' '), ' widget-')" +
313 " or contains(concat(' ', normalize-space(@class), ' '), ' widget_')" +
314 " or contains(concat(' ', normalize-space(@class), ' '), '-widget ')" +
315 " or contains(concat(' ', normalize-space(@class), ' '), '_widget ')" +
316 " or contains(@class, 'social')" +
317 " or contains(concat(' ', normalize-space(@class), ' '), ' popup ') or contains(concat(' ', normalize-space(@class), ' '), ' popup-') or contains(concat(' ', normalize-space(@class), ' '), '-popup ')" +
318 " or contains(concat(' ', normalize-space(@role), ' '), ' navigation ')]",
321 "//a[string-length(normalize-space(.)) < 15 and (contains(.,'next') or contains(.,'prev') or contains(.,'›') or contains(.,'«'))]"
323 MainContentCandidates =
new List<string>
326 "//*[contains(@id, 'content') or contains(@class, 'content') or contains(@id, 'main') or contains(@class, 'main') or contains(@id, 'dp') or contains(@class, 'section') and contains(@class, 'spacing-none')]",
330 EnabledPasses =
new List<string>
332 "CollapseExcessiveBlanks",
333 "RemoveCountersAndEmptyIcons",
338 "RejoinLinkedImages",
339 "DedupeConsecutiveImages",
340 "ProductImageBrandPrice",
343 "StripEscapeArtifacts",
346 "SplitConsecutiveLinks",
347 "ResolveRelativeLinks"
355 DomainPatterns =
new List<string> {
"indeed.com" },
356 JunkSelectors =
new List<string>
358 "//div[contains(@class,'gnav')]",
359 "//div[contains(@class,'footer')]",
360 "//div[contains(@id,'popover')]",
361 "//div[contains(@class,'jobsearch-HiringInsights')]",
362 "//div[contains(@class,'jobsearch-CompanyInfoWithReview')]",
364 MainContentCandidates =
new List<string>
366 "//div[@id='jobDescriptionText']",
367 "//div[contains(@class,'jobsearch-JobComponent')]",
368 "//div[contains(@class,'jobsearch-ResultsList')]"
370 SitemapBaseUrl =
"https://www.indeed.com",
371 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
378 DomainPatterns =
new List<string> {
"wikipedia.org" },
379 JunkSelectors =
new List<string>
381 "//div[@id='mw-navigation']",
382 "//div[@id='mw-head-base']",
383 "//div[@id='mw-page-base']",
384 "//div[@id='catlinks']",
385 "//div[@id='footer']",
386 "//*[contains(@class,'navbox')]",
387 "//*[contains(@class,'mw-editsection')]",
388 "//*[contains(@class,'reflist')]",
389 "//*[contains(@class,'sistersitebox')]",
390 "//*[contains(@class,'noprint')]"
392 MainContentCandidates =
new List<string>
394 "//div[@id='mw-content-text']"
396 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
403 DomainPatterns =
new List<string> {
"github.com" },
404 JunkSelectors =
new List<string>
408 "//*[contains(@class,'file-navigation')]",
409 "//*[contains(@class,'js-flash-container')]",
410 "//*[contains(@class,'signup-prompt')]",
411 "//*[contains(@class,'repository-topics')]",
412 "//*[contains(@class,'BorderGrid-cell') and contains(@class,'hide-sm')]"
414 MainContentCandidates =
new List<string>
416 "//*[contains(@class,'markdown-body')]",
417 "//div[@id='readme']",
420 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
426 Name =
"stackoverflow",
427 DomainPatterns =
new List<string> {
"stackoverflow.com",
"stackexchange.com" },
428 JunkSelectors =
new List<string>
430 "//div[@id='left-sidebar']",
431 "//div[@id='sidebar']",
432 "//*[contains(@class,'s-topbar')]",
433 "//*[contains(@class,'post-menu')]",
434 "//*[contains(@class,'s-sidebarwidget')]",
435 "//*[contains(@class,'js-consent-banner')]",
436 "//div[@id='feed-link']",
437 "//div[@id='hot-network-questions')]"
439 MainContentCandidates =
new List<string>
441 "//div[@id='question']",
442 "//div[@id='content']"
444 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
451 DomainPatterns =
new List<string> {
"amazon.com" },
452 JunkSelectors =
new List<string>
454 "//div[@id='nav-belt']",
455 "//div[@id='navFooter']",
456 "//div[@id='nav-subnav']",
457 "//div[@id='HeroQuickPromo']",
459 "//*[contains(@id,'carousel')]",
460 "//*[contains(@class,'a-carousel-row')]",
461 "//div[@id='sims-consolidated-1']",
462 "//div[@id='sims-consolidated-2']"
464 MainContentCandidates =
new List<string>
467 "//div[@id='centerCol']"
475 DomainPatterns =
new List<string> {
"news.ycombinator.com" },
476 JunkSelectors =
new List<string>
478 "//td[@bgcolor='#ff6600']",
479 "//span[@class='yclinks']",
480 "//tr[@class='spacer']"
482 MainContentCandidates =
new List<string>
484 "//table[@class='comment-tree']",
485 "//table[@class='fatitem']",
486 "//table[@class='itemlist']"
488 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
495 DomainPatterns =
new List<string> {
"old.reddit.com" },
496 JunkSelectors =
new List<string>
498 "//div[@id='header']",
499 "//div[@id='footer']",
500 "//div[@class='side']",
501 "//div[@id='sr-header-area']"
503 MainContentCandidates =
new List<string>
505 "//div[@class='commentarea']",
506 "//div[@id='siteTable']",
507 "//div[@id='content']"
509 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks" }
516 DomainPatterns =
new List<string> {
"reddit.com" },
517 JunkSelectors =
new List<string>
523 MainContentCandidates =
new List<string>
526 "//div[@id='main-content']",
529 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks" }
536 DomainPatterns =
new List<string> {
"medium.com" },
537 JunkSelectors =
new List<string>
542 "//*[contains(@class,'pw-responses')]",
543 "//*[contains(@class,'pw-post-tags')]"
545 MainContentCandidates =
new List<string>
549 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
556 DomainPatterns =
new List<string> {
"substack.com" },
557 JunkSelectors =
new List<string>
561 "//*[contains(@class,'subscription-widget')]",
562 "//*[contains(@class,'paywall')]",
563 "//*[contains(@class,'post-upsell')]"
565 MainContentCandidates =
new List<string>
567 "//div[contains(@class,'available-content')]",
570 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
577 DomainPatterns =
new List<string> {
"ebay.com" },
578 JunkSelectors =
new List<string>
580 "//div[@id='gh-oa']",
581 "//div[@id='glbfooter']",
582 "//div[@id='vi-recentlyViewed']",
583 "//*[contains(@id,'vi-recsalso')]"
585 MainContentCandidates =
new List<string>
587 "//div[@id='mainContent']",
588 "//div[@id='vi-desc']"
596 DomainPatterns =
new List<string> {
"linkedin.com" },
597 JunkSelectors =
new List<string>
602 "//*[contains(@class,'similar-jobs')]",
603 "//*[contains(@class,'jobs-premium-upsell')]"
605 MainContentCandidates =
new List<string>
607 "//div[contains(@class,'jobs-description')]",
608 "//section[contains(@class,'description')]",
611 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
618 DomainPatterns =
new List<string> {
"glassdoor.com" },
619 JunkSelectors =
new List<string>
623 "//*[contains(@class,'LoginModal') or contains(@id,'LoginModal')]",
624 "//*[contains(@class,'HardsellOverlay')]"
626 MainContentCandidates =
new List<string>
628 "//div[@id='JobDescriptionContainer']",
629 "//div[@id='job-desc']",
632 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
638 Name =
"ziprecruiter",
639 DomainPatterns =
new List<string> {
"ziprecruiter.com" },
640 JunkSelectors =
new List<string>
644 "//*[contains(@class,'suggested_jobs')]",
645 "//*[contains(@class,'email_signup')]"
647 MainContentCandidates =
new List<string>
649 "//div[contains(@class,'job_description')]",
650 "//div[@id='job_desc']",
653 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
660 DomainPatterns =
new List<string> {
"walmart.com" },
661 JunkSelectors =
new List<string>
665 "//*[contains(@class,'RecommendedItems')]",
666 "//*[contains(@id,'recommendations')]"
668 MainContentCandidates =
new List<string>
670 "//div[@id='maincontent']",
679 DomainPatterns =
new List<string> {
"reuters.com" },
680 JunkSelectors =
new List<string>
684 "//*[contains(@class,'related-articles')]",
685 "//*[contains(@class,'newsletter-signup')]"
687 MainContentCandidates =
new List<string>
689 "//*[@data-testid='article-body']",
692 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
699 DomainPatterns =
new List<string> {
"apnews.com" },
700 JunkSelectors =
new List<string>
704 "//*[contains(@class,'RelatedStories')]",
705 "//*[contains(@class,'Advertisement')]"
707 MainContentCandidates =
new List<string>
709 "//div[contains(@class,'ArticleBody')]",
712 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
719 DomainPatterns =
new List<string> {
"zillow.com" },
720 JunkSelectors =
new List<string>
724 "//*[contains(@class,'zsg-modal')]",
725 "//*[contains(@id,'contact-form')]"
727 MainContentCandidates =
new List<string>
729 "//div[contains(@class,'hdp')]",
732 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
739 DomainPatterns =
new List<string> {
"redfin.com" },
740 JunkSelectors =
new List<string>
744 "//*[contains(@class,'ContactAgent')]",
745 "//*[contains(@class,'similar-homes')]"
747 MainContentCandidates =
new List<string>
749 "//div[@id='content']",
750 "//div[contains(@class,'home-main-stats')]",
753 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
760 DomainPatterns =
new List<string> {
"developer.mozilla.org" },
761 JunkSelectors =
new List<string>
767 "//*[contains(@class,'document-toc-container')]",
768 "//*[contains(@class,'on-github')]"
770 MainContentCandidates =
new List<string>
775 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing",
"SplitConsecutiveLinks",
"ResolveRelativeLinks" }
784 DomainPatterns =
new List<string> {
"bestbuy.com" },
785 JunkSelectors =
new List<string>
790 "//*[contains(@class,'sponsored')]",
791 "//*[contains(@class,'carousel')]",
792 "//*[contains(@class,'recommendation')]",
793 "//*[contains(@class,'similar-products') or contains(@class,'related-products')]",
794 "//*[contains(@class,'customers-ultimately-bought')]"
796 MainContentCandidates =
new List<string>
798 "//div[contains(@class,'shop-product-details')]",
799 "//div[@id='shop-content']",
800 "//div[contains(@class,'column-left')]",
812 generic, indeed, wikipedia, github, stackoverflow, amazon,
813 hackernews, redditOld, reddit, medium, substack,
814 ebay, linkedin, glassdoor, ziprecruiter, walmart,
815 reuters, apnews, zillow, redfin, mdn, bestbuy
One named set of LLM-digest cleanup rules: which nodes are junk, where the main content usually lives...
List< string > EnabledPasses
Names of the post-conversion markdown cleanup passes to run, e.g. "PriceGluing".
List< string > JunkSelectors
XPath expressions for nodes to strip before conversion (ads, nav, cookie banners, etc....
List< string > DomainPatterns
Substrings matched (case-insensitive) against the current page URL to decide whether this rule set ap...
string Name
Name of this rule set, e.g. "generic" or "indeed".
List< string > MainContentCandidates
XPath expressions tried in order to find the main content container.
string SitemapBaseUrl
Base URL used to resolve relative links when a page is treated as a sitemap. Empty means derive it fr...
Loads and saves the named DigestRuleSets used by HtmlToOptimizedMarkdown, stored as ....
static string CurrentSchemaVersion
Version stamped into the file, taken from the GPAL assembly version so the rules file records which b...
List< DigestRuleSet > RuleSets
GPAL-managed rule sets. Refreshed from Default on every load; edits here are not preserved.
DigestRuleSet SelectFor(string url, string ruleSetName=null)
Picks the rule set to use for url : an explicit ruleSetName wins if it exists, otherwise the first n...
static void Save(GPALFile file=null)
Writes a fresh config (GPAL defaults, empty user section) to the file as a starter for editing....
List< DigestRuleSet > UserRuleSets
User-owned rule sets. GPAL never modifies, removes, or re-adds these. Moving a GPAL rule set here "ad...
static DigestRulesConfig Default()
Built-in rule sets, used when ./LLMDigestRules.yaml does not exist. Call DigestRulesConfig....
static DigestRulesConfig Load(GPALFile file=null)
Loads the digest rules config, refreshing the GPAL-managed section from Default while preserving the ...
string Version
The GPAL version that last wrote the file.
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
static IAllowConverterInput Converter
New GPAL Convertor.
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...