From c30cde400c3d0b0b7a73a8814e391c72d51c8cee Mon Sep 17 00:00:00 2001 From: Theo-Hafsaoui Date: Sun, 24 Nov 2024 10:15:24 +0100 Subject: [PATCH] WIP formating --- Makefile | 3 + cmd/root.go | 59 +- cv/eng/professional.md | 21 + cv/eng/project.md | 4 +- internal/adapters/input/cli.go | 28 +- internal/adapters/input/input_test.go | 250 ++++---- .../adapters/input/markdown_tree_reader.go | 243 ++++---- internal/adapters/input/yaml.go | 22 +- internal/adapters/output/compiler.go | 108 ++-- internal/adapters/output/latex_test.go | 194 +++--- internal/adapters/output/latex_writer.go | 151 ++--- internal/adapters/output/template_sections.go | 3 +- internal/core/generate.go | 338 +++++----- internal/core/generate_test.go | 590 +++++++++--------- internal/core/ports.go | 113 ++-- main.go | 2 +- 16 files changed, 1105 insertions(+), 1024 deletions(-) diff --git a/Makefile b/Makefile index f6b2f52..aaf7cae 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,9 @@ build: lint: golangci-lint run ./... +fmt: + gofmt -s -w . + tidy: go mod tidy diff --git a/cmd/root.go b/cmd/root.go index 753b6a0..69cb3e6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,43 +1,42 @@ package cmd import ( - "os" - "github.com/spf13/cobra" "anemon/internal/adapters/input" + "github.com/spf13/cobra" + "os" ) var rootCmd = &cobra.Command{ - Use: "anemon", - Short: "A CV generator", - Long: `This CLI tool, automates the generation of customized CVs from Markdown files based on a specified configuration.`, - RunE: func(cmd *cobra.Command, args []string) error { - threshold, err := cmd.Flags().GetInt("threshold") - if err != nil { - return err - } - input.ChangeOverflowThreshold(threshold) + Use: "anemon", + Short: "A CV generator", + Long: `This CLI tool, automates the generation of customized CVs from Markdown files based on a specified configuration.`, + RunE: func(cmd *cobra.Command, args []string) error { + threshold, err := cmd.Flags().GetInt("threshold") + if err != nil { + return err + } + input.ChangeOverflowThreshold(threshold) - generate, err := cmd.Flags().GetBool("generate") - if err != nil { - return err - } - if generate { - root, err := os.Getwd() - if err != nil { - return err - } - return input.GenerateCVFromMarkDownToLatex(root) - } + generate, err := cmd.Flags().GetBool("generate") + if err != nil { + return err + } + if generate { + root, err := os.Getwd() + if err != nil { + return err + } + return input.GenerateCVFromMarkDownToLatex(root) + } - return nil - }, + return nil + }, } func Execute() { - rootCmd.Flags().IntP("threshold", "t", 1, "Set the page overflow threshold (default 1)") - rootCmd.Flags().BoolP("generate", "g", false, "Generate a CV") - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } + rootCmd.Flags().IntP("threshold", "t", 1, "Set the page overflow threshold (default 1)") + rootCmd.Flags().BoolP("generate", "g", false, "Generate a CV") + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } } - diff --git a/cv/eng/professional.md b/cv/eng/professional.md index f8228f4..a502656 100644 --- a/cv/eng/professional.md +++ b/cv/eng/professional.md @@ -16,3 +16,24 @@ - Set up a deployment pipeline, automating and improving the efficiency of software updates. - Optimized system performance, significantly reducing response times and improving user experience. +# Gigerrish +## June 2023 -- September 2023 +### DevSolutions +#### Internship +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/cv/eng/project.md b/cv/eng/project.md index f0a9b48..71f248e 100644 --- a/cv/eng/project.md +++ b/cv/eng/project.md @@ -1,4 +1,4 @@ -## Project A +# Project A ## Language A, Framework A, Database A, Version Control A ### [Link to Repository](https://github.com/ExampleUser/projectA) - Developed using Language A and Framework A, with Database A for data management. @@ -11,5 +11,3 @@ - Separated front-end and back-end using Containerization Tool B, with orchestration handled by Container Orchestration B. - Ensured secure communication between components via a RESTful API with API Security B tokens. - Collaborated within a team using agile methodologies, specifically SCRUM, and applied DevOps practices for streamlined development. - - diff --git a/internal/adapters/input/cli.go b/internal/adapters/input/cli.go index 8ec24dd..36e0d4b 100644 --- a/internal/adapters/input/cli.go +++ b/internal/adapters/input/cli.go @@ -5,20 +5,20 @@ import ( "anemon/internal/core" ) -//Use the implementation for markdown and latex to generate latex CV from a tree dir of mardown document -func GenerateCVFromMarkDownToLatex(root string)error{ - 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() +// Use the implementation for markdown and latex to generate latex CV from a tree dir of mardown document +func GenerateCVFromMarkDownToLatex(root string) error { + 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() } -//Change the threshold for the regeration of the PDF -func ChangeOverflowThreshold(newThreshold int){ - core.SetOverflowThreshold(newThreshold) +// Change the threshold for the regeration of the PDF +func ChangeOverflowThreshold(newThreshold int) { + core.SetOverflowThreshold(newThreshold) } diff --git a/internal/adapters/input/input_test.go b/internal/adapters/input/input_test.go index 18fd73b..399ed35 100644 --- a/internal/adapters/input/input_test.go +++ b/internal/adapters/input/input_test.go @@ -8,8 +8,8 @@ import ( "testing" ) -var( - yamlContent = ` +var ( + yamlContent = ` info: name: Doe firstname: John @@ -24,7 +24,7 @@ variante: optionB: - "value3" ` - work_input = ` + work_input = ` # Back-End Intern ## February 2024 -- August 2024 ### TechCorp @@ -41,31 +41,29 @@ variante: - Participated in the migration of core backend logic from a monolithic application to a microservice architecture, improving system performance and scalability. - Enhanced system monitoring and reliability by implementing tracing mechanisms and performance objectives.` - skill_input =` + skill_input = ` **Langage** - Langage A, Langage B, Langage C, Langage D, Langage E, Langage F, Langage G/H **Langage** - Langage A, Langage B, Langage C, Langage D, Langage E, Langage F, Langage G/H` - invalid_skill_input =` + invalid_skill_input = ` - Langage A, Langage B, Langage C, Langage D, Langage E, Langage F, Langage G/H **Langag - Langage A, Langage B, Langage C, Langage D, Langage E, Langage F, Langage G/H` + skill_paragraphe = core.Paragraphe{H1: "Langage", H2: "Langage A, Langage B, Langage C, Langage D, Langage E, Langage F, Langage G/H"} + work_paragraphe = core.Paragraphe{H1: "Back-End Intern", H2: "February 2024 -- August 2024", + H3: "TechCorp", H4: "Internship", Items: []string{ + "Assisted in developing and optimizing a key business process for expanding into new markets, collaborating with various teams to ensure compliance and smooth integration.", + "Participated in the migration of core backend logic from a monolithic application to a microservice architecture, improving system performance and scalability.", + "Enhanced system monitoring and reliability by implementing tracing mechanisms and performance objectives."}} - skill_paragraphe = core.Paragraphe{H1: "Langage", H2: "Langage A, Langage B, Langage C, Langage D, Langage E, Langage F, Langage G/H"} - - work_paragraphe = core.Paragraphe{H1: "Back-End Intern", H2: "February 2024 -- August 2024", - H3: "TechCorp", H4: "Internship",Items: []string{ - "Assisted in developing and optimizing a key business process for expanding into new markets, collaborating with various teams to ensure compliance and smooth integration.", - "Participated in the migration of core backend logic from a monolithic application to a microservice architecture, improving system performance and scalability.", - "Enhanced system monitoring and reliability by implementing tracing mechanisms and performance objectives."}} - - invalid_input = "ajsdlhsaeld##dafdbhkbhkjsd##" - work_expected_result = []core.Paragraphe{work_paragraphe,work_paragraphe} - skill_expected_result = []core.Paragraphe{skill_paragraphe,skill_paragraphe} + invalid_input = "ajsdlhsaeld##dafdbhkbhkjsd##" + work_expected_result = []core.Paragraphe{work_paragraphe, work_paragraphe} + skill_expected_result = []core.Paragraphe{skill_paragraphe, skill_paragraphe} paths = []struct { relativePath string @@ -79,48 +77,47 @@ variante: {"cv/fr/work.md", work_input}, {"cv/fr/skill.md", skill_input}, } - ) func TestParagraphe(t *testing.T) { - t.Run("Work Paragraphes should return a slice of valid Paragraphe", func (t *testing.T) { - got := getParagrapheFrom(work_input) - want := work_expected_result - if !reflect.DeepEqual(got[0], want[0]){ - t.Fatalf("the first Paragraphe should be :\n%s\n got :%s",want,got) - } - - if !reflect.DeepEqual(got[1], want[1]){ - t.Fatalf("the first Paragraphe should be :\n%s\n got :%s",want,got) - } - }) - - t.Run("Invalid input should return nothing", func (t *testing.T) { - result := getParagrapheFrom(invalid_input) - if result != nil{ - t.Fatalf("Invalid input should return nil got %v",result) - } - }) - - t.Run("Skill Paragraphe should be return from valid input", func (t *testing.T) { - got := getParagrapheFrom(skill_input) - want := skill_expected_result - if !reflect.DeepEqual(got,want){ - t.Fatalf("the first Paragraphe should be :\n%s\n got :%s",want,got) - } - }) - - t.Run("Invalid skill Paragraphe should return nil", func (t *testing.T) { - got := getParagrapheFrom(invalid_skill_input) - if got != nil{ - t.Fatalf("Invalid input should return nil got %v",got) - } - }) + t.Run("Work Paragraphes should return a slice of valid Paragraphe", func(t *testing.T) { + got := getParagrapheFrom(work_input) + want := work_expected_result + if !reflect.DeepEqual(got[0], want[0]) { + t.Fatalf("the first Paragraphe should be :\n%s\n got :%s", want, got) + } + + if !reflect.DeepEqual(got[1], want[1]) { + t.Fatalf("the first Paragraphe should be :\n%s\n got :%s", want, got) + } + }) + + t.Run("Invalid input should return nothing", func(t *testing.T) { + result := getParagrapheFrom(invalid_input) + if result != nil { + t.Fatalf("Invalid input should return nil got %v", result) + } + }) + + t.Run("Skill Paragraphe should be return from valid input", func(t *testing.T) { + got := getParagrapheFrom(skill_input) + want := skill_expected_result + if !reflect.DeepEqual(got, want) { + t.Fatalf("the first Paragraphe should be :\n%s\n got :%s", want, got) + } + }) + + t.Run("Invalid skill Paragraphe should return nil", func(t *testing.T) { + got := getParagrapheFrom(invalid_skill_input) + if got != nil { + t.Fatalf("Invalid input should return nil got %v", got) + } + }) } func TestSections(t *testing.T) { - rootDir := t.TempDir() + rootDir := t.TempDir() for _, p := range paths { fullPath := filepath.Join(rootDir, p.relativePath) if err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm); err != nil { @@ -131,80 +128,85 @@ func TestSections(t *testing.T) { } } - t.Run("Should return a valid list of cv", func (t *testing.T) { - source := MarkdownSource{} - got,err := source.GetCVsFrom(rootDir) - if err!=nil{ - t.Fatalf("Failed to getCV got %s",err) - } - - lang_got := got[len(got)-1].Lang - l_want := "fr" - if lang_got != l_want{ - t.Fatalf("Should have %s got %s",l_want,lang_got) - } - - sec_got := len(got[len(got)-1].Sections) - s_want := 3 - if sec_got != s_want{ - t.Fatalf("Should have %d got %d",s_want,sec_got) - } - - t_sec_got := got[len(got)-1].Sections[len(got[len(got)-1].Sections)-1].Title - t_s_want := "work" - if t_sec_got != t_s_want{ - t.Fatalf("Should have %s got %s",t_s_want,t_sec_got) - } - - p_t_sec_got := got[len(got)-1].Sections[len(got[len(got)-1].Sections)-1].Paragraphes - p_t_s_want := work_expected_result - - if len(p_t_sec_got) != len(p_t_s_want){ t.Fatalf("Should have len %d got %d",len(p_t_s_want),len(p_t_sec_got)) } - - if p_t_sec_got[len(p_t_sec_got)-1].H1 != p_t_s_want[len(p_t_s_want)-1].H1{ - t.Fatalf("Should have title %s got %s",p_t_s_want[len(p_t_s_want)-1].H1,p_t_sec_got[len(p_t_sec_got)-1].H1) - } - - }) -} + t.Run("Should return a valid list of cv", func(t *testing.T) { + source := MarkdownSource{} + got, err := source.GetCVsFrom(rootDir) + if err != nil { + t.Fatalf("Failed to getCV got %s", err) + } + + lang_got := got[len(got)-1].Lang + l_want := "fr" + if lang_got != l_want { + t.Fatalf("Should have %s got %s", l_want, lang_got) + } + sec_got := len(got[len(got)-1].Sections) + s_want := 3 + if sec_got != s_want { + t.Fatalf("Should have %d got %d", s_want, sec_got) + } + + t_sec_got := got[len(got)-1].Sections[len(got[len(got)-1].Sections)-1].Title + t_s_want := "work" + if t_sec_got != t_s_want { + t.Fatalf("Should have %s got %s", t_s_want, t_sec_got) + } + + p_t_sec_got := got[len(got)-1].Sections[len(got[len(got)-1].Sections)-1].Paragraphes + p_t_s_want := work_expected_result + + if len(p_t_sec_got) != len(p_t_s_want) { + t.Fatalf("Should have len %d got %d", len(p_t_s_want), len(p_t_sec_got)) + } + + if p_t_sec_got[len(p_t_sec_got)-1].H1 != p_t_s_want[len(p_t_s_want)-1].H1 { + t.Fatalf("Should have title %s got %s", p_t_s_want[len(p_t_s_want)-1].H1, p_t_sec_got[len(p_t_sec_got)-1].H1) + } + + }) +} func TestGetParamsFrom(t *testing.T) { - tempDir, err := os.MkdirTemp("", "test") - if err != nil { t.Fatalf("Failed to create temp directory: %v", err) } - defer os.RemoveAll(tempDir) - - yamlFilePath := filepath.Join(tempDir, "params.yml") - err = os.WriteFile(yamlFilePath, []byte(yamlContent), 0644) - if err != nil { t.Fatalf("Failed to write YAML file: %v", err) } - - source := &YamlSource{} - params, err := source.GetParamsFrom(tempDir) - if err != nil { - t.Fatalf("GetParamsFrom returned an error: %v", err) - } - expectedParams := core.Params{ - Info: struct { - Name string `yaml:"name"` - FirstName string `yaml:"firstname"` - Number string `yaml:"number"` - Mail string `yaml:"mail"` - GitHub string `yaml:"github"` - LinkedIn string `yaml:"linkedin"` - }{ - Name: "Doe", - FirstName: "John", - Number: "12345", - Mail: "john.doe@example.com", - GitHub: "johndoe", - LinkedIn: "john-doe-linkedin", - }, - Variante: map[string][]string{ - "optionA": {"value1", "value2"}, - "optionB": {"value3"}, - }, - } - if !reflect.DeepEqual(params, expectedParams) { - t.Errorf("Expected %+v, but got %+v", expectedParams, params) - } + tempDir, err := os.MkdirTemp("", "test") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + yamlFilePath := filepath.Join(tempDir, "params.yml") + err = os.WriteFile(yamlFilePath, []byte(yamlContent), 0644) + if err != nil { + t.Fatalf("Failed to write YAML file: %v", err) + } + + source := &YamlSource{} + params, err := source.GetParamsFrom(tempDir) + if err != nil { + t.Fatalf("GetParamsFrom returned an error: %v", err) + } + expectedParams := core.Params{ + Info: struct { + Name string `yaml:"name"` + FirstName string `yaml:"firstname"` + Number string `yaml:"number"` + Mail string `yaml:"mail"` + GitHub string `yaml:"github"` + LinkedIn string `yaml:"linkedin"` + }{ + Name: "Doe", + FirstName: "John", + Number: "12345", + Mail: "john.doe@example.com", + GitHub: "johndoe", + LinkedIn: "john-doe-linkedin", + }, + Variante: map[string][]string{ + "optionA": {"value1", "value2"}, + "optionB": {"value3"}, + }, + } + if !reflect.DeepEqual(params, expectedParams) { + t.Errorf("Expected %+v, but got %+v", expectedParams, params) + } } diff --git a/internal/adapters/input/markdown_tree_reader.go b/internal/adapters/input/markdown_tree_reader.go index 8a0cf4c..ac2e2ee 100644 --- a/internal/adapters/input/markdown_tree_reader.go +++ b/internal/adapters/input/markdown_tree_reader.go @@ -21,71 +21,78 @@ var hashtagRegex = regexp.MustCompile(`^#+`) // is a Markdown (.md) document. Each document may contain multiple paragraphs, but the headers // should not repeat within the same document. func (*MarkdownSource) GetCVsFrom(root string) ([]core.CV, error) { - cvsPath := root+"/cv" - cvs := make([]core.CV,0) - - current_lang := "" - current_sections := make([]core.Section,0) - has_been_inside_dir := false - - err := filepath.Walk(cvsPath, func(path string, info os.FileInfo, err error) error { - if err != nil { return err } - if info.IsDir(){ - if has_been_inside_dir { - cvs[len(cvs)-1].Sections=current_sections - current_sections = make([]core.Section,0) - has_been_inside_dir = false - } - current_lang = info.Name() - cvs = append(cvs, core.CV{Lang: current_lang}) - } - if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") { - has_been_inside_dir = true - if current_lang == "" { return errors.New("markdown file found before lang directory") } - content, err := os.ReadFile(path) - if err != nil { return err } - - new_section := core.Section{Title: strings.TrimRight(info.Name(),".md"), - Paragraphes: getParagrapheFrom(string(content))} - - current_sections = append(current_sections, new_section) - } - return nil - }) - - if err != nil { - return nil, err - } - cvs[len(cvs)-1].Sections=current_sections - - return cvs,nil + cvsPath := root + "/cv" + cvs := make([]core.CV, 0) + + current_lang := "" + current_sections := make([]core.Section, 0) + has_been_inside_dir := false + + err := filepath.Walk(cvsPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if has_been_inside_dir { + cvs[len(cvs)-1].Sections = current_sections + current_sections = make([]core.Section, 0) + has_been_inside_dir = false + } + current_lang = info.Name() + cvs = append(cvs, core.CV{Lang: current_lang}) + } + if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") { + has_been_inside_dir = true + if current_lang == "" { + return errors.New("markdown file found before lang directory") + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + + new_section := core.Section{Title: strings.TrimRight(info.Name(), ".md"), + Paragraphes: getParagrapheFrom(string(content))} + + current_sections = append(current_sections, new_section) + } + return nil + }) + + if err != nil { + return nil, err + } + cvs[len(cvs)-1].Sections = current_sections + + return cvs, nil } - -//Take string in the md format and return a slice of core.Paragraphe for each paragraph inside of it -//We define a paragraph by text block where \n\n indicate a separation -func getParagrapheFrom(s_section string)[]core.Paragraphe{ - paragraphs := make([]core.Paragraphe,0) - for _,paragraphe := range strings.Split(s_section, "\n\n"){ - n_paragraphe,err := parse_paragraphe(paragraphe) - if is_empty(n_paragraphe){ - continue - } - if err != nil { - fmt.Println("Failed to parse paragraphe ") - } else{ - paragraphs = append(paragraphs, n_paragraphe) - } - } - if len(paragraphs) == 0 { return nil } - return paragraphs +// Take string in the md format and return a slice of core.Paragraphe for each paragraph inside of it +// We define a paragraph by text block where \n\n indicate a separation +func getParagrapheFrom(s_section string) []core.Paragraphe { + paragraphs := make([]core.Paragraphe, 0) + for _, paragraphe := range strings.Split(s_section, "\n\n") { + n_paragraphe, err := parse_paragraphe(paragraphe) + if is_empty(n_paragraphe) { + continue + } + if err != nil { + fmt.Println("Failed to parse paragraphe ") + } else { + paragraphs = append(paragraphs, n_paragraphe) + } + } + if len(paragraphs) == 0 { + return nil + } + return paragraphs } -//Return if a paragraphs has no header and no items -func is_empty(p core.Paragraphe)bool{ - no_header := p.H1 == "" && p.H2 == "" && p.H3 == "" && p.H4 == "" - no_items := len(p.Items)==0 - return no_header && no_items +// Return if a paragraphs has no header and no items +func is_empty(p core.Paragraphe) bool { + no_header := p.H1 == "" && p.H2 == "" && p.H3 == "" && p.H4 == "" + no_items := len(p.Items) == 0 + return no_header && no_items } /* @@ -94,67 +101,67 @@ extracting headings and descriptions based on the number of leading hashtags or Returns an error if the format is invalid. */ func parse_paragraphe(paragraph string) (core.Paragraphe, error) { - var ( - n_paragraphe core.Paragraphe - bulletPrefix = "- " - skillAsteriskCount = 4 // Number of asterisks that signify a skill block - ) - if len(strings.Split(paragraph, "\n\n")) > 1 { - return n_paragraphe, errors.New("Tried to parse multiple paragraphs into a single section") - } - wasASkill := false - lines := strings.Split(strings.TrimRight(paragraph, "\n"), "\n") - for _, line := range lines { - if len(line) == 0 { - continue - } - nbHashtags := len(hashtagRegex.FindString(line)) - if wasASkill { - wasASkill = false - n_paragraphe.H2 = strings.TrimLeft(line, bulletPrefix) - continue - } - if nbHashtags == 0 && strings.HasPrefix(line, "*") && len(strings.Trim(line, "*")) == len(line)-skillAsteriskCount { - n_paragraphe.H1 = strings.Trim(line, "*") - wasASkill = true - continue - } - if err := handleLine(&n_paragraphe, line, nbHashtags); err != nil { - return n_paragraphe, err - } - } - return n_paragraphe, nil + var ( + n_paragraphe core.Paragraphe + bulletPrefix = "- " + skillAsteriskCount = 4 // Number of asterisks that signify a skill block + ) + if len(strings.Split(paragraph, "\n\n")) > 1 { + return n_paragraphe, errors.New("Tried to parse multiple paragraphs into a single section") + } + wasASkill := false + lines := strings.Split(strings.TrimRight(paragraph, "\n"), "\n") + for _, line := range lines { + if len(line) == 0 { + continue + } + nbHashtags := len(hashtagRegex.FindString(line)) + if wasASkill { + wasASkill = false + n_paragraphe.H2 = strings.TrimLeft(line, bulletPrefix) + continue + } + if nbHashtags == 0 && strings.HasPrefix(line, "*") && len(strings.Trim(line, "*")) == len(line)-skillAsteriskCount { + n_paragraphe.H1 = strings.Trim(line, "*") + wasASkill = true + continue + } + if err := handleLine(&n_paragraphe, line, nbHashtags); err != nil { + return n_paragraphe, err + } + } + return n_paragraphe, nil } // handleLine processes a line based on the number of leading hashtags func handleLine(n_paragraphe *core.Paragraphe, line string, nbHashtags int) error { - if nbHashtags > 0 && line[nbHashtags] != ' '{ - return fmt.Errorf("Err: cannot parse this md line {%s}, '#' should be followed by space", line) - } - switch nbHashtags { - case 1: - processesHeader(&n_paragraphe.H1,line,nbHashtags) - case 2: - processesHeader(&n_paragraphe.H2,line,nbHashtags) - case 3: - processesHeader(&n_paragraphe.H3,line,nbHashtags) - case 4: - processesHeader(&n_paragraphe.H4,line,nbHashtags) - case 0 : - if strings.HasPrefix(line, "- ") && len(line) > 1 { - n_paragraphe.Items = append(n_paragraphe.Items, strings.TrimLeft(line, "- ")) - } - default: - return fmt.Errorf("cannot parse this md line {%s}", line) - } - return nil + if nbHashtags > 0 && line[nbHashtags] != ' ' { + return fmt.Errorf("Err: cannot parse this md line {%s}, '#' should be followed by space", line) + } + switch nbHashtags { + case 1: + processesHeader(&n_paragraphe.H1, line, nbHashtags) + case 2: + processesHeader(&n_paragraphe.H2, line, nbHashtags) + case 3: + processesHeader(&n_paragraphe.H3, line, nbHashtags) + case 4: + processesHeader(&n_paragraphe.H4, line, nbHashtags) + case 0: + if strings.HasPrefix(line, "- ") && len(line) > 1 { + n_paragraphe.Items = append(n_paragraphe.Items, strings.TrimLeft(line, "- ")) + } + default: + return fmt.Errorf("cannot parse this md line {%s}", line) + } + return nil } -//Affect to the Header the line without the `-` -func processesHeader (pt_header *string, line string, nbHashtags int){ - if *pt_header != ""{ - slog.Warn("Trying to overload Header","oldHeader", - *pt_header,"newHeader",line[nbHashtags+1:]) - } - *pt_header = line[nbHashtags+1:] +// Affect to the Header the line without the `-` +func processesHeader(pt_header *string, line string, nbHashtags int) { + if *pt_header != "" { + slog.Warn("Trying to overload Header", "oldHeader", + *pt_header, "newHeader", line[nbHashtags+1:]) + } + *pt_header = line[nbHashtags+1:] } diff --git a/internal/adapters/input/yaml.go b/internal/adapters/input/yaml.go index a9e480f..2564191 100644 --- a/internal/adapters/input/yaml.go +++ b/internal/adapters/input/yaml.go @@ -2,21 +2,21 @@ package input import ( core "anemon/internal/core" - "os" "gopkg.in/yaml.v3" + "os" ) type YamlSource struct{} func (*YamlSource) GetParamsFrom(root string) (core.Params, error) { - params := core.Params{} - yamlFile, err := os.ReadFile(root + "/params.yml") - if err != nil { - return params, err - } - err = yaml.Unmarshal(yamlFile, ¶ms) - if err != nil { - return params, err - } - return params, nil + params := core.Params{} + yamlFile, err := os.ReadFile(root + "/params.yml") + if err != nil { + return params, err + } + err = yaml.Unmarshal(yamlFile, ¶ms) + if err != nil { + return params, err + } + return params, nil } diff --git a/internal/adapters/output/compiler.go b/internal/adapters/output/compiler.go index 8e850df..8e74924 100644 --- a/internal/adapters/output/compiler.go +++ b/internal/adapters/output/compiler.go @@ -11,63 +11,69 @@ import ( "sync" ) - const COMPILER = "pdflatex" + var REGEX = regexp.MustCompile(`(\d+) page`) + type LatexCompiler struct{} -//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 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, 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 max_nb_of_page,nil +// 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 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, 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 max_nb_of_page, nil } -//Compile the template into a pdf -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 +// Compile the template into a pdf +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 -func getListOfTemplate(root string)([]string, error){ - var res []string - templates, err := os.ReadDir(root + "/assets/latex/output"); if err != nil { - slog.Error("failed to read directory because: "+ err.Error()) - return res,err - } - for _, template := range templates { - if strings.HasSuffix(template.Name(),".tex"){ - res = append(res, root+"/assets/latex/output/"+template.Name()) - } - } - fmt.Println(res) - return res,nil +// Return the path of latex file inside the template directory +func getListOfTemplate(root string) ([]string, error) { + var res []string + templates, err := os.ReadDir(root + "/assets/latex/output") + if err != nil { + slog.Error("failed to read directory because: " + err.Error()) + return res, err + } + for _, template := range templates { + if strings.HasSuffix(template.Name(), ".tex") { + res = append(res, root+"/assets/latex/output/"+template.Name()) + } + } + fmt.Println(res) + return res, nil } diff --git a/internal/adapters/output/latex_test.go b/internal/adapters/output/latex_test.go index e6575c2..eaf75db 100644 --- a/internal/adapters/output/latex_test.go +++ b/internal/adapters/output/latex_test.go @@ -2,23 +2,23 @@ package output import ( "anemon/internal/core" - "path/filepath" "os" + "path/filepath" "strings" "testing" ) func removeWhitespace(s string) string { - return strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "\n", "") + return strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "\n", "") } func TestApplySectionToTemplate(t *testing.T) { - t.Run("happy path - should apply Professional section template with headers and items", func(t *testing.T) { - template := "Start\n%EXPERIENCE_SECTIONS%\nEnd" - headers := []string{"Company Name", "Position", "https://company.com", "Company"} - items := []string{"Task 1", "Task 2"} - - want := strings.TrimSpace(`Start + t.Run("happy path - should apply Professional section template with headers and items", func(t *testing.T) { + template := "Start\n%EXPERIENCE_SECTIONS%\nEnd" + headers := []string{"Company Name", "Position", "https://company.com", "Company"} + items := []string{"Task 1", "Task 2"} + + want := strings.TrimSpace(`Start %EXPERIENCE_SECTIONS% \resumeSubheading {Company Name}{Position} @@ -28,23 +28,23 @@ func TestApplySectionToTemplate(t *testing.T) { \resumeItem{Task 2} \resumeItemListEnd End`) - - processor := LatexProccesor{} - got, err := processor.ApplySectionToTemplate(template, headers, items, "Professional") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if removeWhitespace(got) != removeWhitespace(want) { - t.Errorf("ApplySectionToTemplate happy path failed;\nwant:\n%s\ngot:\n%s", want, got) - } - }) - - t.Run("happy path - should apply Project section template with headers and items", func(t *testing.T) { - template := "Start\n%PROJECTS_SECTIONS%\nEnd" - headers := []string{"Project Name", "Project Description", "https://github.com/project"} - items := []string{"Feature 1", "Feature 2"} - - want := strings.TrimSpace(`Start + + processor := LatexProccesor{} + got, err := processor.ApplySectionToTemplate(template, headers, items, "Professional") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if removeWhitespace(got) != removeWhitespace(want) { + t.Errorf("ApplySectionToTemplate happy path failed;\nwant:\n%s\ngot:\n%s", want, got) + } + }) + + t.Run("happy path - should apply Project section template with headers and items", func(t *testing.T) { + template := "Start\n%PROJECTS_SECTIONS%\nEnd" + headers := []string{"Project Name", "Project Description", "https://github.com/project"} + items := []string{"Feature 1", "Feature 2"} + + want := strings.TrimSpace(`Start %PROJECTS_SECTIONS% \resumeProjectHeading {\textbf{Project Name} | \emph{Project Description \href{https://github.com/project}{\faIcon{github}}}}{} @@ -53,35 +53,35 @@ End`) \resumeItem{Feature 2} \resumeItemListEnd End`) - - processor := LatexProccesor{} - got, err := processor.ApplySectionToTemplate(template, headers, items, "Project") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if removeWhitespace(got) != removeWhitespace(want) { - t.Errorf("ApplySectionToTemplate happy path failed;\nwant:\n%s\ngot:\n%s", want, got) - } - }) - - t.Run("failure - should return error for unsupported section", func(t *testing.T) { - template := "Start\n%EXPERIENCE_SECTIONS%\nEnd" - headers := []string{"Company Name", "Position", "https://company.com", "Company"} - items := []string{"Task 1", "Task 2"} - - processor := LatexProccesor{} - _, err := processor.ApplySectionToTemplate(template, headers, items, "UnsupportedSection") - if err == nil || err.Error() != "Don't know type UnsupportedSection" { - t.Errorf("expected error 'Don't know type UnsupportedSection', got %v", err) - } - }) - - t.Run("missing headers - should handle missing headers gracefully in Project section", func(t *testing.T) { - template := "Start\n%PROJECTS_SECTIONS%\nEnd" - headers := []string{"Project Name"} - items := []string{"Feature 1", "Feature 2"} - - want := strings.TrimSpace(`Start + + processor := LatexProccesor{} + got, err := processor.ApplySectionToTemplate(template, headers, items, "Project") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if removeWhitespace(got) != removeWhitespace(want) { + t.Errorf("ApplySectionToTemplate happy path failed;\nwant:\n%s\ngot:\n%s", want, got) + } + }) + + t.Run("failure - should return error for unsupported section", func(t *testing.T) { + template := "Start\n%EXPERIENCE_SECTIONS%\nEnd" + headers := []string{"Company Name", "Position", "https://company.com", "Company"} + items := []string{"Task 1", "Task 2"} + + processor := LatexProccesor{} + _, err := processor.ApplySectionToTemplate(template, headers, items, "UnsupportedSection") + if err == nil || err.Error() != "Don't know type UnsupportedSection" { + t.Errorf("expected error 'Don't know type UnsupportedSection', got %v", err) + } + }) + + t.Run("missing headers - should handle missing headers gracefully in Project section", func(t *testing.T) { + template := "Start\n%PROJECTS_SECTIONS%\nEnd" + headers := []string{"Project Name"} + items := []string{"Feature 1", "Feature 2"} + + want := strings.TrimSpace(`Start %PROJECTS_SECTIONS% \resumeProjectHeading {\textbf{Project Name} | \emph{$2 \href{$3}{\faIcon{github}}}}{} @@ -90,25 +90,25 @@ End`) \resumeItem{Feature 2} \resumeItemListEnd End`) - - processor := LatexProccesor{} - got, err := processor.ApplySectionToTemplate(template, headers, items, "Project") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if removeWhitespace(got) != removeWhitespace(want) { - t.Errorf("ApplySectionToTemplate missing headers case failed;\nwant:\n%s\ngot:\n%s", want, got) - } - }) + + processor := LatexProccesor{} + got, err := processor.ApplySectionToTemplate(template, headers, items, "Project") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if removeWhitespace(got) != removeWhitespace(want) { + t.Errorf("ApplySectionToTemplate missing headers case failed;\nwant:\n%s\ngot:\n%s", want, got) + } + }) } func TestReplaceWithSectionTemplate(t *testing.T) { - t.Run("happy path - should replace hook with section template including headers and items", func(t *testing.T) { - template := "Start\n%PROJECTS_SECTIONS%\nEnd" - headers := []string{"Project Name", "Project Description", "https://github.com/project"} - items := []string{"Feature 1", "Feature 2"} - want := strings.TrimSpace(`Start + t.Run("happy path - should replace hook with section template including headers and items", func(t *testing.T) { + template := "Start\n%PROJECTS_SECTIONS%\nEnd" + headers := []string{"Project Name", "Project Description", "https://github.com/project"} + items := []string{"Feature 1", "Feature 2"} + want := strings.TrimSpace(`Start %PROJECTS_SECTIONS% \resumeProjectHeading {\textbf{Project Name} | \emph{Project Description \href{https://github.com/project}{\faIcon {github}}}}{} @@ -120,17 +120,17 @@ func TestReplaceWithSectionTemplate(t *testing.T) { End`) - got := strings.TrimSpace(replaceWithSectionTemplate(template, ProjectTemplate, headers, items)) - if removeWhitespace(got) != removeWhitespace(want) { - t.Errorf("replaceWithSectionTemplate happy path failed;\nwant:\n%s\ngot:\n%s", want, got) - } - }) - - t.Run("failure - should work even with missing headers", func(t *testing.T) { - template := "Start\n%PROJECTS_SECTIONS%\nEnd" - headers := []string{"Project Name", "Project Description"} - items := []string{"Feature 1", "Feature 2"} - want := strings.TrimSpace(`Start + got := strings.TrimSpace(replaceWithSectionTemplate(template, ProjectTemplate, headers, items)) + if removeWhitespace(got) != removeWhitespace(want) { + t.Errorf("replaceWithSectionTemplate happy path failed;\nwant:\n%s\ngot:\n%s", want, got) + } + }) + + t.Run("failure - should work even with missing headers", func(t *testing.T) { + template := "Start\n%PROJECTS_SECTIONS%\nEnd" + headers := []string{"Project Name", "Project Description"} + items := []string{"Feature 1", "Feature 2"} + want := strings.TrimSpace(`Start %PROJECTS_SECTIONS% \resumeProjectHeading {\textbf{Project Name} | \emph{Project Description \href{$3}{\faIcon {github}}}}{} @@ -142,11 +142,11 @@ End`) End`) - got := strings.TrimSpace(replaceWithSectionTemplate(template, ProjectTemplate, headers, items)) - if removeWhitespace(got) != removeWhitespace(want) { - t.Errorf("replaceWithSectionTemplate happy path failed;\nwant:\n%s\ngot:\n%s", want, got) - } - }) + got := strings.TrimSpace(replaceWithSectionTemplate(template, ProjectTemplate, headers, items)) + if removeWhitespace(got) != removeWhitespace(want) { + t.Errorf("replaceWithSectionTemplate happy path failed;\nwant:\n%s\ngot:\n%s", want, got) + } + }) } func TestReplaceHeaders(t *testing.T) { @@ -195,8 +195,6 @@ func TestReplaceHeaders(t *testing.T) { }) } - - func TestReplaceItems(t *testing.T) { t.Run("happy path - should replace %ITEMS% with formatted items", func(t *testing.T) { template := "Start\n%ITEMS% End" @@ -231,7 +229,8 @@ func TestReplaceItems(t *testing.T) { }) } -func TestSanitize(t *testing.T) { t.Run("happy path - should sanitize special characters", func(t *testing.T) { +func TestSanitize(t *testing.T) { + t.Run("happy path - should sanitize special characters", func(t *testing.T) { got := "100% **bold text** and *italic text*" expected := "100\\% \\textbf{bold text} and \\emph{italic text}" result := sanitize(got) @@ -287,12 +286,11 @@ func TestApplyInfoToTemplate(t *testing.T) { \def\LinkedIn{https://linkedin.com/in/johndoe}` got := ApplyInfoToTemplate(template, params) - if removeWhitespace(got) != removeWhitespace(want) { - t.Errorf("expected:\n%s\ngot:\n%s", want, got) - } + if removeWhitespace(got) != removeWhitespace(want) { + t.Errorf("expected:\n%s\ngot:\n%s", want, got) + } } - func TestGetListOfTemplate(t *testing.T) { root := "testdata" templateDir := filepath.Join(root, "assets", "latex", "output") @@ -302,7 +300,7 @@ func TestGetListOfTemplate(t *testing.T) { } defer os.RemoveAll(root) - files := []string{"foo.tex", "bar.tex","garbage"} + files := []string{"foo.tex", "bar.tex", "garbage"} for _, file := range files { f, err := os.Create(filepath.Join(templateDir, file)) if err != nil { @@ -316,13 +314,13 @@ func TestGetListOfTemplate(t *testing.T) { t.Fatalf("getListOfTemplate returned an error: %v", err) } expected := files - if len(got) != len(expected)-1{//Should ommit garbage + if len(got) != len(expected)-1 { //Should ommit garbage t.Errorf("expected %d files, got %d", len(expected), len(got)) } for _, filePath := range got { - _, err := os.Stat(filePath) - if err != nil { - t.Errorf("expected path %s not found: %v", filePath, err) - } + _, err := os.Stat(filePath) + if err != nil { + t.Errorf("expected path %s not found: %v", filePath, err) + } } } diff --git a/internal/adapters/output/latex_writer.go b/internal/adapters/output/latex_writer.go index 5192412..eb6f5c1 100644 --- a/internal/adapters/output/latex_writer.go +++ b/internal/adapters/output/latex_writer.go @@ -13,95 +13,98 @@ import ( type LatexReader struct{} type LatexProccesor struct{} - -//Write the template file in the assets directory -func (*LatexProccesor)MakeNewTemplate(path string, template string, name string)error{ - err := os.WriteFile(path+"/assets/latex/output/"+name, - []byte(template), 0644) - return err +// Write the template file in the assets directory +func (*LatexProccesor) MakeNewTemplate(path string, template string, name string) error { + err := os.WriteFile(path+"/assets/latex/output/"+name, + []byte(template), 0644) + return err } -//Read the template file in the assets directory from the root dir and apply the params given to it -func (*LatexReader)ReadCVTemplate(root string,params core.Params)(string, error){ - file, err := os.ReadFile(root+"/assets/latex/template/template.tex") - if err != nil { - return "", err - } - return ApplyInfoToTemplate(string(file),params), nil +// Read the template file in the assets directory from the root dir and apply the params given to it +func (*LatexReader) ReadCVTemplate(root string, params core.Params) (string, error) { + file, err := os.ReadFile(root + "/assets/latex/template/template.tex") + if err != nil { + return "", err + } + return ApplyInfoToTemplate(string(file), params), nil } -//Apply general information(name, mail..) to a template -func ApplyInfoToTemplate(template string, params core.Params) (string){ - var varsBuilder strings.Builder - infoValue := reflect.ValueOf(params.Info) - for i := 0; i < infoValue.NumField(); i++ { - field := infoValue.Type().Field(i) - fieldValue := infoValue.Field(i) - varsBuilder.WriteString("\\def\\"+field.Name+"{"+fieldValue.String()+"}\n") - } - return replaceVars(template,varsBuilder.String()) +// Apply general information(name, mail..) to a template +func ApplyInfoToTemplate(template string, params core.Params) string { + var varsBuilder strings.Builder + infoValue := reflect.ValueOf(params.Info) + for i := 0; i < infoValue.NumField(); i++ { + field := infoValue.Type().Field(i) + fieldValue := infoValue.Field(i) + varsBuilder.WriteString("\\def\\" + field.Name + "{" + fieldValue.String() + "}\n") + } + return replaceVars(template, varsBuilder.String()) } -//Apply a section to a section type on a latex template -func (*LatexProccesor)ApplySectionToTemplate(template string, headers []string,item []string, section string) (string,error){ - if len(section)<2{ return "",errors.New("Don't know type "+section) } - section = strings.ToUpper(string(section[0]))+section[1:] - switch{ - case section == "Professional": - template = replaceWithSectionTemplate(template,ProfessionalTemplate, - headers,item) - case section == "Project": - template = replaceWithSectionTemplate(template,ProjectTemplate, - headers,item) - case section == "Education": - template = replaceWithSectionTemplate(template,EducationTemplate, - headers,nil) - case section == "Skill": - template = replaceWithSectionTemplate(template,SkillTemplate, - headers,nil) - default: - fmt.Println("Don't know type "+section) - return "",errors.New("Don't know type "+section) - } - return template,nil +// Apply a section to a section type on a latex template +func (*LatexProccesor) ApplySectionToTemplate(template string, headers []string, item []string, section string) (string, error) { + if len(section) < 2 { + return "", errors.New("Don't know type " + section) + } + section = strings.ToUpper(string(section[0])) + section[1:] + switch { + case section == "Professional": + template = replaceWithSectionTemplate(template, ProfessionalTemplate, + headers, item) + case section == "Project": + template = replaceWithSectionTemplate(template, ProjectTemplate, + headers, item) + case section == "Education": + template = replaceWithSectionTemplate(template, EducationTemplate, + headers, nil) + case section == "Skill": + template = replaceWithSectionTemplate(template, SkillTemplate, + headers, nil) + default: + fmt.Println("Don't know type " + section) + return "", errors.New("Don't know type " + section) + } + return template, nil } -//Replace with the template defined in the template_sections.go const -func replaceWithSectionTemplate(template string,SectionTemplate TemplateStruct,headers []string,items []string)string{ - updated_template := strings.Replace(template,SectionTemplate.hook, - SectionTemplate.hook+replace_headers(SectionTemplate.template,headers),1) - if items!=nil{ updated_template = replace_items(updated_template, items) } - return updated_template +// Replace with the template defined in the template_sections.go const +func replaceWithSectionTemplate(template string, SectionTemplate TemplateStruct, headers []string, items []string) string { + updated_template := strings.Replace(template, SectionTemplate.hook, + SectionTemplate.hook+replace_headers(SectionTemplate.template, headers), 1) + if items != nil { + updated_template = replace_items(updated_template, items) + } + return updated_template } -//Replace the %vars$ with the vars -func replaceVars(template string,vars string)string{ - updated_template := strings.Replace(template,"%VARS%",vars,1) - return updated_template +// Replace the %vars$ with the vars +func replaceVars(template string, vars string) string { + updated_template := strings.Replace(template, "%VARS%", vars, 1) + return updated_template } -//Search and replace the headers in the template by their replacement -func replace_headers(sec_template string, replacements []string)string{ - for i := 0; i < len(replacements); i++ { - position := fmt.Sprintf("$%d", i+1) - sec_template = strings.Replace(sec_template, - position, replacements[i], 1) - } - return sanitize(sec_template) +// Search and replace the headers in the template by their replacement +func replace_headers(sec_template string, replacements []string) string { + for i := 0; i < len(replacements); i++ { + position := fmt.Sprintf("$%d", i+1) + sec_template = strings.Replace(sec_template, + position, replacements[i], 1) + } + return sanitize(sec_template) } -func replace_items(template string, section_items []string)string{ - items := "" - for _,item := range section_items{ - items += strings.Replace(single_item_template,"%ITEM%",item,1) - } - template = strings.Replace(template, - "%ITEMS%", items, 1) - return sanitize(template) +func replace_items(template string, section_items []string) string { + items := "" + for _, item := range section_items { + items += strings.Replace(single_item_template, "%ITEM%", item, 1) + } + template = strings.Replace(template, + "%ITEMS%", items, 1) + return sanitize(template) } -//Sanitize the special charactere -func sanitize(template string)(string){ +// Sanitize the special charactere +func sanitize(template string) string { replacements := []struct { pattern string replacement string @@ -115,5 +118,5 @@ func sanitize(template string)(string){ re := regexp.MustCompile(r.pattern) template = re.ReplaceAllString(template, r.replacement) } - return template + return template } diff --git a/internal/adapters/output/template_sections.go b/internal/adapters/output/template_sections.go index 97ebc54..9c1bc6f 100644 --- a/internal/adapters/output/template_sections.go +++ b/internal/adapters/output/template_sections.go @@ -1,11 +1,12 @@ package output + type TemplateStruct struct { template string hook string } var ( - single_item_template = "\\resumeItem{%ITEM%}\n" + single_item_template = "\\resumeItem{%ITEM%}\n" ProfessionalTemplate = TemplateStruct{ template: ` diff --git a/internal/core/generate.go b/internal/core/generate.go index f5cb777..3b23b2e 100644 --- a/internal/core/generate.go +++ b/internal/core/generate.go @@ -1,233 +1,241 @@ package core import ( - "log/slog" "fmt" - "sync" + "log/slog" "sort" "strings" + "sync" ) var TresholdPageOverFlow struct { - value int - mutex sync.Mutex + value int + mutex sync.Mutex } -type CVService struct{ - root string - source Source - paramsSource SourceParams - templateReader TemplateReader - templateProcessor TemplateProcessor - compiler Compiler +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 +type BuilderService struct { + root string + source Source + paramsSource SourceParams + templateReader TemplateReader + templateProcessor TemplateProcessor + compiler Compiler } -//generate the templates for the cvs defined in the assets directory +// generate the templates for the cvs defined in the assets directory func (s *CVService) GenerateTemplates() error { - slog.Info("--Generating CVs--") - 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) -} - -//Compile the CV into PDF, and if they are too long regenrate theme with one less section + slog.Info("--Generating CVs--") + 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) +} + +// 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 { - threshold := GetOverflowThreshold() - maxNbPage, err := s.compiler.CompileTemplate(s.root) - if err != nil { - return fmt.Errorf("failed to compile template: %w", err) - } - for maxNbPage > threshold { - slog.Warn("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) - } - } - return nil -} - -//Use the the slice of CV to write in the output directory the new CV template + threshold := GetOverflowThreshold() + maxNbPage, err := s.compiler.CompileTemplate(s.root) + if err != nil { + return fmt.Errorf("failed to compile template: %w", err) + } + for maxNbPage > threshold { + slog.Warn("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) + } + } + 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 + 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 +// generate a template for the cv with the given params func generateCVFrom(cv CV, params Params, root string, - 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} - } - for vari, keywords:= range params.Variante { - 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, - paragraph.H3, paragraph.H4} - items:=paragraph.Items - sortByScore(items,keywords) - cvTemplate,err = processor.ApplySectionToTemplate( - cvTemplate,headers,items,section.Title) - if err != nil{ return err } - } - err = processor.MakeNewTemplate(root,cvTemplate,cvName) - if err != nil{ return err } - } - } - 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:]...) + 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} + } + for vari, keywords := range params.Variante { + 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, + paragraph.H3, paragraph.H4} + items := paragraph.Items + sortByScore(items, keywords) + cvTemplate, err = processor.ApplySectionToTemplate( + cvTemplate, headers, items, section.Title) + if err != nil { + return err + } + } + err = processor.MakeNewTemplate(root, cvTemplate, cvName) + if err != nil { + return err + } + } + } + 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 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) + 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 ? +// 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 + 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) -func sortByScore(items []string, keywords []string){ - sort.Slice(items, func(i, j int) bool { - return getScore(items[i],keywords)>getScore(items[j],keywords) - }) +// The sort is done in ascending order as the section append work like a stack(Lifo) +func sortByScore(items []string, keywords []string) { + sort.Slice(items, func(i, j int) bool { + return getScore(items[i], keywords) > getScore(items[j], keywords) + }) } -//take and item and a list of keyword and return the number of keyword inside the item -func getScore(item string, keywords []string)int{ - score := 0 - for _,keyword := range keywords{ - score += strings.Count(item, keyword) - } - return score +// take and item and a list of keyword and return the number of keyword inside the item +func getScore(item string, keywords []string) int { + score := 0 + for _, keyword := range keywords { + score += strings.Count(item, keyword) + } + return score } func (cv *BuilderService) SetRoot(root string) { - cv.root = root + cv.root = root } func (cv *BuilderService) SetSource(source Source) { - cv.source = source + cv.source = source } func (cv *BuilderService) SetParamsSource(paramsSource SourceParams) { - cv.paramsSource = paramsSource + cv.paramsSource = paramsSource } func (cv *BuilderService) SetTemplateReader(templateReader TemplateReader) { - cv.templateReader = templateReader + cv.templateReader = templateReader } func (cv *BuilderService) SetTemplateProcessor(templateProcessor TemplateProcessor) { - cv.templateProcessor = templateProcessor + cv.templateProcessor = templateProcessor } func (cv *BuilderService) SetCompiler(compiler Compiler) { - cv.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, - } + return CVService{ + root: s.root, + source: s.source, + paramsSource: s.paramsSource, + templateReader: s.templateReader, + templateProcessor: s.templateProcessor, + compiler: s.compiler, + } } // Set the threshold value dynamically func SetOverflowThreshold(newThreshold int) { - TresholdPageOverFlow.mutex.Lock() - defer TresholdPageOverFlow.mutex.Unlock() - TresholdPageOverFlow.value = newThreshold + TresholdPageOverFlow.mutex.Lock() + defer TresholdPageOverFlow.mutex.Unlock() + TresholdPageOverFlow.value = newThreshold } // Get the current threshold value func GetOverflowThreshold() int { - TresholdPageOverFlow.mutex.Lock() - defer TresholdPageOverFlow.mutex.Unlock() - return TresholdPageOverFlow.value + TresholdPageOverFlow.mutex.Lock() + defer TresholdPageOverFlow.mutex.Unlock() + return TresholdPageOverFlow.value } func init() { - TresholdPageOverFlow.value = 1 + TresholdPageOverFlow.value = 1 } diff --git a/internal/core/generate_test.go b/internal/core/generate_test.go index a1a61d2..19d0fb3 100644 --- a/internal/core/generate_test.go +++ b/internal/core/generate_test.go @@ -7,16 +7,19 @@ import ( type MockParamsSource struct { Params Params - Err error + Err error } func (s *MockParamsSource) GetParamsFrom(root string) (Params, error) { - if s.Err != nil { return s.Params, s.Err } + if s.Err != nil { + return s.Params, s.Err + } return s.Params, nil } -type MockCompiler struct {} -func (c *MockCompiler) CompileTemplate(root string) (int,error) { return 0,nil } +type MockCompiler struct{} + +func (c *MockCompiler) CompileTemplate(root string) (int, error) { return 0, nil } type MockSource struct { CVs []CV @@ -24,7 +27,9 @@ type MockSource struct { } func (s *MockSource) GetCVsFrom(root string) ([]CV, error) { - if s.Err != nil { return nil, s.Err } + if s.Err != nil { + return nil, s.Err + } return s.CVs, nil } @@ -34,7 +39,9 @@ type MockTemplateReader struct { } func (r *MockTemplateReader) ReadCVTemplate(path string, params Params) (string, error) { - if r.Err != nil { return "", r.Err } + if r.Err != nil { + return "", r.Err + } return r.Template, nil } @@ -46,13 +53,17 @@ type MockTemplateProcessor struct { } func (p *MockTemplateProcessor) MakeNewTemplate(path string, template string, name string) error { - if p.MakeErr != nil { return p.MakeErr } + if p.MakeErr != nil { + return p.MakeErr + } p.GeneratedFiles[name] = template return nil } func (p *MockTemplateProcessor) ApplySectionToTemplate(template string, headers []string, items []string, section string) (string, error) { - if p.ApplyErr != nil { return "", p.ApplyErr } + if p.ApplyErr != nil { + return "", p.ApplyErr + } result := template + " | Section: " + section + " | Headers: " + headers[0] + ", " + headers[1] + ", " + headers[2] + ", " + headers[3] for _, item := range items { result += " | Item: " + item @@ -64,27 +75,27 @@ func (p *MockTemplateProcessor) ApplySectionToTemplate(template string, headers func TestGenerateTemplates(t *testing.T) { root := "testRoot" baseTemplate := "base template content" - params := Params{ - Info: struct { - Name string `yaml:"name"` - FirstName string `yaml:"firstname"` - Number string `yaml:"number"` - Mail string `yaml:"mail"` - GitHub string `yaml:"github"` - LinkedIn string `yaml:"linkedin"` - }{ - Name: "Doe", - FirstName: "John", - Number: "12345", - Mail: "john.doe@example.com", - GitHub: "johndoe", - LinkedIn: "john-doe-linkedin", - }, - Variante: map[string][]string{ - "optionA": {"value1", "value2"}, - "optionB": {"value3"}, - }, - } + params := Params{ + Info: struct { + Name string `yaml:"name"` + FirstName string `yaml:"firstname"` + Number string `yaml:"number"` + Mail string `yaml:"mail"` + GitHub string `yaml:"github"` + LinkedIn string `yaml:"linkedin"` + }{ + Name: "Doe", + FirstName: "John", + Number: "12345", + Mail: "john.doe@example.com", + GitHub: "johndoe", + LinkedIn: "john-doe-linkedin", + }, + Variante: map[string][]string{ + "optionA": {"value1", "value2"}, + "optionB": {"value3"}, + }, + } cv := CV{ Lang: "EN", @@ -104,273 +115,282 @@ func TestGenerateTemplates(t *testing.T) { }, } - t.Run("When giving one language and two variante should generate two cv in the language", func (t *testing.T) { - source := &MockSource{ CVs: []CV{cv}, } - paramsSource := &MockParamsSource{ Params: params, } - templateReader := &MockTemplateReader{ Template: baseTemplate } - templateProcessor := &MockTemplateProcessor{ GeneratedFiles: make(map[string]string) } - compiler := &MockCompiler{} - 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)) } - - generatedTemplate, exists := templateProcessor.GeneratedFiles["CV-EN-optionA.tex"] - if !exists { t.Fatalf("expected generated file 'CV-EN-optionA.tex' to exist") } - - expectedContent := baseTemplate + " | Section: Work Experience | Headers: Job Title, Company, Location, Date | Item: Managed projects | Item: Led team" - if !reflect.DeepEqual(generatedTemplate, expectedContent) { - t.Errorf("expected generated template content %v, got %v", expectedContent, generatedTemplate) - } - }) - - t.Run("When giving one language and zero info or variante should generate one cv in the language", func (t *testing.T) { - source := &MockSource{ CVs: []CV{cv}, } - paramsSource := &MockParamsSource{ Params: Params{}, } - templateReader := &MockTemplateReader{ Template: baseTemplate } - templateProcessor := &MockTemplateProcessor{ GeneratedFiles: make(map[string]string) } - compiler := &MockCompiler{} - 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)) } - - generatedTemplate, exists := templateProcessor.GeneratedFiles["CV-EN-simple.tex"] - if !exists { t.Fatalf("expected generated file 'CV-EN-simple.tex' to exist") } - - expectedContent := baseTemplate + " | Section: Work Experience | Headers: Job Title, Company, Location, Date | Item: Managed projects | Item: Led team" - if !reflect.DeepEqual(generatedTemplate, expectedContent) { - t.Errorf("expected generated template content %v, got %v", expectedContent, generatedTemplate) - } - }) + t.Run("When giving one language and two variante should generate two cv in the language", func(t *testing.T) { + source := &MockSource{CVs: []CV{cv}} + paramsSource := &MockParamsSource{Params: params} + templateReader := &MockTemplateReader{Template: baseTemplate} + templateProcessor := &MockTemplateProcessor{GeneratedFiles: make(map[string]string)} + compiler := &MockCompiler{} + 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)) + } + + generatedTemplate, exists := templateProcessor.GeneratedFiles["CV-EN-optionA.tex"] + if !exists { + t.Fatalf("expected generated file 'CV-EN-optionA.tex' to exist") + } + + expectedContent := baseTemplate + " | Section: Work Experience | Headers: Job Title, Company, Location, Date | Item: Managed projects | Item: Led team" + if !reflect.DeepEqual(generatedTemplate, expectedContent) { + t.Errorf("expected generated template content %v, got %v", expectedContent, generatedTemplate) + } + }) + + t.Run("When giving one language and zero info or variante should generate one cv in the language", func(t *testing.T) { + source := &MockSource{CVs: []CV{cv}} + paramsSource := &MockParamsSource{Params: Params{}} + templateReader := &MockTemplateReader{Template: baseTemplate} + templateProcessor := &MockTemplateProcessor{GeneratedFiles: make(map[string]string)} + compiler := &MockCompiler{} + 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)) + } + + generatedTemplate, exists := templateProcessor.GeneratedFiles["CV-EN-simple.tex"] + if !exists { + t.Fatalf("expected generated file 'CV-EN-simple.tex' to exist") + } + + expectedContent := baseTemplate + " | Section: Work Experience | Headers: Job Title, Company, Location, Date | Item: Managed projects | Item: Led team" + if !reflect.DeepEqual(generatedTemplate, expectedContent) { + t.Errorf("expected generated template content %v, got %v", expectedContent, generatedTemplate) + } + }) } func TestGetScore(t *testing.T) { - tests := []struct { - item string - keywords []string - expected int - }{ - {item: "foo bar baz", keywords: []string{"foo", "bar", "baz"}, expected: 3}, - {item: "foo qux", keywords: []string{"foo", "bar", "baz"}, expected: 1}, - {item: "patate douce", keywords: []string{"foo", "bar", "baz"}, expected: 0}, - {item: "Foo Bar Baz", keywords: []string{"foo", "bar", "baz"}, expected: 0}, - {item: "foo bar baz", keywords: []string{}, expected: 0}, - {item: "", keywords: []string{"foo", "bar", "baz"}, expected: 0}, - {item: "foo bar foo baz", keywords: []string{"foo", "foo bar"}, expected: 3}, - } - - for _, tt := range tests { - t.Run(tt.item, func(t *testing.T) { - score := getScore(tt.item, tt.keywords) - if score != tt.expected { - t.Errorf("for item=%q and keywords=%v, expected %d, got %d", tt.item, tt.keywords, tt.expected, score) - } - }) - } -} + tests := []struct { + item string + keywords []string + expected int + }{ + {item: "foo bar baz", keywords: []string{"foo", "bar", "baz"}, expected: 3}, + {item: "foo qux", keywords: []string{"foo", "bar", "baz"}, expected: 1}, + {item: "patate douce", keywords: []string{"foo", "bar", "baz"}, expected: 0}, + {item: "Foo Bar Baz", keywords: []string{"foo", "bar", "baz"}, expected: 0}, + {item: "foo bar baz", keywords: []string{}, expected: 0}, + {item: "", keywords: []string{"foo", "bar", "baz"}, expected: 0}, + {item: "foo bar foo baz", keywords: []string{"foo", "foo bar"}, expected: 3}, + } + for _, tt := range tests { + t.Run(tt.item, func(t *testing.T) { + score := getScore(tt.item, tt.keywords) + if score != tt.expected { + t.Errorf("for item=%q and keywords=%v, expected %d, got %d", tt.item, tt.keywords, tt.expected, score) + } + }) + } +} func TestSortByScore(t *testing.T) { - keywords := []string{"foo", "bar"} - tests := []struct { - items []string - expected []string - }{ - { - items: []string{" with foo", " with foo and bar", " with neither", " with bar"}, - expected: []string{" with foo and bar", " with foo", " with bar", " with neither"}, - }, - - { - items: []string{" with foo bar foo bar", " with foo", " with bar bar bar", " with foo bar"}, - expected: []string{" with foo bar foo bar", " with bar bar bar", " with foo bar", " with foo"}, - }, - { - items: []string{" without keywords", " with bar", "Another without keywords"}, - expected: []string{" with bar", " without keywords", "Another without keywords"}, - }, - } - - for _, tt := range tests { - items := make([]string, len(tt.items)) - copy(items, tt.items) - sortByScore(items, keywords) - - for i := range items { - if items[i] != tt.expected[i] { - t.Errorf("after sorting, expected %v but got %v", tt.expected, items) - } - } - } -} + keywords := []string{"foo", "bar"} + tests := []struct { + items []string + expected []string + }{ + { + items: []string{" with foo", " with foo and bar", " with neither", " with bar"}, + expected: []string{" with foo and bar", " with foo", " with bar", " with neither"}, + }, + { + items: []string{" with foo bar foo bar", " with foo", " with bar bar bar", " with foo bar"}, + expected: []string{" with foo bar foo bar", " with bar bar bar", " with foo bar", " with foo"}, + }, + { + items: []string{" without keywords", " with bar", "Another without keywords"}, + expected: []string{" with bar", " without keywords", "Another without keywords"}, + }, + } + + for _, tt := range tests { + items := make([]string, len(tt.items)) + copy(items, tt.items) + sortByScore(items, keywords) + + for i := range items { + if items[i] != tt.expected[i] { + t.Errorf("after sorting, expected %v but got %v", tt.expected, items) + } + } + } +} 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) - } - } + 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) - } - } + 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 993eaeb..a22fc96 100644 --- a/internal/core/ports.go +++ b/internal/core/ports.go @@ -6,84 +6,99 @@ import ( ) type ICVservice interface { - generateTemplates(root string, source Source, templateReader TemplateReader, templateProcessor TemplateProcessor)error + generateTemplates(root string, source Source, templateReader TemplateReader, templateProcessor TemplateProcessor) error } type Compiler interface { - CompileTemplate(root string) (int,error) + CompileTemplate(root string) (int, error) } type SourceParams interface { - GetParamsFrom(root string) (Params, error) + GetParamsFrom(root string) (Params, error) } type Source interface { - GetCVsFrom(root string) ([]CV, error) + GetCVsFrom(root string) ([]CV, error) } type TemplateReader interface { - ReadCVTemplate(root string, params Params)(string, error) + ReadCVTemplate(root string, params Params) (string, error) } type TemplateProcessor interface { - MakeNewTemplate(path string, template string, name string)error - ApplySectionToTemplate(template string, headers []string,item []string, section string) (string,error) + MakeNewTemplate(path string, template string, name string) error + ApplySectionToTemplate(template string, headers []string, item []string, section string) (string, error) } -//CV with Language and Sections +// CV with Language and Sections type CV struct { - Lang string - Sections []Section + Lang string + Sections []Section } type Section struct { - Title string - Paragraphes []Paragraphe + Title string + Paragraphes []Paragraphe } type Paragraphe struct { - H1 string - H2 string - H3 string - H4 string - Items []string + H1 string + H2 string + H3 string + H4 string + Items []string } type Params struct { - Info struct { - Name string `yaml:"name"` - FirstName string `yaml:"firstname"` - Number string `yaml:"number"` - Mail string `yaml:"mail"` - GitHub string `yaml:"github"` - LinkedIn string `yaml:"linkedin"` - } `yaml:"info"` - Variante map[string][]string `yaml:"variante"` + Info struct { + Name string `yaml:"name"` + FirstName string `yaml:"firstname"` + Number string `yaml:"number"` + Mail string `yaml:"mail"` + GitHub string `yaml:"github"` + LinkedIn string `yaml:"linkedin"` + } `yaml:"info"` + Variante map[string][]string `yaml:"variante"` } -//Print Method for CV +// Print Method for CV func (cv *CV) Print() { - fmt.Println("CV Language: "+ cv.Lang) - fmt.Println(strings.Repeat("=", 40)) - fmt.Printf("With %d Sections\n",len(cv.Sections)) - for _, section := range cv.Sections { - fmt.Printf("Section: %s\n", section.Title) - fmt.Println(strings.Repeat("-", 40)) - for _, p := range section.Paragraphes { - if p.H1 != "" { fmt.Printf("H1: %s\n", p.H1) }else{fmt.Printf("No H1")} - if p.H2 != "" { fmt.Printf(" H2: %s\n", p.H2) }else{fmt.Printf("No H2")} - if p.H3 != "" { fmt.Printf(" H3: %s\n", p.H3) }else{fmt.Printf("No H3")} - if p.H4 != "" { fmt.Printf(" H4: %s\n", p.H4) }else{fmt.Printf("No H4")} + fmt.Println("CV Language: " + cv.Lang) + fmt.Println(strings.Repeat("=", 40)) + fmt.Printf("With %d Sections\n", len(cv.Sections)) + for _, section := range cv.Sections { + fmt.Printf("Section: %s\n", section.Title) + fmt.Println(strings.Repeat("-", 40)) + for _, p := range section.Paragraphes { + if p.H1 != "" { + fmt.Printf("H1: %s\n", p.H1) + } else { + fmt.Printf("No H1") + } + if p.H2 != "" { + fmt.Printf(" H2: %s\n", p.H2) + } else { + fmt.Printf("No H2") + } + if p.H3 != "" { + fmt.Printf(" H3: %s\n", p.H3) + } else { + fmt.Printf("No H3") + } + if p.H4 != "" { + fmt.Printf(" H4: %s\n", p.H4) + } else { + fmt.Printf("No H4") + } - if len(p.Items) > 0 { - fmt.Println(" Items:") - for _, item := range p.Items { - fmt.Printf(" - %s\n", item) - } - } - fmt.Println() - } - fmt.Println() - } + if len(p.Items) > 0 { + fmt.Println(" Items:") + for _, item := range p.Items { + fmt.Printf(" - %s\n", item) + } + } + fmt.Println() + } + fmt.Println() + } } - diff --git a/main.go b/main.go index ddfa18b..426d190 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "anemon/cmd" + "anemon/cmd" ) func main() {