Skip to content

Commit

Permalink
feat(parser): support natural cross references
Browse files Browse the repository at this point in the history
reference with section title instead of ID

Fixes #1044

Signed-off-by: Xavier Coulon <[email protected]>
  • Loading branch information
xcoulon committed Jun 18, 2022
1 parent 96db47f commit 5be2b8a
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 89 deletions.
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

0 comments on commit 5be2b8a

Please sign in to comment.