diff --git a/EMBEDDING.md b/EMBEDDING.md index a83082b..327e923 100644 --- a/EMBEDDING.md +++ b/EMBEDDING.md @@ -87,6 +87,16 @@ Alternatively, you can specify a fragment using `start` and `end` patterns: Patterns match the first and last lines of the desired fragment. If a pattern is omitted, the fragment will start at the beginning or end at the end of the file, respectively. +To embed a single line, use `line` with the same pattern syntax: + +````markdown + +```java +``` +```` + +The `line` attribute cannot be combined with `start`, `end`, or `fragment`. + ### Pattern syntax The tool supports an extended glob syntax for matching lines: diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index d061cd8..8628455 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -43,6 +43,8 @@ import ( // EndPattern — an optional glob-like pattern. If specified, lines after the matching one // are excluded. // +// LinePattern — an optional glob-like pattern. If specified, only the matching line is embedded. +// // CommentMode — specifies which comments are retained in the embedded code. // // DocumentationFile — a documentation file containing the instruction. @@ -55,6 +57,7 @@ type Instruction struct { Fragment string StartPattern *Pattern EndPattern *Pattern + LinePattern *Pattern CommentMode commentfilter.Mode DocumentationFile string DocumentationLine int @@ -93,6 +96,7 @@ func (e PatternNotFoundError) Error() string { // - start — an optional glob-like pattern. If specified, lines before the matching one // are excluded; // - end — an optional glob-like pattern. If specified, lines after the matching one are excluded. +// - line — an optional glob-like pattern. If specified, only the matching line is embedded. // - comments — an optional comment filtering mode. If omitted, all comments are retained. // // config — a Configuration with all embed-code settings. @@ -104,16 +108,22 @@ func NewInstruction( fragment := attributes["fragment"] startValue := attributes["start"] endValue := attributes["end"] + lineValue := attributes["line"] commentMode, err := commentfilter.ParseMode(attributes["comments"]) if err != nil { return Instruction{}, err } - if fragment != "" && (startValue != "" || endValue != "") { + if fragment != "" && (startValue != "" || endValue != "" || lineValue != "") { + return Instruction{}, + fmt.Errorf(" must NOT specify both a fragment name and start/end/line patterns") + } + if lineValue != "" && (startValue != "" || endValue != "") { return Instruction{}, - fmt.Errorf(" must NOT specify both a fragment name and start/end patterns") + fmt.Errorf(" must NOT specify both a line pattern and start/end patterns") } var end *Pattern + var line *Pattern var start *Pattern if startValue != "" { @@ -124,12 +134,17 @@ func NewInstruction( endPattern := NewPattern(endValue) end = &endPattern } + if lineValue != "" { + linePattern := NewPattern(lineValue) + line = &linePattern + } return Instruction{ CodeFile: codeFile, Fragment: fragment, StartPattern: start, EndPattern: end, + LinePattern: line, CommentMode: commentMode, Configuration: config, }, nil @@ -143,7 +158,7 @@ func (e Instruction) Content() ([]string, error) { if err != nil { return nil, err } - if e.StartPattern != nil || e.EndPattern != nil { + if e.StartPattern != nil || e.EndPattern != nil || e.LinePattern != nil { codeFileReference, err := fragmentation.ResolveCodeFileReference(e.CodeFile, e.Configuration) if err != nil { return nil, err @@ -166,15 +181,28 @@ func (e Instruction) Content() ([]string, error) { // Returns string representation of Instruction. func (e Instruction) String() string { return fmt.Sprintf( - "EmbeddingInstruction[file=`%s`, fragment=`%s`, start=`%s`, end=`%s`, comments=`%s`]", - e.CodeFile, e.Fragment, e.StartPattern, e.EndPattern, e.CommentMode, + "EmbeddingInstruction[file=`%s`, fragment=`%s`, start=`%s`, end=`%s`, line=`%s`, comments=`%s`]", + e.CodeFile, e.Fragment, e.StartPattern, e.EndPattern, e.LinePattern, e.CommentMode, ) } -// Filters and returns a subset of input lines based on start and end patterns. +// Filters and returns a subset of input lines based on start, end, or line patterns. // // lines — a list of strings representing the input lines. func (e Instruction) matchingLines(lines []string, codeFileReference string) ([]string, error) { + if e.LinePattern != nil { + linePosition, err := e.matchGlob( + e.LinePattern, lines, 0, "line", codeFileReference, + ) + if err != nil { + return nil, err + } + requiredLines := []string{lines[linePosition]} + indentation := indent.MaxCommonIndentation(requiredLines) + + return indent.CutIndent(requiredLines, indentation), nil + } + startPosition := 0 if e.StartPattern != nil { var err error diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index 80e7981..0425f98 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -37,6 +37,7 @@ type TestInstructionParams struct { fragment string startGlob string endGlob string + lineGlob string comments string closeTag bool } @@ -205,6 +206,36 @@ var _ = Describe("Instruction", func() { Expect(parsing.FromXML(xmlString, config)).Error().Should(HaveOccurred()) }) + It("should have an error when parsing fragment with line glob", func() { + instructionParams := TestInstructionParams{ + fragment: "fragment", + lineGlob: "public void hello()", + } + xmlString := buildInstruction("org/example/Hello.java", instructionParams) + + Expect(parsing.FromXML(xmlString, config)).Error().Should(HaveOccurred()) + }) + + It("should have an error when parsing line glob with start glob", func() { + instructionParams := TestInstructionParams{ + startGlob: "public class*", + lineGlob: "public void hello()", + } + xmlString := buildInstruction("org/example/Hello.java", instructionParams) + + Expect(parsing.FromXML(xmlString, config)).Error().Should(HaveOccurred()) + }) + + It("should have an error when parsing line glob with end glob", func() { + instructionParams := TestInstructionParams{ + endGlob: "*System.out*", + lineGlob: "public void hello()", + } + xmlString := buildInstruction("org/example/Hello.java", instructionParams) + + Expect(parsing.FromXML(xmlString, config)).Error().Should(HaveOccurred()) + }) + It("should successfully parse XML from start to end glob", func() { instructionParams := TestInstructionParams{ startGlob: "public class*", @@ -257,6 +288,19 @@ var _ = Describe("Instruction", func() { Expect(actualLines[0]).Should(Equal(expectedFirstLine)) }) + It("should embed only the matching line when line glob is specified", func() { + instructionParams := TestInstructionParams{ + lineGlob: "*class*", + } + + actualLines := getXMLExtractionContent( + "org/example/Hello.java", instructionParams, config) + + Expect(actualLines).Should(Equal([]string{ + "public class Hello {", + })) + }) + It("should successfully parse XML by only end glob", func() { instructionParams := TestInstructionParams{ endGlob: "package*", @@ -308,7 +352,7 @@ var _ = Describe("Instruction", func() { Expect(actualLines[1]).Should(MatchRegexp(expectedLastLinePattern)) }) - It("should embed one line when the start and end globs match the same line", func() { + It("should embed one line when the start and end globs match the same line", func() { instructionParams := TestInstructionParams{ startGlob: "*spine.enableJava()*", endGlob: "*.server()", @@ -423,6 +467,10 @@ func buildInstruction(fileName string, params TestInstructionParams) string { endAttr := xmlAttribute("end", params.endGlob) instructionLine += " " + endAttr } + if len(params.lineGlob) > 0 { + lineAttr := xmlAttribute("line", params.lineGlob) + instructionLine += " " + lineAttr + } if len(params.comments) > 0 { commentsAttr := xmlAttribute("comments", params.comments) instructionLine += " " + commentsAttr diff --git a/embedding/parsing/xml_parse.go b/embedding/parsing/xml_parse.go index 330d87e..9d4dcda 100644 --- a/embedding/parsing/xml_parse.go +++ b/embedding/parsing/xml_parse.go @@ -47,6 +47,7 @@ type Item struct { // - start — an optional glob-like pattern. If specified, lines before the matching one // are excluded; // - end — an optional glob-like pattern. If specified, lines after the matching one are excluded. +// - line — an optional glob-like pattern. If specified, only the matching line is embedded. // - comments — an optional comment filtering mode. If omitted, all comments are retained. // // config — a Configuration with all embed-code settings.