Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser/renderer): support source code blocks with language #255

Merged
merged 1 commit into from
Dec 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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