diff --git a/ast/node.go b/ast/node.go index fff74b0..5d95e41 100644 --- a/ast/node.go +++ b/ast/node.go @@ -39,6 +39,16 @@ const ( DocumentMatterBack ) +// CitationTypes holds the type of a citation, informative, normative or suppressed +type CitationTypes int + +const ( + CitationTypeNone CitationTypes = iota + CitationTypeSuppressed + CitationTypeInformative + CitationTypeNormative +) + // Node defines an ast node type Node interface { AsContainer() *Container @@ -240,6 +250,14 @@ type CrossReference struct { Destination []byte // Destination is where the reference points to } +// Citation is a citation node. +type Citation struct { + Leaf + + Destination [][]byte // Destination is where the citation points to. Multiple ones are allowed. + Type []CitationTypes // 1:1 mapping of destination and citation type +} + // Image represents markdown image node type Image struct { Container diff --git a/html/renderer.go b/html/renderer.go index 9968278..33d299c 100644 --- a/html/renderer.go +++ b/html/renderer.go @@ -849,6 +849,24 @@ func (r *Renderer) matter(w io.Writer, node *ast.DocumentMatter, entering bool) r.documentMatter = node.Matter } +func (r *Renderer) citation(w io.Writer, node *ast.Citation) { + for i, c := range node.Destination { + attr := []string{`class="none"`} + switch node.Type[i] { + case ast.CitationTypeNormative: + attr[0] = `class="normative"` + case ast.CitationTypeInformative: + attr[0] = `class="informative"` + case ast.CitationTypeSuppressed: + attr[0] = `class="suppressed"` + } + r.outTag(w, "") + } +} + // RenderNode renders a markdown node to HTML func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus { if r.opts.RenderNodeHook != nil { @@ -882,6 +900,8 @@ func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Wal case *ast.CrossReference: link := &ast.Link{Destination: node.Destination} r.link(w, link, entering) + case *ast.Citation: + r.citation(w, node) case *ast.Image: if r.opts.Flags&SkipImages != 0 { return ast.SkipChildren diff --git a/parser/citation.go b/parser/citation.go new file mode 100644 index 0000000..a97108c --- /dev/null +++ b/parser/citation.go @@ -0,0 +1,72 @@ +package parser + +import ( + "bytes" + + "github.com/gomarkdown/markdown/ast" +) + +// citation parses a citation. In its most simple form [@ref], we allow multiple +// being separated by semicolons and a sub reference inside ala pandoc: [@ref p. 23]. +// Each citation can have a modifier: !, ? or - wich mean: +// +// ! - normative +// ? - formative +// - - suppressed +func citation(p *Parser, data []byte, offset int) (int, ast.Node) { + // look for the matching closing bracket + i := offset + 1 + for level := 1; level > 0 && i < len(data); i++ { + switch { + case data[i] == '\n': + // no newlines allowed. + return 0, nil + + case data[i-1] == '\\': + continue + + case data[i] == '[': + level++ + + case data[i] == ']': + level-- + if level <= 0 { + i-- // compensate for extra i++ in for loop + } + } + } + + if i >= len(data) { + return 0, nil + } + + node := &ast.Citation{} + + citations := bytes.Split(data[1:i], []byte(";")) + for _, citation := range citations { + citation = bytes.TrimSpace(citation) + j := 0 + if citation[j] != '@' { + // not a citation, drop out entirely. + return 0, nil + } + + citeType := ast.CitationTypeInformative + j = 1 + switch citation[j] { + case '!': + citeType = ast.CitationTypeNormative + j++ + case '?': + citeType = ast.CitationTypeInformative + j++ + case '-': + citeType = ast.CitationTypeSuppressed + j++ + } + node.Destination = append(node.Destination, citation[j:]) + node.Type = append(node.Type, citeType) + } + + return i + 1, node +} diff --git a/parser/inline.go b/parser/inline.go index de6ebfc..52f1163 100644 --- a/parser/inline.go +++ b/parser/inline.go @@ -177,6 +177,7 @@ const ( linkImg linkDeferredFootnote linkInlineFootnote + linkCitation ) func isReferenceStyleLink(data []byte, pos int, t linkType) bool { @@ -200,7 +201,7 @@ func maybeInlineFootnote(p *Parser, data []byte, offset int) (int, ast.Node) { return 0, nil } -// '[': parse a link or an image or a footnote +// '[': parse a link or an image or a footnote or a citation func link(p *Parser, data []byte, offset int) (int, ast.Node) { // no links allowed inside regular links, footnote, and deferred footnotes if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { @@ -217,6 +218,10 @@ func link(p *Parser, data []byte, offset int) (int, ast.Node) { case offset >= 0 && data[offset] == '!': t = linkImg offset++ + // [@citation], [@-citation], [@?citation], [@!citation] + case p.extensions&Mmark != 0 && len(data)-1 > offset && data[offset+1] == '@': + t = linkCitation + // [text] == regular link // ^[text] == inline footnote // [^refId] == deferred footnote case p.extensions&Footnotes != 0: @@ -226,13 +231,16 @@ func link(p *Parser, data []byte, offset int) (int, ast.Node) { } else if len(data)-1 > offset && data[offset+1] == '^' { t = linkDeferredFootnote } - // [text] == regular link default: t = linkNormal } data = data[offset:] + if t == linkCitation { + return citation(p, data, 0) + } + var ( i = 1 noteID int diff --git a/testdata/mmark.test b/testdata/mmark.test index e00ef07..b46740e 100644 --- a/testdata/mmark.test +++ b/testdata/mmark.test @@ -44,3 +44,24 @@ Caption: Shakespeare.
Shakespeare.
+--- +# Test Citations +[@RFC1034] +--- +

Test_Citations

+ +

[RFC1034]

+--- +# Test Multiple Citations +[@RFC1034; @!RFC1035] +--- +

Test Multiple Citations

+ +

[RFC1034][RFC1035]

+--- +# Test Multiple Citations with modifier +[@-RFC1034] [@?RFC1035] +--- +

Test Multiple Citations with modifier

+ +

[RFC1034] [RFC1035]