cli/bpmetadata/markdown.go (167 lines of code) (raw):

package bpmetadata import ( "fmt" "regexp" "strconv" "strings" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/ast" ) type mdContent struct { literal string url string listItems []mdListItem } type mdListItem struct { text string url string } var reTimeEstimate = regexp.MustCompile(`(Configuration|Deployment):\s([0-9]+)\smins`) // getMdContent accepts 3 types of content requests and return and mdContent object // with the relevant content info. The 3 scenarios are: // 1: get heading literal by (level and/or order) OR by title // 2: get paragraph content immediately following a heading by (level and/or order) OR by title // 3: get list item content immediately following a heading by (level and/or order) OR by title // A -1 value to headLevel/headOrder enforces the content to be matchd by headTitle func getMdContent(content []byte, headLevel int, headOrder int, headTitle string, getContent bool) (*mdContent, error) { mdDocument := markdown.Parse(content, nil) orderCtr := 0 mdSections := mdDocument.GetChildren() var foundHead bool for _, section := range mdSections { // if the first child is nil, it's a comment and we don't // need to evaluate it if ast.GetFirstChild(section) == nil { continue } currLeaf := ast.GetFirstChild(section).AsLeaf() switch sectionType := section.(type) { case *ast.Heading: foundHead = false if headTitle == string(currLeaf.Literal) { foundHead = true } if headLevel == sectionType.Level { orderCtr++ } if !getContent && (headOrder == orderCtr || foundHead) { return &mdContent{ literal: string(currLeaf.Literal), }, nil } case *ast.Paragraph: if getContent && (headOrder == orderCtr || foundHead) { // check if the content is a link l := ast.GetLastChild(currLeaf.Parent) lNode, isLink := l.(*ast.Link) if isLink { return &mdContent{ literal: string(ast.GetFirstChild(lNode).AsLeaf().Literal), url: string(lNode.Destination), }, nil } return &mdContent{ literal: string(currLeaf.Literal), }, nil } case *ast.List: if getContent && (headOrder == orderCtr || foundHead) { var mdListItems []mdListItem for _, c := range sectionType.Children { var listItem mdListItem // each item is a list with data and metadata about the list item itemConfigs := ast.GetFirstChild(c).AsContainer().Children // if the length of the child node is 1, it is a plain text list item // if the length is greater the 1, it is a list item with a link if len(itemConfigs) == 1 { listItemText := string(itemConfigs[0].AsLeaf().Literal) listItem = mdListItem{ text: listItemText, } } else if len(itemConfigs) > 1 { // the second child node has the link data and metadata listItemLink := itemConfigs[1].(*ast.Link) listItemText := string(ast.GetFirstChild(listItemLink).AsLeaf().Literal) listItem = mdListItem{ text: listItemText, url: string(listItemLink.Destination), } } mdListItems = append(mdListItems, listItem) } return &mdContent{ listItems: mdListItems, }, nil } } } return nil, fmt.Errorf("unable to find md content") } // getDeploymentDuration creates the deployment and configuration time // estimates for the blueprint from README.md func getDeploymentDuration(content []byte, headTitle string) (*BlueprintTimeEstimate, error) { durationDetails, err := getMdContent(content, -1, -1, headTitle, true) if err != nil { return nil, err } matches := reTimeEstimate.FindAllStringSubmatch(durationDetails.literal, -1) if len(matches) == 0 { return nil, fmt.Errorf("unable to find deployment duration") } var timeEstimate BlueprintTimeEstimate for _, m := range matches { // each m[2] will have the time in mins i, err := strconv.ParseInt(m[2], 10, 64) if err != nil { continue } if m[1] == "Configuration" { timeEstimate.ConfigurationSecs = i * 60 continue } if m[1] == "Deployment" { timeEstimate.DeploymentSecs = i * 60 continue } } return &timeEstimate, nil } // getCostEstimate creates the cost estimates from the cost calculator // links provided in README.md func getCostEstimate(content []byte, headTitle string) (*BlueprintCostEstimate, error) { costDetails, err := getMdContent(content, -1, -1, headTitle, true) if err != nil { return nil, err } return &BlueprintCostEstimate{ Description: costDetails.literal, Url: costDetails.url, }, nil } // getArchitctureInfo parses and builds Architecture details from README.md func getArchitctureInfo(content []byte, headTitle string) (*BlueprintArchitecture, error) { mdDocument := markdown.Parse(content, nil) if mdDocument == nil { return nil, fmt.Errorf("unable to parse md content") } children := mdDocument.GetChildren() for _, node := range children { h, isHeading := node.(*ast.Heading) if !isHeading { continue } // check if this is the architecture heading hLiteral := string(ast.GetFirstChild(h).AsLeaf().Literal) if hLiteral != headTitle { continue } //get architecture details infoNode := ast.GetNextNode(h) paraNode, isPara := infoNode.(*ast.Paragraph) if !isPara { continue } t := ast.GetLastChild(paraNode) _, isText := t.(*ast.Text) if !isText { continue } d := strings.TrimLeft(string(t.AsLeaf().Literal), "\n") dList := strings.Split(d, "\n") i := ast.GetPrevNode(t) iNode, isImage := i.(*ast.Image) if isImage { return &BlueprintArchitecture{ Description: dList, DiagramUrl: string(iNode.Destination), }, nil } lNode, isLink := i.(*ast.Link) if isLink { return &BlueprintArchitecture{ Description: dList, DiagramUrl: string(lNode.Destination), }, nil } } return nil, fmt.Errorf("unable to find architecture content") }