diff --git a/Makefile b/Makefile index ad5baed..f6b2f52 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ - run: echo "Not yet complete" diff --git a/internal/adapters/input/cli.go b/internal/adapters/input/cli.go index 1c024d0..34ce5f9 100644 --- a/internal/adapters/input/cli.go +++ b/internal/adapters/input/cli.go @@ -7,11 +7,13 @@ import ( //Use the implementation for markdown and latex to generate latex CV from a tree dir of mardown document func GenerateCVFromMarkDownToLatex(root string)error{ - var source core.Source = &MarkdownSource{} - var paramsSource core.SourceParams = &YamlSource{} - var templateReader core.TemplateReader = &output.LatexReader{} - var templateProccesor core.TemplateProcessor = &output.LatexProccesor{} - var compiler core.Compiler = &output.LatexCompiler{} - service := &core.CVService{} - return service.GenerateTemplates(root,source, paramsSource,templateReader,templateProccesor,compiler) + var builder core.BuilderService = core.BuilderService{} + builder.SetRoot(root) + builder.SetSource(&MarkdownSource{}) + builder.SetParamsSource(&YamlSource{}) + builder.SetTemplateReader(&output.LatexReader{}) + builder.SetTemplateProcessor(&output.LatexProccesor{}) + builder.SetCompiler(&output.LatexCompiler{}) + service := builder.GetService() + return service.GenerateTemplates() } diff --git a/internal/adapters/output/compiler.go b/internal/adapters/output/compiler.go index ff06e7b..8e850df 100644 --- a/internal/adapters/output/compiler.go +++ b/internal/adapters/output/compiler.go @@ -5,37 +5,55 @@ import ( "log/slog" "os" "os/exec" + "regexp" + "strconv" "strings" "sync" ) const COMPILER = "pdflatex" +var REGEX = regexp.MustCompile(`(\d+) page`) type LatexCompiler struct{} -//Compile the template into PDF -func (*LatexCompiler) CompileTemplate(root string)error{ +//Compile the templates into PDFs and return the number of page of the longest one +func (*LatexCompiler) CompileTemplate(root string)(int,error){ templates,err := getListOfTemplate(root);if err != nil { - return err + return 0,err } + max_nb_of_page := 0 + page_nb := make(chan int, len(templates)) var wg sync.WaitGroup for _,template := range templates{ wg.Add(1) - go compile(template,root,&wg) + go compile(template,root,&wg, page_nb) + pdf_pages_nb := <-page_nb + slog.Info("Number of pages", "pages", pdf_pages_nb) + max_nb_of_page = max(pdf_pages_nb,max_nb_of_page) } + close(page_nb) wg.Wait() - return nil + return max_nb_of_page,nil } //Compile the template into a pdf -func compile(template string, root string, wg* sync.WaitGroup){ - defer wg.Done() - cmd := exec.Command(COMPILER,"-interaction=nonstopmode", - "-output-directory="+root+"/assets/latex/output", template ) - log, err := cmd.Output(); if err != nil { - slog.Info("---FROM pdflatex ---\n"+string(log)) - slog.Error("failed to compile file:"+template) +func compile(template string, root string, wg* sync.WaitGroup, c_page_nb chan int){ + defer wg.Done() + cmd := exec.Command(COMPILER,"-interaction=nonstopmode", + "-output-directory="+root+"/assets/latex/output", template ) + log, err := cmd.Output(); if err != nil { + slog.Warn("error(s) to compile file:"+template) } + page_nb := -1 + log_page := REGEX.FindStringSubmatch(string(log)) + if len(log_page)<1{ + slog.Error("failed to compile file didnt get the number of pages:"+template) + fmt.Println(log_page) + }else{ + page_nb,err = strconv.Atoi(log_page[1]);if err != nil{ + slog.Error(err.Error())} + } + c_page_nb <- page_nb } //Return the path of latex file inside the template directory diff --git a/internal/core/generate.go b/internal/core/generate.go index f33e928..09585cc 100644 --- a/internal/core/generate.go +++ b/internal/core/generate.go @@ -2,36 +2,80 @@ package core import ( "log/slog" + "fmt" "sort" "strings" ) +const THESHOLD_PAGEOVERFLOW = 1 -type CVService struct{} +type CVService struct{ + root string + source Source + paramsSource SourceParams + templateReader TemplateReader + templateProcessor TemplateProcessor + compiler Compiler +} + +type BuilderService struct{ + root string + source Source + paramsSource SourceParams + templateReader TemplateReader + templateProcessor TemplateProcessor + compiler Compiler +} -//generate the template for the cvs defined in the assets directory -func (g *CVService) GenerateTemplates(root string, source Source, paramsSource SourceParams, - templateReader TemplateReader, templateProcessor TemplateProcessor, compiler Compiler)error{ +//generate the templates for the cvs defined in the assets directory +func (s *CVService) GenerateTemplates() error { slog.Info("--Generating CVs--") - cvs,err := source.GetCVsFrom(root);if err != nil{ return err } - params,err := paramsSource.GetParamsFrom(root);if err != nil{ return err } - generiqueTemplate,err := templateReader.ReadCVTemplate(root,params); if err != nil{ + cvs, err := s.source.GetCVsFrom(s.root); if err != nil { + return fmt.Errorf("failed to get CVs: %w", err) + } + params, err := s.paramsSource.GetParamsFrom(s.root); if err != nil { + return fmt.Errorf("failed to get parameters: %w", err) + } + generiqueTemplate, err := s.templateReader.ReadCVTemplate(s.root, params); if err != nil { + return fmt.Errorf("failed to read generic template: %w", err) + } + if err := s.generateAllCVs(cvs, params, generiqueTemplate, false); err != nil { return err } + return s.compileWithOverflowHandling(cvs, params, generiqueTemplate) +} - for _, cv := range cvs { - err = generateCVFrom(cv,params,root,generiqueTemplate,templateProcessor) - if err != nil{ +//Compile the CV into PDF, and if they are too long regenrate theme with one less section +func (s *CVService) compileWithOverflowHandling(cvs []CV, params Params, template string) error { + maxNbPage, err := s.compiler.CompileTemplate(s.root) + if err != nil { + return fmt.Errorf("failed to compile template: %w", err) + } + for maxNbPage > THESHOLD_PAGEOVERFLOW { + slog.Info("Page overflow detected; adjusting layout and regenerating CVs") + if err := s.generateAllCVs(cvs, params, template, true); err != nil { return err } + maxNbPage, err = s.compiler.CompileTemplate(s.root) + if err != nil { + return fmt.Errorf("failed to recompile template after adjustment: %w", err) + } } - - err = compiler.CompileTemplate(root); if err != nil{ return err } return nil } +//Use the the slice of CV to write in the output directory the new CV template +func (s *CVService) generateAllCVs(cvs []CV, params Params, template string, adjustLayout bool) error { + for _, cv := range cvs { + if err := generateCVFrom(cv, params, s.root, template, s.templateProcessor, adjustLayout); err != nil { + return fmt.Errorf("failed to generate CV: %w", err) + } + } + return nil +} +//generate a template for the cv with the given params func generateCVFrom(cv CV, params Params, root string, - template string, processor TemplateProcessor)(error){ + template string, processor TemplateProcessor, shouldBeShorter bool)(error){ var err error if len(params.Variante)==0{//if no variante create simple CV params.Variante = map[string][]string{"simple": nil} @@ -40,6 +84,9 @@ func generateCVFrom(cv CV, params Params, root string, cvName := "CV-"+cv.Lang+"-"+vari+".tex" slog.Info("Generating for:"+cvName) cvTemplate := template + if shouldBeShorter { + removeLowestSection(&cv,keywords) + } for _, section := range cv.Sections { for _, paragraph := range section.Paragraphes { headers := []string{ paragraph.H1, paragraph.H2, @@ -57,6 +104,57 @@ func generateCVFrom(cv CV, params Params, root string, return nil } +//Sort items and after remove the last items +func removeLowestSection(cv *CV, keywords []string){ + if len(cv.Sections)== 0{ return } + l_i := getLowestSection(*cv,keywords) + cv.Sections = append(cv.Sections[:l_i], cv.Sections[l_i+1:]...) +} + +// Return the index of the section from the CV with the lowest score +func getLowestSection(cv CV, keywords []string) int { + return getLowestIndex(cv.Sections, keywords, getScoreSection) +} + +// Return the index of the paragraph from the section with the lowest score +func getLowestParagraphe(section Section, keywords []string) int { + return getLowestIndex(section.Paragraphes, keywords, getScoreParagraphe) +} + +//generic function to get the index of the element with the lowest score +//TODO ? an interface to avoid the any ? +func getLowestIndex[T any](items []T, keywords []string, getScore func(T, []string) int) int { + min_score := getScore(items[0], keywords) + min_idx := 0 + for idx, item := range items[1:] { + current_score := getScore(item, keywords) + if current_score < min_score { + min_idx = idx + 1 + min_score = current_score + } + } + return min_idx +} + +//Get items of a section and sum they score to get global score of the items +func getScoreSection (section Section, keywords []string)int{ + res := 0 + for _, paragraph := range section.Paragraphes { + res+=getScoreParagraphe(paragraph,keywords) + } + return res +} + +//Get items of a single paragraph and sum they score to get global score of the items +func getScoreParagraphe (paragraph Paragraphe, keywords []string)int{ + res := 0 + for _,item := range paragraph.Items{ + res += getScore(item,keywords) + } + return res +} + + //Sorte a slice of items by the number of keyword // //The sort is done in ascending order as the section append work like a stack(Lifo) @@ -74,3 +172,38 @@ func getScore(item string, keywords []string)int{ } return score } + +func (cv *BuilderService) SetRoot(root string) { + cv.root = root +} + +func (cv *BuilderService) SetSource(source Source) { + cv.source = source +} + +func (cv *BuilderService) SetParamsSource(paramsSource SourceParams) { + cv.paramsSource = paramsSource +} + +func (cv *BuilderService) SetTemplateReader(templateReader TemplateReader) { + cv.templateReader = templateReader +} + +func (cv *BuilderService) SetTemplateProcessor(templateProcessor TemplateProcessor) { + cv.templateProcessor = templateProcessor +} + +func (cv *BuilderService) SetCompiler(compiler Compiler) { + cv.compiler = compiler +} + +func (s *BuilderService) GetService() CVService { + return CVService{ + root: s.root, + source: s.source, + paramsSource: s.paramsSource, + templateReader: s.templateReader, + templateProcessor: s.templateProcessor, + compiler: s.compiler, + } +} diff --git a/internal/core/generate_test.go b/internal/core/generate_test.go index 4bc2d1c..a1a61d2 100644 --- a/internal/core/generate_test.go +++ b/internal/core/generate_test.go @@ -16,7 +16,7 @@ func (s *MockParamsSource) GetParamsFrom(root string) (Params, error) { } type MockCompiler struct {} -func (c *MockCompiler) CompileTemplate(root string) error { return nil } +func (c *MockCompiler) CompileTemplate(root string) (int,error) { return 0,nil } type MockSource struct { CVs []CV @@ -110,8 +110,19 @@ func TestGenerateTemplates(t *testing.T) { templateReader := &MockTemplateReader{ Template: baseTemplate } templateProcessor := &MockTemplateProcessor{ GeneratedFiles: make(map[string]string) } compiler := &MockCompiler{} - service := CVService{} - err := service.GenerateTemplates(root, source, paramsSource, templateReader, templateProcessor, compiler ) + var builder BuilderService + + builder.SetRoot(root) + builder.SetSource(source) + builder.SetParamsSource(paramsSource) + builder.SetTemplateReader(templateReader) + builder.SetTemplateProcessor(templateProcessor) + builder.SetCompiler(compiler) + + service := builder.GetService() + + err := service.GenerateTemplates() + if err != nil { t.Fatalf("expected no error, got %v", err) } if len(templateProcessor.GeneratedFiles) != 2 { t.Fatalf("expected 2 generated file, got %d", len(templateProcessor.GeneratedFiles)) } @@ -131,8 +142,17 @@ func TestGenerateTemplates(t *testing.T) { templateReader := &MockTemplateReader{ Template: baseTemplate } templateProcessor := &MockTemplateProcessor{ GeneratedFiles: make(map[string]string) } compiler := &MockCompiler{} - service := CVService{} - err := service.GenerateTemplates(root, source, paramsSource, templateReader, templateProcessor, compiler) + var builder BuilderService + + builder.SetRoot(root) + builder.SetSource(source) + builder.SetParamsSource(paramsSource) + builder.SetTemplateReader(templateReader) + builder.SetTemplateProcessor(templateProcessor) + builder.SetCompiler(compiler) + + service := builder.GetService() + err := service.GenerateTemplates() if err != nil { t.Fatalf("expected no error, got %v", err) } if len(templateProcessor.GeneratedFiles) != 1 { t.Fatalf("expected 1 generated file, got %d", len(templateProcessor.GeneratedFiles)) } @@ -207,3 +227,150 @@ func TestSortByScore(t *testing.T) { } } } + + +func TestGetLowestSection(t *testing.T) { + keywords := []string{"experience", "skills", "projects"} + tests := []struct { + cv CV + expected int + }{ + { + cv: CV{ + Lang: "en", + Sections: []Section{ + { + Title: "Work Experience", + Paragraphes: []Paragraphe{ + {H1: "Experience 1", Items: []string{"experience", "project"}}, + }, + }, + { + Title: "Education", + Paragraphes: []Paragraphe{ + {H1: "Education 1", Items: []string{"education", "degree"}}, + }, + }, + { + Title: "Skills", + Paragraphes: []Paragraphe{ + {H1: "Skills", Items: []string{"skills", "knowledge"}}, + }, + }, + }, + }, + expected: 1, + }, + { + cv: CV{ + Lang: "en", + Sections: []Section{ + { + Title: "Skills", + Paragraphes: []Paragraphe{ + {H1: "Technical Skills", Items: []string{"skills", "knowledge"}}, + }, + }, + { + Title: "Projects", + Paragraphes: []Paragraphe{ + {H1: "Project 1", Items: []string{"project", "skill"}}, + }, + }, + { + Title: "Summary", + Paragraphes: []Paragraphe{ + {H1: "Personal Summary", Items: []string{"summary"}}, + }, + }, + }, + }, + expected: 1, + }, + { + cv: CV{ + Lang: "en", + Sections: []Section{ + { + Title: "Summary", + Paragraphes: []Paragraphe{ + {H1: "Summary", Items: []string{"introduction"}}, + }, + }, + { + Title: "Skills", + Paragraphes: []Paragraphe{ + {H1: "Skills", Items: []string{"skills"}}, + }, + }, + }, + }, + expected: 0, + }, + } + + for _, tt := range tests { + result := getLowestSection(tt.cv, keywords) + if result != tt.expected { + t.Errorf("expected index %v but got %v", tt.expected, result) + } + } +} + +func TestGetLowestParagraphe(t *testing.T) { + keywords := []string{"skills", "experience", "project"} + tests := []struct { + section Section + expected int + }{ + { + section: Section{ + Title: "Work Experience", + Paragraphes: []Paragraphe{ + {H1: "Experience 1", Items: []string{"skills", "project"}}, + {H1: "Experience 2", Items: []string{"experience", "skills"}}, + {H1: "Experience 3", Items: []string{"project"}}, + }, + }, + expected: 2, + }, + { + section: Section{ + Title: "Projects", + Paragraphes: []Paragraphe{ + {H1: "Project A", Items: []string{"project", "skills"}}, + {H1: "Project B", Items: []string{"skills", "experience"}}, + {H1: "Project C", Items: []string{"project", "experience"}}, + }, + }, + expected: 0, + }, + { + section: Section{ + Title: "Skills", + Paragraphes: []Paragraphe{ + {H1: "Skillset 1", Items: []string{"skills", "tools", "project"}}, + {H1: "Skillset 2", Items: []string{"experience", "skills"}}, + }, + }, + expected: 0, + }, + { + section: Section{ + Title: "Summary", + Paragraphes: []Paragraphe{ + {H1: "Summary", Items: []string{}}, + {H1: "Overview", Items: []string{}}, + }, + }, + expected: 0, + }, + } + + for _, tt := range tests { + result := getLowestParagraphe(tt.section, keywords) + if result != tt.expected { + t.Errorf("expected index %v but got %v", tt.expected, result) + } + } +} diff --git a/internal/core/ports.go b/internal/core/ports.go index 8ceef1f..993eaeb 100644 --- a/internal/core/ports.go +++ b/internal/core/ports.go @@ -10,7 +10,7 @@ type ICVservice interface { } type Compiler interface { - CompileTemplate(root string) error + CompileTemplate(root string) (int,error) } type SourceParams interface {