Skip to content

Commit

Permalink
feat(parser): support Markdown style quote blocks
Browse files Browse the repository at this point in the history
includes support for first line or all lines prefixed with `> `
also includes support for attribution on the last line,
using the `-- ` prefix

Fixes bytesparadise#561

Signed-off-by: Xavier Coulon <[email protected]>
  • Loading branch information
xcoulon committed May 16, 2020
1 parent 4662a8b commit b21c333
Show file tree
Hide file tree
Showing 8 changed files with 2,175 additions and 1,703 deletions.
2 changes: 1 addition & 1 deletion make/go.mk
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ generate: install-pigeon
generate-optimized: install-pigeon
@echo "generating the parser (optimized)..."
@pigeon -optimize-parser \
-alternate-entrypoints AsciidocDocument,VerbatimDocument,TextDocument,DocumentBlock,FileLocation,IncludedFileLine,InlineLinks,LabeledListItemTerm,VerbatimContent,NormalBlockContent \
-alternate-entrypoints AsciidocDocument,VerbatimDocument,TextDocument,DocumentBlock,FileLocation,IncludedFileLine,InlineLinks,LabeledListItemTerm,NormalBlockContent,VerseBlockContent,MarkdownQuoteBlockAttribution \
-o ./pkg/parser/parser.go ./pkg/parser/parser.peg

.PHONY: build
Expand Down
185 changes: 185 additions & 0 deletions pkg/parser/delimited_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,191 @@ foo
})
})

Context("markdown-style quote blocks", func() {

It("with single marker without author", func() {
source := `> some text
on *multiple lines*`

expected := types.DraftDocument{
Blocks: []interface{}{
types.DelimitedBlock{
Attributes: types.ElementAttributes{},
Kind: types.MarkdownQuote,
Elements: []interface{}{
types.Paragraph{
Attributes: types.ElementAttributes{},
Lines: [][]interface{}{
{
types.StringElement{
Content: "some text",
},
},
{
types.StringElement{
Content: "on ",
},
types.QuotedText{
Kind: types.Bold,
Elements: []interface{}{
types.StringElement{
Content: "multiple lines",
},
},
},
},
},
},
},
},
},
}
Expect(ParseDraftDocument(source)).To(Equal(expected))
})

It("with marker on each line without author", func() {
source := `> some text
> on *multiple lines*`

expected := types.DraftDocument{
Blocks: []interface{}{
types.DelimitedBlock{
Attributes: types.ElementAttributes{},
Kind: types.MarkdownQuote,
Elements: []interface{}{
types.Paragraph{
Attributes: types.ElementAttributes{},
Lines: [][]interface{}{
{
types.StringElement{
Content: "some text",
},
},
{
types.StringElement{
Content: "on ",
},
types.QuotedText{
Kind: types.Bold,
Elements: []interface{}{
types.StringElement{
Content: "multiple lines",
},
},
},
},
},
},
},
},
},
}
Expect(ParseDraftDocument(source)).To(Equal(expected))
})

It("with marker on each line with author", func() {
source := `> some text
> on *multiple lines*
> -- John Doe`
expected := types.DraftDocument{
Blocks: []interface{}{
types.DelimitedBlock{
Attributes: types.ElementAttributes{
types.AttrQuoteAuthor: "John Doe",
},
Kind: types.MarkdownQuote,
Elements: []interface{}{
types.Paragraph{
Attributes: types.ElementAttributes{},
Lines: [][]interface{}{
{
types.StringElement{
Content: "some text",
},
},
{
types.StringElement{
Content: "on ",
},
types.QuotedText{
Kind: types.Bold,
Elements: []interface{}{
types.StringElement{
Content: "multiple lines",
},
},
},
},
},
},
},
},
},
}
Expect(ParseDraftDocument(source)).To(Equal(expected))
})

It("with marker on each line with author and title", func() {
source := `.title
> some text
> on *multiple lines*
> -- John Doe`
expected := types.DraftDocument{
Blocks: []interface{}{
types.DelimitedBlock{
Attributes: types.ElementAttributes{
types.AttrTitle: "title",
types.AttrQuoteAuthor: "John Doe",
},
Kind: types.MarkdownQuote,
Elements: []interface{}{
types.Paragraph{
Attributes: types.ElementAttributes{},
Lines: [][]interface{}{
{
types.StringElement{
Content: "some text",
},
},
{
types.StringElement{
Content: "on ",
},
types.QuotedText{
Kind: types.Bold,
Elements: []interface{}{
types.StringElement{
Content: "multiple lines",
},
},
},
},
},
},
},
},
},
}
Expect(ParseDraftDocument(source)).To(Equal(expected))
})

It("with with author only", func() {
source := `> -- John Doe`
expected := types.DraftDocument{
Blocks: []interface{}{
types.DelimitedBlock{
Attributes: types.ElementAttributes{
types.AttrQuoteAuthor: "John Doe",
},
Kind: types.MarkdownQuote,
Elements: []interface{}{},
},
},
}
Expect(ParseDraftDocument(source)).To(Equal(expected))
})
})

Context("verse blocks", func() {

It("single line verse with author and title", func() {
Expand Down
51 changes: 40 additions & 11 deletions pkg/parser/document_preprocessing.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@ func processFileInclusions(elements []interface{}, attrs types.DocumentAttribute
}
}
// next, parse the elements with the grammar rule that corresponds to the delimited block substitutions (based on its type)
elmts, err = parseDelimitedBlockContent(config.Filename, e.Kind, elmts, options...)
extraAttrs, elmts, err := parseDelimitedBlockContent(config.Filename, e.Kind, elmts, options...)
if err != nil {
return nil, err
}
e.Attributes.AddAll(extraAttrs)
result = append(result, types.DelimitedBlock{
Attributes: e.Attributes,
Kind: e.Kind,
Expand Down Expand Up @@ -123,37 +124,65 @@ func processFileInclusions(elements []interface{}, attrs types.DocumentAttribute

// parseDelimitedBlockContent parses the given verbatim elements, depending on the given delimited block kind.
// May return the elements unchanged, or convert the elements to a source doc and parse with a custom entrypoint
func parseDelimitedBlockContent(filename string, kind types.BlockKind, elements []interface{}, options ...Option) ([]interface{}, error) {
func parseDelimitedBlockContent(filename string, kind types.BlockKind, elements []interface{}, options ...Option) (types.ElementAttributes, []interface{}, error) {
switch kind {
case types.Fenced, types.Listing, types.Literal, types.Source, types.Comment:
// return the verbatim elements
return elements, nil
return types.ElementAttributes{}, elements, nil
case types.Example, types.Quote, types.Sidebar:
return parseDelimitedBlockElements(filename, elements, append(options, Entrypoint("NormalBlockContent"))...)
case types.MarkdownQuote:
return parseMarkdownQuoteBlockElements(filename, elements, append(options, Entrypoint("NormalBlockContent"))...)
case types.Verse:
return parseDelimitedBlockElements(filename, elements, append(options, Entrypoint("VerseBlockContent"))...)
default:
return nil, fmt.Errorf("unexpected kind of delimited block: '%s'", kind)
return nil, nil, fmt.Errorf("unexpected kind of delimited block: '%s'", kind)
}
}

func parseDelimitedBlockElements(filename string, elements []interface{}, options ...Option) ([]interface{}, error) {
verbatim, err := marshal(elements)
func parseDelimitedBlockElements(filename string, elements []interface{}, options ...Option) (types.ElementAttributes, []interface{}, error) {
verbatim, err := serialize(elements)
if err != nil {
return nil, err
return nil, nil, err
}
e, err := ParseReader(filename, verbatim, options...)
if err != nil {
return nil, err
return nil, nil, err
}
if result, ok := e.([]interface{}); ok {
return result, nil
return types.ElementAttributes{}, result, nil
}
return nil, fmt.Errorf("unexpected type of element after parsing the content of a delimited block: '%T'", e)
return nil, nil, fmt.Errorf("unexpected type of element after parsing the content of a delimited block: '%T'", e)
}

func parseMarkdownQuoteBlockElements(filename string, elements []interface{}, options ...Option) (types.ElementAttributes, []interface{}, error) {
author := parseMarkdownQuoteBlockAttribution(filename, elements)
if author != "" {
elements = elements[:len(elements)-1]
}
attrs, lines, err := parseDelimitedBlockElements(filename, elements, options...)
attrs.AddNonEmpty(types.AttrQuoteAuthor, author)
return attrs, lines, err
}

func parseMarkdownQuoteBlockAttribution(filename string, elements []interface{}) string {
// first, check if last line is an attribution (author)
if lastLine, ok := elements[len(elements)-1].(types.VerbatimLine); ok {
buf := bytes.NewBuffer(nil)
buf.WriteString(lastLine.Content)
a, err := ParseReader(filename, buf, Entrypoint("MarkdownQuoteBlockAttribution"))
// assume that the last line is not an author attribution if an error occurred
if err != nil {
return ""
}
if a, ok := a.(string); ok {
return a
}
}
return ""
}

func marshal(elements []interface{}) (io.Reader, error) {
func serialize(elements []interface{}) (io.Reader, error) {
buf := bytes.NewBuffer(nil)
for _, e := range elements {
if r, ok := e.(types.VerbatimLine); ok {
Expand Down
Loading

0 comments on commit b21c333

Please sign in to comment.