Merge pull request #260 from mrueg/switch-to-goldmark

Replace blackfriday with goldmark
This commit is contained in:
Manuel Rüger 2023-03-28 16:54:04 +02:00 committed by GitHub
commit 93218f1e69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 308 additions and 222 deletions

2
go.mod
View File

@ -4,7 +4,6 @@ go 1.19
require (
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/kovetskiy/blackfriday/v2 v2.3.0
github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69
github.com/kovetskiy/ko v1.6.1
github.com/kovetskiy/lorg v1.2.0
@ -12,6 +11,7 @@ require (
github.com/reconquest/pkg v1.3.0
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4
github.com/stretchr/testify v1.8.1
github.com/yuin/goldmark v1.5.4
golang.org/x/tools v0.7.0
gopkg.in/yaml.v3 v3.0.1
)

4
go.sum
View File

@ -8,8 +8,6 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/kovetskiy/blackfriday/v2 v2.3.0 h1:KKABLPopQ2+DWKtM/ifx0RijGz09mNlCuEcZy5KvZVA=
github.com/kovetskiy/blackfriday/v2 v2.3.0/go.mod h1:ES7tjNJdnHp1h8dib5cmoa//rgvQeYrtzGzGM/Kozk4=
github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69 h1:vn82v0gKhTTm67znr7nxYBNW4mJ8zfY7dywZivUy3tY=
github.com/kovetskiy/gopencils v0.0.0-20230119081704-a73db75b2f69/go.mod h1:t7LFI5v8Q5+nl9sqId9PS0C9H9F4c5d4XlhkLve1MCM=
github.com/kovetskiy/ko v1.6.1 h1:EO5v6CrW6x6vzxo7CKbN0r+foIRjz06U6wVSgxUVqMc=
@ -41,6 +39,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3 h1:BhVaeQJc3xalHGONn215FylzuxdQBIT3d/aRjDg4nXQ=
github.com/zazab/zhash v0.0.0-20210630080733-6e809466f8d3/go.mod h1:NtepZ8TEXErPsmQDMUoN72f8aIy4+xNinSJ3f1giess=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=

View File

@ -1,14 +1,21 @@
package mark
import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
bf "github.com/kovetskiy/blackfriday/v2"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/reconquest/pkg/log"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
var reBlockDetails = regexp.MustCompile(
@ -17,20 +24,69 @@ var reBlockDetails = regexp.MustCompile(
`^(?:(\w*)|-)\s*\b(\S.*?\S?)??\s*(?:\btitle\s+(\S.*\S?))?$`,
)
type BlockQuoteLevelMap map[*bf.Node]int
// Define BlockQuoteType enum
type BlockQuoteType int
func (m BlockQuoteLevelMap) Level(node *bf.Node) int {
const (
Info BlockQuoteType = iota
Note
Warn
None
)
func (t BlockQuoteType) String() string {
return []string{"info", "note", "warn", "none"}[t]
}
type BlockQuoteLevelMap map[ast.Node]int
func (m BlockQuoteLevelMap) Level(node ast.Node) int {
return m[node]
}
// Renderer renders anchor [Node]s.
type ConfluenceRenderer struct {
bf.Renderer
html.Config
Stdlib *stdlib.Lib
LevelMap BlockQuoteLevelMap
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceRenderer(stdlib *stdlib.Lib, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
LevelMap: nil,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// blocks
// reg.Register(ast.KindDocument, r.renderNode)
// reg.Register(ast.KindHeading, r.renderNode)
reg.Register(ast.KindBlockquote, r.renderBlockQuote)
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
reg.Register(ast.KindFencedCodeBlock, r.renderCodeBlock)
// reg.Register(ast.KindHTMLBlock, r.renderNode)
// reg.Register(ast.KindList, r.renderNode)
// reg.Register(ast.KindListItem, r.renderNode)
// reg.Register(ast.KindParagraph, r.renderNode)
// reg.Register(ast.KindTextBlock, r.renderNode)
// reg.Register(ast.KindThematicBreak, r.renderNode)
// inlines
// reg.Register(ast.KindAutoLink, r.renderNode)
// reg.Register(ast.KindCodeSpan, r.renderNode)
// reg.Register(ast.KindEmphasis, r.renderNode)
// reg.Register(ast.KindImage, r.renderNode)
reg.Register(ast.KindLink, r.renderLink)
// reg.Register(ast.KindRawHTML, r.renderNode)
// reg.Register(ast.KindText, r.renderNode)
// reg.Register(ast.KindString, r.renderNode)
}
func ParseLanguage(lang string) string {
// lang takes the following form: language? "collapse"? ("title"? <any string>*)?
// let's split it by spaces
@ -62,26 +118,13 @@ func ParseTitle(lang string) string {
return ""
}
// Define BlockQuoteType enum
type BlockQuoteType int
const (
Info BlockQuoteType = iota
Note
Warn
None
)
func (t BlockQuoteType) String() string {
return []string{"info", "note", "warn", "none"}[t]
}
func ClasifyingBlockQuote(literal string) BlockQuoteType {
// ClassifyingBlockQuote compares a string against a set of patterns and returns a BlockQuoteType
func ClassifyingBlockQuote(literal string) BlockQuoteType {
infoPattern := regexp.MustCompile(`info|Info|INFO`)
notePattern := regexp.MustCompile(`note|Note|NOTE`)
warnPattern := regexp.MustCompile(`warn|Warn|WARN`)
var t BlockQuoteType = None
var t = None
switch {
case infoPattern.MatchString(literal):
t = Info
@ -93,191 +136,259 @@ func ClasifyingBlockQuote(literal string) BlockQuoteType {
return t
}
func ParseBlockQuoteType(node *bf.Node) BlockQuoteType {
var t BlockQuoteType = None
// ParseBlockQuoteType parses the first line of a blockquote and returns its type
func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
var t = None
countParagraphs := 0
node.Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
if node.Type == bf.Paragraph && entering {
if node.Kind() == ast.KindParagraph && entering {
countParagraphs += 1
}
// Type of block quote should be defined on the first blockquote line
if node.Type == bf.Text && countParagraphs < 2 {
t = ClasifyingBlockQuote(string(node.Literal))
} else if countParagraphs > 1 {
return bf.Terminate
if countParagraphs < 2 && entering {
if node.Kind() == ast.KindText {
n := node.(*ast.Text)
t = ClassifyingBlockQuote(string(n.Text(source)))
countParagraphs += 1
}
if node.Kind() == ast.KindHTMLBlock {
n := node.(*ast.HTMLBlock)
for i := 0; i < n.BaseBlock.Lines().Len(); i++ {
line := n.BaseBlock.Lines().At(i)
t = ClassifyingBlockQuote(string(line.Value(source)))
if t != None {
break
}
}
countParagraphs += 1
}
} else if countParagraphs > 1 && entering {
return ast.WalkStop, nil
}
return bf.GoToNext
return ast.WalkContinue, nil
})
return t
}
func GenerateBlockQuoteLevel(someNode *bf.Node) BlockQuoteLevelMap {
// GenerateBlockQuoteLevel walks a given node and returns a map of blockquote levels
func GenerateBlockQuoteLevel(someNode ast.Node) BlockQuoteLevelMap {
// We define state variable that track BlockQuote level while we walk the tree
blockQuoteLevel := 0
blockQuoteLevelMap := make(map[*bf.Node]int)
blockQuoteLevelMap := make(map[ast.Node]int)
rootNode := someNode
for rootNode.Parent != nil {
rootNode = rootNode.Parent
for rootNode.Parent() != nil {
rootNode = rootNode.Parent()
}
rootNode.Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
if node.Type == bf.BlockQuote && entering {
_ = ast.Walk(rootNode, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
if node.Kind() == ast.KindBlockquote && entering {
blockQuoteLevelMap[node] = blockQuoteLevel
blockQuoteLevel += 1
}
if node.Type == bf.BlockQuote && !entering {
if node.Kind() == ast.KindBlockquote && !entering {
blockQuoteLevel -= 1
}
return bf.GoToNext
return ast.WalkContinue, nil
})
return blockQuoteLevelMap
}
func (renderer ConfluenceRenderer) RenderNode(
writer io.Writer,
node *bf.Node,
entering bool,
) bf.WalkStatus {
// renderBlockQuote will render a BlockQuote
func (r *ConfluenceRenderer) renderBlockQuote(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
// Initialize BlockQuote level map
if renderer.LevelMap == nil {
renderer.LevelMap = GenerateBlockQuoteLevel(node)
if r.LevelMap == nil {
r.LevelMap = GenerateBlockQuoteLevel(node)
}
if node.Type == bf.CodeBlock {
quoteType := ParseBlockQuoteType(node, source)
quoteLevel := r.LevelMap.Level(node)
groups := reBlockDetails.FindStringSubmatch(string(node.Info))
linenumbers := false
firstline := 0
theme := ""
collapse := false
lang := ""
var options []string
title := ""
if len(groups) > 0 {
lang, options, title = groups[1], strings.Fields(groups[2]), groups[3]
for _, option := range options {
if option == "collapse" {
collapse = true
continue
}
if option == "nocollapse" {
collapse = false
continue
}
var i int
if _, err := fmt.Sscanf(option, "%d", &i); err == nil {
linenumbers = i > 0
firstline = i
continue
}
theme = option
}
if quoteLevel == 0 && entering && quoteType != None {
prefix := fmt.Sprintf("<ac:structured-macro ac:name=\"%s\"><ac:parameter ac:name=\"icon\">true</ac:parameter><ac:rich-text-body>\n", quoteType)
if _, err := writer.Write([]byte(prefix)); err != nil {
return ast.WalkStop, err
}
err := renderer.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:code",
struct {
Language string
Collapse bool
Title string
Theme string
Linenumbers bool
Firstline int
Text string
}{
lang,
collapse,
title,
theme,
linenumbers,
firstline,
strings.TrimSuffix(string(node.Literal), "\n"),
},
)
if err != nil {
panic(err)
}
return bf.GoToNext
return ast.WalkContinue, nil
}
if node.Type == bf.Link && string(node.Destination[0:3]) == "ac:" {
if quoteLevel == 0 && !entering && quoteType != None {
suffix := "</ac:rich-text-body></ac:structured-macro>\n"
if _, err := writer.Write([]byte(suffix)); err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}
return r.goldmarkRenderBlockquote(writer, source, node, entering)
}
// goldmarkRenderBlockquote is the default renderBlockquote implementation from https://github.com/yuin/goldmark/blob/9d6f314b99ca23037c93d76f248be7b37de6220a/renderer/html/html.go#L286
func (r *ConfluenceRenderer) goldmarkRenderBlockquote(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<blockquote")
html.RenderAttributes(w, n, html.BlockquoteAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<blockquote>\n")
}
} else {
_, _ = w.WriteString("</blockquote>\n")
}
return ast.WalkContinue, nil
}
// renderLink renders links specifically for confluence
func (r *ConfluenceRenderer) renderLink(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if string(node.(*ast.Link).Destination[0:3]) == "ac:" {
if entering {
_, err := writer.Write([]byte("<ac:link><ri:page ri:content-title=\""))
if err != nil {
panic(err)
return ast.WalkStop, err
}
if len(node.Destination) < 4 {
_, err := writer.Write(node.FirstChild.Literal)
if len(node.(*ast.Link).Destination) < 4 {
_, err := writer.Write(node.FirstChild().Text(source))
if err != nil {
panic(err)
return ast.WalkStop, err
}
} else {
_, err := writer.Write(node.Destination[3:])
_, err := writer.Write(node.(*ast.Link).Destination[3:])
if err != nil {
panic(err)
return ast.WalkStop, err
}
}
_, err = writer.Write([]byte("\"/><ac:plain-text-link-body><![CDATA["))
if err != nil {
panic(err)
return ast.WalkStop, err
}
_, err = writer.Write(node.FirstChild.Literal)
_, err = writer.Write(node.FirstChild().Text(source))
if err != nil {
panic(err)
return ast.WalkStop, err
}
_, err = writer.Write([]byte("]]></ac:plain-text-link-body></ac:link>"))
if err != nil {
panic(err)
return ast.WalkStop, err
}
return bf.SkipChildren
}
return bf.GoToNext
}
if node.Type == bf.BlockQuote {
quoteType := ParseBlockQuoteType(node)
quoteLevel := renderer.LevelMap.Level(node)
re := regexp.MustCompile(`[\n\t]`)
if quoteLevel == 0 && entering && quoteType != None {
if _, err := writer.Write([]byte(re.ReplaceAllString(fmt.Sprintf(`
<ac:structured-macro ac:name="%s">
<ac:parameter ac:name="icon">true</ac:parameter>
<ac:rich-text-body>
`, quoteType), ""))); err != nil {
panic(err)
}
return bf.GoToNext
}
if quoteLevel == 0 && !entering && quoteType != None {
if _, err := writer.Write([]byte(re.ReplaceAllString(`
</ac:rich-text-body>
</ac:structured-macro>
`, ""))); err != nil {
panic(err)
}
return bf.GoToNext
return ast.WalkSkipChildren, nil
}
}
return renderer.Renderer.RenderNode(writer, node, entering)
return r.goldmarkRenderLink(writer, source, node, entering)
}
// goldmarkRenderLink is the default renderLink implementation from https://github.com/yuin/goldmark/blob/9d6f314b99ca23037c93d76f248be7b37de6220a/renderer/html/html.go#L552
func (r *ConfluenceRenderer) goldmarkRenderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Link)
if entering {
_, _ = w.WriteString("<a href=\"")
if r.Unsafe || !html.IsDangerousURL(n.Destination) {
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
}
_ = w.WriteByte('"')
if n.Title != nil {
_, _ = w.WriteString(` title="`)
r.Writer.Write(w, n.Title)
_ = w.WriteByte('"')
}
if n.Attributes() != nil {
html.RenderAttributes(w, n, html.LinkAttributeFilter)
}
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("</a>")
}
return ast.WalkContinue, nil
}
// renderCodeBlock renders a (Fenced)CodeBlock
func (r *ConfluenceRenderer) renderCodeBlock(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
var info []byte
nodeFencedCodeBlock := node.(*ast.FencedCodeBlock)
if nodeFencedCodeBlock.Info != nil {
segment := nodeFencedCodeBlock.Info.Segment
info = segment.Value(source)
}
groups := reBlockDetails.FindStringSubmatch(string(info))
linenumbers := false
firstline := 0
theme := ""
collapse := false
lang := ""
var options []string
title := ""
if len(groups) > 0 {
lang, options, title = groups[1], strings.Fields(groups[2]), groups[3]
for _, option := range options {
if option == "collapse" {
collapse = true
continue
}
if option == "nocollapse" {
collapse = false
continue
}
var i int
if _, err := fmt.Sscanf(option, "%d", &i); err == nil {
linenumbers = i > 0
firstline = i
continue
}
theme = option
}
}
var lval []byte
lines := node.Lines().Len()
for i := 0; i < lines; i++ {
line := node.Lines().At(i)
lval = append(lval, line.Value(source)...)
}
err := r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:code",
struct {
Language string
Collapse bool
Title string
Theme string
Linenumbers bool
Firstline int
Text string
}{
lang,
collapse,
title,
theme,
linenumbers,
firstline,
strings.TrimSuffix(string(lval), "\n"),
},
)
if err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}
// compileMarkdown will replace tags like <ac:rich-tech-body> with escaped
// equivalent, because bf markdown parser replaces that tags with
// equivalent, because goldmark markdown parser replaces that tags with
// <a href="ac:rich-text-body">ac:rich-text-body</a> because of the autolink
// rule.
func CompileMarkdown(
markdown []byte,
stdlib *stdlib.Lib,
) string {
func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib) string {
log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
colon := regexp.MustCompile(`---bf-COLON---`)
@ -289,42 +400,33 @@ func CompileMarkdown(
[]byte(`<$1`+colon.String()+`$2>`),
)
renderer := ConfluenceRenderer{
Renderer: bf.NewHTMLRenderer(
bf.HTMLRendererParameters{
Flags: bf.UseXHTML |
bf.Smartypants |
bf.SmartypantsFractions |
bf.SmartypantsDashes |
bf.SmartypantsLatexDashes,
},
converter := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Footnote,
extension.DefinitionList,
extension.Typographer,
),
Stdlib: stdlib,
LevelMap: nil,
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithXHTML(),
html.WithUnsafe(),
))
converter.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewConfluenceRenderer(stdlib), 100),
))
var buf bytes.Buffer
err := converter.Convert(markdown, &buf)
if err != nil {
panic(err)
}
html := bf.Run(
markdown,
bf.WithRenderer(renderer),
bf.WithExtensions(
bf.NoIntraEmphasis|
bf.Tables|
bf.FencedCode|
bf.Autolink|
bf.LaxHTMLBlocks|
bf.Strikethrough|
bf.SpaceHeadings|
bf.HeadingIDs|
bf.AutoHeadingIDs|
bf.Titleblock|
bf.BackslashLineBreak|
bf.DefinitionLists|
bf.NoEmptyLineBeforeBlock|
bf.Footnotes,
),
)
html = colon.ReplaceAll(html, []byte(`:`))
html := colon.ReplaceAll(buf.Bytes(), []byte(`:`))
log.Tracef(nil, "rendered markdown to html:\n%s", string(html))

View File

@ -10,10 +10,6 @@ import (
"github.com/stretchr/testify/assert"
)
const (
NL = "\n"
)
func TestCompileMarkdown(t *testing.T) {
test := assert.New(t)

View File

@ -20,7 +20,6 @@
<ac:parameter ac:name="collapse">false</ac:parameter>
<ac:plain-text-body><![CDATA[unknown code]]></ac:plain-text-body>
</ac:structured-macro>
<p>text
text 2</p>
<ac:structured-macro ac:name="code">

View File

@ -1,13 +1,7 @@
<h1 id="a">a</h1>
<h2 id="b">b</h2>
<h3 id="c">c</h3>
<h4 id="d">d</h4>
<h5 id="e">e</h5>
<h1 id="f">f</h1>
<h2 id="g">g</h2>

View File

@ -1,15 +1,11 @@
<p>Use <a href="https://example.com">https://example.com</a></p>
<p>Use <ac:rich-text-body>aaa</ac:rich-text-body></p>
<p>Use footnotes link <sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup></p>
<div class="footnotes">
<p>Use footnotes link <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:1">a footnote link</li>
<li id="fn:1">
<p>a footnote link&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

View File

@ -2,20 +2,18 @@
<li>dash 1-1</li>
<li>dash 1-2</li>
<li>dash 1-3
<ul>
<li>dash 1-3-1</li>
<li>dash 1-3-2</li>
<li>dash 1-3-3
<ul>
<li>dash 1-3-3-1</li>
</ul></li>
</ul></li>
</ul>
</li>
</ul>
</li>
</ul>
<p>text</p>
<ul>
<li>a</li>
<li>b</li>

View File

@ -1,16 +1,10 @@
<p>one-1
one-2</p>
<p>two-1</p>
<p>two-2</p>
<p>three-1</p>
<p>three-2</p>
<p>space-1
space-2</p>
<p>2space-1<br />
2space-2</p>

View File

@ -1,32 +1,31 @@
<h1 id="main-heading">Main Heading</h1>
<h2 id="first-heading">First Heading</h2>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>NOTES:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a
b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="second-heading">Second Heading</h2>
<ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>Warn</strong></p>
<ul>
<li>Warn bullet 1</li>
<li>Warn bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h2 id="third-heading">Third Heading</h2>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<!-- Info -->
<p>Test</p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="simple-blockquote">Simple Blockquote</h2>
<blockquote>
<p>This paragraph is a simple blockquote</p>
</blockquote>

View File

@ -19,6 +19,11 @@
> * Warn bullet 1
> * Warn bullet 2
## Third Heading
> <!-- Info -->
> Test
## Simple Blockquote
> This paragraph is a simple blockquote

View File

@ -5,7 +5,6 @@
<th>HEADER2</th>
</tr>
</thead>
<tbody>
<tr>
<td>row1</td>

View File

@ -1,5 +1,6 @@
<p><b>bold</b>
<strong>bold</strong></p>
<p><i>vitalik</i>
<em>vitalik</em></p>
<p><s>strikethrough</s>
<del>strikethrough</del></p>

View File

@ -3,3 +3,6 @@
<i>vitalik</i>
*vitalik*
<s>strikethrough</s>
~~strikethrough~~