From f7e80fd1dd90beef93678b56d4625da07968f070 Mon Sep 17 00:00:00 2001 From: Xavier Coulon Date: Sun, 21 Nov 2021 20:20:20 +0100 Subject: [PATCH] fix(parser/renderer): support cross references in elements defined later (#864) covers sections, tables, delimited blocks and paragraphs. Fixes #863 Signed-off-by: Xavier Coulon --- pkg/parser/cross_reference_test.go | 197 +++++++++++++++++- pkg/parser/delimited_block_literal_test.go | 9 + pkg/parser/delimited_block_source_test.go | 3 + pkg/parser/document_processing_aggregate.go | 6 + pkg/parser/paragraph_test.go | 9 + pkg/parser/table_test.go | 3 + pkg/renderer/sgml/cross_reference.go | 9 +- .../sgml/html5/cross_reference_test.go | 43 +++- pkg/types/types.go | 36 +++- 9 files changed, 307 insertions(+), 8 deletions(-) diff --git a/pkg/parser/cross_reference_test.go b/pkg/parser/cross_reference_test.go index 9017f030..f81d4c94 100644 --- a/pkg/parser/cross_reference_test.go +++ b/pkg/parser/cross_reference_test.go @@ -14,7 +14,7 @@ var _ = Describe("cross references", func() { Context("internal references", func() { - It("cross reference with custom id alone", func() { + It("with custom id alone", func() { source := `[[thetitle]] == a title @@ -57,7 +57,7 @@ with some content linked to <>!` Expect(ParseDocument(source)).To(MatchDocument(expected)) }) - It("cross reference with custom id and label", func() { + It("with custom id and label", func() { source := `[[thetitle]] == a title @@ -100,6 +100,199 @@ with some content linked to <>!` } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) + + It("to section defined later in the document", func() { + source := `a reference to <
> + +[#section] +== A section with a link to https://example.com + +some content` + title := []interface{}{ + &types.StringElement{ + Content: "A section with a link to ", + }, + &types.InlineLink{ + Location: &types.Location{ + Scheme: "https://", + Path: []interface{}{ + &types.StringElement{ + Content: "example.com", + }, + }, + }, + }, + } + expected := &types.Document{ + Elements: []interface{}{ + &types.Paragraph{ + Elements: []interface{}{ + &types.StringElement{ + Content: "a reference to ", + }, + &types.InternalCrossReference{ + ID: "section", + }, + }, + }, + &types.Section{ + Attributes: types.Attributes{ + types.AttrID: "section", + types.AttrCustomID: true, + }, + Level: 1, + Title: title, + Elements: []interface{}{ + &types.Paragraph{ + Elements: []interface{}{ + &types.StringElement{ + Content: "some content", + }, + }, + }, + }, + }, + }, + ElementReferences: types.ElementReferences{ + "section": title, + }, + } + Expect(ParseDocument(source)).To(MatchDocument(expected)) + }) + + It("to delimited block defined later in the document", func() { + source := `a reference to <> + +[#block] +.The block +---- +some content +----` + + expected := &types.Document{ + Elements: []interface{}{ + &types.Paragraph{ + Elements: []interface{}{ + &types.StringElement{ + Content: "a reference to ", + }, + &types.InternalCrossReference{ + ID: "block", + }, + }, + }, + &types.DelimitedBlock{ + Kind: types.Listing, + Attributes: types.Attributes{ + types.AttrID: "block", + types.AttrTitle: "The block", + }, + Elements: []interface{}{ + &types.StringElement{ + Content: "some content", + }, + }, + }, + }, + ElementReferences: types.ElementReferences{ + "block": "The block", + }, + } + Expect(ParseDocument(source)).To(MatchDocument(expected)) + }) + + It("to paragraph defined later in the document", func() { + source := `a reference to <> + +[#a-paragraph] +.another paragraph +some content` + expected := &types.Document{ + Elements: []interface{}{ + &types.Paragraph{ + Elements: []interface{}{ + &types.StringElement{ + Content: "a reference to ", + }, + &types.InternalCrossReference{ + ID: "a-paragraph", + }, + }, + }, + &types.Paragraph{ + Attributes: types.Attributes{ + types.AttrID: "a-paragraph", + types.AttrTitle: "another paragraph", + }, + Elements: []interface{}{ + &types.StringElement{ + Content: "some content", + }, + }, + }, + }, + ElementReferences: types.ElementReferences{ + "a-paragraph": "another paragraph", + }, + } + Expect(ParseDocument(source)).To(MatchDocument(expected)) + }) + + It("to table defined later in the document", func() { + source := `a reference to <> + +[#table] +.The table +|=== +| A | B +|=== +` + + expected := &types.Document{ + Elements: []interface{}{ + &types.Paragraph{ + Elements: []interface{}{ + &types.StringElement{ + Content: "a reference to ", + }, + &types.InternalCrossReference{ + ID: "table", + }, + }, + }, + &types.Table{ + Attributes: types.Attributes{ + types.AttrID: "table", + types.AttrTitle: "The table", + }, + Rows: []*types.TableRow{ + { + Cells: []*types.TableCell{ + { + Elements: []interface{}{ + &types.StringElement{ + Content: "A ", + }, + }, + }, + { + Elements: []interface{}{ + &types.StringElement{ + Content: "B", + }, + }, + }, + }, + }, + }, + }, + }, + ElementReferences: types.ElementReferences{ + "table": "The table", + }, + } + Expect(ParseDocument(source)).To(MatchDocument(expected)) + }) }) Context("external references", func() { diff --git a/pkg/parser/delimited_block_literal_test.go b/pkg/parser/delimited_block_literal_test.go index c1c5c80c..128af832 100644 --- a/pkg/parser/delimited_block_literal_test.go +++ b/pkg/parser/delimited_block_literal_test.go @@ -108,6 +108,9 @@ a normal paragraph.` }, }, }, + ElementReferences: types.ElementReferences{ + "ID": "title", + }, } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) @@ -166,6 +169,9 @@ a normal paragraph.` }, }, }, + ElementReferences: types.ElementReferences{ + "ID": "title", + }, } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) @@ -234,6 +240,9 @@ a normal paragraph.` }, }, }, + ElementReferences: types.ElementReferences{ + "ID": "title", + }, } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) diff --git a/pkg/parser/delimited_block_source_test.go b/pkg/parser/delimited_block_source_test.go index 3ed33eae..18302046 100644 --- a/pkg/parser/delimited_block_source_test.go +++ b/pkg/parser/delimited_block_source_test.go @@ -110,6 +110,9 @@ end`, }, }, }, + ElementReferences: types.ElementReferences{ + "id-for-source-block": "app.rb", + }, } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) diff --git a/pkg/parser/document_processing_aggregate.go b/pkg/parser/document_processing_aggregate.go index 0b3ca6f7..d67c8c7a 100644 --- a/pkg/parser/document_processing_aggregate.go +++ b/pkg/parser/document_processing_aggregate.go @@ -92,6 +92,12 @@ func aggregate(ctx *ParseContext, fragmentStream <-chan types.DocumentFragment) return nil, nil, err } } + // also, check if the element has refs + if e, ok := element.(types.Referencable); ok { + if id, title := e.Ref(); id != "" && title != nil { + refs[id] = title + } + } } } diff --git a/pkg/parser/paragraph_test.go b/pkg/parser/paragraph_test.go index 57e8bc78..83e988f2 100644 --- a/pkg/parser/paragraph_test.go +++ b/pkg/parser/paragraph_test.go @@ -1530,6 +1530,9 @@ NOTE: this is a note.` }, }, }, + ElementReferences: types.ElementReferences{ + "cookie": "chocolate", + }, } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) @@ -1587,6 +1590,9 @@ this is a }, }, }, + ElementReferences: types.ElementReferences{ + "cookie": "chocolate", + }, } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) @@ -1722,6 +1728,9 @@ I am a verse paragraph.` }, }, }, + ElementReferences: types.ElementReferences{ + "universal": "universe", + }, } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) diff --git a/pkg/parser/table_test.go b/pkg/parser/table_test.go index 4f621fc4..17b13ee5 100644 --- a/pkg/parser/table_test.go +++ b/pkg/parser/table_test.go @@ -348,6 +348,9 @@ var _ = Describe("tables", func() { }, }, }, + ElementReferences: types.ElementReferences{ + "anchor": "table title", + }, } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) diff --git a/pkg/renderer/sgml/cross_reference.go b/pkg/renderer/sgml/cross_reference.go index af92eca5..b4eaf96d 100644 --- a/pkg/renderer/sgml/cross_reference.go +++ b/pkg/renderer/sgml/cross_reference.go @@ -20,13 +20,16 @@ func (r *sgmlRenderer) renderInternalCrossReference(ctx *renderer.Context, xref if xrefLabel, ok := xref.Label.(string); ok { label = xrefLabel } else if target, found := ctx.ElementReferences[xrefID]; found { - if t, ok := target.([]interface{}); ok { - renderedContent, err := r.renderElement(ctx, t) + switch t := target.(type) { + 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") } label = renderedContent - } else { + default: return "", errors.Errorf("unable to process internal cross reference to element of type %T", target) } } else { diff --git a/pkg/renderer/sgml/html5/cross_reference_test.go b/pkg/renderer/sgml/html5/cross_reference_test.go index c97a1f2e..6451521a 100644 --- a/pkg/renderer/sgml/html5/cross_reference_test.go +++ b/pkg/renderer/sgml/html5/cross_reference_test.go @@ -11,7 +11,7 @@ var _ = Describe("cross references", func() { Context("internal references", func() { - It("cross reference with custom id", func() { + It("with custom id", func() { source := `[[thetitle]] == a title @@ -29,7 +29,7 @@ with some content linked to <>!` Expect(RenderHTML(source)).To(MatchHTML(expected)) }) - It("cross reference with custom id and label", func() { + It("custom id and label", func() { source := `[[thetitle]] == a title @@ -46,6 +46,45 @@ with some content linked to <>!` Expect(RenderHTML(source)).To(MatchHTML(expected)) }) + It("to paragraph defined later in the document", func() { + source := `a reference to <> + +[#a-paragraph] +.another paragraph +some content` + expected := `
+

a reference to another paragraph

+
+
+
another paragraph
+

some content

+
+` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("to section defined later in the document", func() { + source := `a reference to <
> + +[#section] +== A section with a link to https://example.com + +some content` + expected := ` +
+

A section with a link to https://example.com

+
+
+

some content

+
+
+
+` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + It("invalid section reference", func() { source := `[[thetitle]] diff --git a/pkg/types/types.go b/pkg/types/types.go index 9bd8685b..eaad92ee 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -64,6 +64,10 @@ type BlockWithLocation interface { SetLocation(*Location) // TODO: unused? } +type Referencable interface { + Ref() (string, interface{}) +} + // ------------------------------------------ // Substitution support // ------------------------------------------ @@ -1503,6 +1507,14 @@ func (p *Paragraph) mapAttributes() { } } +var _ Referencable = &Paragraph{} + +func (p *Paragraph) Ref() (string, interface{}) { + id := p.Attributes.GetAsStringWithDefault(AttrID, "") + title := p.Attributes[AttrTitle] + return id, title +} + var _ WithFootnotes = &Paragraph{} // SubstituteFootnotes replaces the footnotes in the paragraph lines @@ -1968,6 +1980,14 @@ func (b *DelimitedBlock) mapAttributes() { } } +var _ Referencable = &DelimitedBlock{} + +func (b *DelimitedBlock) Ref() (string, interface{}) { + id := b.Attributes.GetAsStringWithDefault(AttrID, "") + title := b.Attributes[AttrTitle] + return id, title +} + // TODO: not needed? const ( // AttrLiteralBlockType the type of literal block, ie, how it was parsed @@ -2040,7 +2060,6 @@ func (s *Section) ResolveID(attrs map[string]interface{}, refs ElementReferences log.Debugf("updated section id to '%s' (to avoid duplicate refs)", s.Attributes[AttrID]) } if _, exists := refs[id]; !exists { - refs[id] = s.Title s.Attributes[AttrID] = id break } @@ -2077,6 +2096,13 @@ func (s *Section) resolveID(attrs Attributes) (string, error) { return id, nil } +var _ Referencable = &Section{} + +func (s *Section) Ref() (string, interface{}) { + id := s.Attributes.GetAsStringWithDefault(AttrID, "") + return id, s.Title +} + var _ WithElementAddition = &Section{} // AddElement adds the given child element to this section @@ -3021,6 +3047,14 @@ func (t *Table) SetAttributes(attributes Attributes) { } } +var _ Referencable = &Table{} + +func (t *Table) Ref() (string, interface{}) { + id := t.Attributes.GetAsStringWithDefault(AttrID, "") + title := t.Attributes[AttrTitle] + return id, title +} + type HAlign string const (