package docx import ( "regexp" "sort" "strings" ) // StyleProfile represents the writing style characteristics of a resume. type StyleProfile struct { ActionVerbs []string // top 20 most frequent action verbs from bullet starts SummaryStyle string // first 2 sentences from any "Summary" or "Profile" section BulletPatterns []string // first 5 words of each bullet (up to 30 bullets) } // StyleExtractor analyzes parsed resume markdown text. type StyleExtractor struct { markdown string } // NewStyleExtractor creates a new style extractor for the given markdown. func NewStyleExtractor(markdown string) *StyleExtractor { return &StyleExtractor{markdown: markdown} } // Extract analyzes the markdown and returns a StyleProfile. func (s *StyleExtractor) Extract() StyleProfile { return StyleProfile{ ActionVerbs: s.extractActionVerbs(), SummaryStyle: s.extractSummary(), BulletPatterns: s.extractBulletPatterns(), } } // extractActionVerbs finds lines starting with bullet points, extracts the first word // (action verb), deduplicates, and returns the top 20 by frequency. func (s *StyleExtractor) extractActionVerbs() []string { lines := strings.Split(s.markdown, "\n") verbCount := make(map[string]int) for _, line := range lines { line = strings.TrimSpace(line) // Match lines starting with "- " or "* " if strings.HasPrefix(line, "- ") { line = strings.TrimPrefix(line, "- ") } else if strings.HasPrefix(line, "* ") { line = strings.TrimPrefix(line, "* ") } else { continue } // Extract first word fields := strings.Fields(line) if len(fields) > 0 { verb := strings.ToLower(fields[0]) verbCount[verb]++ } } // Sort by frequency (descending) and then alphabetically for stability type verbFreq struct { verb string count int } var sorted []verbFreq for verb, count := range verbCount { sorted = append(sorted, verbFreq{verb, count}) } sort.Slice(sorted, func(i, j int) bool { if sorted[i].count != sorted[j].count { return sorted[i].count > sorted[j].count } return sorted[i].verb < sorted[j].verb }) var result []string for i, vf := range sorted { if i >= 20 { break } result = append(result, vf.verb) } return result } // extractSummary finds a heading matching Summary|Profile|About|Objective and // returns the next 2 non-empty lines. func (s *StyleExtractor) extractSummary() string { lines := strings.Split(s.markdown, "\n") headingRegex := regexp.MustCompile(`(?i)^\s*#+\s+(summary|profile|about|objective)\s*$`) for i, line := range lines { if headingRegex.MatchString(line) { // Collect next 2 non-empty lines var summaryLines []string for j := i + 1; j < len(lines) && len(summaryLines) < 2; j++ { trimmed := strings.TrimSpace(lines[j]) if trimmed != "" && !strings.HasPrefix(trimmed, "#") { summaryLines = append(summaryLines, trimmed) } } if len(summaryLines) > 0 { return strings.Join(summaryLines, " ") } } } return "" } // extractBulletPatterns extracts the first 5 words of each bullet line (up to 30 bullets). func (s *StyleExtractor) extractBulletPatterns() []string { lines := strings.Split(s.markdown, "\n") var patterns []string for _, line := range lines { if len(patterns) >= 30 { break } line = strings.TrimSpace(line) // Match lines starting with "- " or "* " if strings.HasPrefix(line, "- ") { line = strings.TrimPrefix(line, "- ") } else if strings.HasPrefix(line, "* ") { line = strings.TrimPrefix(line, "* ") } else { continue } // Extract first 5 words fields := strings.Fields(line) if len(fields) > 5 { fields = fields[:5] } if len(fields) > 0 { pattern := strings.Join(fields, " ") patterns = append(patterns, pattern) } } return patterns }