From c62a594ed5b66bfccd4d215e8a566053531d42bb Mon Sep 17 00:00:00 2001 From: Xavier Coulon Date: Sun, 19 Apr 2020 15:23:46 +0200 Subject: [PATCH] feat(validator): validate manpage document Add a new `pkg/validator` package which validates the given document, reports problems and in the case of a `manpage` document, changes the doctype to `article` if a problem was found. Fixes #529 Signed-off-by: Xavier Coulon --- .golangci.yml | 4 +- libasciidoc.go | 16 +- libasciidoc_test.go | 420 +++++++++++++++------ pkg/configuration/configuration.go | 9 +- pkg/parser/document_processing.go | 1 + pkg/renderer/html5/document_details.go | 2 +- pkg/renderer/html5/html5.go | 9 +- pkg/renderer/html5/html5_test.go | 2 +- pkg/validator/validator.go | 133 +++++++ pkg/validator/validator_suite_test.go | 13 + pkg/validator/validator_test.go | 488 +++++++++++++++++++++++++ 11 files changed, 977 insertions(+), 120 deletions(-) create mode 100644 pkg/validator/validator.go create mode 100644 pkg/validator/validator_suite_test.go create mode 100644 pkg/validator/validator_test.go diff --git a/.golangci.yml b/.golangci.yml index 04fa88ae..b1f0eb12 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,6 @@ run: skip-dirs: - - pkg/parser/includes - - pkg/renderer/html5/includes + - test/includes skip-files: - pkg/parser/parser.go # generated @@ -10,6 +9,7 @@ linters: - megacheck - govet - gocyclo + - unused enable-all: false disable: - maligned diff --git a/libasciidoc.go b/libasciidoc.go index 2f5dc5fe..b89d7e29 100644 --- a/libasciidoc.go +++ b/libasciidoc.go @@ -12,6 +12,7 @@ import ( "github.com/bytesparadise/libasciidoc/pkg/renderer" htmlrenderer "github.com/bytesparadise/libasciidoc/pkg/renderer/html5" "github.com/bytesparadise/libasciidoc/pkg/types" + "github.com/bytesparadise/libasciidoc/pkg/validator" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -57,8 +58,19 @@ func ConvertToHTML(r io.Reader, output io.Writer, config configuration.Configura if err != nil { return types.Metadata{}, err } - rendererCtx := renderer.NewContext(doc, config) - metadata, err := htmlrenderer.Render(rendererCtx, doc, output) + // validate the document + problems := validator.Validate(&doc) + for _, problem := range problems { + switch problem.Severity { + case validator.Error: + log.Error(problem.Message) + case validator.Warning: + log.Warn(problem.Message) + } + } + // render + ctx := renderer.NewContext(doc, config) + metadata, err := htmlrenderer.Render(ctx, doc, output) if err != nil { return types.Metadata{}, err } diff --git a/libasciidoc_test.go b/libasciidoc_test.go index d9a712df..5463d867 100644 --- a/libasciidoc_test.go +++ b/libasciidoc_test.go @@ -28,35 +28,37 @@ var _ = Describe("documents", func() { log.SetLevel(level) }) - Context("document Body", func() { + lastUpdated := time.Now() - lastUpdated := time.Now() + Context("article", func() { - It("empty document", func() { - // main title alone is not rendered in the body - source := "" - expectedContent := "" - Expect(RenderHTML(source)).To(Equal(expectedContent)) - Expect(RenderHTML5Title(source)).To(Equal("")) - }) + Context("document body", func() { - It("document with no section", func() { - // main title alone is not rendered in the body - source := "= a document title" - expectedTitle := "a document title" - expectedContent := "" - Expect(RenderHTML(source)).To(Equal(expectedContent)) - Expect(RenderHTML5Title(source)).To(Equal(expectedTitle)) - }) + It("empty document", func() { + // main title alone is not rendered in the body + source := "" + expectedContent := "" + Expect(RenderHTML(source)).To(Equal(expectedContent)) + Expect(RenderHTML5Title(source)).To(Equal("")) + }) - It("section levels 0 and 1", func() { - source := `= a document title + It("document with no section", func() { + // main title alone is not rendered in the body + source := "= a document title" + expectedTitle := "a document title" + expectedContent := "" + Expect(RenderHTML(source)).To(Equal(expectedContent)) + Expect(RenderHTML5Title(source)).To(Equal(expectedTitle)) + }) + + It("section levels 0 and 1", func() { + source := `= a document title == Section A a paragraph with *bold content*` - expectedTitle := "a document title" - expectedContent := `
+ expectedTitle := "a document title" + expectedContent := `

Section A

@@ -64,15 +66,15 @@ a paragraph with *bold content*`
` - Expect(RenderHTML(source)).To(Equal(expectedContent)) - Expect(RenderHTML5Title(source)).To(Equal(expectedTitle)) - }) + Expect(RenderHTML(source)).To(Equal(expectedContent)) + Expect(RenderHTML5Title(source)).To(Equal(expectedTitle)) + }) - It("section level 1 with a paragraph", func() { - source := `== Section A + It("section level 1 with a paragraph", func() { + source := `== Section A a paragraph with *bold content*` - expectedContent := `
+ expectedContent := `

Section A

@@ -80,12 +82,12 @@ a paragraph with *bold content*`
` - Expect(RenderHTML(source)).To(Equal(expectedContent)) - Expect(RenderHTML5Title(source)).To(Equal("")) - }) + Expect(RenderHTML(source)).To(Equal(expectedContent)) + Expect(RenderHTML5Title(source)).To(Equal("")) + }) - It("section levels 0, 1 and 3", func() { - source := `= a document title + It("section levels 0, 1 and 3", func() { + source := `= a document title == Section A @@ -94,8 +96,8 @@ a paragraph with *bold content* ==== Section A.a.a a paragraph` - expectedTitle := "a document title" - expectedContent := `
+ expectedTitle := "a document title" + expectedContent := `

Section A

@@ -109,33 +111,33 @@ a paragraph`
` - Expect(RenderHTML(source)).To(Equal(expectedContent)) - Expect(RenderHTML5Title(source)).To(Equal(expectedTitle)) - Expect(DocumentMetadata(source, lastUpdated)).To(Equal(types.Metadata{ - Title: "a document title", - LastUpdated: lastUpdated.Format(configuration.LastUpdatedFormat), - TableOfContents: types.TableOfContents{ - Sections: []types.ToCSection{ - { - ID: "_section_a", - Level: 1, - Title: "Section A", - Children: []types.ToCSection{ - { - ID: "_section_a_a_a", - Level: 3, - Title: "Section A.a.a", - Children: []types.ToCSection{}, + Expect(RenderHTML(source)).To(Equal(expectedContent)) + Expect(RenderHTML5Title(source)).To(Equal(expectedTitle)) + Expect(DocumentMetadata(source, lastUpdated)).To(Equal(types.Metadata{ + Title: "a document title", + LastUpdated: lastUpdated.Format(configuration.LastUpdatedFormat), + TableOfContents: types.TableOfContents{ + Sections: []types.ToCSection{ + { + ID: "_section_a", + Level: 1, + Title: "Section A", + Children: []types.ToCSection{ + { + ID: "_section_a_a_a", + Level: 3, + Title: "Section A.a.a", + Children: []types.ToCSection{}, + }, }, }, }, }, - }, - })) - }) + })) + }) - It("section levels 1, 2, 3 and 2", func() { - source := `= a document title + It("section levels 1, 2, 3 and 2", func() { + source := `= a document title == Section A @@ -148,8 +150,8 @@ a paragraph == Section B a paragraph with _italic content_` - expectedTitle := "a document title" - expectedContent := `
+ expectedTitle := "a document title" + expectedContent := `

Section A

@@ -171,40 +173,40 @@ a paragraph with _italic content_`
` - Expect(RenderHTML(source)).To(Equal(expectedContent)) - Expect(RenderHTML5Title(source)).To(Equal(expectedTitle)) - Expect(DocumentMetadata(source, lastUpdated)).To(Equal(types.Metadata{ - Title: "a document title", - LastUpdated: lastUpdated.Format(configuration.LastUpdatedFormat), - TableOfContents: types.TableOfContents{ - Sections: []types.ToCSection{ - { - ID: "_section_a", - Level: 1, - Title: "Section A", - Children: []types.ToCSection{ - { - ID: "_section_a_a", - Level: 2, - Title: "Section A.a", - Children: []types.ToCSection{}, + Expect(RenderHTML(source)).To(Equal(expectedContent)) + Expect(RenderHTML5Title(source)).To(Equal(expectedTitle)) + Expect(DocumentMetadata(source, lastUpdated)).To(Equal(types.Metadata{ + Title: "a document title", + LastUpdated: lastUpdated.Format(configuration.LastUpdatedFormat), + TableOfContents: types.TableOfContents{ + Sections: []types.ToCSection{ + { + ID: "_section_a", + Level: 1, + Title: "Section A", + Children: []types.ToCSection{ + { + ID: "_section_a_a", + Level: 2, + Title: "Section A.a", + Children: []types.ToCSection{}, + }, }, }, - }, - { - ID: "_section_b", - Level: 1, - Title: "Section B", - Children: []types.ToCSection{}, + { + ID: "_section_b", + Level: 1, + Title: "Section B", + Children: []types.ToCSection{}, + }, }, }, - }, - })) - }) + })) + }) - It("should include adoc file without leveloffset from local file", func() { - source := "include::test/includes/grandchild-include.adoc[]" - expected := `
+ It("should include adoc file without leveloffset from local file", func() { + source := "include::test/includes/grandchild-include.adoc[]" + expected := `

grandchild title

@@ -215,28 +217,28 @@ a paragraph with _italic content_`
` - Expect(RenderHTML(source, configuration.WithFilename("test.adoc"), configuration.WithLastUpdated(lastUpdated))).To(Equal(expected)) - Expect(DocumentMetadata(source, lastUpdated)).To(Equal(types.Metadata{ - Title: "", - LastUpdated: lastUpdated.Format(configuration.LastUpdatedFormat), - TableOfContents: types.TableOfContents{ - Sections: []types.ToCSection{ - { - ID: "_grandchild_title", - Level: 1, - Title: "grandchild title", - Children: []types.ToCSection{}, + Expect(RenderHTML(source, configuration.WithFilename("test.adoc"))).To(Equal(expected)) + Expect(DocumentMetadata(source, lastUpdated)).To(Equal(types.Metadata{ + Title: "", + LastUpdated: lastUpdated.Format(configuration.LastUpdatedFormat), + TableOfContents: types.TableOfContents{ + Sections: []types.ToCSection{ + { + ID: "_grandchild_title", + Level: 1, + Title: "grandchild title", + Children: []types.ToCSection{}, + }, }, }, - }, - })) + })) + }) }) - }) - Context("complete Document ", func() { + Context("complete Document ", func() { - It("using existing file", func() { - expectedContent := ` + It("using existing file", func() { + expectedContent := ` @@ -262,10 +264,212 @@ Last updated {{.LastUpdated}}
` - filename := "test/includes/chapter-a.adoc" - stat, err := os.Stat(filename) - Expect(err).NotTo(HaveOccurred()) - Expect(RenderHTML5Document(filename, configuration.WithCSS("path/to/style.css"), configuration.WithHeaderFooter(true))).To(MatchHTMLTemplate(expectedContent, stat.ModTime())) + filename := "test/includes/chapter-a.adoc" + stat, err := os.Stat(filename) + Expect(err).NotTo(HaveOccurred()) + Expect(RenderHTML5Document(filename, configuration.WithCSS("path/to/style.css"), configuration.WithHeaderFooter(true))).To(MatchHTMLTemplate(expectedContent, stat.ModTime())) + }) + }) + }) + + Context("manpage", func() { + + Context("document body", func() { + + It("should render valid manpage", func() { + source := `= eve(1) +Andrew Stanton +v1.0.0 + +== Name + +eve - analyzes an image to determine if it's a picture of a life form + +== Synopsis + +*eve* [_OPTION_]... _FILE_... + +== Copying + +Copyright (C) 2008 {author}. + +Free use of this software is granted under the terms of the MIT License.` + + expectedContent := `

Name

+
+

eve - analyzes an image to determine if it's a picture of a life form

+
+
+

Synopsis

+
+
+

eve [OPTION]…​ FILE…​

+
+
+
+
+

Copying

+
+
+

Copyright © 2008 Andrew Stanton.
+Free use of this software is granted under the terms of the MIT License.

+
+
+
` + Expect(RenderHTML(source, configuration.WithAttribute(types.AttrDocType, "manpage"))).To(Equal(expectedContent)) + }) + }) + + Context("full document", func() { + + It("should render valid manpage", func() { + source := `= eve(1) +Andrew Stanton +v1.0.0 + +== Name + +eve - analyzes an image to determine if it's a picture of a life form + +== Synopsis + +*eve* [_OPTION_]... _FILE_... + +== Copying + +Copyright (C) 2008 {author}. + +Free use of this software is granted under the terms of the MIT License.` + + expectedContent := ` + + + + + + + + +eve(1) + + + +
+
+

Synopsis

+
+
+

eve [OPTION]…​ FILE…​

+
+
+
+
+

Copying

+
+
+

Copyright © 2008 Andrew Stanton.
+Free use of this software is granted under the terms of the MIT License.

+
+
+
+
+ + +` + Expect(RenderHTML(source, + configuration.WithAttribute(types.AttrDocType, "manpage"), + configuration.WithLastUpdated(lastUpdated), + configuration.WithCSS("path/to/style.css"), + configuration.WithHeaderFooter(true))).To(MatchHTMLTemplate(expectedContent, lastUpdated)) + }) + + It("should render invalid manpage as article", func() { + source := `= eve(1) +Andrew Stanton +v1.0.0 + +== Foo + +eve - analyzes an image to determine if it's a picture of a life form + +== Synopsis + +*eve* [_OPTION_]... _FILE_... + +== Copying + +Copyright (C) 2008 {author}. + +Free use of this software is granted under the terms of the MIT License.` + + expectedContent := ` + + + + + + + + +eve(1) + + + +
+
+

Foo

+
+
+

eve - analyzes an image to determine if it's a picture of a life form

+
+
+
+
+

Synopsis

+
+
+

eve [OPTION]…​ FILE…​

+
+
+
+
+

Copying

+
+
+

Copyright © 2008 Andrew Stanton.
+Free use of this software is granted under the terms of the MIT License.

+
+
+
+
+ + +` + Expect(RenderHTML(source, + configuration.WithAttribute(types.AttrDocType, "manpage"), + configuration.WithLastUpdated(lastUpdated), + configuration.WithCSS("path/to/style.css"), + configuration.WithHeaderFooter(true))).To(MatchHTMLTemplate(expectedContent, lastUpdated)) + }) }) }) diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index ff7bc10d..1e56b51e 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -69,6 +69,13 @@ func WithAttributes(attrs map[string]string) Setting { } } +// WithAttribute function to set an attribute as if it was passed as an argument in the CLI +func WithAttribute(key, value string) Setting { + return func(config *Configuration) { + config.AttributeOverrides[key] = value + } +} + // WithHeaderFooter function to set the `include header/footer` setting in the config func WithHeaderFooter(value bool) Setting { return func(config *Configuration) { @@ -90,7 +97,7 @@ func WithFilename(filename string) Setting { } } -// WithMacro defines the given template to a user macro with the given name +// WithMacroTemplate defines the given template to a user macro with the given name func WithMacroTemplate(name string, t MacroTemplate) Setting { return func(config *Configuration) { config.macros[name] = t diff --git a/pkg/parser/document_processing.go b/pkg/parser/document_processing.go index d5a82b21..81e64b35 100644 --- a/pkg/parser/document_processing.go +++ b/pkg/parser/document_processing.go @@ -53,6 +53,7 @@ func ParseDocument(r io.Reader, config configuration.Configuration) (types.Docum doc.Attributes.AddAll(attrs.All()) // also insert the table of contents doc = includeTableOfContentsPlaceHolder(doc) + // finally if log.IsLevelEnabled(log.DebugLevel) { log.Debug("final document:") spew.Dump(doc) diff --git a/pkg/renderer/html5/document_details.go b/pkg/renderer/html5/document_details.go index 06d0dd38..7de313fe 100644 --- a/pkg/renderer/html5/document_details.go +++ b/pkg/renderer/html5/document_details.go @@ -17,7 +17,7 @@ var documentAuthorDetailsTmpl texttemplate.Template func init() { documentDetailsTmpl = newTextTemplate("document details", `
{{ if .Authors }} {{ .Authors }}{{ end }}{{ if .RevNumber }} -version {{ .RevNumber }},{{ end }}{{ if .RevDate }} +version {{ .RevNumber }}{{ if .RevDate }},{{ end }}{{ end }}{{ if .RevDate }} {{ .RevDate }}{{ end }}{{ if .RevRemark }}
{{ .RevRemark }}{{ end }}
`) diff --git a/pkg/renderer/html5/html5.go b/pkg/renderer/html5/html5.go index 536ffd51..9ed6d528 100644 --- a/pkg/renderer/html5/html5.go +++ b/pkg/renderer/html5/html5.go @@ -27,9 +27,9 @@ func init() { {{ if .Generator }} -{{ end }}{{ if .CSS}} -{{ end }}{{ if .Authors }} -{{ end }} +{{ end }}{{ if .Authors }} +{{ end }}{{ if .CSS}} +{{ end }} {{ escape .Title }} {{ if .IncludeHeader }} @@ -81,7 +81,6 @@ func Render(ctx renderer.Context, doc types.Document, output io.Writer) (types.M if ctx.Config.IncludeHeaderFooter { log.Debugf("Rendering full document...") - revNumber, _ := doc.Attributes.GetAsString("revnumber") err = articleTmpl.Execute(output, struct { Generator string Doctype string @@ -101,7 +100,7 @@ func Render(ctx renderer.Context, doc types.Document, output io.Writer) (types.M Authors: renderAuthors(doc.Attributes.GetAuthors()), Header: string(renderedHeader), Content: htmltemplate.HTML(string(renderedContent)), //nolint: gosec - RevNumber: revNumber, + RevNumber: doc.Attributes.GetAsStringWithDefault("revnumber", ""), LastUpdated: ctx.Config.LastUpdated.Format(configuration.LastUpdatedFormat), CSS: ctx.Config.CSS, IncludeHeader: !doc.Attributes.Has(types.AttrNoHeader), diff --git a/pkg/renderer/html5/html5_test.go b/pkg/renderer/html5/html5_test.go index 4ef94e38..c2e286dc 100644 --- a/pkg/renderer/html5/html5_test.go +++ b/pkg/renderer/html5/html5_test.go @@ -171,8 +171,8 @@ Free use of this software is granted under the terms of the MIT License.` - + eve(1) diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go new file mode 100644 index 00000000..db30ee0a --- /dev/null +++ b/pkg/validator/validator.go @@ -0,0 +1,133 @@ +package validator + +import ( + "strings" + + "github.com/bytesparadise/libasciidoc/pkg/types" +) + +// Validate validates the given document +// May also alter some attributes (eg: doctype from `manpage` to `article`) +func Validate(doc *types.Document) []Problem { + problems := []Problem{} + if doctype, exists := doc.Attributes.GetAsString(types.AttrDocType); exists && doctype == "manpage" { + problems = append(problems, validateManpage(doc)...) + } + return problems +} + +// Problem a problem detected during validation +// Must have a severity and an associated message +// TODO: include element position once available in the AST. +type Problem struct { + Severity Severity + Message string +} + +// Severity the problem severity +type Severity string + +const ( + // Error the severity level for errors. + Error Severity = "Error" + // Warning the severity level for warning + Warning Severity = "Warning" +) + +// validateManpage checks that the document has the expected structure, ie: +// A document header +// a section named `Name` (case insensitive) with a single paragraph +// a section named `Synopsis` +// +// If the document is invalid, its doctype is set to `article` (ie, the default doctype) +func validateManpage(doc *types.Document) []Problem { + problems := []Problem{} + // checks the presence of a header + if header, ok := assertThatElement(doc.Elements[0]).isHeader(); !ok { + problems = append(problems, Problem{ + Severity: Error, + Message: "manpage document is missing a header", + }) + } else if nameSection, ok := assertThatElement(header.Elements[0]).isSection(withLevel(1), withTitle("name")); !ok { + problems = append(problems, Problem{ + Severity: Error, + Message: "manpage document is missing the 'Name' section'", + }) + } else if ok := assertThatElements(nameSection.Elements).haveCount(1); !ok { + problems = append(problems, Problem{ + Severity: Error, + Message: "'Name' section' should contain a single paragraph", + }) + } else if _, ok := assertThatElement(header.Elements[1]).isSection(withLevel(1), withTitle("synopsis")); !ok { + problems = append(problems, Problem{ + Severity: Error, + Message: "manpage document is missing the 'Synopsis' section'", + }) + } + // if any problem found, change the doctype to render the document as a regular article + if len(problems) > 0 { + doc.Attributes.Add(types.AttrDocType, "article") + } + return problems +} + +// assert performs a set of assertions on a given element +func assertThatElement(element interface{}) elementAssertion { + return elementAssertion{ + element: element, + } +} + +type elementAssertion struct { + element interface{} +} + +func (e elementAssertion) isHeader() (types.Section, bool) { + s, ok := e.element.(types.Section) + return s, ok && s.Level == 0 +} + +func (e elementAssertion) isSection(assertions ...sectionAssertion) (types.Section, bool) { + s, ok := e.element.(types.Section) + if !ok { + return types.Section{}, false + } + match := true + for _, assert := range assertions { + match = match && assert(s) + } + return s, match +} + +type sectionAssertion func(s types.Section) bool + +func withTitle(title string) sectionAssertion { + return func(s types.Section) bool { + if len(s.Title) != 1 { + return false + } + str, ok := s.Title[0].(types.StringElement) + return ok && strings.ToLower(str.Content) == title + } +} + +func withLevel(level int) sectionAssertion { + return func(s types.Section) bool { + return s.Level == level + } +} + +// assert performs a set of assertions on a given slice of elements +func assertThatElements(elements []interface{}) elementsAssertion { + return elementsAssertion{ + elements: elements, + } +} + +type elementsAssertion struct { + elements []interface{} +} + +func (e elementsAssertion) haveCount(count int) bool { + return len(e.elements) == count +} diff --git a/pkg/validator/validator_suite_test.go b/pkg/validator/validator_suite_test.go new file mode 100644 index 00000000..49fabbd8 --- /dev/null +++ b/pkg/validator/validator_suite_test.go @@ -0,0 +1,13 @@ +package validator_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestValidator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Validator Suite") +} diff --git a/pkg/validator/validator_test.go b/pkg/validator/validator_test.go new file mode 100644 index 00000000..e1004a7c --- /dev/null +++ b/pkg/validator/validator_test.go @@ -0,0 +1,488 @@ +package validator + +import ( + "github.com/bytesparadise/libasciidoc/pkg/types" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("document validator", func() { + + Context("article", func() { + + It("should not report problems", func() { + // given + doc := types.Document{ + Attributes: types.DocumentAttributes{}, + ElementReferences: types.ElementReferences{}, + Footnotes: []types.Footnote{}, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 0, + Title: []interface{}{ + types.StringElement{ + Content: "foo", + }, + }, + }, + }, + } + + // when + problems := Validate(&doc) + + // then + Expect(problems).To(BeEmpty()) // no problem found + }) + }) + + Context("manpage", func() { + + It("should not report problems", func() { + // given + doc := types.Document{ + Attributes: types.DocumentAttributes{ + types.AttrDocType: "manpage", + }, + ElementReferences: types.ElementReferences{}, + Footnotes: []types.Footnote{}, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 0, + Title: []interface{}{ + types.StringElement{ + Content: "foo", + }, + }, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Name", + }, + }, + Elements: []interface{}{ + types.Paragraph{ + Attributes: types.ElementAttributes{}, + Lines: [][]interface{}{ + { + types.StringElement{ + Content: "a single paragraph to describe the program", + }, + }, + }, + }, + }, + }, + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Synopsis", + }, + }, + Elements: []interface{}{}, + }, + }, + }, + }, + } + + // when + problems := Validate(&doc) + + // then + Expect(problems).To(BeEmpty()) // no problem found + Expect(doc.Attributes.GetAsStringWithDefault(types.AttrDocType, "")).To(Equal("manpage")) // unchanged + }) + + Context("should report problems", func() { + + It("missing header - invalid level", func() { + // given + doc := types.Document{ + Attributes: types.DocumentAttributes{ + types.AttrDocType: "manpage", + }, + ElementReferences: types.ElementReferences{}, + Footnotes: []types.Footnote{}, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, // invalid level + Title: []interface{}{ + types.StringElement{ + Content: "foo", + }, + }, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Name", + }, + }, + Elements: []interface{}{ + types.Paragraph{ + Attributes: types.ElementAttributes{}, + Lines: [][]interface{}{ + { + types.StringElement{ + Content: "a single paragraph to describe the program", + }, + }, + }, + }, + }, + }, + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Synopsis", + }, + }, + Elements: []interface{}{}, + }, + }, + }, + }, + } + + // when + problems := Validate(&doc) + + // then + Expect(problems).To(ContainElement(Problem{ + Severity: Error, + Message: "manpage document is missing a header", + })) + Expect(doc.Attributes.GetAsStringWithDefault(types.AttrDocType, "")).To(Equal("article")) // changed + }) + + It("missing name section - invalid level", func() { + // given + doc := types.Document{ + Attributes: types.DocumentAttributes{ + types.AttrDocType: "manpage", + }, + ElementReferences: types.ElementReferences{}, + Footnotes: []types.Footnote{}, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 0, + Title: []interface{}{ + types.StringElement{ + Content: "foo", + }, + }, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 2, // invalid level + Title: []interface{}{ + types.StringElement{ + Content: "Name", + }, + }, + Elements: []interface{}{ + types.Paragraph{ + Attributes: types.ElementAttributes{}, + Lines: [][]interface{}{ + { + types.StringElement{ + Content: "a single paragraph to describe the program", + }, + }, + }, + }, + }, + }, + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Synopsis", + }, + }, + Elements: []interface{}{}, + }, + }, + }, + }, + } + + // when + problems := Validate(&doc) + + // then + Expect(problems).To(ContainElement(Problem{ + Severity: Error, + Message: "manpage document is missing the 'Name' section'", + })) + Expect(doc.Attributes.GetAsStringWithDefault(types.AttrDocType, "")).To(Equal("article")) // changed + }) + + It("missing name section - invalid title", func() { + // given + doc := types.Document{ + Attributes: types.DocumentAttributes{ + types.AttrDocType: "manpage", + }, + ElementReferences: types.ElementReferences{}, + Footnotes: []types.Footnote{}, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 0, + Title: []interface{}{ + types.StringElement{ + Content: "foo", + }, + }, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "bar", // invalid title + }, + }, + Elements: []interface{}{ + types.Paragraph{ + Attributes: types.ElementAttributes{}, + Lines: [][]interface{}{ + { + types.StringElement{ + Content: "a single paragraph to describe the program", + }, + }, + }, + }, + }, + }, + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Synopsis", + }, + }, + Elements: []interface{}{}, + }, + }, + }, + }, + } + + // when + problems := Validate(&doc) + + // then + Expect(problems).To(ContainElement(Problem{ + Severity: Error, + Message: "manpage document is missing the 'Name' section'", + })) + Expect(doc.Attributes.GetAsStringWithDefault(types.AttrDocType, "")).To(Equal("article")) // changed + }) + + It("missing name section - invalid elements", func() { + // given + doc := types.Document{ + Attributes: types.DocumentAttributes{ + types.AttrDocType: "manpage", + }, + ElementReferences: types.ElementReferences{}, + Footnotes: []types.Footnote{}, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 0, + Title: []interface{}{ + types.StringElement{ + Content: "foo", + }, + }, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Name", + }, + }, + Elements: []interface{}{}, // invalid length + }, + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Synopsis", + }, + }, + Elements: []interface{}{}, + }, + }, + }, + }, + } + + // when + problems := Validate(&doc) + + // then + Expect(problems).To(ContainElement(Problem{ + Severity: Error, + Message: "'Name' section' should contain a single paragraph", + })) + Expect(doc.Attributes.GetAsStringWithDefault(types.AttrDocType, "")).To(Equal("article")) // changed + }) + + It("missing synopsis section - invalid level", func() { + // given + doc := types.Document{ + Attributes: types.DocumentAttributes{ + types.AttrDocType: "manpage", + }, + ElementReferences: types.ElementReferences{}, + Footnotes: []types.Footnote{}, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 0, + Title: []interface{}{ + types.StringElement{ + Content: "foo", + }, + }, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Name", + }, + }, + Elements: []interface{}{ + types.Paragraph{ + Attributes: types.ElementAttributes{}, + Lines: [][]interface{}{ + { + types.StringElement{ + Content: "a single paragraph to describe the program", + }, + }, + }, + }, + }, + }, + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 2, // invalid level + Title: []interface{}{ + types.StringElement{ + Content: "Synopsis", + }, + }, + Elements: []interface{}{}, + }, + }, + }, + }, + } + + // when + problems := Validate(&doc) + + // then + Expect(problems).To(ContainElement(Problem{ + Severity: Error, + Message: "manpage document is missing the 'Synopsis' section'", + })) + Expect(doc.Attributes.GetAsStringWithDefault(types.AttrDocType, "")).To(Equal("article")) // changed + }) + + It("missing synopsis section - invalid title", func() { + // given + doc := types.Document{ + Attributes: types.DocumentAttributes{ + types.AttrDocType: "manpage", + }, + ElementReferences: types.ElementReferences{}, + Footnotes: []types.Footnote{}, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 0, + Title: []interface{}{ + types.StringElement{ + Content: "foo", + }, + }, + Elements: []interface{}{ + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "Name", + }, + }, + Elements: []interface{}{ + types.Paragraph{ + Attributes: types.ElementAttributes{}, + Lines: [][]interface{}{ + { + types.StringElement{ + Content: "a single paragraph to describe the program", + }, + }, + }, + }, + }, + }, + types.Section{ + Attributes: types.ElementAttributes{}, + Level: 1, + Title: []interface{}{ + types.StringElement{ + Content: "bar", // invalid title + }, + }, + Elements: []interface{}{}, + }, + }, + }, + }, + } + + // when + problems := Validate(&doc) + + // then + Expect(problems).To(ContainElement(Problem{ + Severity: Error, + Message: "manpage document is missing the 'Synopsis' section'", + })) + Expect(doc.Attributes.GetAsStringWithDefault(types.AttrDocType, "")).To(Equal("article")) // changed + }) + }) + }) + +})