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))
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>();
144 BackupLegacyFile(ConfigFilePath);
153 var userNames =
new HashSet<string>(
154 loaded.
UserRuleSets.Select(r => r?.Name).Where(n =>
false ==
string.IsNullOrWhiteSpace(n)),
155 StringComparer.OrdinalIgnoreCase);
157 loaded.RuleSets =
Default().
RuleSets.Where(r =>
false == userNames.Contains(r.Name)).ToList();
172 if (file !=
null) ConfigFilePath = file.Filename;
173 WriteConfigFile(
Default(), ConfigFilePath);
180 WriteConfigFile(cfg, ConfigFilePath);
184 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not write LLM digest rules file",
null, GPALObjectType.None, ex);
193 private static void WriteConfigFile(DigestRulesConfig cfg,
string path)
197 string tmp = path +
".gpaltmp.yaml";
198 GPAL.Converter.WithInput(cfg).SaveTo((GPALFile)GPAL.File.WithFileName(tmp));
200 string yaml = File.ReadAllText(tmp);
201 try { File.Delete(tmp); }
catch { }
204 yaml = Regex.Replace(yaml,
@"(?m)^ruleSets:", GpalBanner + Environment.NewLine +
"ruleSets:");
205 yaml = Regex.Replace(yaml,
@"(?m)^userRuleSets:", Environment.NewLine + UserBanner + Environment.NewLine +
"userRuleSets:");
207 string full = FileHeader + yaml;
209 if (
false == File.Exists(path) || File.ReadAllText(path) != full)
210 File.WriteAllText(path, full);
213 private static void BackupLegacyFile(
string path)
217 string backup = path +
".v0.bak";
219 while (File.Exists(backup))
220 backup = $
"{path}.v0.{n++}.bak";
222 File.Copy(path, backup);
223 GPAL.PublishSimpleEvent(GPALEventType.INFO,
224 $
"Migrated legacy LLM digest rules to versioned format. Old file backed up to [{backup}].",
225 null, GPALObjectType.None);
229 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
230 $
"Could not back up legacy LLM digest rules file",
null, GPALObjectType.None, ex);
243 var gpal =
RuleSets ??
new List<DigestRuleSet>();
244 var all = user.Concat(gpal).ToList();
246 var
generic = user.FirstOrDefault(r =>
"generic" == r.Name)
247 ?? gpal.FirstOrDefault(r =>
"generic" == r.Name)
252 if (
false ==
string.IsNullOrWhiteSpace(ruleSetName))
253 selected = all.FirstOrDefault(r =>
string.Equals(r.Name, ruleSetName, StringComparison.OrdinalIgnoreCase));
255 if (
null == selected &&
false ==
string.IsNullOrWhiteSpace(url))
256 selected = all.FirstOrDefault(r =>
257 "generic" != r.Name &&
258 null != r.DomainPatterns &&
259 r.DomainPatterns.Any(p => 0 <= url.IndexOf(p, StringComparison.OrdinalIgnoreCase)));
261 if (
null == selected || selected ==
generic)
266 Name = selected.
Name,
268 JunkSelectors = 0 < selected.JunkSelectors.Count ? selected.JunkSelectors :
generic.JunkSelectors,
269 MainContentCandidates = 0 < selected.MainContentCandidates.Count ? selected.MainContentCandidates :
generic.MainContentCandidates,
270 SitemapBaseUrl =
false ==
string.IsNullOrWhiteSpace(selected.
SitemapBaseUrl) ? selected.SitemapBaseUrl :
generic.SitemapBaseUrl,
271 EnabledPasses = 0 < selected.EnabledPasses.Count ? selected.EnabledPasses : generic.EnabledPasses
284 JunkSelectors =
new List<string>
291 "//*[contains(@class,'cookie') or contains(@class,'consent') or contains(@class,'gdpr') or contains(@id,'cookie')]",
295 "//header[not(ancestor::main or contains(@class,'hero') or contains(@class,'main') or contains(@class,'content'))]",
298 "//*[contains(@role,'navigation') or contains(@role,'banner') and not(ancestor::main)]",
301 "//*[contains(@class,'ad') or contains(@class,'advert') or contains(@class,'popup') or contains(@class,'modal') or contains(@class,'skip') or contains(@id,'skip') or contains(text(),'Skip to content')]",
304 "//*[contains(@class,'social') or contains(@class,'share') or contains(@class,'follow')]",
308 "//*[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')]",
311 "//*[contains(@class,'accessibility') or contains(@class,'legal') or contains(text(),'©') and count(.//a) > 10]",
314 "//*[contains(@class, 'sidebar') or contains(@class, 'widget') or contains(@class, 'social') or contains(@class, 'popup') or contains(@role, 'navigation')]",
317 "//a[string-length(normalize-space(.)) < 15 and (contains(.,'next') or contains(.,'prev') or contains(.,'›') or contains(.,'«'))]"
319 MainContentCandidates =
new List<string>
322 "//*[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')]",
326 EnabledPasses =
new List<string>
328 "CollapseExcessiveBlanks",
329 "RemoveCountersAndEmptyIcons",
334 "RejoinLinkedImages",
335 "DedupeConsecutiveImages",
336 "ProductImageBrandPrice",
339 "StripEscapeArtifacts",
349 DomainPatterns =
new List<string> {
"indeed.com" },
350 JunkSelectors =
new List<string>
352 "//div[contains(@class,'gnav')]",
353 "//div[contains(@class,'footer')]",
354 "//div[contains(@id,'popover')]",
355 "//div[contains(@class,'jobsearch-HiringInsights')]",
356 "//div[contains(@class,'jobsearch-CompanyInfoWithReview')]",
358 MainContentCandidates =
new List<string>
360 "//div[@id='jobDescriptionText']",
361 "//div[contains(@class,'jobsearch-JobComponent')]",
362 "//div[contains(@class,'jobsearch-ResultsList')]"
364 SitemapBaseUrl =
"https://www.indeed.com",
365 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
372 DomainPatterns =
new List<string> {
"wikipedia.org" },
373 JunkSelectors =
new List<string>
375 "//div[@id='mw-navigation']",
376 "//div[@id='mw-head-base']",
377 "//div[@id='mw-page-base']",
378 "//div[@id='catlinks']",
379 "//div[@id='footer']",
380 "//*[contains(@class,'navbox')]",
381 "//*[contains(@class,'mw-editsection')]",
382 "//*[contains(@class,'reflist')]",
383 "//*[contains(@class,'sistersitebox')]",
384 "//*[contains(@class,'noprint')]"
386 MainContentCandidates =
new List<string>
388 "//div[@id='mw-content-text']"
390 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
397 DomainPatterns =
new List<string> {
"github.com" },
398 JunkSelectors =
new List<string>
402 "//*[contains(@class,'file-navigation')]",
403 "//*[contains(@class,'js-flash-container')]",
404 "//*[contains(@class,'signup-prompt')]",
405 "//*[contains(@class,'repository-topics')]",
406 "//*[contains(@class,'BorderGrid-cell') and contains(@class,'hide-sm')]"
408 MainContentCandidates =
new List<string>
410 "//*[contains(@class,'markdown-body')]",
411 "//div[@id='readme']",
414 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
420 Name =
"stackoverflow",
421 DomainPatterns =
new List<string> {
"stackoverflow.com",
"stackexchange.com" },
422 JunkSelectors =
new List<string>
424 "//div[@id='left-sidebar']",
425 "//div[@id='sidebar']",
426 "//*[contains(@class,'s-topbar')]",
427 "//*[contains(@class,'post-menu')]",
428 "//*[contains(@class,'s-sidebarwidget')]",
429 "//*[contains(@class,'js-consent-banner')]",
430 "//div[@id='feed-link']",
431 "//div[@id='hot-network-questions')]"
433 MainContentCandidates =
new List<string>
435 "//div[@id='question']",
436 "//div[@id='content']"
438 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
445 DomainPatterns =
new List<string> {
"amazon.com" },
446 JunkSelectors =
new List<string>
448 "//div[@id='nav-belt']",
449 "//div[@id='navFooter']",
450 "//div[@id='nav-subnav']",
451 "//div[@id='HeroQuickPromo']",
453 "//*[contains(@id,'carousel')]",
454 "//*[contains(@class,'a-carousel-row')]",
455 "//div[@id='sims-consolidated-1']",
456 "//div[@id='sims-consolidated-2']"
458 MainContentCandidates =
new List<string>
461 "//div[@id='centerCol']"
469 DomainPatterns =
new List<string> {
"news.ycombinator.com" },
470 JunkSelectors =
new List<string>
472 "//td[@bgcolor='#ff6600']",
473 "//span[@class='yclinks']",
474 "//tr[@class='spacer']"
476 MainContentCandidates =
new List<string>
478 "//table[@class='comment-tree']",
479 "//table[@class='fatitem']",
480 "//table[@class='itemlist']"
482 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
489 DomainPatterns =
new List<string> {
"old.reddit.com" },
490 JunkSelectors =
new List<string>
492 "//div[@id='header']",
493 "//div[@id='footer']",
494 "//div[@class='side']",
495 "//div[@id='sr-header-area']"
497 MainContentCandidates =
new List<string>
499 "//div[@class='commentarea']",
500 "//div[@id='siteTable']",
501 "//div[@id='content']"
503 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks" }
510 DomainPatterns =
new List<string> {
"reddit.com" },
511 JunkSelectors =
new List<string>
517 MainContentCandidates =
new List<string>
520 "//div[@id='main-content']",
523 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks" }
530 DomainPatterns =
new List<string> {
"medium.com" },
531 JunkSelectors =
new List<string>
536 "//*[contains(@class,'pw-responses')]",
537 "//*[contains(@class,'pw-post-tags')]"
539 MainContentCandidates =
new List<string>
543 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
550 DomainPatterns =
new List<string> {
"substack.com" },
551 JunkSelectors =
new List<string>
555 "//*[contains(@class,'subscription-widget')]",
556 "//*[contains(@class,'paywall')]",
557 "//*[contains(@class,'post-upsell')]"
559 MainContentCandidates =
new List<string>
561 "//div[contains(@class,'available-content')]",
564 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
571 DomainPatterns =
new List<string> {
"ebay.com" },
572 JunkSelectors =
new List<string>
574 "//div[@id='gh-oa']",
575 "//div[@id='glbfooter']",
576 "//div[@id='vi-recentlyViewed']",
577 "//*[contains(@id,'vi-recsalso')]"
579 MainContentCandidates =
new List<string>
581 "//div[@id='mainContent']",
582 "//div[@id='vi-desc']"
590 DomainPatterns =
new List<string> {
"linkedin.com" },
591 JunkSelectors =
new List<string>
596 "//*[contains(@class,'similar-jobs')]",
597 "//*[contains(@class,'jobs-premium-upsell')]"
599 MainContentCandidates =
new List<string>
601 "//div[contains(@class,'jobs-description')]",
602 "//section[contains(@class,'description')]",
605 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
612 DomainPatterns =
new List<string> {
"glassdoor.com" },
613 JunkSelectors =
new List<string>
617 "//*[contains(@class,'LoginModal') or contains(@id,'LoginModal')]",
618 "//*[contains(@class,'HardsellOverlay')]"
620 MainContentCandidates =
new List<string>
622 "//div[@id='JobDescriptionContainer']",
623 "//div[@id='job-desc']",
626 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
632 Name =
"ziprecruiter",
633 DomainPatterns =
new List<string> {
"ziprecruiter.com" },
634 JunkSelectors =
new List<string>
638 "//*[contains(@class,'suggested_jobs')]",
639 "//*[contains(@class,'email_signup')]"
641 MainContentCandidates =
new List<string>
643 "//div[contains(@class,'job_description')]",
644 "//div[@id='job_desc']",
647 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
654 DomainPatterns =
new List<string> {
"walmart.com" },
655 JunkSelectors =
new List<string>
659 "//*[contains(@class,'RecommendedItems')]",
660 "//*[contains(@id,'recommendations')]"
662 MainContentCandidates =
new List<string>
664 "//div[@id='maincontent']",
673 DomainPatterns =
new List<string> {
"reuters.com" },
674 JunkSelectors =
new List<string>
678 "//*[contains(@class,'related-articles')]",
679 "//*[contains(@class,'newsletter-signup')]"
681 MainContentCandidates =
new List<string>
683 "//*[@data-testid='article-body']",
686 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
693 DomainPatterns =
new List<string> {
"apnews.com" },
694 JunkSelectors =
new List<string>
698 "//*[contains(@class,'RelatedStories')]",
699 "//*[contains(@class,'Advertisement')]"
701 MainContentCandidates =
new List<string>
703 "//div[contains(@class,'ArticleBody')]",
706 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
713 DomainPatterns =
new List<string> {
"zillow.com" },
714 JunkSelectors =
new List<string>
718 "//*[contains(@class,'zsg-modal')]",
719 "//*[contains(@id,'contact-form')]"
721 MainContentCandidates =
new List<string>
723 "//div[contains(@class,'hdp')]",
726 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
733 DomainPatterns =
new List<string> {
"redfin.com" },
734 JunkSelectors =
new List<string>
738 "//*[contains(@class,'ContactAgent')]",
739 "//*[contains(@class,'similar-homes')]"
741 MainContentCandidates =
new List<string>
743 "//div[@id='content']",
744 "//div[contains(@class,'home-main-stats')]",
747 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
754 DomainPatterns =
new List<string> {
"developer.mozilla.org" },
755 JunkSelectors =
new List<string>
761 "//*[contains(@class,'document-toc-container')]",
762 "//*[contains(@class,'on-github')]"
764 MainContentCandidates =
new List<string>
769 EnabledPasses =
new List<string> {
"CollapseExcessiveBlanks",
"HeadingSpacing" }
778 DomainPatterns =
new List<string> {
"bestbuy.com" },
779 JunkSelectors =
new List<string>
784 "//*[contains(@class,'sponsored')]",
785 "//*[contains(@class,'carousel')]",
786 "//*[contains(@class,'recommendation')]",
787 "//*[contains(@class,'similar-products') or contains(@class,'related-products')]",
788 "//*[contains(@class,'customers-ultimately-bought')]"
790 MainContentCandidates =
new List<string>
792 "//div[contains(@class,'shop-product-details')]",
793 "//div[@id='shop-content']",
794 "//div[contains(@class,'column-left')]",
806 generic, indeed, wikipedia, github, stackoverflow, amazon,
807 hackernews, redditOld, reddit, medium, substack,
808 ebay, linkedin, glassdoor, ziprecruiter, walmart,
809 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
Version of the loaded/saved file. Null or empty means a pre-versioning (legacy) 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...
static IAllowFileName File
Instantiates a new fluent File SETTINGS object. This data object defines settings for use by the ....