From c08a7f3e6a1535b6aca009575fd042c27f9134bd Mon Sep 17 00:00:00 2001 From: Xavier Coulon Date: Fri, 23 Jun 2017 23:16:03 +0200 Subject: [PATCH] feat(parser): add support for meta-elements: ID, link and title Support meta-elements alone, they shall be associated with their target element later in the process. Signed-off-by: Xavier Coulon --- parser/asciidoc-grammar.peg | 62 ++++++++++++-- parser/asciidoc_parser_test.go | 152 +++++++++++++++++++++++++++++++-- types/types.go | 96 ++++++++++++++++----- 3 files changed, 272 insertions(+), 38 deletions(-) diff --git a/parser/asciidoc-grammar.peg b/parser/asciidoc-grammar.peg index eac3addc..962e3536 100644 --- a/parser/asciidoc-grammar.peg +++ b/parser/asciidoc-grammar.peg @@ -13,7 +13,7 @@ Document <- lines:Line* EOF { return types.NewDocument(lines.([]interface{})) } -Line <- line:(Heading / ListItem / BlockImage / Inline / EmptyLine) { +Line <- line:(Heading / ListItem / BlockImage / MetaElement / Inline / EmptyLine) { return line, nil } @@ -21,16 +21,18 @@ Heading <- level:("="+) WS+ content:Inline { return types.NewHeading(level, content.(*types.InlineContent)) } +// --------------------- +// Lists +// --------------------- //TODO: Blank lines are required before and after a list //TODO: Additionally, blank lines are permitted, but not required, between list items. ListItem <- WS* ('*' / '-') WS+ content:(Inline) { return types.NewListItem(content.(*types.InlineContent)) } -Inline <- !NEWLINE elements:(BoldQuote / ExternalLink / BlockImage / Word / WS)+ NEWLINE? { - return types.NewInlineContent(elements.([]interface{})) -} - +// --------------------- +// Quotes +// --------------------- BoldQuote <- '*' content:(BoldContent) '*' { return types.NewBoldQuote(content) } @@ -39,18 +41,59 @@ BoldContent <- (BoldContentWord WS+)* BoldContentWord { return string(c.text), nil } +BoldContentWord <- (!NEWLINE !WS !'*' .)+ { + return string(c.text), nil +} + +// --------------------- +// Links +// --------------------- ExternalLink <- url:(URL_SCHEME URL) text:('[' (URL_TEXT)* ']')? { if text != nil { return types.NewExternalLink(url.([]interface{}), text.([]interface{})) } return types.NewExternalLink(url.([]interface{}), nil) - } -BlockImage <- "image::" path:(URL) altText:('[' (URL_TEXT)* ']') { +// --------------------- +// Images +// --------------------- +BlockImage <- "image::" path:(URL) altText:('[' (URL_TEXT)* ']') (NEWLINE/EOF) { return types.NewBlockImage(path.(string), altText.([]interface{})) } +// --------------------- +// Inline content +// --------------------- +Inline <- !NEWLINE elements:(BoldQuote / ExternalLink / Word / WS)+ (NEWLINE/EOF) { + return types.NewInlineContent(elements.([]interface{})) +} + +// --------------------- +// meta-element types +// --------------------- +MetaElement <- meta:(ElementLink / ElementID / ElementTitle) { + return meta, nil +} + +// a link attached to an element, such as a BlockImage +ElementLink <- "[" WS* "link" WS* "=" WS* path:URL WS* "]" (NEWLINE/EOF) { + return types.NewElementLink(path.(string)) +} + +// an id attached to an element, such as a BlockImage +ElementID <- "[" WS* id:(ID) WS* "]" (NEWLINE/EOF) { + return types.NewElementID(id.(string)) +} + +// a title attached to an element, such as a BlockImage +ElementTitle <- "." !WS title:(!NEWLINE .)+ (NEWLINE/EOF) { + return types.NewElementTitle(title.([]interface{})) +} + +// --------------------- +// Base types +// --------------------- Word <- (!NEWLINE !WS .)+ { return string(c.text), nil } @@ -59,14 +102,15 @@ URL <- (!NEWLINE !WS !'[' !']' .)+ { return string(c.text), nil } -URL_TEXT <- (!NEWLINE !'[' !']' .)+ { +ID <- '#' (!NEWLINE !WS !'[' !']' .)+ { return string(c.text), nil } -BoldContentWord <- (!NEWLINE !WS !'*' .)+ { +URL_TEXT <- (!NEWLINE !'[' !']' .)+ { return string(c.text), nil } + EmptyLine <- NEWLINE { return types.NewEmptyLine() } diff --git a/parser/asciidoc_parser_test.go b/parser/asciidoc_parser_test.go index ebf6f521..f980377a 100644 --- a/parser/asciidoc_parser_test.go +++ b/parser/asciidoc_parser_test.go @@ -39,7 +39,7 @@ func TestHeadingOnly(t *testing.T) { assert.EqualValues(t, expectedDocument, actualDocument) } -func TestInvalidHeading1(t *testing.T) { +func TestHeadingInvalid1(t *testing.T) { // given an invalid heading (missing space after '=') actualDocument, errs := ParseString("=a heading") require.Nil(t, errs) @@ -56,7 +56,7 @@ func TestInvalidHeading1(t *testing.T) { log.Debugf("expected document: %s", expectedDocument.String()) assert.EqualValues(t, expectedDocument, actualDocument) } -func TestInvalidHeading2(t *testing.T) { +func TestHeadingInvalid2(t *testing.T) { // given an invalid heading (extra space before '=') actualDocument, errs := ParseString(" = a heading") require.Nil(t, errs) @@ -535,7 +535,7 @@ func TestBlockImageWithAltText(t *testing.T) { assert.EqualValues(t, expectedDocument, actualDocument) } -func TestBlockImageWithIDAndTitleAndDimensions(t *testing.T) { +func TestBlockImageWithDimensionsAndIDLinkTitleMeta(t *testing.T) { // given an inline with an external lin actualDocument, errs := ParseString(`[#img-foobar] .A title to foobar @@ -549,9 +549,9 @@ image::images/foo.png[the foo.png image,600,400]`) height := "400" expectedDocument := &types.Document{ Elements: []types.DocElement{ - &types.InlineContent{Elements: []types.DocElement{&types.StringElement{Content: "[#img-foobar]"}}}, - &types.InlineContent{Elements: []types.DocElement{&types.StringElement{Content: ".A title to foobar"}}}, - &types.InlineContent{Elements: []types.DocElement{&types.StringElement{Content: "[link=http://foo.bar]"}}}, + &types.ElementID{ID: "#img-foobar"}, + &types.ElementTitle{Content: "A title to foobar"}, + &types.ElementLink{Path: "http://foo.bar"}, &types.BlockImage{ Path: "images/foo.png", AltText: &altText, @@ -563,3 +563,143 @@ image::images/foo.png[the foo.png image,600,400]`) log.Debugf("expected document: %s", expectedDocument.String()) assert.EqualValues(t, expectedDocument, actualDocument) } + +func TestElementLink(t *testing.T) { + // given an inline with an external lin + actualDocument, errs := ParseString(`[link=http://foo.bar]`) + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.ElementLink{Path: "http://foo.bar"}, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} + +func TestElementLinkWithSpaces(t *testing.T) { + // given an inline with an element link + actualDocument, errs := ParseString(`[ link = http://foo.bar ]`) + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.ElementLink{Path: "http://foo.bar"}, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} + +func TestElementLinkInvalid(t *testing.T) { + // given an inline with an element link with missing ']' + actualDocument, errs := ParseString(`[ link = http://foo.bar`) + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.InlineContent{ + Elements: []types.DocElement{ + &types.StringElement{Content: "[ link = "}, + &types.ExternalLink{URL: "http://foo.bar"}, + }, + }, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} + +func TestElementID(t *testing.T) { + // given an inline with an element ID + actualDocument, errs := ParseString(`[#img-foobar]`) + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.ElementID{ID: "#img-foobar"}, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} + +func TestElementIDWithSpaces(t *testing.T) { + // given an inline with an element ID + actualDocument, errs := ParseString("[ #img-foobar ]") + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.ElementID{ID: "#img-foobar"}, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} + +func TestElementIDInvalid(t *testing.T) { + // given an inline with an element ID with missing ']' + actualDocument, errs := ParseString(`[#img-foobar`) + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.InlineContent{Elements: []types.DocElement{&types.StringElement{Content: "[#img-foobar"}}}, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} + +func TestElementTitle(t *testing.T) { + // given an inline with an element title + actualDocument, errs := ParseString(`.a title`) + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.ElementTitle{Content: "a title"}, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} + +func TestElementTitleInvalid1(t *testing.T) { + // given an inline with an element title with extra space after '.' + actualDocument, errs := ParseString(". a title") + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.InlineContent{Elements: []types.DocElement{&types.StringElement{Content: ". a title"}}}, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} + +func TestElementTitleInvalid2(t *testing.T) { + // given an inline with an element ID with missing '.' as first character + actualDocument, errs := ParseString(`!a title`) + require.Nil(t, errs) + log.Debugf("actual document: %s", actualDocument.String()) + // then + expectedDocument := &types.Document{ + Elements: []types.DocElement{ + &types.InlineContent{Elements: []types.DocElement{&types.StringElement{Content: "!a title"}}}, + }, + } + log.Debugf("expected document: %s", expectedDocument.String()) + assert.EqualValues(t, expectedDocument, actualDocument) +} diff --git a/types/types.go b/types/types.go index 765c72a7..4c29dc9e 100644 --- a/types/types.go +++ b/types/types.go @@ -129,9 +129,59 @@ func (c InlineContent) String() string { return fmt.Sprintf(" %[1]v", c.Elements, len(c.Elements)) } -// ***************************** +// ----------------------------- +// Meta Elements +// ----------------------------- + +// ElementLink the structure for element links +type ElementLink struct { + Path string +} + +//NewElementLink initializes a new `ElementLink` from the given path +func NewElementLink(path string) (*ElementLink, error) { + log.Debugf("New ElementLink with path=%s", path) + return &ElementLink{Path: path}, nil +} + +func (e ElementLink) String() string { + return fmt.Sprintf(" %s", e.Path) +} + +// ElementID the structure for element IDs +type ElementID struct { + ID string +} + +//NewElementID initializes a new `ElementID` from the given path +func NewElementID(id string) (*ElementID, error) { + log.Debugf("New ElementID with ID=%s", id) + return &ElementID{ID: id}, nil +} + +func (e ElementID) String() string { + return fmt.Sprintf(" %s", e.ID) +} + +// ElementTitle the structure for element IDs +type ElementTitle struct { + Content string +} + +//NewElementTitle initializes a new `ElementTitle` from the given content +func NewElementTitle(content []interface{}) (*ElementTitle, error) { + c := stringify(merge(content)) + log.Debugf("New ElementTitle with content=%s", c) + return &ElementTitle{Content: c}, nil +} + +func (e ElementTitle) String() string { + return fmt.Sprintf(" %s", e.Content) +} + +// ----------------------------- // StringElement -// ***************************** +// ----------------------------- // StringElement the structure for strings type StringElement struct { @@ -147,9 +197,9 @@ func (e StringElement) String() string { return fmt.Sprintf(" %s (%d)", e.Content, len(e.Content)) } -// ***************************** -// BoldQuote -// ***************************** +// ----------------------------- +// Quotes +// ----------------------------- // BoldQuote the structure for the bold quotes type BoldQuote struct { @@ -165,9 +215,9 @@ func (b BoldQuote) String() string { return fmt.Sprintf(" %v", b.Content) } -// ***************************** +// ----------------------------- // EmptyLine -// ***************************** +// ----------------------------- // EmptyLine the structure for the empty lines, which are used to separate logical blocks type EmptyLine struct { @@ -182,9 +232,9 @@ func (e EmptyLine) String() string { return "" } -// ***************************** -// ExternalLink -// ***************************** +// ----------------------------- +// Links +// ----------------------------- // ExternalLink the structure for the external links type ExternalLink struct { @@ -206,11 +256,11 @@ func (e ExternalLink) String() string { return fmt.Sprintf(" %s[%s]", e.URL, e.Text) } -// ***************************** -// BlockImage -// ***************************** +// ----------------------------- +// Images +// ----------------------------- -// BlockImage the structure for the block images +// BlockImage the structure for the block image macross type BlockImage struct { Path string AltText *string @@ -218,7 +268,7 @@ type BlockImage struct { Height *string } -//NewBlockImage initializes a new `BlockImage` +//NewBlockImage initializes a new `BlockImageMacro` func NewBlockImage(path string, altText []interface{}) (*BlockImage, error) { var width, height *string alt := stringify(merge(altText)) @@ -251,16 +301,16 @@ func NewBlockImage(path string, altText []interface{}) (*BlockImage, error) { Height: height}, nil } -func (i BlockImage) String() string { +func (m BlockImage) String() string { var altText, width, height string - if i.AltText != nil { - altText = *i.AltText + if m.AltText != nil { + altText = *m.AltText } - if i.Width != nil { - width = *i.Width + if m.Width != nil { + width = *m.Width } - if i.Height != nil { - height = *i.Height + if m.Height != nil { + height = *m.Height } - return fmt.Sprintf(" %s[%s,w=%s h=%s]", i.Path, altText, width, height) + return fmt.Sprintf(" %s[%s,w=%s h=%s]", m.Path, altText, width, height) }