Skip to content

Commit

Permalink
feat(parser/renderer): support source code blocks with language
Browse files Browse the repository at this point in the history
fixes #229

Signed-off-by: Xavier Coulon <[email protected]>
  • Loading branch information
xcoulon committed Dec 26, 2018
1 parent 84ea8ee commit 23b53e4
Show file tree
Hide file tree
Showing 10 changed files with 14,977 additions and 13,511 deletions.
13 changes: 12 additions & 1 deletion pkg/parser/asciidoc-grammar.peg
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,12 @@ DocumentElement <- !EOF // when reaching EOF, do not try to parse a new document
// Element Attributes
// ------------------------------------------
ElementAttribute <- &("[" / "." / "#") // skip if the content does not start with one of those characters
attr:(ElementID / ElementTitle / ElementRole / QuoteAttributes / VerseAttributes / AdmonitionMarkerAttribute / HorizontalLayout / AttributeGroup) WS* EOL {
attr:(ElementID / ElementTitle / ElementRole / SourceAttributes / QuoteAttributes / VerseAttributes / AdmonitionMarkerAttribute / HorizontalLayout / AttributeGroup) WS* EOL {
return attr, nil // avoid returning something like `[]interface{}{attr, EOL}`
}

ElementAttributePrefixMatch <- "[" / "." / "#"

// identify all attributes that masquerade a block element into something else.
MasqueradeAttribute <- QuoteAttributes / VerseAttributes

Expand Down Expand Up @@ -187,6 +189,15 @@ AdmonitionMarkerAttribute <- "[" k:(AdmonitionKind) "]" {
return types.NewAdmonitionAttribute(k.(types.AdmonitionKind))
}

// a paragraph or a delimited block may contain source code in a given language
SourceAttributes <- "[source]" {
return types.NewSourceAttributes("")
} / "[source," language:((!NEWLINE !"]" .)+ {
return string(c.text), nil
})"]" {
return types.NewSourceAttributes(language.(string))
}

// one or more attributes. eg: [foo, key1=value1, key2 = value2 , ]
AttributeGroup <- "[" !WS attributes:(GenericAttribute)* "]" {
return types.NewAttributeGroup(attributes.([]interface{}))
Expand Down
28,184 changes: 14,675 additions & 13,509 deletions pkg/parser/asciidoc_parser.go

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions pkg/parser/delimited_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,151 @@ foo
})
})

Context("source blocks", func() {

It("with source attribute only", func() {
actualContent := `[source]
----
require 'sinatra'
get '/hi' do
"Hello World!"
end
----`
expectedResult := types.DelimitedBlock{
Attributes: types.ElementAttributes{
types.AttrKind: types.Source,
types.AttrLanguage: "",
},
Elements: []interface{}{
types.Paragraph{
Attributes: types.ElementAttributes{},
Lines: []types.InlineElements{
{
types.StringElement{
Content: "require 'sinatra'",
},
},
{},
{
types.StringElement{
Content: "get '/hi' do",
},
},
{
types.StringElement{
Content: " \"Hello World!\"",
},
},
{
types.StringElement{
Content: "end",
},
},
},
},
},
}
verify(GinkgoT(), expectedResult, actualContent, parser.Entrypoint("DocumentBlock"))
})

It("with source and languages attributes", func() {
actualContent := `[source,ruby]
----
require 'sinatra'
get '/hi' do
"Hello World!"
end
----`
expectedResult := types.DelimitedBlock{
Attributes: types.ElementAttributes{
types.AttrKind: types.Source,
types.AttrLanguage: "ruby",
},
Elements: []interface{}{
types.Paragraph{
Attributes: types.ElementAttributes{},
Lines: []types.InlineElements{
{
types.StringElement{
Content: "require 'sinatra'",
},
},
{},
{
types.StringElement{
Content: "get '/hi' do",
},
},
{
types.StringElement{
Content: " \"Hello World!\"",
},
},
{
types.StringElement{
Content: "end",
},
},
},
},
},
}
verify(GinkgoT(), expectedResult, actualContent, parser.Entrypoint("DocumentBlock"))
})

It("with id, title, source and languages attributes", func() {
actualContent := `[#id-for-source-block]
[source,ruby]
.app.rb
----
require 'sinatra'
get '/hi' do
"Hello World!"
end
----`
expectedResult := types.DelimitedBlock{
Attributes: types.ElementAttributes{
types.AttrKind: types.Source,
types.AttrLanguage: "ruby",
types.AttrID: "id-for-source-block",
types.AttrTitle: "app.rb",
},
Elements: []interface{}{
types.Paragraph{
Attributes: types.ElementAttributes{},
Lines: []types.InlineElements{
{
types.StringElement{
Content: "require 'sinatra'",
},
},
{},
{
types.StringElement{
Content: "get '/hi' do",
},
},
{
types.StringElement{
Content: " \"Hello World!\"",
},
},
{
types.StringElement{
Content: "end",
},
},
},
},
},
}
verify(GinkgoT(), expectedResult, actualContent, parser.Entrypoint("DocumentBlock"))
})
})

Context("sidebar blocks", func() {

It("sidebar block with paragraph", func() {
Expand Down
34 changes: 34 additions & 0 deletions pkg/renderer/html5/delimited_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

var fencedBlockTmpl texttemplate.Template
var listingBlockTmpl texttemplate.Template
var sourceBlockTmpl texttemplate.Template
var exampleBlockTmpl texttemplate.Template
var admonitionBlockTmpl texttemplate.Template
var quoteBlockTmpl texttemplate.Template
Expand All @@ -37,6 +38,17 @@ func init() {
<div class="content">
<pre>{{ range $index, $element := .Elements }}{{ renderPlainString $ctx $element | printf "%s" }}{{ end }}</pre>
</div>
</div>{{ end }}`,
texttemplate.FuncMap{
"renderPlainString": renderPlainString,
})

sourceBlockTmpl = newTextTemplate("source block",
`{{ $ctx := .Context }}{{ with .Data }}<div {{ if .ID }}id="{{ .ID }}" {{ end }}class="listingblock">{{ if .Title }}
<div class="title">{{ .Title }}</div>{{ end }}
<div class="content">
<pre class="highlight"><code{{ if .Language}} class="language-{{ .Language}}" data-lang="{{ .Language}}"{{ end }}>{{ range $index, $element := .Elements }}{{ renderPlainString $ctx $element | printf "%s" }}{{ end }}</code></pre>
</div>
</div>{{ end }}`,
texttemplate.FuncMap{
"renderPlainString": renderPlainString,
Expand Down Expand Up @@ -159,6 +171,28 @@ func renderDelimitedBlock(ctx *renderer.Context, b types.DelimitedBlock) ([]byte
Elements: elements,
},
})
case types.Source:
previouslyWithin := ctx.SetWithinDelimitedBlock(true)
previouslyInclude := ctx.SetIncludeBlankLine(true)
defer func() {
ctx.SetWithinDelimitedBlock(previouslyWithin)
ctx.SetIncludeBlankLine(previouslyInclude)
}()
language := b.Attributes.GetAsString(types.AttrLanguage)
err = sourceBlockTmpl.Execute(result, ContextualPipeline{
Context: ctx,
Data: struct {
ID string
Title string
Language string
Elements []interface{}
}{
ID: id,
Title: title,
Language: language,
Elements: elements,
},
})
case types.Example:
if k, ok := b.Attributes[types.AttrAdmonitionKind].(types.AdmonitionKind); ok {
err = admonitionBlockTmpl.Execute(result, ContextualPipeline{
Expand Down
70 changes: 70 additions & 0 deletions pkg/renderer/html5/delimited_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,76 @@ some source code
<div class="content">
<pre>some source code</pre>
</div>
</div>`
verify(GinkgoT(), expectedResult, actualContent)
})

})

Context("source blocks", func() {

It("with source attribute only", func() {
actualContent := `[source]
----
require 'sinatra'
get '/hi' do
"Hello World!"
end
----`
expectedResult := `<div class="listingblock">
<div class="content">
<pre class="highlight"><code>require 'sinatra'
get '/hi' do
"Hello World!"
end</code></pre>
</div>
</div>`
verify(GinkgoT(), expectedResult, actualContent)
})

It("with source and languages attributes", func() {
actualContent := `[source,ruby]
----
require 'sinatra'
get '/hi' do
"Hello World!"
end
----`
expectedResult := `<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-ruby" data-lang="ruby">require 'sinatra'
get '/hi' do
"Hello World!"
end</code></pre>
</div>
</div>`
verify(GinkgoT(), expectedResult, actualContent)
})

It("with id, title, source and languages attributes", func() {
actualContent := `[#id-for-source-block]
[source,ruby]
.app.rb
----
require 'sinatra'
get '/hi' do
"Hello World!"
end
----`
expectedResult := `<div id="id-for-source-block" class="listingblock">
<div class="title">app.rb</div>
<div class="content">
<pre class="highlight"><code class="language-ruby" data-lang="ruby">require 'sinatra'
get '/hi' do
"Hello World!"
end</code></pre>
</div>
</div>`
verify(GinkgoT(), expectedResult, actualContent)
})
Expand Down
27 changes: 27 additions & 0 deletions pkg/renderer/html5/paragraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
var paragraphTmpl texttemplate.Template
var admonitionParagraphTmpl texttemplate.Template
var listParagraphTmpl texttemplate.Template
var sourceParagraphTmpl texttemplate.Template
var verseParagraphTmpl texttemplate.Template
var quoteParagraphTmpl texttemplate.Template

Expand Down Expand Up @@ -52,6 +53,16 @@ func init() {
"renderLines": renderLinesAsString,
})

sourceParagraphTmpl = newTextTemplate("source paragraph",
`{{ $ctx := .Context }}{{ with .Data }}<div class="listingblock">
<div class="content">
<pre class="highlight">{{ if .Language }}<code class="language-{{ .Language }}" data-lang="{{ .Language }}">{{ else }}<code>{{ end }}{{ renderLines $ctx .Lines | printf "%s" }}</code></pre>
</div>
</div>{{ end }}`,
texttemplate.FuncMap{
"renderLines": renderPlainString,
})

verseParagraphTmpl = newTextTemplate("verse block", `{{ $ctx := .Context }}{{ with .Data }}<div {{ if .ID }}id="{{ .ID }}" {{ end }}class="verseblock">{{ if .Title }}
<div class="title">{{ .Title }}</div>{{ end }}
<pre class="content">{{ renderElements $ctx .Lines | printf "%s" }}</pre>{{ if .Attribution.First }}
Expand Down Expand Up @@ -115,6 +126,22 @@ func renderParagraph(ctx *renderer.Context, p types.Paragraph) ([]byte, error) {
Lines: p.Lines,
},
})
} else if kind, ok := p.Attributes[types.AttrKind]; ok && kind == types.Source {
log.Debug("rendering source paragraph...")
err = sourceParagraphTmpl.Execute(result, ContextualPipeline{
Context: ctx,
Data: struct {
ID string
Title string
Language string
Lines []types.InlineElements
}{
ID: id,
Title: getTitle(p.Attributes[types.AttrTitle]),
Language: p.Attributes.GetAsString(types.AttrLanguage),
Lines: p.Lines,
},
})
} else if kind, ok := p.Attributes[types.AttrKind]; ok && kind == types.Verse {
log.Debug("rendering verse paragraph...")
var attribution struct {
Expand Down
11 changes: 11 additions & 0 deletions pkg/types/element_attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const (
AttrQuoteAuthor string = "quoteAuthor"
// AttrQuoteTitle attribute for the title of a verse
AttrQuoteTitle string = "quoteTitle"
// AttrSource the "source" attribute for a source block or a source paragraph (this is a placeholder, ie, it does not expect any value for this attribute)
AttrSource string = "source"
// AttrLanguage the associated "language" attribute for a source block or a source paragraph
AttrLanguage string = "language"
)
Expand Down Expand Up @@ -133,6 +135,15 @@ func NewLiteralAttribute() (ElementAttributes, error) {
return ElementAttributes{AttrKind: Literal}, nil
}

// NewSourceAttributes initializes a new attribute map with two entries, one for the kind of element ("source") and another optional one for the language of the source code
func NewSourceAttributes(language string) (ElementAttributes, error) {
log.Debugf("initializing a new source attribute (language='%s')", language)
return ElementAttributes{
AttrKind: Source,
AttrLanguage: strings.TrimSpace(language),
}, nil
}

// WithAttributes set the attributes on the given elements if its type is supported, otherwise returns an error
func WithAttributes(element interface{}, attributes []interface{}) (interface{}, error) {
attrbs := NewElementAttributes(attributes)
Expand Down
Loading

0 comments on commit 23b53e4

Please sign in to comment.