diff --git a/internal/adapters/input/cli.go b/internal/adapters/input/cli.go new file mode 100644 index 0000000..de1ab6f --- /dev/null +++ b/internal/adapters/input/cli.go @@ -0,0 +1 @@ +package input diff --git a/internal/adapters/input/input_test.go b/internal/adapters/input/input_test.go new file mode 100644 index 0000000..352d54d --- /dev/null +++ b/internal/adapters/input/input_test.go @@ -0,0 +1,156 @@ +package input + +import ( + core "anemon/internal/core" + "os" + "path/filepath" + "reflect" + "testing" +) + +var( + work_input = ` +# Back-End Intern +## February 2024 -- August 2024 +### TechCorp +#### Internship +- 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. + +# Back-End Intern +## February 2024 -- August 2024 +### TechCorp +#### Internship +- 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_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 =` + - 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."}} + + 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 + content string + }{ + {"eng/education.md", work_input}, + {"eng/project.md", work_input}, + {"eng/work.md", work_input}, + {"eng/skill.md", skill_input}, + {"fr/education.md", work_input}, + {"fr/work.md", work_input}, + {"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) + } + }) +} + +func TestSections(t *testing.T) { + rootDir := t.TempDir() + for _, p := range paths { + fullPath := filepath.Join(rootDir, p.relativePath) + if err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm); err != nil { + t.Fatalf("Failed to create directories: %v", err) + } + if err := os.WriteFile(fullPath, []byte(p.content), os.ModePerm); err != nil { + t.Fatalf("Failed to write file: %v", err) + } + } + + t.Run("Should return a valid list of cv", func (t *testing.T) { + got,err := GetCVsFrom(rootDir) + got[1].Print() + 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) + } + + }) +} diff --git a/internal/adapters/input/markdown_tree_reader.go b/internal/adapters/input/markdown_tree_reader.go new file mode 100644 index 0000000..38bf266 --- /dev/null +++ b/internal/adapters/input/markdown_tree_reader.go @@ -0,0 +1,147 @@ +package input + +import ( + core "anemon/internal/core" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" +) + +var hashtagRegex = regexp.MustCompile(`^#+`) + +// `GetCVFrom` takes a root directory and extracts Markdown documents to generate a list of CV type. +// +// This function assumes the directory structure is a tree with a depth of 3, where each leaf +// is a Markdown (.md) document. Each document may contain multiple paragraphs, but the headers +// should not repeat within the same document. +func GetCVsFrom(root string) ([]core.CV, error) { + cvs := make([]core.CV,0) + current_lang := "" + current_sections := make([]core.Section,0) + has_been_inside_dir := false + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { return err } + if info.IsDir(){ + if has_been_inside_dir { + println( cvs[len(cvs)-2].Lang ) + 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") { + println( len(current_sections) ) + 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 +} + +//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 +} + +/* +Parse parses a Markdown-like `paragraph` into a `Paragraph`, +extracting headings and descriptions based on the number of leading hashtags or stars. +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 +} + +// handleLine processes a line based on the number of leading hashtags +func handleLine(n_paragraphe *core.Paragraphe, line string, nbHashtags int) error { + switch { + case nbHashtags > 0 && line[nbHashtags] != ' ': + return fmt.Errorf("Err: cannot parse this md line {%s}, '#' should be followed by space", line) + case nbHashtags == 1: + n_paragraphe.H1 = line[nbHashtags+1:] + case nbHashtags == 2: + n_paragraphe.H2 = line[nbHashtags+1:] + case nbHashtags == 3: + n_paragraphe.H3 = line[nbHashtags+1:] + case nbHashtags == 4: + n_paragraphe.H4 = line[nbHashtags+1:] + case nbHashtags == 0 && len(line) > 1: + if strings.HasPrefix(line, "- ") { + n_paragraphe.Items = append(n_paragraphe.Items, strings.TrimLeft(line, "- ")) + } + case nbHashtags > 4: + return fmt.Errorf("Err: cannot parse this md line {%s}", line) + } + return nil +} diff --git a/internal/core/ports.go b/internal/core/ports.go new file mode 100644 index 0000000..1686ae6 --- /dev/null +++ b/internal/core/ports.go @@ -0,0 +1,52 @@ +package core + +import ( + "fmt" + "strings" +) + +//CV with Language and Sections +type CV struct { + Lang string + Sections []Section +} + +type Section struct { + Title string + Paragraphes []Paragraphe +} + +type Paragraphe struct { + H1 string + H2 string + H3 string + H4 string + Items []string +} + +//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")} + + 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/internal/walker/cv.go b/internal/walker/cv.go index ffd93a4..315d044 100644 --- a/internal/walker/cv.go +++ b/internal/walker/cv.go @@ -12,9 +12,8 @@ import ( func WalkCV(root string) (map[string]map[string]string, error) { fileMap := make(map[string]string) err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } + println(info.Name()) + println(info.IsDir()) if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") { relativePath := strings.TrimSuffix(strings.TrimPrefix(path, root+string(os.PathSeparator)), ".md") content, err := os.ReadFile(path)