16bb195cb5
- Update CLAUDE.md: replace incorrect 'no XML doc on internal code' rule with the correct convention (XML doc on all public methods and non-trivial private/protected helpers) - Restore /// <summary> on FileDownloadController private helpers (HandleRangeRequest, StreamRangeAsync) - Add full XML doc to all service contracts: ICaptchaVerifier, IEmailSender, ICvMatcherService, IJobTextExtractor, IJobTokenService, IDocumentClassifier, IRagService, ITextChunker, ITextExtractor, IEmailTemplateService, ITemplateService - Add /// <summary> and /// <inheritdoc /> to all concrete service classes and their methods: RecaptchaVerifier, EmailApiEmailSender, SmtpEmailDispatcher, CvMatcherService, JobTextExtractor, JobTokenService, RagService, DocumentClassifier, TextChunker, TextExtractor, HtmlJobSearcher, CvSearchEmailSender, CvSearchJobTask, EmailTemplateService, DbTemplateService Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
59 lines
2.2 KiB
C#
59 lines
2.2 KiB
C#
using System.Net;
|
|
using System.Text.RegularExpressions;
|
|
using CvMatcher.Models.Settings;
|
|
using Api.Services.Contracts;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Api.Services;
|
|
|
|
/// <summary>
|
|
/// Extracts normalised plain text from a job posting, either from a pasted description or by
|
|
/// fetching and stripping the HTML of the job page URL.
|
|
/// </summary>
|
|
public sealed class JobTextExtractor : IJobTextExtractor
|
|
{
|
|
private readonly HttpClient _http;
|
|
private readonly MatcherSettings _settings;
|
|
|
|
public JobTextExtractor(HttpClient http, IOptions<MatcherSettings> options)
|
|
{
|
|
_http = http;
|
|
_settings = options.Value;
|
|
_http.Timeout = TimeSpan.FromSeconds(25);
|
|
_http.DefaultRequestHeaders.UserAgent.ParseAdd("MyAi.ro CV Matcher/1.0");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<string> ExtractAsync(string? jobUrl, string? jobDescription, CancellationToken ct)
|
|
{
|
|
var pasted = Normalize(jobDescription ?? string.Empty);
|
|
if (!string.IsNullOrWhiteSpace(pasted)) return Limit(pasted);
|
|
|
|
if (string.IsNullOrWhiteSpace(jobUrl)) return string.Empty;
|
|
if (!Uri.TryCreate(jobUrl, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https"))
|
|
{
|
|
throw new InvalidOperationException("Invalid job URL.");
|
|
}
|
|
|
|
var html = await _http.GetStringAsync(uri, ct);
|
|
html = Regex.Replace(html, "<script[\\s\\S]*?</script>", " ", RegexOptions.IgnoreCase);
|
|
html = Regex.Replace(html, "<style[\\s\\S]*?</style>", " ", RegexOptions.IgnoreCase);
|
|
html = Regex.Replace(html, "<[^>]+>", " ");
|
|
return Limit(Normalize(WebUtility.HtmlDecode(html)));
|
|
}
|
|
|
|
/// <summary>Truncates text to the configured maximum character count.</summary>
|
|
private string Limit(string value)
|
|
{
|
|
var max = Math.Max(4000, _settings.MaxJobTextChars);
|
|
return value.Length <= max ? value : value[..max];
|
|
}
|
|
|
|
/// <summary>Collapses all whitespace runs to single spaces and trims the result.</summary>
|
|
private static string Normalize(string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
|
return string.Join(' ', value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).Trim();
|
|
}
|
|
}
|