Files
apex-public/internal/docx/style.go
T
2026-07-06 11:05:50 -04:00

142 lines
3.7 KiB
Go

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
}