diff --git a/pkg/parser/cross_reference_test.go b/pkg/parser/cross_reference_test.go index 76740711..0b4a7397 100644 --- a/pkg/parser/cross_reference_test.go +++ b/pkg/parser/cross_reference_test.go @@ -464,6 +464,110 @@ Here's a reference to the definition of <>.` } Expect(ParseDocument(source)).To(MatchDocument(expected)) }) + + It("natural ref to section with plaintext title", func() { + source := `see <
>. + +== 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*` + 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() { diff --git a/pkg/parser/document_processing_aggregate.go b/pkg/parser/document_processing_aggregate.go index b98e9e1c..558b85a0 100644 --- a/pkg/parser/document_processing_aggregate.go +++ b/pkg/parser/document_processing_aggregate.go @@ -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 { @@ -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) { @@ -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) @@ -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) @@ -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, diff --git a/pkg/renderer/sgml/cross_reference.go b/pkg/renderer/sgml/cross_reference.go index 9315c985..4eb8ac70 100644 --- a/pkg/renderer/sgml/cross_reference.go +++ b/pkg/renderer/sgml/cross_reference.go @@ -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) @@ -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) } diff --git a/pkg/renderer/sgml/elements.go b/pkg/renderer/sgml/elements.go index a4600d97..2e4be3fd 100644 --- a/pkg/renderer/sgml/elements.go +++ b/pkg/renderer/sgml/elements.go @@ -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" ) @@ -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)) diff --git a/pkg/renderer/sgml/html5/cross_reference_test.go b/pkg/renderer/sgml/html5/cross_reference_test.go index 9578a8a7..3d93ed70 100644 --- a/pkg/renderer/sgml/html5/cross_reference_test.go +++ b/pkg/renderer/sgml/html5/cross_reference_test.go @@ -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 <>!` expected := `
-

a title

+

a title

-

with some content linked to a title!

+

with some content linked to a title!

+
+
+
+` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("with custom id to section afterwards", func() { + + source := `see <> + +[#thetitle] +== a *title* +` + expected := `
+

see a title

+
+

a title

+
` @@ -116,6 +135,38 @@ with some content linked to <>!` +` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("natural ref to section with plaintext title", func() { + source := `see <
>. + +== Section 1` + expected := `
+

see Section 1.

+
+
+

Section 1

+
+
+
+` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("natural ref to section with rich title", func() { + source := `see <
>. + +== Section *1*` + expected := `
+

see Section 1.

+
+
+

Section 1

+
+
+
` Expect(RenderHTML(source)).To(MatchHTML(expected)) }) diff --git a/pkg/types/non_alphanumerics_replacement.go b/pkg/types/non_alphanumerics_replacement.go index 2d255bfa..b1bb612c 100644 --- a/pkg/types/non_alphanumerics_replacement.go +++ b/pkg/types/non_alphanumerics_replacement.go @@ -6,27 +6,21 @@ import ( ) // ReplaceNonAlphanumerics replace all non alpha numeric characters with the given `replacement` -func ReplaceNonAlphanumerics(elements []interface{}, prefix, separator string) (string, error) { - replacement, err := replaceNonAlphanumericsOnElements(elements, separator) - if err != nil { - return "", err - } +func ReplaceNonAlphanumerics(elements []interface{}, prefix, separator string) string { + replacement := replaceNonAlphanumericsOnElements(elements, separator) // avoid duplicate prefix if strings.HasPrefix(replacement, prefix) { - return replacement, nil + return replacement } - return prefix + replacement, nil + return prefix + replacement } -func replaceNonAlphanumericsOnElements(elements []interface{}, separator string) (string, error) { +func replaceNonAlphanumericsOnElements(elements []interface{}, separator string) string { result := &strings.Builder{} for i, element := range elements { switch e := element.(type) { case *QuotedText: - r, err := replaceNonAlphanumericsOnElements(e.Elements, separator) - if err != nil { - return "", err - } + r := replaceNonAlphanumericsOnElements(e.Elements, separator) result.WriteString(r) result.WriteString(separator) case *StringElement: @@ -58,8 +52,7 @@ func replaceNonAlphanumericsOnElements(elements []interface{}, separator string) } r := strings.TrimSuffix(result.String(), separator) // avoid duplicate separators - r = strings.ReplaceAll(r, separator+separator, separator) - return r, nil + return strings.ReplaceAll(r, separator+separator, separator) } func replaceNonAlphanumerics(content, replacement string) string { diff --git a/pkg/types/types.go b/pkg/types/types.go index ea8b94b5..fd09164e 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -1862,6 +1862,23 @@ func NewInternalCrossReference(id, label interface{}) (*InternalCrossReference, }, nil } +func (x *InternalCrossReference) ResolveID(attrs Attributes) { + prefix := attrs.GetAsStringWithDefault(AttrIDPrefix, DefaultIDPrefix) + separator := attrs.GetAsStringWithDefault(AttrIDSeparator, DefaultIDSeparator) + switch id := x.ID.(type) { + case []interface{}: + x.ID = ReplaceNonAlphanumerics(id, prefix, separator) + case string: + if strings.Contains(id, " ") || id != strings.ToLower(id) { + x.ID = ReplaceNonAlphanumerics([]interface{}{ + &StringElement{ + Content: id, + }, + }, prefix, separator) + } + } +} + // ExternalCrossReference the struct for Cross References type ExternalCrossReference struct { Location *Location @@ -2458,12 +2475,8 @@ func (s *Section) SetAttributes(attributes Attributes) { } // ResolveID resolves/updates the "ID" attribute in the section (in case the title changed after some document attr substitution) -func (s *Section) ResolveID(attrs map[string]interface{}, refs ElementReferences) error { - base, err := s.resolveID(attrs) - if err != nil { - return err - } - +func (s *Section) ResolveID(attrs Attributes, refs ElementReferences) { + base := s.resolveID(attrs) for i := 1; ; i++ { var id string if i == 1 { @@ -2477,28 +2490,24 @@ func (s *Section) ResolveID(attrs map[string]interface{}, refs ElementReferences break } } - return nil } // resolveID resolves/updates the "ID" attribute in the section (in case the title changed after some document attr substitution) -func (s *Section) resolveID(attrs Attributes) (string, error) { +func (s *Section) resolveID(attrs Attributes) string { if s.Attributes == nil { s.Attributes = Attributes{} } // block attribute if id := s.Attributes.GetAsStringWithDefault(AttrID, ""); id != "" { - return id, nil + return id } log.Debugf("resolving section id") prefix := attrs.GetAsStringWithDefault(AttrIDPrefix, DefaultIDPrefix) separator := attrs.GetAsStringWithDefault(AttrIDSeparator, DefaultIDSeparator) - id, err := ReplaceNonAlphanumerics(s.Title, prefix, separator) - if err != nil { - return "", errors.Wrapf(err, "failed to generate default ID on Section element") - } + id := ReplaceNonAlphanumerics(s.Title, prefix, separator) s.Attributes[AttrID] = id log.Debugf("updated section id to '%s'", s.Attributes[AttrID]) - return id, nil + return id } var _ Referencable = &Section{} diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go index 60f6e656..06e362a0 100644 --- a/pkg/types/types_test.go +++ b/pkg/types/types_test.go @@ -494,9 +494,8 @@ var _ = Describe("section id resolution", func() { }, } // when - err := section.ResolveID(types.Attributes{}, types.ElementReferences{}) + section.ResolveID(types.Attributes{}, types.ElementReferences{}) // then - Expect(err).NotTo(HaveOccurred()) Expect(section.Attributes[types.AttrID]).To(Equal("_foo")) }) @@ -518,9 +517,8 @@ var _ = Describe("section id resolution", func() { }, } // when - err := section.ResolveID(types.Attributes{}, types.ElementReferences{}) + section.ResolveID(types.Attributes{}, types.ElementReferences{}) // then - Expect(err).NotTo(HaveOccurred()) Expect(section.Attributes[types.AttrID]).To(Equal("_a_link_to_httpsfoo_com")) // TODO: should be `httpsfoo` }) @@ -536,9 +534,8 @@ var _ = Describe("section id resolution", func() { }, } // when - err := section.ResolveID(types.Attributes{}, types.ElementReferences{}) + section.ResolveID(types.Attributes{}, types.ElementReferences{}) // then - Expect(err).NotTo(HaveOccurred()) Expect(section.Attributes[types.AttrID]).To(Equal("_foo")) }) }) @@ -557,14 +554,13 @@ var _ = Describe("section id resolution", func() { }, } // when - err := section.ResolveID( + section.ResolveID( types.Attributes{ types.AttrIDPrefix: "custom_", }, types.ElementReferences{}, ) // then - Expect(err).NotTo(HaveOccurred()) Expect(section.Attributes[types.AttrID]).To(Equal("custom_foo")) }) @@ -586,14 +582,13 @@ var _ = Describe("section id resolution", func() { }, } // when - err := section.ResolveID( + section.ResolveID( types.Attributes{ types.AttrIDPrefix: "custom_", }, types.ElementReferences{}, ) // then - Expect(err).NotTo(HaveOccurred()) Expect(section.Attributes[types.AttrID]).To(Equal("custom_a_link_to_httpsfoo_com")) // TODO: should be `httpsfoo` }) }) @@ -614,14 +609,13 @@ var _ = Describe("section id resolution", func() { }, } // when - err := section.ResolveID( + section.ResolveID( types.Attributes{ types.AttrIDPrefix: "custom_", }, types.ElementReferences{}, ) // then - Expect(err).NotTo(HaveOccurred()) Expect(section.Attributes[types.AttrID]).To(Equal("bar")) }) @@ -645,14 +639,13 @@ var _ = Describe("section id resolution", func() { }, } // when - err := section.ResolveID( + section.ResolveID( types.Attributes{ types.AttrIDPrefix: "custom_", }, types.ElementReferences{}, ) // then - Expect(err).NotTo(HaveOccurred()) Expect(section.Attributes[types.AttrID]).To(Equal("bar")) })