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): support natural cross references #1045

Merged
merged 1 commit into from
Jun 18, 2022
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
104 changes: 104 additions & 0 deletions pkg/parser/cross_reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,110 @@ Here's a reference to the definition of <<a_term>>.`
}
Expect(ParseDocument(source)).To(MatchDocument(expected))
})

It("natural ref to section with plaintext title", func() {
source := `see <<Section 1>>.

== Section 1`
sectionTitle := []interface{}{
&types.StringElement{
Content: "Section 1",
},
}
expected := &types.Document{
Elements: []interface{}{
&types.Paragraph{
Elements: []interface{}{
&types.StringElement{
Content: "see ",
},
&types.InternalCrossReference{
ID: "_section_1",
},
&types.StringElement{
Content: ".",
},
},
},
&types.Section{
Level: 1,
Attributes: types.Attributes{
types.AttrID: "_section_1",
},
Title: sectionTitle,
},
},
TableOfContents: &types.TableOfContents{
MaxDepth: 2,
Sections: []*types.ToCSection{
{
ID: "_section_1",
Level: 1,
},
},
},
ElementReferences: types.ElementReferences{
"_section_1": sectionTitle,
},
}
Expect(ParseDocument(source)).To(MatchDocument(expected))
})

It("natural ref to section with rich title", func() {
source := `see <<Section *1*>>.

== Section *1*`
sectionTitle := []interface{}{
&types.StringElement{
Content: "Section ",
},
&types.QuotedText{
Kind: types.SingleQuoteBold,
Elements: []interface{}{
&types.StringElement{
Content: "1",
},
},
},
}
expected := &types.Document{
Elements: []interface{}{
&types.Paragraph{
Elements: []interface{}{
&types.StringElement{
Content: "see ",
},
&types.InternalCrossReference{
ID: "_section_1",
},
&types.StringElement{
Content: ".",
},
},
},
&types.Section{
Level: 1,
Attributes: types.Attributes{
types.AttrID: "_section_1",
},
Title: sectionTitle,
},
},
TableOfContents: &types.TableOfContents{
MaxDepth: 2,
Sections: []*types.ToCSection{
{
ID: "_section_1",
Level: 1,
},
},
},
ElementReferences: types.ElementReferences{
"_section_1": sectionTitle,
},
}
Expect(ParseDocument(source)).To(MatchDocument(expected))
})
})

Context("external references", func() {
Expand Down
72 changes: 35 additions & 37 deletions pkg/parser/document_processing_aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func aggregate(ctx *ParseContext, fragmentStream <-chan types.DocumentFragment)
// TODO: update `toc.MaxDepth` when `AttrTableOfContentsLevels` is declared afterwards
toc := types.NewTableOfContents(attrs.getAsIntWithDefault(types.AttrTableOfContentsLevels, 2))

lvls := &levels{
a := &aggregator{
doc,
}
for f := range fragmentStream {
Expand All @@ -46,15 +46,8 @@ func aggregate(ctx *ParseContext, fragmentStream <-chan types.DocumentFragment)
log.Debugf("setting ToC.MaxDepth to %d", maxDepth)
toc.MaxDepth = maxDepth
}
// yet, retain the element, in case we need it during rendering (eg: `figure-caption`, etc.)
if err := lvls.appendElement(e); err != nil {
return nil, err
}
case *types.FrontMatter:
attrs.setAll(e.Attributes)
if err := lvls.appendElement(e); err != nil {
return nil, err
}
case *types.DocumentHeader:
for _, elmt := range e.Elements {
switch attr := elmt.(type) {
Expand All @@ -70,33 +63,29 @@ func aggregate(ctx *ParseContext, fragmentStream <-chan types.DocumentFragment)
ctx.attributes.unset(attr.Name)
}
}
if err := lvls.appendElement(e); err != nil {
return nil, err
}
// do not add header to ToC
case *types.AttributeReset:
attrs.unset(e.Name)
// yet, retain the element, in case we need it during rendering (eg: `figure-caption`, etc.)
if err := lvls.appendElement(e); err != nil {
return nil, err
}
case *types.BlankLine, *types.SinglelineComment:
// ignore
case *types.Section:
if err := e.ResolveID(attrs.allAttributes(), refs); err != nil {
return nil, err
}
if err := lvls.appendSection(e); err != nil {
return nil, err
}
e.ResolveID(attrs.allAttributes(), refs)
if toc != nil {
toc.Add(e)
}
default:
if err := lvls.appendElement(e); err != nil {
return nil, err
case *types.Paragraph:
for _, elmt := range e.Elements {
switch elmt := elmt.(type) {
case *types.InternalCrossReference:
elmt.ResolveID(attrs.allAttributes())
}
}
}
// also, retain the element
// yet, retain the element, in case we need it during rendering (eg: `figure-caption`, etc.)
if err := a.append(element); err != nil {
return nil, err
}
// also, check if the element has refs
if e, ok := element.(types.Referencable); ok {
e.Reference(refs)
Expand All @@ -114,27 +103,40 @@ func aggregate(ctx *ParseContext, fragmentStream <-chan types.DocumentFragment)
return doc, nil
}

type levels []types.WithElementAddition
type aggregator []types.WithElementAddition

func (a *aggregator) append(e interface{}) error {
switch e := e.(type) {
case *types.Section:
return a.appendSection(e)
default:
return a.appendElement(e)
}
}

func (a *aggregator) appendElement(e interface{}) error {
return (*a)[len(*a)-1].AddElement(e)
}

func (l *levels) appendSection(s *types.Section) error {
func (a *aggregator) appendSection(s *types.Section) error {
// note: section levels start at 0, but first level is root (doc)
if idx, found := l.indexOfParent(s); found {
*l = (*l)[:idx+1] // trim to parent level
if idx, found := a.indexOfParent(s); found {
*a = (*a)[:idx+1] // trim to parent level
}
log.Debugf("adding section with level %d at position %d in levels", s.Level, len(*l))
log.Debugf("adding section with level %d at position %d in levels", s.Level, len(*a))
// append
if err := (*l)[len(*l)-1].AddElement(s); err != nil {
if err := (*a)[len(*a)-1].AddElement(s); err != nil {
return err
}
*l = append(*l, s)
*a = append(*a, s)
return nil
}

// return the index of the parent element for the given section,
// taking account the given section's level, and also gaps in other
// sections (eg: `1,2,4` instead of `0,1,2`)
func (l *levels) indexOfParent(s *types.Section) (int, bool) {
for i, e := range *l {
func (a *aggregator) indexOfParent(s *types.Section) (int, bool) {
for i, e := range *a {
if p, ok := e.(*types.Section); ok {
if p.Level >= s.Level {
log.Debugf("found parent at index %d for section with level %d", i-1, s.Level)
Expand All @@ -146,10 +148,6 @@ func (l *levels) indexOfParent(s *types.Section) (int, bool) {
return -1, false
}

func (l *levels) appendElement(e interface{}) error {
return (*l)[len(*l)-1].AddElement(e)
}

func insertPreamble(doc *types.Document) {
preamble := newPreamble(doc)
// if no element in the preamble, or if no section in the document,
Expand Down
29 changes: 24 additions & 5 deletions pkg/renderer/sgml/cross_reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import (

"github.com/bytesparadise/libasciidoc/pkg/renderer"
"github.com/bytesparadise/libasciidoc/pkg/types"

"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)

func (r *sgmlRenderer) renderInternalCrossReference(ctx *renderer.Context, xref *types.InternalCrossReference) (string, error) {
// log.Debugf("rendering cross reference with ID: %s", xref.ID)
if log.IsLevelEnabled(log.DebugLevel) {
log.Debugf("rendering cross reference with ID: %s", spew.Sdump(xref.ID))
}
result := &strings.Builder{}
var label string
xrefID, ok := xref.ID.(string)
Expand All @@ -24,11 +29,25 @@ func (r *sgmlRenderer) renderInternalCrossReference(ctx *renderer.Context, xref
case string:
label = t
case []interface{}:
renderedContent, err := r.renderPlainText(ctx, t)
if err != nil {
return "", errors.Wrap(err, "error while rendering internal cross reference")
// render as usual except for links as plain text (since the cross reference is already displayed as a link)
buff := &strings.Builder{}
for _, e := range t {
switch e := e.(type) {
case *types.InlineLink:
renderedElement, err := r.renderPlainText(ctx, e)
if err != nil {
return "", err
}
buff.WriteString(renderedElement)
default:
renderedElement, err := r.renderElement(ctx, e)
if err != nil {
return "", err
}
buff.WriteString(renderedElement)
}
}
label = renderedContent
label = buff.String()
default:
return "", errors.Errorf("unable to process internal cross reference to element of type %T", target)
}
Expand Down
5 changes: 4 additions & 1 deletion pkg/renderer/sgml/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/bytesparadise/libasciidoc/pkg/renderer"
"github.com/bytesparadise/libasciidoc/pkg/types"
log "github.com/sirupsen/logrus"

"github.com/pkg/errors"
)
Expand Down Expand Up @@ -118,7 +119,9 @@ func (r *sgmlRenderer) renderElement(ctx *renderer.Context, element interface{})
}

func (r *sgmlRenderer) renderPlainText(ctx *renderer.Context, element interface{}) (string, error) {
// log.Debugf("rendering plain string for element of type %T", element)
if log.IsLevelEnabled(log.DebugLevel) {
log.Debugf("rendering plain string for element of type %T", element)
}
switch e := element.(type) {
case []interface{}:
return r.renderInlineElements(ctx, e, withRenderer(r.renderPlainText))
Expand Down
59 changes: 55 additions & 4 deletions pkg/renderer/sgml/html5/cross_reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,37 @@ var _ = Describe("cross references", func() {

Context("using shorthand syntax", func() {

It("with custom id", func() {
It("with custom id to section above with rich title", func() {

source := `[[thetitle]]
== a title
== a *title*

with some content linked to <<thetitle>>!`
expected := `<div class="sect1">
<h2 id="thetitle">a title</h2>
<h2 id="thetitle">a <strong>title</strong></h2>
<div class="sectionbody">
<div class="paragraph">
<p>with some content linked to <a href="#thetitle">a title</a>!</p>
<p>with some content linked to <a href="#thetitle">a <strong>title</strong></a>!</p>
</div>
</div>
</div>
`
Expect(RenderHTML(source)).To(MatchHTML(expected))
})

It("with custom id to section afterwards", func() {

source := `see <<thetitle>>

[#thetitle]
== a *title*
`
expected := `<div class="paragraph">
<p>see <a href="#thetitle">a <strong>title</strong></a></p>
</div>
<div class="sect1">
<h2 id="thetitle">a <strong>title</strong></h2>
<div class="sectionbody">
</div>
</div>
`
Expand Down Expand Up @@ -116,6 +135,38 @@ with some content linked to <<thewrongtitle>>!`
</div>
</div>
</div>
`
Expect(RenderHTML(source)).To(MatchHTML(expected))
})

It("natural ref to section with plaintext title", func() {
source := `see <<Section 1>>.

== Section 1`
expected := `<div class="paragraph">
<p>see <a href="#_section_1">Section 1</a>.</p>
</div>
<div class="sect1">
<h2 id="_section_1">Section 1</h2>
<div class="sectionbody">
</div>
</div>
`
Expect(RenderHTML(source)).To(MatchHTML(expected))
})

It("natural ref to section with rich title", func() {
source := `see <<Section *1*>>.

== Section *1*`
expected := `<div class="paragraph">
<p>see <a href="#_section_1">Section <strong>1</strong></a>.</p>
</div>
<div class="sect1">
<h2 id="_section_1">Section <strong>1</strong></h2>
<div class="sectionbody">
</div>
</div>
`
Expect(RenderHTML(source)).To(MatchHTML(expected))
})
Expand Down
Loading