New Upstream Release - golang-github-gomarkdown-markdown

Ready changes

Summary

Merged new upstream version: 0.0~git20230716.531d2d7 (was: 0.0~git20220731.dcdaee8).

Diff

diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 4f3f025..4c9bed1 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -14,12 +14,13 @@ jobs:
         uses: actions/checkout@v3
 
       - name: Test
-        run: go test -v ./...
+        run: go test -v . && go test -v ./ast && go test -v ./parser && go test -v ./html
 
       - name: Benchmark
         run: go test -run=^$ -bench=BenchmarkReference -benchmem
 
-      - name: Staticcheck
-        uses: dominikh/staticcheck-action@v1.2.0
-        with:
-          version: "2022.1"
+      # not compatible with examples directory and no way to exclude it (?)
+      # - name: Staticcheck
+      #   uses: dominikh/staticcheck-action@v1.3.0
+      #   with:
+      #     version: "2022.1.3"
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index dde1f1d..0000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-# This configuration file was automatically generated by Gitpod.
-# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
-# and commit this file to your remote git repository to share the goodness with others.
-
-tasks:
-  - init: go get && go build ./... && go test ./...
-    command: go run
-
-
diff --git a/README.md b/README.md
index a85ce69..a60b8ba 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,16 @@ Package `github.com/gomarkdown/markdown` is a Go library for parsing Markdown te
 
 It's very fast and supports common extensions.
 
-Try code examples online: https://replit.com/@kjk1?path=folder/gomarkdown
+Tutorial: https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html
+
+Code examples:
+* https://onlinetool.io/goplayground/#txO7hJ-ibeU : basic markdown => HTML
+* https://onlinetool.io/goplayground/#yFRIWRiu-KL : customize HTML renderer
+* https://onlinetool.io/goplayground/#2yV5-HDKBUV : modify AST
+* https://onlinetool.io/goplayground/#9fqKwRbuJ04 : customize parser
+* https://onlinetool.io/goplayground/#Bk0zTvrzUDR : syntax highlight
+
+Those examples are also in [examples](./examples) directory.
 
 ## API Docs:
 
@@ -15,101 +24,58 @@ Try code examples online: https://replit.com/@kjk1?path=folder/gomarkdown
 - https://pkg.go.dev/github.com/gomarkdown/markdown/parser : parser
 - https://pkg.go.dev/github.com/gomarkdown/markdown/html : html renderer
 
-## Users
-
-Some tools using this package: https://pkg.go.dev/github.com/gomarkdown/markdown?tab=importedby
-
 ## Usage
 
 To convert markdown text to HTML using reasonable defaults:
 
 ```go
-md := []byte("## markdown document")
-output := markdown.ToHTML(md, nil, nil)
-```
-
-Try it online: https://replit.com/@kjk1/gomarkdown-basic
-
-## Customizing markdown parser
+package main
 
-Markdown format is loosely specified and there are multiple extensions invented after original specification was created.
-
-The parser supports several [extensions](https://pkg.go.dev/github.com/gomarkdown/markdown/parser#Extensions).
+import (
+	"os"
 
-Default parser uses most common `parser.CommonExtensions` but you can easily use parser with custom extension:
+	"github.com/gomarkdown/markdown"
+	"github.com/gomarkdown/markdown/ast"
+	"github.com/gomarkdown/markdown/html"
+	"github.com/gomarkdown/markdown/parser"
 
-```go
-import (
-    "github.com/gomarkdown/markdown"
-    "github.com/gomarkdown/markdown/parser"
+	"fmt"
 )
 
-extensions := parser.CommonExtensions | parser.AutoHeadingIDs
-parser := parser.NewWithExtensions(extensions)
+var mds = `# header
 
-md := []byte("markdown text")
-html := markdown.ToHTML(md, parser, nil)
-```
-
-Try it online: https://replit.com/@kjk1/gomarkdown-customized-html-renderer
+Sample text.
 
-## Customizing HTML renderer
+[link](http://example.com)
+`
 
-Similarly, HTML renderer can be configured with different [options](https://pkg.go.dev/github.com/gomarkdown/markdown/html#RendererOptions)
+func mdToHTML(md []byte) []byte {
+	// create markdown parser with extensions
+	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
+	p := parser.NewWithExtensions(extensions)
+	doc := p.Parse(md)
 
-Here's how to use a custom renderer:
+	// create HTML renderer with extensions
+	htmlFlags := html.CommonFlags | html.HrefTargetBlank
+	opts := html.RendererOptions{Flags: htmlFlags}
+	renderer := html.NewRenderer(opts)
 
-```go
-import (
-    "github.com/gomarkdown/markdown"
-    "github.com/gomarkdown/markdown/html"
-)
+	return markdown.Render(doc, renderer)
+}
 
-htmlFlags := html.CommonFlags | html.HrefTargetBlank
-opts := html.RendererOptions{Flags: htmlFlags}
-renderer := html.NewRenderer(opts)
+func main() {
+	md := []byte(mds)
+	html := mdToHTML(md)
 
-md := []byte("markdown text")
-html := markdown.ToHTML(md, nil, renderer)
+	fmt.Printf("--- Markdown:\n%s\n\n--- HTML:\n%s\n", md, html)
+}
 ```
 
-Try it online: https://replit.com/@kjk1/gomarkdown-customized-html-renderer
-
-HTML renderer also supports reusing most of the logic and overriding rendering of only specific nodes.
-
-You can provide [RenderNodeFunc](https://pkg.go.dev/github.com/gomarkdown/markdown/html#RenderNodeFunc) in [RendererOptions](https://pkg.go.dev/github.com/gomarkdown/markdown/html#RendererOptions).
-
-The function is called for each node in AST, you can implement custom rendering logic and tell HTML renderer to skip rendering this node.
-
-Here's the simplest example that drops all code blocks from the output:
-
-````go
-import (
-    "github.com/gomarkdown/markdown"
-    "github.com/gomarkdown/markdown/ast"
-    "github.com/gomarkdown/markdown/html"
-)
+Try it online: https://onlinetool.io/goplayground/#txO7hJ-ibeU
 
-// return (ast.GoToNext, true) to tell html renderer to skip rendering this node
-// (because you've rendered it)
-func renderHookDropCodeBlock(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
-    // skip all nodes that are not CodeBlock nodes
-	if _, ok := node.(*ast.CodeBlock); !ok {
-		return ast.GoToNext, false
-    }
-    // custom rendering logic for ast.CodeBlock. By doing nothing it won't be
-    // present in the output
-	return ast.GoToNext, true
-}
+For more documentation read [this guide](https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html)
 
-opts := html.RendererOptions{
-    Flags: html.CommonFlags,
-    RenderNodeHook: renderHookDropCodeBlock,
-}
-renderer := html.NewRenderer(opts)
-md := "test\n```\nthis code block will be dropped from output\n```\ntext"
-html := markdown.ToHTML([]byte(md), nil, renderer)
-````
+Comparing to other markdown parsers: https://babelmark.github.io/
 
 ## Sanitize untrusted content
 
@@ -129,12 +95,6 @@ maybeUnsafeHTML := markdown.ToHTML(md, nil, nil)
 html := bluemonday.UGCPolicy().SanitizeBytes(maybeUnsafeHTML)
 ```
 
-## Windows / Mac newlines
-
-The library only supports Unix newlines. If you have markdown text with possibly
-Windows / Mac newlines, normalize newlines before calling this library using
-`d = markdown.NormalizeNewlines(d)`
-
 ## mdtohtml command-line tool
 
 https://github.com/gomarkdown/mdtohtml is a command-line markdown to html
@@ -323,26 +283,15 @@ implements the following extensions:
 
 - **Mmark support**, see <https://mmark.miek.nl/post/syntax/> for all new syntax elements this adds.
 
-## Todo
+## Users
 
-- port https://github.com/russross/blackfriday/issues/348
-- port [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX):
-  renders output as LaTeX.
-- port https://github.com/shurcooL/github_flavored_markdown to markdown
-- port [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
-  but for markdown.
-- More unit testing
-- Improve unicode support. It does not understand all unicode
-  rules (about what constitutes a letter, a punctuation symbol,
-  etc.), so it may fail to detect word boundaries correctly in
-  some instances. It is safe on all utf-8 input.
+Some tools using this package: https://pkg.go.dev/github.com/gomarkdown/markdown?tab=importedby
 
 ## History
 
-markdown is a fork of v2 of https://github.com/russross/blackfriday that is:
+markdown is a fork of v2 of https://github.com/russross/blackfriday.
 
-- actively maintained (sadly in Feb 2018 blackfriday was inactive for 5 months with many bugs and pull requests accumulated)
-- refactored API (split into ast/parser/html sub-packages)
+I refactored the API (split into ast/parser/html sub-packages).
 
 Blackfriday itself was based on C implementation [sundown](https://github.com/vmg/sundown) which in turn was based on [libsoldout](http://fossil.instinctive.eu/libsoldout/home).
 
diff --git a/ast/node.go b/ast/node.go
index 0d7175c..8f802db 100644
--- a/ast/node.go
+++ b/ast/node.go
@@ -92,6 +92,12 @@ type Container struct {
 	*Attribute // Block level attribute
 }
 
+// return true if can contain children of a given node type
+// used by custom nodes to over-ride logic in canNodeContain
+type CanContain interface {
+	CanContain(Node) bool
+}
+
 // AsContainer returns itself as *Container
 func (c *Container) AsContainer() *Container {
 	return c
@@ -157,9 +163,13 @@ func (l *Leaf) GetChildren() []Node {
 	return nil
 }
 
-// SetChildren will panic becuase Leaf cannot have children
+// SetChildren will panic if trying to set non-empty children
+// because Leaf cannot have children
 func (l *Leaf) SetChildren(newChildren []Node) {
-	panic("leaf node cannot have children")
+	if len(newChildren) != 0 {
+		panic("leaf node cannot have children")
+	}
+
 }
 
 // Document represents markdown document node, a root of ast
@@ -272,6 +282,7 @@ type CrossReference struct {
 	Container
 
 	Destination []byte // Destination is where the reference points to
+	Suffix      []byte // Potential citation suffix, i.e. (#myid, text)
 }
 
 // Citation is a citation node.
diff --git a/ast/node_test.go b/ast/node_test.go
new file mode 100644
index 0000000..3c32cd6
--- /dev/null
+++ b/ast/node_test.go
@@ -0,0 +1,85 @@
+package ast
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestContainerCanHaveChildren(t *testing.T) {
+	parent := &Container{}
+	child := &Leaf{}
+
+	if len(parent.GetChildren()) != 0 {
+		t.Error("Parent did not start out without children")
+	}
+
+	newChildren := []Node{child}
+	parent.SetChildren(newChildren)
+	child.Parent = parent
+
+	if !reflect.DeepEqual(parent.GetChildren(), newChildren) {
+		t.Error("Failed to set children")
+	}
+}
+
+func TestLeafCannotHaveChildren(t *testing.T) {
+	parent := &Leaf{}
+	child := &Leaf{}
+
+	newChildren := []Node{child}
+
+	// Expect that SetChildren panics
+	defer func() {
+		if r := recover(); r == nil {
+			t.Error("Leaf.SetChildren did not panic but was expected to")
+		}
+	}()
+
+	parent.SetChildren(newChildren)
+	child.Parent = parent
+}
+
+func TestLeafCanSetEmptyChildren(t *testing.T) {
+	parent := &Leaf{}
+
+	parent.SetChildren(nil)
+	parent.SetChildren([]Node{})
+}
+
+func TestRemoveLeaveFromTree(t *testing.T) {
+	// Create a tree to remove nodes from:
+	/*
+	      grandparent
+	           |
+	         parent
+	        /      \
+	 toBeRemoved  sibling
+	*/
+
+	grandparent := &Container{}
+	parent := &Container{}
+	toBeRemoved := &Leaf{}
+	sibling := &Leaf{}
+
+	grandparent.SetChildren([]Node{parent})
+	parent.Parent = grandparent
+
+	parent.SetChildren(([]Node{toBeRemoved, sibling}))
+	toBeRemoved.Parent = parent
+	sibling.Parent = parent
+
+	RemoveFromTree(toBeRemoved)
+
+	if !reflect.DeepEqual(grandparent.GetChildren(), []Node{parent}) {
+		t.Error("Unexpectedly modified children of grandparent when removing grandchild")
+	}
+
+	if !reflect.DeepEqual(parent.GetChildren(), []Node{sibling}) {
+		t.Errorf("Unexpected modification of removed node's siblings: %v", parent.GetChildren())
+	}
+
+	// The parent reference of the removed node is left intact
+	if toBeRemoved.Parent != parent {
+		t.Errorf("Unexpectedly modified parent of removed node to: %v", toBeRemoved.Parent)
+	}
+}
diff --git a/ast/print.go b/ast/print.go
index b186ec0..a4e3d62 100644
--- a/ast/print.go
+++ b/ast/print.go
@@ -157,6 +157,8 @@ func printRecur(w io.Writer, node Node, prefix string, depth int) {
 			content += "flags=" + flags + " "
 		}
 		printDefault(w, indent, typeName, content)
+	case *CodeBlock:
+		printDefault(w, indent, typeName + ":" + string(v.Info), content)
 	default:
 		printDefault(w, indent, typeName, content)
 	}
diff --git a/block_test.go b/block_test.go
index 1f2a1ef..e59b296 100644
--- a/block_test.go
+++ b/block_test.go
@@ -3,7 +3,6 @@ package markdown
 import (
 	"bytes"
 	"testing"
-
 	"github.com/gomarkdown/markdown/ast"
 	"github.com/gomarkdown/markdown/html"
 	"github.com/gomarkdown/markdown/parser"
@@ -194,8 +193,22 @@ func TestBug126(t *testing.T) {
 	var buf bytes.Buffer
 	ast.Print(&buf, doc)
 	got := buf.String()
-	// TODO: needs fixing https://github.com/gomarkdown/markdown/issues/126
-	exp := "BlockQuote\n  CodeBlock '> fenced pre block 1\\n> ```\\n\\n'\n  Paragraph\n    Text 'fenced pre block 2\\n````'\n"
+	// TODO: needs fixing https://github.com/gomarkdown/markdown/issues/126 
+	exp := "BlockQuote\n  CodeBlock: '> fenced pre block 1\\n> ```\\n\\n'\n  Paragraph\n    Text 'fenced pre block 2\\n````'\n"
+	if got != exp {
+		t.Errorf("\nInput   [%#v]\nExpected[%#v]\nGot     [%#v]\n",
+			input, exp, got)
+	}
+}
+
+func TestPull288(t *testing.T) {
+	input := "```go\nfmt.Println(\"Hello world!\")\n```\n"
+	p := parser.NewWithExtensions(parser.CommonExtensions)
+	doc := p.Parse([]byte(input))
+	var buf bytes.Buffer
+	ast.Print(&buf, doc)
+	got := buf.String()
+	exp := "CodeBlock:go 'fmt.Println(\"Hello world!\")\\n'\n"
 	if got != exp {
 		t.Errorf("\nInput   [%#v]\nExpected[%#v]\nGot     [%#v]\n",
 			input, exp, got)
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index f681ff1..0000000
--- a/codecov.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-coverage:
-  status:
-    project:
-      default:
-        # basic
-        target: 60%
-        threshold: 2%
-        base: auto
diff --git a/debian/changelog b/debian/changelog
index d57b81a..c1f4e28 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-gomarkdown-markdown (0.0~git20230716.531d2d7-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 10 Aug 2023 05:33:45 -0000
+
 golang-github-gomarkdown-markdown (0.0~git20220731.dcdaee8-2) unstable; urgency=medium
 
   * Source-only upload for migration to testing
diff --git a/examples/basic.go b/examples/basic.go
new file mode 100644
index 0000000..0323af3
--- /dev/null
+++ b/examples/basic.go
@@ -0,0 +1,50 @@
+package main
+
+// example for https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html
+
+import (
+	"os"
+
+	"github.com/gomarkdown/markdown"
+	"github.com/gomarkdown/markdown/ast"
+	"github.com/gomarkdown/markdown/html"
+	"github.com/gomarkdown/markdown/parser"
+
+	"fmt"
+)
+
+var mds = `# header
+
+Sample text.
+
+[link](http://example.com)
+`
+
+var printAst = false
+
+func mdToHTML(md []byte) []byte {
+	// create markdown parser with extensions
+	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
+	p := parser.NewWithExtensions(extensions)
+	doc := p.Parse(md)
+
+	if printAst {
+		fmt.Print("--- AST tree:\n")
+		ast.Print(os.Stdout, doc)
+		fmt.Print("\n")
+	}
+
+	// create HTML renderer with extensions
+	htmlFlags := html.CommonFlags | html.HrefTargetBlank
+	opts := html.RendererOptions{Flags: htmlFlags}
+	renderer := html.NewRenderer(opts)
+
+	return markdown.Render(doc, renderer)
+}
+
+func main() {
+	md := []byte(mds)
+	html := mdToHTML(md)
+
+	fmt.Printf("--- Markdown:\n%s\n\n--- HTML:\n%s\n", md, html)
+}
diff --git a/examples/code_hightlight.go b/examples/code_hightlight.go
new file mode 100644
index 0000000..97bedb4
--- /dev/null
+++ b/examples/code_hightlight.go
@@ -0,0 +1,93 @@
+package main
+
+// example for https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/gomarkdown/markdown"
+	"github.com/gomarkdown/markdown/ast"
+	mdhtml "github.com/gomarkdown/markdown/html"
+
+	"github.com/alecthomas/chroma"
+	"github.com/alecthomas/chroma/formatters/html"
+	"github.com/alecthomas/chroma/lexers"
+	"github.com/alecthomas/chroma/styles"
+)
+
+var (
+	htmlFormatter  *html.Formatter
+	highlightStyle *chroma.Style
+)
+
+func init() {
+	htmlFormatter = html.New(html.WithClasses(true), html.TabWidth(2))
+	if htmlFormatter == nil {
+		panic("couldn't create html formatter")
+	}
+	styleName := "monokailight"
+	highlightStyle = styles.Get(styleName)
+	if highlightStyle == nil {
+		panic(fmt.Sprintf("didn't find style '%s'", styleName))
+	}
+}
+
+// based on https://github.com/alecthomas/chroma/blob/master/quick/quick.go
+func htmlHighlight(w io.Writer, source, lang, defaultLang string) error {
+	if lang == "" {
+		lang = defaultLang
+	}
+	l := lexers.Get(lang)
+	if l == nil {
+		l = lexers.Analyse(source)
+	}
+	if l == nil {
+		l = lexers.Fallback
+	}
+	l = chroma.Coalesce(l)
+
+	it, err := l.Tokenise(nil, source)
+	if err != nil {
+		return err
+	}
+	return htmlFormatter.Format(w, highlightStyle, it)
+}
+
+// an actual rendering of Paragraph is more complicated
+func renderCode(w io.Writer, codeBlock *ast.CodeBlock, entering bool) {
+	defaultLang := ""
+	lang := string(codeBlock.Info)
+	htmlHighlight(w, string(codeBlock.Literal), lang, defaultLang)
+}
+
+func myRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
+	if code, ok := node.(*ast.CodeBlock); ok {
+		renderCode(w, code, entering)
+		return ast.GoToNext, true
+	}
+	return ast.GoToNext, false
+}
+
+func newCustomizedRender() *mdhtml.Renderer {
+	opts := mdhtml.RendererOptions{
+		Flags:          mdhtml.CommonFlags,
+		RenderNodeHook: myRenderHook,
+	}
+	return mdhtml.NewRenderer(opts)
+}
+
+var mds = "code block:\n```go\nvar n = 384\n```"
+
+func codeHighlight() {
+	md := []byte(mds)
+
+	renderer := newCustomizedRender()
+	html := markdown.ToHTML(md, nil, renderer)
+
+	fmt.Printf("--- Markdown:\n%s\n\n--- HTML:\n%s\n", md, html)
+}
+
+func main() {
+	codeHighlight()
+}
diff --git a/examples/modify_ast.go b/examples/modify_ast.go
new file mode 100644
index 0000000..7215fa5
--- /dev/null
+++ b/examples/modify_ast.go
@@ -0,0 +1,63 @@
+package main
+
+// example for https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html
+
+import (
+	"fmt"
+
+	"github.com/gomarkdown/markdown"
+	"github.com/gomarkdown/markdown/ast"
+	"github.com/gomarkdown/markdown/html"
+	"github.com/gomarkdown/markdown/parser"
+
+	"strings"
+)
+
+func modifyAst(doc ast.Node) ast.Node {
+	ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
+		if img, ok := node.(*ast.Image); ok && entering {
+			attr := img.Attribute
+			if attr == nil {
+				attr = &ast.Attribute{}
+			}
+			// TODO: might be duplicate
+			attr.Classes = append(attr.Classes, []byte("blog-img"))
+			img.Attribute = attr
+		}
+
+		if link, ok := node.(*ast.Link); ok && entering {
+			isExternalURI := func(uri string) bool {
+				return (strings.HasPrefix(uri, "https://") || strings.HasPrefix(uri, "http://")) && !strings.Contains(uri, "blog.kowalczyk.info")
+			}
+			if isExternalURI(string(link.Destination)) {
+				link.AdditionalAttributes = append(link.AdditionalAttributes, `target="_blank"`)
+			}
+		}
+
+		return ast.GoToNext
+	})
+	return doc
+}
+
+var mds = `[link](http://example.com)`
+
+func modifyAstExample() {
+	md := []byte(mds)
+
+	extensions := parser.CommonExtensions
+	p := parser.NewWithExtensions(extensions)
+	doc := p.Parse(md)
+
+	doc = modifyAst(doc)
+
+	htmlFlags := html.CommonFlags
+	opts := html.RendererOptions{Flags: htmlFlags}
+	renderer := html.NewRenderer(opts)
+	html := markdown.Render(doc, renderer)
+
+	fmt.Printf("-- Markdown:\n%s\n\n--- HTML:\n%s\n", md, html)
+}
+
+func main() {
+	modifyAstExample()
+}
diff --git a/examples/parser_hook.go b/examples/parser_hook.go
new file mode 100644
index 0000000..39643e5
--- /dev/null
+++ b/examples/parser_hook.go
@@ -0,0 +1,102 @@
+package main
+
+// example for https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html
+
+import (
+	"fmt"
+
+	"github.com/gomarkdown/markdown"
+	"github.com/gomarkdown/markdown/ast"
+	"github.com/gomarkdown/markdown/html"
+	"github.com/gomarkdown/markdown/parser"
+
+	"bytes"
+	"io"
+	"strings"
+)
+
+type Gallery struct {
+	ast.Leaf
+	ImageURLS []string
+}
+
+var gallery = []byte(":gallery\n")
+
+func parseGallery(data []byte) (ast.Node, []byte, int) {
+	if !bytes.HasPrefix(data, gallery) {
+		return nil, nil, 0
+	}
+	fmt.Printf("Found a gallery!\n\n")
+	i := len(gallery)
+	// find empty line
+	// TODO: should also consider end of document
+	end := bytes.Index(data[i:], []byte("\n\n"))
+	if end < 0 {
+		return nil, data, 0
+	}
+	end = end + i
+	lines := string(data[i:end])
+	parts := strings.Split(lines, "\n")
+	res := &Gallery{
+		ImageURLS: parts,
+	}
+	return res, nil, end
+}
+
+func parserHook(data []byte) (ast.Node, []byte, int) {
+	if node, d, n := parseGallery(data); node != nil {
+		return node, d, n
+	}
+	return nil, nil, 0
+}
+
+func newMarkdownParser() *parser.Parser {
+	extensions := parser.CommonExtensions
+	p := parser.NewWithExtensions(extensions)
+	p.Opts.ParserHook = parserHook
+	return p
+}
+
+func galleryRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
+	if _, ok := node.(*Gallery); ok {
+		if entering {
+			// note: just for illustration purposes
+			// actual implemenation of gallery in HTML / JavaScript is long
+			io.WriteString(w, "\n<gallery></gallery>\n\n")
+		}
+		return ast.GoToNext, true
+	}
+	return ast.GoToNext, false
+}
+
+func newGalleryRender() *html.Renderer {
+	opts := html.RendererOptions{
+		Flags:          html.CommonFlags,
+		RenderNodeHook: galleryRenderHook,
+	}
+	return html.NewRenderer(opts)
+}
+
+var mds = `document
+
+:gallery
+/img/image-1.png
+/img/image-2.png
+
+Rest of the document.`
+
+func parserHookExample() {
+	md := []byte(mds)
+
+	p := newMarkdownParser()
+	doc := p.Parse([]byte(md))
+
+	renderer := newGalleryRender()
+	html := markdown.Render(doc, renderer)
+
+	fmt.Printf("--- Markdown:\n%s\n\n--- HTML:\n%s\n", md, html)
+}
+
+func main() {
+	parserHookExample()
+}
diff --git a/examples/readme.md b/examples/readme.md
new file mode 100644
index 0000000..158ddfd
--- /dev/null
+++ b/examples/readme.md
@@ -0,0 +1,14 @@
+Here you can find examples of advanced uses of this library.
+
+You can use them as base for your own code.
+
+They are described in more detail in https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html
+
+You can run each of them with: `go run <program.go>`.
+
+The examples:
+* `basic.go` : simplest markdown => HTML example
+* `render_hook.go` : shows how to customize HTML renderer with render hook function
+* `code_highlight.go` : shows how to syntax highlight code blocks using `github.com/alecthomas/chroma`
+* `parser_hook.go` : shows how to extend parser to recognize custom block-level syntax
+* `modify_ast.go` : shows how to modify AST after parsing but before HTML rendering
diff --git a/examples/render_hook.go b/examples/render_hook.go
new file mode 100644
index 0000000..b022e27
--- /dev/null
+++ b/examples/render_hook.go
@@ -0,0 +1,52 @@
+package main
+
+// example for https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/gomarkdown/markdown"
+	"github.com/gomarkdown/markdown/ast"
+	"github.com/gomarkdown/markdown/html"
+)
+
+// an actual rendering of Paragraph is more complicated
+func renderParagraph(w io.Writer, p *ast.Paragraph, entering bool) {
+	if entering {
+		io.WriteString(w, "<div>")
+	} else {
+		io.WriteString(w, "</div>")
+	}
+}
+
+func myRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
+	if para, ok := node.(*ast.Paragraph); ok {
+		renderParagraph(w, para, entering)
+		return ast.GoToNext, true
+	}
+	return ast.GoToNext, false
+}
+
+func newCustomizedRender() *html.Renderer {
+	opts := html.RendererOptions{
+		Flags:          html.CommonFlags,
+		RenderNodeHook: myRenderHook,
+	}
+	return html.NewRenderer(opts)
+}
+
+var mds = `foo`
+
+func renderHookExmple() {
+	md := []byte(mds)
+
+	renderer := newCustomizedRender()
+	html := markdown.ToHTML(md, nil, renderer)
+
+	fmt.Printf("--- Markdown:\n%s\n\n--- HTML:\n%s\n", md, html)
+}
+
+func main() {
+	renderHookExmple()
+}
diff --git a/helpers_test.go b/helpers_test.go
index aa4cb95..34fc754 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -8,7 +8,6 @@ import (
 	"testing"
 
 	"github.com/gomarkdown/markdown/html"
-	"github.com/gomarkdown/markdown/internal/valid"
 	"github.com/gomarkdown/markdown/parser"
 )
 
@@ -118,7 +117,7 @@ func transformLinks(tests []string, prefix string) []string {
 
 func newSafeURLOverride(uris []string) func(url []byte) bool {
 	return func(url []byte) bool {
-		if valid.IsSafeURL(url) {
+		if parser.IsSafeURL(url) {
 			return true
 		}
 		for _, prefix := range uris {
diff --git a/html/renderer.go b/html/renderer.go
index 875debe..494e754 100644
--- a/html/renderer.go
+++ b/html/renderer.go
@@ -11,7 +11,6 @@ import (
 	"strings"
 
 	"github.com/gomarkdown/markdown/ast"
-	"github.com/gomarkdown/markdown/internal/valid"
 	"github.com/gomarkdown/markdown/parser"
 )
 
@@ -89,13 +88,15 @@ type RendererOptions struct {
 	// FootnoteReturnLinks flag is enabled. If blank, the string
 	// <sup>[return]</sup> is used.
 	FootnoteReturnLinkContents string
-	// CitationFormatString defines how a citation is rendered. If blnck, the string
+	// CitationFormatString defines how a citation is rendered. If blank, the string
 	// <sup>[%s]</sup> is used. Where %s will be substituted with the citation target.
 	CitationFormatString string
 	// If set, add this text to the front of each Heading ID, to ensure uniqueness.
 	HeadingIDPrefix string
 	// If set, add this text to the back of each Heading ID, to ensure uniqueness.
 	HeadingIDSuffix string
+	// can over-write <p> for paragraph tag
+	ParagraphTag string
 
 	Title string // Document title (used if CompletePage is set)
 	CSS   string // Optional CSS file URL (used if CompletePage is set)
@@ -121,7 +122,7 @@ type RendererOptions struct {
 //
 // Do not create this directly, instead use the NewRenderer function.
 type Renderer struct {
-	opts RendererOptions
+	Opts RendererOptions
 
 	closeTag string // how to end singleton tags: either " />" or ">"
 
@@ -133,7 +134,9 @@ type Renderer struct {
 	// if > 0, will strip html tags in Out and Outs
 	DisableTags int
 
-	// TODO: documentation
+	// IsSafeURLOverride allows overriding the default URL matcher. URL is
+	// safe if the overriding function returns true. Can be used to extend
+	// the default list of safe URLs.
 	IsSafeURLOverride func(url []byte) bool
 
 	sr *SPRenderer
@@ -167,7 +170,7 @@ func EscapeHTML(w io.Writer, d []byte) {
 	}
 }
 
-func escLink(w io.Writer, text []byte) {
+func EscLink(w io.Writer, text []byte) {
 	unesc := html.UnescapeString(string(text))
 	EscapeHTML(w, []byte(unesc))
 }
@@ -206,7 +209,7 @@ func NewRenderer(opts RendererOptions) *Renderer {
 	}
 
 	return &Renderer{
-		opts: opts,
+		Opts: opts,
 
 		closeTag:   closeTag,
 		headingIDs: make(map[string]int),
@@ -216,6 +219,11 @@ func NewRenderer(opts RendererOptions) *Renderer {
 }
 
 func isRelativeLink(link []byte) (yes bool) {
+	// empty links considerd relative
+	if len(link) == 0 {
+		return true
+	}
+
 	// a tag begin with '#'
 	if link[0] == '#' {
 		return true
@@ -244,9 +252,12 @@ func isRelativeLink(link []byte) (yes bool) {
 	return false
 }
 
-func (r *Renderer) addAbsPrefix(link []byte) []byte {
-	if r.opts.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
-		newDest := r.opts.AbsolutePrefix
+func AddAbsPrefix(link []byte, prefix string) []byte {
+	if len(link) == 0 || len(prefix) == 0 {
+		return link
+	}
+	if isRelativeLink(link) && link[0] != '.' {
+		newDest := prefix
 		if link[0] != '/' {
 			newDest += "/"
 		}
@@ -285,13 +296,13 @@ func isMailto(link []byte) bool {
 }
 
 func needSkipLink(r *Renderer, dest []byte) bool {
-	flags := r.opts.Flags
+	flags := r.Opts.Flags
 	if flags&SkipLinks != 0 {
 		return true
 	}
 	isSafeURL := r.IsSafeURLOverride
 	if isSafeURL == nil {
-		isSafeURL = valid.IsSafeURL
+		isSafeURL = parser.IsSafeURL
 	}
 	return flags&Safelink != 0 && !isSafeURL(dest) && !isMailto(dest)
 }
@@ -308,7 +319,7 @@ func appendLanguageAttr(attrs []string, info []byte) []string {
 	return append(attrs, s)
 }
 
-func (r *Renderer) outTag(w io.Writer, name string, attrs []string) {
+func (r *Renderer) OutTag(w io.Writer, name string, attrs []string) {
 	s := name
 	if len(attrs) > 0 {
 		s += " " + strings.Join(attrs, " ")
@@ -317,22 +328,22 @@ func (r *Renderer) outTag(w io.Writer, name string, attrs []string) {
 	r.lastOutputLen = 1
 }
 
-func footnoteRef(prefix string, node *ast.Link) string {
-	urlFrag := prefix + string(slugify(node.Destination))
+func FootnoteRef(prefix string, node *ast.Link) string {
+	urlFrag := prefix + string(Slugify(node.Destination))
 	nStr := strconv.Itoa(node.NoteID)
 	anchor := `<a href="#fn:` + urlFrag + `">` + nStr + `</a>`
 	return `<sup class="footnote-ref" id="fnref:` + urlFrag + `">` + anchor + `</sup>`
 }
 
-func footnoteItem(prefix string, slug []byte) string {
+func FootnoteItem(prefix string, slug []byte) string {
 	return `<li id="fn:` + prefix + string(slug) + `">`
 }
 
-func footnoteReturnLink(prefix, returnLink string, slug []byte) string {
+func FootnoteReturnLink(prefix, returnLink string, slug []byte) string {
 	return ` <a class="footnote-return" href="#fnref:` + prefix + string(slug) + `">` + returnLink + `</a>`
 }
 
-func listItemOpenCR(listItem *ast.ListItem) bool {
+func ListItemOpenCR(listItem *ast.ListItem) bool {
 	if ast.GetPrevNode(listItem) == nil {
 		return false
 	}
@@ -340,13 +351,13 @@ func listItemOpenCR(listItem *ast.ListItem) bool {
 	return !ld.Tight && ld.ListFlags&ast.ListTypeDefinition == 0
 }
 
-func skipParagraphTags(para *ast.Paragraph) bool {
+func SkipParagraphTags(para *ast.Paragraph) bool {
 	parent := para.Parent
 	grandparent := parent.GetParent()
-	if grandparent == nil || !isList(grandparent) {
+	if grandparent == nil || !IsList(grandparent) {
 		return false
 	}
-	isParentTerm := isListItemTerm(parent)
+	isParentTerm := IsListItemTerm(parent)
 	grandparentListData := grandparent.(*ast.List)
 	tightOrTerm := grandparentListData.Tight || isParentTerm
 	return tightOrTerm
@@ -382,35 +393,35 @@ var (
 	closeHTags = []string{"</h1>", "</h2>", "</h3>", "</h4>", "</h5>"}
 )
 
-func headingOpenTagFromLevel(level int) string {
+func HeadingOpenTagFromLevel(level int) string {
 	if level < 1 || level > 5 {
 		return "<h6"
 	}
 	return openHTags[level-1]
 }
 
-func headingCloseTagFromLevel(level int) string {
+func HeadingCloseTagFromLevel(level int) string {
 	if level < 1 || level > 5 {
 		return "</h6>"
 	}
 	return closeHTags[level-1]
 }
 
-func (r *Renderer) outHRTag(w io.Writer, attrs []string) {
+func (r *Renderer) OutHRTag(w io.Writer, attrs []string) {
 	hr := TagWithAttributes("<hr", attrs)
-	r.OutOneOf(w, r.opts.Flags&UseXHTML == 0, hr, "<hr />")
+	r.OutOneOf(w, r.Opts.Flags&UseXHTML == 0, hr, "<hr />")
 }
 
 // Text writes ast.Text node
 func (r *Renderer) Text(w io.Writer, text *ast.Text) {
-	if r.opts.Flags&Smartypants != 0 {
+	if r.Opts.Flags&Smartypants != 0 {
 		var tmp bytes.Buffer
 		EscapeHTML(&tmp, text.Literal)
 		r.sr.Process(w, tmp.Bytes())
 	} else {
 		_, parentIsLink := text.Parent.(*ast.Link)
 		if parentIsLink {
-			escLink(w, text.Literal)
+			EscLink(w, text.Literal)
 		} else {
 			EscapeHTML(w, text.Literal)
 		}
@@ -419,7 +430,7 @@ func (r *Renderer) Text(w io.Writer, text *ast.Text) {
 
 // HardBreak writes ast.Hardbreak node
 func (r *Renderer) HardBreak(w io.Writer, node *ast.Hardbreak) {
-	r.OutOneOf(w, r.opts.Flags&UseXHTML == 0, "<br>", "<br />")
+	r.OutOneOf(w, r.Opts.Flags&UseXHTML == 0, "<br>", "<br />")
 	r.CR(w)
 }
 
@@ -450,7 +461,7 @@ func (r *Renderer) OutOneOfCr(w io.Writer, outFirst bool, first string, second s
 
 // HTMLSpan writes ast.HTMLSpan node
 func (r *Renderer) HTMLSpan(w io.Writer, span *ast.HTMLSpan) {
-	if r.opts.Flags&SkipHTML == 0 {
+	if r.Opts.Flags&SkipHTML == 0 {
 		r.Out(w, span.Literal)
 	}
 }
@@ -458,18 +469,18 @@ func (r *Renderer) HTMLSpan(w io.Writer, span *ast.HTMLSpan) {
 func (r *Renderer) linkEnter(w io.Writer, link *ast.Link) {
 	attrs := link.AdditionalAttributes
 	dest := link.Destination
-	dest = r.addAbsPrefix(dest)
+	dest = AddAbsPrefix(dest, r.Opts.AbsolutePrefix)
 	var hrefBuf bytes.Buffer
 	hrefBuf.WriteString("href=\"")
-	escLink(&hrefBuf, dest)
+	EscLink(&hrefBuf, dest)
 	hrefBuf.WriteByte('"')
 	attrs = append(attrs, hrefBuf.String())
 	if link.NoteID != 0 {
-		r.Outs(w, footnoteRef(r.opts.FootnoteAnchorPrefix, link))
+		r.Outs(w, FootnoteRef(r.Opts.FootnoteAnchorPrefix, link))
 		return
 	}
 
-	attrs = appendLinkAttrs(attrs, r.opts.Flags, dest)
+	attrs = appendLinkAttrs(attrs, r.Opts.Flags, dest)
 	if len(link.Title) > 0 {
 		var titleBuff bytes.Buffer
 		titleBuff.WriteString("title=\"")
@@ -477,7 +488,7 @@ func (r *Renderer) linkEnter(w io.Writer, link *ast.Link) {
 		titleBuff.WriteByte('"')
 		attrs = append(attrs, titleBuff.String())
 	}
-	r.outTag(w, "<a", attrs)
+	r.OutTag(w, "<a", attrs)
 }
 
 func (r *Renderer) linkExit(w io.Writer, link *ast.Link) {
@@ -502,33 +513,34 @@ func (r *Renderer) Link(w io.Writer, link *ast.Link, entering bool) {
 }
 
 func (r *Renderer) imageEnter(w io.Writer, image *ast.Image) {
-	dest := image.Destination
-	dest = r.addAbsPrefix(dest)
-	if r.DisableTags == 0 {
-		//if options.safe && potentiallyUnsafe(dest) {
-		//out(w, `<img src="" alt="`)
-		//} else {
-		if r.opts.Flags&LazyLoadImages != 0 {
-			r.Outs(w, `<img loading="lazy" src="`)
-		} else {
-			r.Outs(w, `<img src="`)
-		}
-		escLink(w, dest)
-		r.Outs(w, `" alt="`)
-		//}
-	}
 	r.DisableTags++
+	if r.DisableTags > 1 {
+		return
+	}
+	src := image.Destination
+	src = AddAbsPrefix(src, r.Opts.AbsolutePrefix)
+	attrs := BlockAttrs(image)
+	if r.Opts.Flags&LazyLoadImages != 0 {
+		attrs = append(attrs, `loading="lazy"`)
+	}
+
+	s := TagWithAttributes("<img", attrs)
+	s = s[:len(s)-1] // hackish: strip off ">" from end
+	r.Outs(w, s+` src="`)
+	EscLink(w, src)
+	r.Outs(w, `" alt="`)
 }
 
 func (r *Renderer) imageExit(w io.Writer, image *ast.Image) {
 	r.DisableTags--
-	if r.DisableTags == 0 {
-		if image.Title != nil {
-			r.Outs(w, `" title="`)
-			EscapeHTML(w, image.Title)
-		}
-		r.Outs(w, `" />`)
+	if r.DisableTags > 0 {
+		return
+	}
+	if image.Title != nil {
+		r.Outs(w, `" title="`)
+		EscapeHTML(w, image.Title)
 	}
+	r.Outs(w, `" />`)
 }
 
 // Image writes ast.Image node
@@ -562,20 +574,28 @@ func (r *Renderer) paragraphEnter(w io.Writer, para *ast.Paragraph) {
 		}
 	}
 
-	tag := TagWithAttributes("<p", BlockAttrs(para))
+	ptag := "<p"
+	if r.Opts.ParagraphTag != "" {
+		ptag = "<" + r.Opts.ParagraphTag
+	}
+	tag := TagWithAttributes(ptag, BlockAttrs(para))
 	r.Outs(w, tag)
 }
 
 func (r *Renderer) paragraphExit(w io.Writer, para *ast.Paragraph) {
-	r.Outs(w, "</p>")
-	if !(isListItem(para.Parent) && ast.GetNextNode(para) == nil) {
+	ptag := "</p>"
+	if r.Opts.ParagraphTag != "" {
+		ptag = "</" + r.Opts.ParagraphTag + ">"
+	}
+	r.Outs(w, ptag)
+	if !(IsListItem(para.Parent) && ast.GetNextNode(para) == nil) {
 		r.CR(w)
 	}
 }
 
 // Paragraph writes ast.Paragraph node
 func (r *Renderer) Paragraph(w io.Writer, para *ast.Paragraph, entering bool) {
-	if skipParagraphTags(para) {
+	if SkipParagraphTags(para) {
 		return
 	}
 	if entering {
@@ -594,7 +614,7 @@ func (r *Renderer) Code(w io.Writer, node *ast.Code) {
 
 // HTMLBlock write ast.HTMLBlock node
 func (r *Renderer) HTMLBlock(w io.Writer, node *ast.HTMLBlock) {
-	if r.opts.Flags&SkipHTML != 0 {
+	if r.Opts.Flags&SkipHTML != 0 {
 		return
 	}
 	r.CR(w)
@@ -602,6 +622,25 @@ func (r *Renderer) HTMLBlock(w io.Writer, node *ast.HTMLBlock) {
 	r.CR(w)
 }
 
+func (r *Renderer) EnsureUniqueHeadingID(id string) string {
+	for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
+		tmp := fmt.Sprintf("%s-%d", id, count+1)
+
+		if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
+			r.headingIDs[id] = count + 1
+			id = tmp
+		} else {
+			id = id + "-1"
+		}
+	}
+
+	if _, found := r.headingIDs[id]; !found {
+		r.headingIDs[id] = 0
+	}
+
+	return id
+}
+
 func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) {
 	var attrs []string
 	var class string
@@ -620,44 +659,25 @@ func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) {
 		attrs = []string{`class="` + class + `"`}
 	}
 
-	ensureUniqueHeadingID := func(id string) string {
-		for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
-			tmp := fmt.Sprintf("%s-%d", id, count+1)
-
-			if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
-				r.headingIDs[id] = count + 1
-				id = tmp
-			} else {
-				id = id + "-1"
-			}
-		}
-
-		if _, found := r.headingIDs[id]; !found {
-			r.headingIDs[id] = 0
-		}
-
-		return id
-	}
-
 	if nodeData.HeadingID != "" {
-		id := ensureUniqueHeadingID(nodeData.HeadingID)
-		if r.opts.HeadingIDPrefix != "" {
-			id = r.opts.HeadingIDPrefix + id
+		id := r.EnsureUniqueHeadingID(nodeData.HeadingID)
+		if r.Opts.HeadingIDPrefix != "" {
+			id = r.Opts.HeadingIDPrefix + id
 		}
-		if r.opts.HeadingIDSuffix != "" {
-			id = id + r.opts.HeadingIDSuffix
+		if r.Opts.HeadingIDSuffix != "" {
+			id = id + r.Opts.HeadingIDSuffix
 		}
 		attrID := `id="` + id + `"`
 		attrs = append(attrs, attrID)
 	}
 	attrs = append(attrs, BlockAttrs(nodeData)...)
 	r.CR(w)
-	r.outTag(w, headingOpenTagFromLevel(nodeData.Level), attrs)
+	r.OutTag(w, HeadingOpenTagFromLevel(nodeData.Level), attrs)
 }
 
 func (r *Renderer) headingExit(w io.Writer, heading *ast.Heading) {
-	r.Outs(w, headingCloseTagFromLevel(heading.Level))
-	if !(isListItem(heading.Parent) && ast.GetNextNode(heading) == nil) {
+	r.Outs(w, HeadingCloseTagFromLevel(heading.Level))
+	if !(IsListItem(heading.Parent) && ast.GetNextNode(heading) == nil) {
 		r.CR(w)
 	}
 }
@@ -674,7 +694,7 @@ func (r *Renderer) Heading(w io.Writer, node *ast.Heading, entering bool) {
 // HorizontalRule writes ast.HorizontalRule node
 func (r *Renderer) HorizontalRule(w io.Writer, node *ast.HorizontalRule) {
 	r.CR(w)
-	r.outHRTag(w, BlockAttrs(node))
+	r.OutHRTag(w, BlockAttrs(node))
 	r.CR(w)
 }
 
@@ -684,15 +704,15 @@ func (r *Renderer) listEnter(w io.Writer, nodeData *ast.List) {
 
 	if nodeData.IsFootnotesList {
 		r.Outs(w, "\n<div class=\"footnotes\">\n\n")
-		if r.opts.Flags&FootnoteNoHRTag == 0 {
-			r.outHRTag(w, nil)
+		if r.Opts.Flags&FootnoteNoHRTag == 0 {
+			r.OutHRTag(w, nil)
 			r.CR(w)
 		}
 	}
 	r.CR(w)
-	if isListItem(nodeData.Parent) {
+	if IsListItem(nodeData.Parent) {
 		grand := nodeData.Parent.GetParent()
-		if isListTight(grand) {
+		if IsListTight(grand) {
 			r.CR(w)
 		}
 	}
@@ -708,7 +728,7 @@ func (r *Renderer) listEnter(w io.Writer, nodeData *ast.List) {
 		openTag = "<dl"
 	}
 	attrs = append(attrs, BlockAttrs(nodeData)...)
-	r.outTag(w, openTag, attrs)
+	r.OutTag(w, openTag, attrs)
 	r.CR(w)
 }
 
@@ -751,12 +771,12 @@ func (r *Renderer) List(w io.Writer, list *ast.List, entering bool) {
 }
 
 func (r *Renderer) listItemEnter(w io.Writer, listItem *ast.ListItem) {
-	if listItemOpenCR(listItem) {
+	if ListItemOpenCR(listItem) {
 		r.CR(w)
 	}
 	if listItem.RefLink != nil {
-		slug := slugify(listItem.RefLink)
-		r.Outs(w, footnoteItem(r.opts.FootnoteAnchorPrefix, slug))
+		slug := Slugify(listItem.RefLink)
+		r.Outs(w, FootnoteItem(r.Opts.FootnoteAnchorPrefix, slug))
 		return
 	}
 
@@ -771,11 +791,11 @@ func (r *Renderer) listItemEnter(w io.Writer, listItem *ast.ListItem) {
 }
 
 func (r *Renderer) listItemExit(w io.Writer, listItem *ast.ListItem) {
-	if listItem.RefLink != nil && r.opts.Flags&FootnoteReturnLinks != 0 {
-		slug := slugify(listItem.RefLink)
-		prefix := r.opts.FootnoteAnchorPrefix
-		link := r.opts.FootnoteReturnLinkContents
-		s := footnoteReturnLink(prefix, link, slug)
+	if listItem.RefLink != nil && r.Opts.Flags&FootnoteReturnLinks != 0 {
+		slug := Slugify(listItem.RefLink)
+		prefix := r.Opts.FootnoteAnchorPrefix
+		link := r.Opts.FootnoteReturnLinkContents
+		s := FootnoteReturnLink(prefix, link, slug)
 		r.Outs(w, s)
 	}
 
@@ -806,7 +826,7 @@ func (r *Renderer) EscapeHTMLCallouts(w io.Writer, d []byte) {
 	ld := len(d)
 Parse:
 	for i := 0; i < ld; i++ {
-		for _, comment := range r.opts.Comments {
+		for _, comment := range r.Opts.Comments {
 			if !bytes.HasPrefix(d[i:], comment) {
 				break
 			}
@@ -844,14 +864,14 @@ func (r *Renderer) CodeBlock(w io.Writer, codeBlock *ast.CodeBlock) {
 	r.Outs(w, "<pre>")
 	code := TagWithAttributes("<code", attrs)
 	r.Outs(w, code)
-	if r.opts.Comments != nil {
+	if r.Opts.Comments != nil {
 		r.EscapeHTMLCallouts(w, codeBlock.Literal)
 	} else {
 		EscapeHTML(w, codeBlock.Literal)
 	}
 	r.Outs(w, "</code>")
 	r.Outs(w, "</pre>")
-	if !isListItem(codeBlock.Parent) {
+	if !IsListItem(codeBlock.Parent) {
 		r.CR(w)
 	}
 }
@@ -901,7 +921,7 @@ func (r *Renderer) TableCell(w io.Writer, tableCell *ast.TableCell, entering boo
 	if ast.GetPrevNode(tableCell) == nil {
 		r.CR(w)
 	}
-	r.outTag(w, openTag, attrs)
+	r.OutTag(w, openTag, attrs)
 }
 
 // TableBody writes ast.TableBody node
@@ -950,8 +970,8 @@ func (r *Renderer) Citation(w io.Writer, node *ast.Citation) {
 		case ast.CitationTypeSuppressed:
 			attr[0] = `class="suppressed"`
 		}
-		r.outTag(w, "<cite", attr)
-		r.Outs(w, fmt.Sprintf(`<a href="#%s">`+r.opts.CitationFormatString+`</a>`, c, c))
+		r.OutTag(w, "<cite", attr)
+		r.Outs(w, fmt.Sprintf(`<a href="#%s">`+r.Opts.CitationFormatString+`</a>`, c, c))
 		r.Outs(w, "</cite>")
 	}
 }
@@ -959,7 +979,7 @@ func (r *Renderer) Citation(w io.Writer, node *ast.Citation) {
 // Callout writes ast.Callout node
 func (r *Renderer) Callout(w io.Writer, node *ast.Callout) {
 	attr := []string{`class="callout"`}
-	r.outTag(w, "<span", attr)
+	r.OutTag(w, "<span", attr)
 	r.Out(w, node.ID)
 	r.Outs(w, "</span>")
 }
@@ -968,14 +988,14 @@ func (r *Renderer) Callout(w io.Writer, node *ast.Callout) {
 func (r *Renderer) Index(w io.Writer, node *ast.Index) {
 	// there is no in-text representation.
 	attr := []string{`class="index"`, fmt.Sprintf(`id="%s"`, node.ID)}
-	r.outTag(w, "<span", attr)
+	r.OutTag(w, "<span", attr)
 	r.Outs(w, "</span>")
 }
 
 // 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 {
-		status, didHandle := r.opts.RenderNodeHook(w, node, entering)
+	if r.Opts.RenderNodeHook != nil {
+		status, didHandle := r.Opts.RenderNodeHook(w, node, entering)
 		if didHandle {
 			return status
 		}
@@ -1010,7 +1030,7 @@ func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Wal
 	case *ast.Citation:
 		r.Citation(w, node)
 	case *ast.Image:
-		if r.opts.Flags&SkipImages != 0 {
+		if r.Opts.Flags&SkipImages != 0 {
 			return ast.SkipChildren
 		}
 		r.Image(w, node, entering)
@@ -1089,7 +1109,7 @@ func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Wal
 // RenderHeader writes HTML document preamble and TOC if requested.
 func (r *Renderer) RenderHeader(w io.Writer, ast ast.Node) {
 	r.writeDocumentHeader(w)
-	if r.opts.Flags&TOC != 0 {
+	if r.Opts.Flags&TOC != 0 {
 		r.writeTOC(w, ast)
 	}
 }
@@ -1100,18 +1120,18 @@ func (r *Renderer) RenderFooter(w io.Writer, _ ast.Node) {
 		r.Outs(w, "</section>\n")
 	}
 
-	if r.opts.Flags&CompletePage == 0 {
+	if r.Opts.Flags&CompletePage == 0 {
 		return
 	}
 	io.WriteString(w, "\n</body>\n</html>\n")
 }
 
 func (r *Renderer) writeDocumentHeader(w io.Writer) {
-	if r.opts.Flags&CompletePage == 0 {
+	if r.Opts.Flags&CompletePage == 0 {
 		return
 	}
 	ending := ""
-	if r.opts.Flags&UseXHTML != 0 {
+	if r.Opts.Flags&UseXHTML != 0 {
 		io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
 		io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
 		io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
@@ -1122,35 +1142,35 @@ func (r *Renderer) writeDocumentHeader(w io.Writer) {
 	}
 	io.WriteString(w, "<head>\n")
 	io.WriteString(w, "  <title>")
-	if r.opts.Flags&Smartypants != 0 {
-		r.sr.Process(w, []byte(r.opts.Title))
+	if r.Opts.Flags&Smartypants != 0 {
+		r.sr.Process(w, []byte(r.Opts.Title))
 	} else {
-		EscapeHTML(w, []byte(r.opts.Title))
+		EscapeHTML(w, []byte(r.Opts.Title))
 	}
 	io.WriteString(w, "</title>\n")
-	io.WriteString(w, r.opts.Generator)
+	io.WriteString(w, r.Opts.Generator)
 	io.WriteString(w, "\"")
 	io.WriteString(w, ending)
 	io.WriteString(w, ">\n")
 	io.WriteString(w, "  <meta charset=\"utf-8\"")
 	io.WriteString(w, ending)
 	io.WriteString(w, ">\n")
-	if r.opts.CSS != "" {
+	if r.Opts.CSS != "" {
 		io.WriteString(w, "  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
-		EscapeHTML(w, []byte(r.opts.CSS))
+		EscapeHTML(w, []byte(r.Opts.CSS))
 		io.WriteString(w, "\"")
 		io.WriteString(w, ending)
 		io.WriteString(w, ">\n")
 	}
-	if r.opts.Icon != "" {
+	if r.Opts.Icon != "" {
 		io.WriteString(w, "  <link rel=\"icon\" type=\"image/x-icon\" href=\"")
-		EscapeHTML(w, []byte(r.opts.Icon))
+		EscapeHTML(w, []byte(r.Opts.Icon))
 		io.WriteString(w, "\"")
 		io.WriteString(w, ending)
 		io.WriteString(w, ">\n")
 	}
-	if r.opts.Head != nil {
-		w.Write(r.opts.Head)
+	if r.Opts.Head != nil {
+		w.Write(r.Opts.Head)
 	}
 	io.WriteString(w, "</head>\n")
 	io.WriteString(w, "<body>\n\n")
@@ -1212,31 +1232,31 @@ func (r *Renderer) writeTOC(w io.Writer, doc ast.Node) {
 	r.lastOutputLen = buf.Len()
 }
 
-func isList(node ast.Node) bool {
+func IsList(node ast.Node) bool {
 	_, ok := node.(*ast.List)
 	return ok
 }
 
-func isListTight(node ast.Node) bool {
+func IsListTight(node ast.Node) bool {
 	if list, ok := node.(*ast.List); ok {
 		return list.Tight
 	}
 	return false
 }
 
-func isListItem(node ast.Node) bool {
+func IsListItem(node ast.Node) bool {
 	_, ok := node.(*ast.ListItem)
 	return ok
 }
 
-func isListItemTerm(node ast.Node) bool {
+func IsListItemTerm(node ast.Node) bool {
 	data, ok := node.(*ast.ListItem)
 	return ok && data.ListFlags&ast.ListTypeTerm != 0
 }
 
 // TODO: move to internal package
 // Create a url-safe slug for fragments
-func slugify(in []byte) []byte {
+func Slugify(in []byte) []byte {
 	if len(in) == 0 {
 		return in
 	}
@@ -1269,33 +1289,6 @@ func slugify(in []byte) []byte {
 	return out[a : b+1]
 }
 
-// TODO: move to internal package
-// isAlnum returns true if c is a digit or letter
-// TODO: check when this is looking for ASCII alnum and when it should use unicode
-func isAlnum(c byte) bool {
-	return (c >= '0' && c <= '9') || isLetter(c)
-}
-
-// isSpace returns true if c is a white-space charactr
-func isSpace(c byte) bool {
-	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
-}
-
-// isLetter returns true if c is ascii letter
-func isLetter(c byte) bool {
-	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
-}
-
-// isPunctuation returns true if c is a punctuation symbol.
-func isPunctuation(c byte) bool {
-	for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
-		if c == r {
-			return true
-		}
-	}
-	return false
-}
-
 // BlockAttrs takes a node and checks if it has block level attributes set. If so it
 // will return a slice each containing a "key=value(s)" string.
 func BlockAttrs(node ast.Node) []string {
diff --git a/html/smartypants.go b/html/smartypants.go
index a09866b..706e4ff 100644
--- a/html/smartypants.go
+++ b/html/smartypants.go
@@ -3,10 +3,18 @@ package html
 import (
 	"bytes"
 	"io"
+
+	"github.com/gomarkdown/markdown/parser"
 )
 
 // SmartyPants rendering
 
+var (
+	isSpace       = parser.IsSpace
+	isAlnum       = parser.IsAlnum
+	isPunctuation = parser.IsPunctuation
+)
+
 // SPRenderer is a struct containing state of a Smartypants renderer.
 type SPRenderer struct {
 	inSingleQuote bool
diff --git a/html_renderer_test.go b/html_renderer_test.go
index 2000844..0936ecd 100644
--- a/html_renderer_test.go
+++ b/html_renderer_test.go
@@ -55,6 +55,21 @@ func TestRenderNodeHookCode(t *testing.T) {
 	doTestsParam(t, tests, params)
 }
 
+func TestTagParagraphCode(t *testing.T) {
+	tests := []string{
+		"test",
+		"<div>test</div>\n",
+	}
+	opts := html.RendererOptions{
+		ParagraphTag: "div",
+	}
+	params := TestParams{
+		RendererOptions: opts,
+		extensions:      parser.CommonExtensions,
+	}
+	doTestsParam(t, tests, params)
+}
+
 func TestRenderNodeHookLinkAttrs(t *testing.T) {
 	tests := []string{
 		`[Click Me](gopher://foo.bar "Click Me")`,
diff --git a/inline_test.go b/inline_test.go
index 8073cda..250300c 100644
--- a/inline_test.go
+++ b/inline_test.go
@@ -341,7 +341,7 @@ func TestInlineLink(t *testing.T) {
 		"<p><a href=\"/bar/ title with no quotes\">foo with a title</a></p>\n",
 
 		"[foo]()\n",
-		"<p>[foo]()</p>\n",
+		"<p><a href=\"\">foo</a></p>\n",
 
 		"![foo](/bar/)\n",
 		"<p><img src=\"/bar/\" alt=\"foo\" /></p>\n",
@@ -365,7 +365,7 @@ func TestInlineLink(t *testing.T) {
 		"<p><a href=\"url\">link</a></p>\n",
 
 		"![foo]()\n",
-		"<p>![foo]()</p>\n",
+		"<p><img src=\"\" alt=\"foo\" /></p>\n",
 
 		"[a link]\t(/with_a_tab/)\n",
 		"<p><a href=\"/with_a_tab/\">a link</a></p>\n",
diff --git a/internal/valid/valid.go b/internal/valid/valid.go
deleted file mode 100644
index 9b3de3e..0000000
--- a/internal/valid/valid.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package valid
-
-import (
-	"bytes"
-)
-
-var URIs = [][]byte{
-	[]byte("http://"),
-	[]byte("https://"),
-	[]byte("ftp://"),
-	[]byte("mailto:"),
-}
-
-var Paths = [][]byte{
-	[]byte("/"),
-	[]byte("./"),
-	[]byte("../"),
-}
-
-// TODO: documentation
-func IsSafeURL(url []byte) bool {
-	nLink := len(url)
-	for _, path := range Paths {
-		nPath := len(path)
-		linkPrefix := url[:nPath]
-		if nLink >= nPath && bytes.Equal(linkPrefix, path) {
-			if nLink == nPath {
-				return true
-			} else if isAlnum(url[nPath]) {
-				return true
-			}
-		}
-	}
-
-	for _, prefix := range URIs {
-		// TODO: handle unicode here
-		// case-insensitive prefix test
-		nPrefix := len(prefix)
-		if nLink > nPrefix {
-			linkPrefix := bytes.ToLower(url[:nPrefix])
-			if bytes.Equal(linkPrefix, prefix) && isAlnum(url[nPrefix]) {
-				return true
-			}
-		}
-	}
-
-	return false
-}
-
-// isAlnum returns true if c is a digit or letter
-// TODO: check when this is looking for ASCII alnum and when it should use unicode
-func isAlnum(c byte) bool {
-	return (c >= '0' && c <= '9') || isLetter(c)
-}
-
-// isLetter returns true if c is ascii letter
-func isLetter(c byte) bool {
-	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
-}
diff --git a/markdown.go b/markdown.go
index 537eb27..2fb73c1 100644
--- a/markdown.go
+++ b/markdown.go
@@ -84,28 +84,7 @@ func ToHTML(markdown []byte, p *parser.Parser, renderer Renderer) []byte {
 	return Render(doc, renderer)
 }
 
-// NormalizeNewlines converts Windows and Mac newlines to Unix newlines
-// The parser only supports Unix newlines. If your mardown content
+// NormalizeNewlines converts Windows and Mac newlines to Unix newlines.
+// The parser only supports Unix newlines. If your markdown content
 // might contain Windows or Mac newlines, use this function to convert to Unix newlines
-func NormalizeNewlines(d []byte) []byte {
-	wi := 0
-	n := len(d)
-	for i := 0; i < n; i++ {
-		c := d[i]
-		// 13 is CR
-		if c != 13 {
-			d[wi] = c
-			wi++
-			continue
-		}
-		// replace CR (mac / win) with LF (unix)
-		d[wi] = 10
-		wi++
-		if i < n-1 && d[i+1] == 10 {
-			// this was CRLF, so skip the LF
-			i++
-		}
-
-	}
-	return d[:wi]
-}
+var NormalizeNewlines = parser.NormalizeNewlines
diff --git a/parser/aside.go b/parser/aside.go
index 96e25fe..9d02ed0 100644
--- a/parser/aside.go
+++ b/parser/aside.go
@@ -25,13 +25,13 @@ func (p *Parser) asidePrefix(data []byte) int {
 // aside ends with at least one blank line
 // followed by something without a aside prefix
 func (p *Parser) terminateAside(data []byte, beg, end int) bool {
-	if p.isEmpty(data[beg:]) <= 0 {
+	if IsEmpty(data[beg:]) <= 0 {
 		return false
 	}
 	if end >= len(data) {
 		return true
 	}
-	return p.asidePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0
+	return p.asidePrefix(data[end:]) == 0 && IsEmpty(data[end:]) == 0
 }
 
 // parse a aside fragment
@@ -66,8 +66,8 @@ func (p *Parser) aside(data []byte) int {
 		beg = end
 	}
 
-	block := p.addBlock(&ast.Aside{})
-	p.block(raw.Bytes())
-	p.finalize(block)
+	block := p.AddBlock(&ast.Aside{})
+	p.Block(raw.Bytes())
+	p.Finalize(block)
 	return end
 }
diff --git a/parser/block.go b/parser/block.go
index 7c2401f..028ae75 100644
--- a/parser/block.go
+++ b/parser/block.go
@@ -103,10 +103,10 @@ func sanitizeHeadingID(text string) string {
 	return string(anchorName)
 }
 
-// Parse block-level data.
+// Parse Block-level data.
 // Note: this function and many that it calls assume that
 // the input buffer ends with a newline.
-func (p *Parser) block(data []byte) {
+func (p *Parser) Block(data []byte) {
 	// this is called recursively: enforce a maximum depth
 	if p.nesting >= p.maxNesting {
 		return
@@ -142,7 +142,7 @@ func (p *Parser) block(data []byte) {
 					}
 				}
 				p.includeStack.Push(path)
-				p.block(included)
+				p.Block(included)
 				p.includeStack.Pop()
 				data = data[consumed:]
 				continue
@@ -156,10 +156,10 @@ func (p *Parser) block(data []byte) {
 				data = data[consumed:]
 
 				if node != nil {
-					p.addBlock(node)
+					p.AddBlock(node)
 					if blockdata != nil {
-						p.block(blockdata)
-						p.finalize(node)
+						p.Block(blockdata)
+						p.Finalize(node)
 					}
 				}
 				continue
@@ -213,7 +213,7 @@ func (p *Parser) block(data []byte) {
 		}
 
 		// blank lines.  note: returns the # of bytes to skip
-		if i := p.isEmpty(data); i > 0 {
+		if i := IsEmpty(data); i > 0 {
 			data = data[i:]
 			continue
 		}
@@ -255,11 +255,11 @@ func (p *Parser) block(data []byte) {
 		// ******
 		// or
 		// ______
-		if p.isHRule(data) {
+		if isHRule(data) {
 			i := skipUntilChar(data, 0, '\n')
 			hr := ast.HorizontalRule{}
 			hr.Literal = bytes.Trim(data[:i], " \n")
-			p.addBlock(&hr)
+			p.AddBlock(&hr)
 			data = data[i:]
 			continue
 		}
@@ -377,7 +377,7 @@ func (p *Parser) block(data []byte) {
 	p.nesting--
 }
 
-func (p *Parser) addBlock(n ast.Node) ast.Node {
+func (p *Parser) AddBlock(n ast.Node) ast.Node {
 	p.closeUnmatchedBlocks()
 
 	if p.attr != nil {
@@ -448,7 +448,7 @@ func (p *Parser) prefixHeading(data []byte) int {
 			p.allHeadingsWithAutoID = append(p.allHeadingsWithAutoID, block)
 		}
 		block.Content = data[i:end]
-		p.addBlock(block)
+		p.AddBlock(block)
 	}
 	return skip
 }
@@ -521,7 +521,7 @@ func (p *Parser) prefixSpecialHeading(data []byte) int {
 		}
 		block.Literal = data[i:end]
 		block.Content = data[i:end]
-		p.addBlock(block)
+		p.AddBlock(block)
 	}
 	return skip
 }
@@ -572,7 +572,7 @@ func (p *Parser) titleBlock(data []byte, doRender bool) int {
 		IsTitleblock: true,
 	}
 	block.Content = data
-	p.addBlock(block)
+	p.AddBlock(block)
 
 	return consumed
 }
@@ -617,14 +617,14 @@ func (p *Parser) html(data []byte, doRender bool) int {
 			}
 
 			// see if it is the only thing on the line
-			if skip := p.isEmpty(data[j:]); skip > 0 {
+			if skip := IsEmpty(data[j:]); skip > 0 {
 				// see if it is followed by a blank line/eof
 				j += skip
 				if j >= len(data) {
 					found = true
 					i = j
 				} else {
-					if skip := p.isEmpty(data[j:]); skip > 0 {
+					if skip := IsEmpty(data[j:]); skip > 0 {
 						j += skip
 						found = true
 						i = j
@@ -667,7 +667,7 @@ func (p *Parser) html(data []byte, doRender bool) int {
 		// trim newlines
 		end := backChar(data, i, '\n')
 		htmlBLock := &ast.HTMLBlock{Leaf: ast.Leaf{Content: data[:end]}}
-		p.addBlock(htmlBLock)
+		p.AddBlock(htmlBLock)
 		finalizeHTMLBlock(htmlBLock)
 	}
 
@@ -683,13 +683,13 @@ func finalizeHTMLBlock(block *ast.HTMLBlock) {
 func (p *Parser) htmlComment(data []byte, doRender bool) int {
 	i := p.inlineHTMLComment(data)
 	// needs to end with a blank line
-	if j := p.isEmpty(data[i:]); j > 0 {
+	if j := IsEmpty(data[i:]); j > 0 {
 		size := i + j
 		if doRender {
 			// trim trailing newlines
 			end := backChar(data, size, '\n')
 			htmlBLock := &ast.HTMLBlock{Leaf: ast.Leaf{Content: data[:end]}}
-			p.addBlock(htmlBLock)
+			p.AddBlock(htmlBLock)
 			finalizeHTMLBlock(htmlBLock)
 		}
 		return size
@@ -715,13 +715,13 @@ func (p *Parser) htmlHr(data []byte, doRender bool) int {
 	}
 	if i < len(data) && data[i] == '>' {
 		i++
-		if j := p.isEmpty(data[i:]); j > 0 {
+		if j := IsEmpty(data[i:]); j > 0 {
 			size := i + j
 			if doRender {
 				// trim newlines
 				end := backChar(data, size, '\n')
 				htmlBlock := &ast.HTMLBlock{Leaf: ast.Leaf{Content: data[:end]}}
-				p.addBlock(htmlBlock)
+				p.AddBlock(htmlBlock)
 				finalizeHTMLBlock(htmlBlock)
 			}
 			return size
@@ -753,7 +753,7 @@ func (p *Parser) htmlFindEnd(tag string, data []byte) int {
 
 	// check that the rest of the line is blank
 	skip := 0
-	if skip = p.isEmpty(data[i:]); skip == 0 {
+	if skip = IsEmpty(data[i:]); skip == 0 {
 		return 0
 	}
 	i += skip
@@ -766,7 +766,7 @@ func (p *Parser) htmlFindEnd(tag string, data []byte) int {
 	if p.extensions&LaxHTMLBlocks != 0 {
 		return i
 	}
-	if skip = p.isEmpty(data[i:]); skip == 0 {
+	if skip = IsEmpty(data[i:]); skip == 0 {
 		// following line must be blank
 		return 0
 	}
@@ -774,7 +774,7 @@ func (p *Parser) htmlFindEnd(tag string, data []byte) int {
 	return i + skip
 }
 
-func (*Parser) isEmpty(data []byte) int {
+func IsEmpty(data []byte) int {
 	// it is okay to call isEmpty on an empty buffer
 	if len(data) == 0 {
 		return 0
@@ -790,7 +790,7 @@ func (*Parser) isEmpty(data []byte) int {
 	return i
 }
 
-func (*Parser) isHRule(data []byte) bool {
+func isHRule(data []byte) bool {
 	i := 0
 
 	// skip up to three spaces
@@ -909,18 +909,18 @@ func syntaxRange(data []byte, iout *int) (int, int) {
 
 		// strip all whitespace at the beginning and the end
 		// of the {} block
-		for syn > 0 && isSpace(data[syntaxStart]) {
+		for syn > 0 && IsSpace(data[syntaxStart]) {
 			syntaxStart++
 			syn--
 		}
 
-		for syn > 0 && isSpace(data[syntaxStart+syn-1]) {
+		for syn > 0 && IsSpace(data[syntaxStart+syn-1]) {
 			syn--
 		}
 
 		i++
 	} else {
-		for i < n && !isSpace(data[i]) {
+		for i < n && !IsSpace(data[i]) {
 			syn++
 			i++
 		}
@@ -976,7 +976,7 @@ func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int {
 		codeBlock.Content = work.Bytes() // TODO: get rid of temp buffer
 
 		if p.extensions&Mmark == 0 {
-			p.addBlock(codeBlock)
+			p.AddBlock(codeBlock)
 			finalizeCodeBlock(codeBlock)
 			return beg
 		}
@@ -988,12 +988,12 @@ func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int {
 			figure.HeadingID = id
 			p.Inline(caption, captionContent)
 
-			p.addBlock(figure)
+			p.AddBlock(figure)
 			codeBlock.AsLeaf().Attribute = figure.AsContainer().Attribute
 			p.addChild(codeBlock)
 			finalizeCodeBlock(codeBlock)
 			p.addChild(caption)
-			p.finalize(figure)
+			p.Finalize(figure)
 
 			beg += consumed
 
@@ -1001,7 +1001,7 @@ func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int {
 		}
 
 		// Still here, normal block
-		p.addBlock(codeBlock)
+		p.AddBlock(codeBlock)
 		finalizeCodeBlock(codeBlock)
 	}
 
@@ -1055,13 +1055,13 @@ func (p *Parser) quotePrefix(data []byte) int {
 // blockquote ends with at least one blank line
 // followed by something without a blockquote prefix
 func (p *Parser) terminateBlockquote(data []byte, beg, end int) bool {
-	if p.isEmpty(data[beg:]) <= 0 {
+	if IsEmpty(data[beg:]) <= 0 {
 		return false
 	}
 	if end >= len(data) {
 		return true
 	}
-	return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0
+	return p.quotePrefix(data[end:]) == 0 && IsEmpty(data[end:]) == 0
 }
 
 // parse a blockquote fragment
@@ -1096,9 +1096,9 @@ func (p *Parser) quote(data []byte) int {
 	}
 
 	if p.extensions&Mmark == 0 {
-		block := p.addBlock(&ast.BlockQuote{})
-		p.block(raw.Bytes())
-		p.finalize(block)
+		block := p.AddBlock(&ast.BlockQuote{})
+		p.Block(raw.Bytes())
+		p.Finalize(block)
 		return end
 	}
 
@@ -1108,24 +1108,24 @@ func (p *Parser) quote(data []byte) int {
 		figure.HeadingID = id
 		p.Inline(caption, captionContent)
 
-		p.addBlock(figure) // this discard any attributes
+		p.AddBlock(figure) // this discard any attributes
 		block := &ast.BlockQuote{}
 		block.AsContainer().Attribute = figure.AsContainer().Attribute
 		p.addChild(block)
-		p.block(raw.Bytes())
-		p.finalize(block)
+		p.Block(raw.Bytes())
+		p.Finalize(block)
 
 		p.addChild(caption)
-		p.finalize(figure)
+		p.Finalize(figure)
 
 		end += consumed
 
 		return end
 	}
 
-	block := p.addBlock(&ast.BlockQuote{})
-	p.block(raw.Bytes())
-	p.finalize(block)
+	block := p.AddBlock(&ast.BlockQuote{})
+	p.Block(raw.Bytes())
+	p.Finalize(block)
 
 	return end
 }
@@ -1152,7 +1152,7 @@ func (p *Parser) code(data []byte) int {
 		i = skipUntilChar(data, i, '\n')
 		i = skipCharN(data, i, '\n', 1)
 
-		blankline := p.isEmpty(data[beg:i]) > 0
+		blankline := IsEmpty(data[beg:i]) > 0
 		if pre := p.codePrefix(data[beg:i]); pre > 0 {
 			beg += pre
 		} else if !blankline {
@@ -1185,7 +1185,7 @@ func (p *Parser) code(data []byte) int {
 	}
 	// TODO: get rid of temp buffer
 	codeBlock.Content = work.Bytes()
-	p.addBlock(codeBlock)
+	p.AddBlock(codeBlock)
 	finalizeCodeBlock(codeBlock)
 
 	return i
@@ -1237,10 +1237,29 @@ func (p *Parser) dliPrefix(data []byte) int {
 	if data[0] != ':' || !(data[1] == ' ' || data[1] == '\t') {
 		return 0
 	}
+	// TODO: this is a no-op (data[0] is ':' so not ' ').
+	// Maybe the intent was to eat spaces before ':' ?
+	// either way, no change in tests
 	i := skipChar(data, 0, ' ')
 	return i + 2
 }
 
+// TODO: maybe it was meant to be like below
+// either way, no change in tests
+/*
+func (p *Parser) dliPrefix(data []byte) int {
+	i := skipChar(data, 0, ' ')
+	if i+len(data) < 2 {
+		return 0
+	}
+	// need a ':' followed by a space or a tab
+	if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') {
+		return 0
+	}
+	return i + 2
+}
+*/
+
 // parse ordered or unordered list block
 func (p *Parser) list(data []byte, flags ast.ListType, start int, delim byte) int {
 	i := 0
@@ -1251,7 +1270,7 @@ func (p *Parser) list(data []byte, flags ast.ListType, start int, delim byte) in
 		Start:     start,
 		Delimiter: delim,
 	}
-	block := p.addBlock(list)
+	block := p.AddBlock(list)
 
 	for i < len(data) {
 		skip := p.listItem(data[i:], &flags)
@@ -1398,7 +1417,7 @@ gatherlines:
 
 		// if it is an empty line, guess that it is part of this item
 		// and move on to the next line
-		if p.isEmpty(data[line:i]) > 0 {
+		if IsEmpty(data[line:i]) > 0 {
 			containsBlankLine = true
 			line = i
 			continue
@@ -1419,10 +1438,20 @@ gatherlines:
 
 		chunk := data[line+indentIndex : i]
 
+		// If there is a fence line (marking starting of a code block)
+		// without indent do not process it as part of the list.
+		if p.extensions&FencedCode != 0 {
+			fenceLineEnd, _ := isFenceLine(chunk, nil, "")
+			if fenceLineEnd > 0 && indent == 0 {
+				*flags |= ast.ListItemEndOfList
+				break gatherlines
+			}
+		}
+
 		// evaluate how this line fits in
 		switch {
 		// is this a nested list item?
-		case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || p.oliPrefix(chunk) > 0 || p.dliPrefix(chunk) > 0:
+		case (p.uliPrefix(chunk) > 0 && !isHRule(chunk)) || p.oliPrefix(chunk) > 0 || p.dliPrefix(chunk) > 0:
 
 			// if indent is 4 or more spaces on unordered or ordered lists
 			// we need to add leadingWhiteSpaces + 1 spaces in the beginning of the chunk
@@ -1474,10 +1503,7 @@ gatherlines:
 		case containsBlankLine && indent < 4:
 			if *flags&ast.ListTypeDefinition != 0 && i < len(data)-1 {
 				// is the next item still a part of this list?
-				next := i
-				for next < len(data) && data[next] != '\n' {
-					next++
-				}
+				next := skipUntilChar(data, i, '\n')
 				for next < len(data)-1 && data[next] == '\n' {
 					next++
 				}
@@ -1516,16 +1542,16 @@ gatherlines:
 		BulletChar: bulletChar,
 		Delimiter:  delimiter,
 	}
-	p.addBlock(listItem)
+	p.AddBlock(listItem)
 
 	// render the contents of the list item
 	if *flags&ast.ListItemContainsBlock != 0 && *flags&ast.ListTypeTerm == 0 {
 		// intermediate render of block item, except for definition term
 		if sublist > 0 {
-			p.block(rawBytes[:sublist])
-			p.block(rawBytes[sublist:])
+			p.Block(rawBytes[:sublist])
+			p.Block(rawBytes[sublist:])
 		} else {
-			p.block(rawBytes)
+			p.Block(rawBytes)
 		}
 	} else {
 		// intermediate render of inline item
@@ -1537,7 +1563,7 @@ gatherlines:
 		}
 		p.addChild(para)
 		if sublist > 0 {
-			p.block(rawBytes[sublist:])
+			p.Block(rawBytes[sublist:])
 		}
 	}
 	return line
@@ -1564,7 +1590,7 @@ func (p *Parser) renderParagraph(data []byte) {
 	}
 	para := &ast.Paragraph{}
 	para.Content = data[beg:end]
-	p.addBlock(para)
+	p.AddBlock(para)
 }
 
 // blockMath handle block surround with $$
@@ -1586,7 +1612,7 @@ func (p *Parser) blockMath(data []byte) int {
 	// render the display math
 	mathBlock := &ast.MathBlock{}
 	mathBlock.Literal = data[2:end]
-	p.addBlock(mathBlock)
+	p.AddBlock(mathBlock)
 
 	return end + 2
 }
@@ -1616,7 +1642,7 @@ func (p *Parser) paragraph(data []byte) int {
 		}
 
 		// did we find a blank line marking the end of the paragraph?
-		if n := p.isEmpty(current); n > 0 {
+		if n := IsEmpty(current); n > 0 {
 			// did this blank line followed by a definition list item?
 			if p.extensions&DefinitionLists != 0 {
 				if i < len(data)-1 && data[i+1] == ':' {
@@ -1653,7 +1679,7 @@ func (p *Parser) paragraph(data []byte) int {
 				}
 
 				block.Content = data[prev:eol]
-				p.addBlock(block)
+				p.AddBlock(block)
 
 				// find the end of the underline
 				return skipUntilChar(data, i, '\n')
@@ -1670,7 +1696,7 @@ func (p *Parser) paragraph(data []byte) int {
 		}
 
 		// if there's a prefixed heading or a horizontal rule after this, paragraph is over
-		if p.isPrefixHeading(current) || p.isPrefixSpecialHeading(current) || p.isHRule(current) {
+		if p.isPrefixHeading(current) || p.isPrefixSpecialHeading(current) || isHRule(current) {
 			p.renderParagraph(data[:i])
 			return i
 		}
@@ -1767,7 +1793,7 @@ func skipUntilChar(data []byte, i int, c byte) int {
 
 func skipAlnum(data []byte, i int) int {
 	n := len(data)
-	for i < n && isAlnum(data[i]) {
+	for i < n && IsAlnum(data[i]) {
 		i++
 	}
 	return i
@@ -1775,7 +1801,7 @@ func skipAlnum(data []byte, i int) int {
 
 func skipSpace(data []byte, i int) int {
 	n := len(data)
-	for i < n && isSpace(data[i]) {
+	for i < n && IsSpace(data[i]) {
 		i++
 	}
 	return i
diff --git a/parser/block_table.go b/parser/block_table.go
index 0bf4f4a..fa8efdf 100644
--- a/parser/block_table.go
+++ b/parser/block_table.go
@@ -12,7 +12,7 @@ func isBackslashEscaped(data []byte, i int) bool {
 }
 
 func (p *Parser) tableRow(data []byte, columns []ast.CellAlignFlags, header bool) {
-	p.addBlock(&ast.TableRow{})
+	p.AddBlock(&ast.TableRow{})
 	col := 0
 
 	i := skipChar(data, 0, '|')
@@ -61,7 +61,7 @@ func (p *Parser) tableRow(data []byte, columns []ast.CellAlignFlags, header bool
 			// an empty cell that we should ignore, it exists because of colspan
 			colspans--
 		} else {
-			p.addBlock(block)
+			p.AddBlock(block)
 		}
 
 		if colspan > 0 {
@@ -75,7 +75,7 @@ func (p *Parser) tableRow(data []byte, columns []ast.CellAlignFlags, header bool
 			IsHeader: header,
 			Align:    columns[col],
 		}
-		p.addBlock(block)
+		p.AddBlock(block)
 	}
 
 	// silently ignore rows with too many cells
@@ -109,7 +109,7 @@ func (p *Parser) tableFooter(data []byte) bool {
 		return false
 	}
 
-	p.addBlock(&ast.TableFooter{})
+	p.AddBlock(&ast.TableFooter{})
 
 	return true
 }
@@ -217,7 +217,7 @@ func (p *Parser) tableHeader(data []byte, doRender bool) (size int, columns []as
 		}
 		// end of column test is messy
 		switch {
-		case dashes < 3:
+		case dashes < 1:
 			// not a valid column
 			return
 
@@ -253,9 +253,9 @@ func (p *Parser) tableHeader(data []byte, doRender bool) (size int, columns []as
 
 	if doRender {
 		table = &ast.Table{}
-		p.addBlock(table)
+		p.AddBlock(table)
 		if header != nil {
-			p.addBlock(&ast.TableHeader{})
+			p.AddBlock(&ast.TableHeader{})
 			p.tableRow(header, columns, true)
 		}
 	}
@@ -277,7 +277,7 @@ func (p *Parser) table(data []byte) int {
 		return 0
 	}
 
-	p.addBlock(&ast.TableBody{})
+	p.AddBlock(&ast.TableBody{})
 
 	for i < len(data) {
 		pipes, rowStart := 0, i
@@ -319,7 +319,7 @@ func (p *Parser) table(data []byte) int {
 		ast.AppendChild(figure, caption)
 
 		p.addChild(figure)
-		p.finalize(figure)
+		p.Finalize(figure)
 
 		i += consumed
 	}
diff --git a/parser/block_table_test.go b/parser/block_table_test.go
index a399f90..b285dcf 100644
--- a/parser/block_table_test.go
+++ b/parser/block_table_test.go
@@ -15,7 +15,7 @@ func TestBug195(t *testing.T) {
 	ast.Print(&buf, doc)
 	got := buf.String()
 	// TODO: change expectations for https://github.com/gomarkdown/markdown/issues/195
-	exp := "Paragraph\n  Text '| a | b |\\n| - | - |\\n|'\n  Code 'foo|bar'\n  Text '| types |'\n"
+	exp := "Table\n  TableHeader\n    TableRow\n      TableCell\n        Text 'a'\n      TableCell\n        Text 'b'\n  TableBody\n    TableRow\n      TableCell\n        Text\n        Code 'foo|bar'\n      TableCell\n        Text 'types'\n"
 	if got != exp {
 		t.Errorf("\nInput   [%#v]\nExpected[%#v]\nGot     [%#v]\n",
 			input, exp, got)
@@ -24,7 +24,7 @@ func TestBug195(t *testing.T) {
 
 func TestBug198(t *testing.T) {
 	// there's a space after end of table header, which used to break table parsing
-	input := `| a | b| 
+	input := `| a | b|
 | :--- | ---: |
 | c | d |`
 	p := NewWithExtensions(CommonExtensions)
@@ -38,3 +38,18 @@ func TestBug198(t *testing.T) {
 			input, exp, got)
 	}
 }
+
+// https://github.com/gomarkdown/markdown/issues/274
+func TestIssue274(t *testing.T) {
+	input := "| a | b |\n| - | - |\n|	foo | bar |\n"
+	p := NewWithExtensions(CommonExtensions)
+	doc := p.Parse([]byte(input))
+	var buf bytes.Buffer
+	ast.Print(&buf, doc)
+	got := buf.String()
+	exp := "Table\n  TableHeader\n    TableRow\n      TableCell\n        Text 'a'\n      TableCell\n        Text 'b'\n  TableBody\n    TableRow\n      TableCell\n        Text '\\tfoo'\n      TableCell\n        Text 'bar'\n"
+	if got != exp {
+		t.Errorf("\nInput   [%#v]\nExpected[%#v]\nGot     [%#v]\n",
+			input, exp, got)
+	}
+}
diff --git a/parser/caption.go b/parser/caption.go
index 54d3f74..0879450 100644
--- a/parser/caption.go
+++ b/parser/caption.go
@@ -11,7 +11,7 @@ func (p *Parser) caption(data, caption []byte) ([]byte, string, int) {
 	}
 	j := len(caption)
 	data = data[j:]
-	end := p.linesUntilEmpty(data)
+	end := LinesUntilEmpty(data)
 
 	data = data[:end]
 
@@ -23,8 +23,8 @@ func (p *Parser) caption(data, caption []byte) ([]byte, string, int) {
 	return data, "", end + j
 }
 
-// linesUntilEmpty scans lines up to the first empty line.
-func (p *Parser) linesUntilEmpty(data []byte) int {
+// LinesUntilEmpty scans lines up to the first empty line.
+func LinesUntilEmpty(data []byte) int {
 	line, i := 0, 0
 
 	for line < len(data) {
@@ -35,7 +35,7 @@ func (p *Parser) linesUntilEmpty(data []byte) int {
 			i++
 		}
 
-		if p.isEmpty(data[line:i]) == 0 {
+		if IsEmpty(data[line:i]) == 0 {
 			line = i
 			continue
 		}
@@ -58,7 +58,7 @@ func captionID(data []byte) (string, int) {
 	}
 	// remains must be whitespace.
 	for l := k + 1; l < end; l++ {
-		if !isSpace(data[l]) {
+		if !IsSpace(data[l]) {
 			return "", 0
 		}
 	}
diff --git a/parser/caption_test.go b/parser/caption_test.go
index 2dc1a67..e093de3 100644
--- a/parser/caption_test.go
+++ b/parser/caption_test.go
@@ -8,7 +8,7 @@ foo bar
 
 first text after empty line`
 
-	l := New().linesUntilEmpty([]byte(data))
+	l := LinesUntilEmpty([]byte(data))
 	if l != 33 {
 		t.Errorf("want %d, got %d", 33, l)
 	}
@@ -16,7 +16,7 @@ first text after empty line`
 	data = `Figure: foo bar bar foo
 foo bar
 `
-	l = New().linesUntilEmpty([]byte(data))
+	l = LinesUntilEmpty([]byte(data))
 	if l != 32 {
 		t.Errorf("want %d, got %d", 33, l)
 	}
diff --git a/parser/figures.go b/parser/figures.go
index 6615449..0566c16 100644
--- a/parser/figures.go
+++ b/parser/figures.go
@@ -98,10 +98,10 @@ func (p *Parser) figureBlock(data []byte, doRender bool) int {
 	}
 
 	figure := &ast.CaptionFigure{}
-	p.addBlock(figure)
-	p.block(raw.Bytes())
+	p.AddBlock(figure)
+	p.Block(raw.Bytes())
 
-	defer p.finalize(figure)
+	defer p.Finalize(figure)
 
 	if captionContent, id, consumed := p.caption(data[beg:], []byte("Figure: ")); consumed > 0 {
 		caption := &ast.Caption{}
@@ -113,7 +113,5 @@ func (p *Parser) figureBlock(data []byte, doRender bool) int {
 
 		beg += consumed
 	}
-
-	p.finalize(figure)
 	return beg
 }
diff --git a/parser/inline.go b/parser/inline.go
index c16eddd..035d90a 100644
--- a/parser/inline.go
+++ b/parser/inline.go
@@ -6,7 +6,6 @@ import (
 	"strconv"
 
 	"github.com/gomarkdown/markdown/ast"
-	"github.com/gomarkdown/markdown/internal/valid"
 )
 
 // Parsing of inline elements
@@ -69,7 +68,7 @@ func emphasis(p *Parser, data []byte, offset int) (int, ast.Node) {
 	if n > 2 && data[1] != c {
 		// whitespace cannot follow an opening emphasis;
 		// strikethrough only takes two characters '~~'
-		if isSpace(data[1]) {
+		if IsSpace(data[1]) {
 			return 0, nil
 		}
 		if p.extensions&SuperSubscript != 0 && c == '~' {
@@ -81,7 +80,7 @@ func emphasis(p *Parser, data []byte, offset int) (int, ast.Node) {
 			}
 			ret++ // we started with data[1:] above.
 			for i := 1; i < ret; i++ {
-				if isSpace(data[i]) && !isEscape(data, i) {
+				if IsSpace(data[i]) && !isEscape(data, i) {
 					return 0, nil
 				}
 			}
@@ -98,7 +97,7 @@ func emphasis(p *Parser, data []byte, offset int) (int, ast.Node) {
 	}
 
 	if n > 3 && data[1] == c && data[2] != c {
-		if isSpace(data[2]) {
+		if IsSpace(data[2]) {
 			return 0, nil
 		}
 		ret, node := helperDoubleEmphasis(p, data[2:], c)
@@ -110,7 +109,7 @@ func emphasis(p *Parser, data []byte, offset int) (int, ast.Node) {
 	}
 
 	if n > 4 && data[1] == c && data[2] == c && data[3] != c {
-		if c == '~' || isSpace(data[3]) {
+		if c == '~' || IsSpace(data[3]) {
 			return 0, nil
 		}
 		ret, node := helperTripleEmphasis(p, data, 3, c)
@@ -156,8 +155,9 @@ func codeSpan(p *Parser, data []byte, offset int) (int, ast.Node) {
 		if data[j] == '\n' {
 			break
 		}
-		if !isSpace(data[j]) {
+		if !IsSpace(data[j]) {
 			hasCharsAfterDelimiter = true
+			break
 		}
 	}
 
@@ -256,7 +256,7 @@ func maybeInlineFootnoteOrSuper(p *Parser, data []byte, offset int) (int, ast.No
 			return 0, nil
 		}
 		for i := offset; i < offset+ret; i++ {
-			if isSpace(data[i]) && !isEscape(data, i) {
+			if IsSpace(data[i]) && !isEscape(data, i) {
 				return 0, nil
 			}
 		}
@@ -421,7 +421,7 @@ func link(p *Parser, data []byte, offset int) (int, ast.Node) {
 
 			// skip whitespace after title
 			titleE = i - 1
-			for titleE > titleB && isSpace(data[titleE]) {
+			for titleE > titleB && IsSpace(data[titleE]) {
 				titleE--
 			}
 
@@ -433,7 +433,7 @@ func link(p *Parser, data []byte, offset int) (int, ast.Node) {
 		}
 
 		// remove whitespace at the end of the link
-		for linkE > linkB && isSpace(data[linkE-1]) {
+		for linkE > linkB && IsSpace(data[linkE-1]) {
 			linkE--
 		}
 
@@ -602,9 +602,8 @@ func link(p *Parser, data []byte, offset int) (int, ast.Node) {
 		}
 
 		// links need something to click on and somewhere to go
-		if len(uLink) == 0 || (t == linkNormal && txtE <= 1) {
-			return 0, nil
-		}
+		// [](http://bla) is legal in CommonMark, so allow txtE <=1 for linkNormal
+		// [bla]() is also legal in CommonMark, so allow empty uLink
 	}
 
 	// call the relevant rendering function
@@ -827,7 +826,9 @@ func linkEndsWithEntity(data []byte, linkEnd int) bool {
 }
 
 // hasPrefixCaseInsensitive is a custom implementation of
-//     strings.HasPrefix(strings.ToLower(s), prefix)
+//
+//	strings.HasPrefix(strings.ToLower(s), prefix)
+//
 // we rolled our own because ToLower pulls in a huge machinery of lowercasing
 // anything from Unicode and that's very slow. Since this func will only be
 // used on ASCII protocol prefixes, we can take shortcuts.
@@ -889,7 +890,7 @@ func autoLink(p *Parser, data []byte, offset int) (int, ast.Node) {
 
 	// scan backward for a word boundary
 	rewind := 0
-	for offset-rewind > 0 && rewind <= 7 && isLetter(data[offset-rewind-1]) {
+	for offset-rewind > 0 && rewind <= 7 && IsLetter(data[offset-rewind-1]) {
 		rewind++
 	}
 	if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters
@@ -901,7 +902,7 @@ func autoLink(p *Parser, data []byte, offset int) (int, ast.Node) {
 
 	isSafeURL := p.IsSafeURLOverride
 	if isSafeURL == nil {
-		isSafeURL = valid.IsSafeURL
+		isSafeURL = IsSafeURL
 	}
 	if !isSafeURL(data) {
 		return 0, nil
@@ -996,7 +997,7 @@ func autoLink(p *Parser, data []byte, offset int) (int, ast.Node) {
 }
 
 func isEndOfLink(char byte) bool {
-	return isSpace(char) || char == '<'
+	return IsSpace(char) || char == '<'
 }
 
 // return the length of the given tag, or 0 is it's not valid
@@ -1018,7 +1019,7 @@ func tagLength(data []byte) (autolink autolinkType, end int) {
 		i = 1
 	}
 
-	if !isAlnum(data[i]) {
+	if !IsAlnum(data[i]) {
 		return notAutolink, 0
 	}
 
@@ -1026,7 +1027,7 @@ func tagLength(data []byte) (autolink autolinkType, end int) {
 	autolink = notAutolink
 
 	// try to find the beginning of an URI
-	for i < len(data) && (isAlnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') {
+	for i < len(data) && (IsAlnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') {
 		i++
 	}
 
@@ -1051,7 +1052,7 @@ func tagLength(data []byte) (autolink autolinkType, end int) {
 		for i < len(data) {
 			if data[i] == '\\' {
 				i += 2
-			} else if data[i] == '>' || data[i] == '\'' || data[i] == '"' || isSpace(data[i]) {
+			} else if data[i] == '>' || data[i] == '\'' || data[i] == '"' || IsSpace(data[i]) {
 				break
 			} else {
 				i++
@@ -1083,7 +1084,7 @@ func isMailtoAutoLink(data []byte) int {
 
 	// address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@'
 	for i, c := range data {
-		if isAlnum(c) {
+		if IsAlnum(c) {
 			continue
 		}
 
@@ -1204,10 +1205,10 @@ func helperEmphasis(p *Parser, data []byte, c byte) (int, ast.Node) {
 			continue
 		}
 
-		if data[i] == c && !isSpace(data[i-1]) {
+		if data[i] == c && !IsSpace(data[i-1]) {
 
 			if p.extensions&NoIntraEmphasis != 0 {
-				if !(i+1 == len(data) || isSpace(data[i+1]) || isPunctuation(data[i+1])) {
+				if !(i+1 == len(data) || IsSpace(data[i+1]) || IsPunctuation(data[i+1])) {
 					continue
 				}
 			}
@@ -1231,7 +1232,7 @@ func helperDoubleEmphasis(p *Parser, data []byte, c byte) (int, ast.Node) {
 		}
 		i += length
 
-		if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isSpace(data[i-1]) {
+		if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !IsSpace(data[i-1]) {
 			var node ast.Node = &ast.Strong{}
 			if c == '~' {
 				node = &ast.Del{}
@@ -1257,7 +1258,7 @@ func helperTripleEmphasis(p *Parser, data []byte, offset int, c byte) (int, ast.
 		i += length
 
 		// skip whitespace preceded symbols
-		if data[i] != c || isSpace(data[i-1]) {
+		if data[i] != c || IsSpace(data[i-1]) {
 			continue
 		}
 
diff --git a/parser/matter.go b/parser/matter.go
index 9268635..df28423 100644
--- a/parser/matter.go
+++ b/parser/matter.go
@@ -29,8 +29,8 @@ func (p *Parser) documentMatter(data []byte) int {
 		return 0
 	}
 	node := &ast.DocumentMatter{Matter: matter}
-	p.addBlock(node)
-	p.finalize(node)
+	p.AddBlock(node)
+	p.Finalize(node)
 
 	return consumed
 }
diff --git a/parser/options_test.go b/parser/options_test.go
index f63371b..0d18126 100644
--- a/parser/options_test.go
+++ b/parser/options_test.go
@@ -2,63 +2,130 @@ package parser
 
 import (
 	"bytes"
+	"strings"
 	"testing"
 
 	"github.com/gomarkdown/markdown/ast"
 )
 
+type CustomNode struct {
+	ast.Container
+}
+
+// we default to true so to test it, we need to test false
+func (n *CustomNode) CanContain(ast.Node) bool {
+	return false
+}
+
+type CustomNode2 struct {
+	ast.Container
+}
+
 func blockTitleHook(data []byte) (ast.Node, []byte, int) {
+	sep := []byte(`%%%`)
 	// parse text between %%% and %%% and return it as a blockQuote.
-	i := 0
-	if len(data) < 3 {
+	if !bytes.HasPrefix(data, sep) {
 		return nil, data, 0
 	}
-	if data[i] != '%' && data[i+1] != '%' && data[i+2] != '%' {
+
+	end := bytes.Index(data[3:], sep)
+	if end < 0 {
 		return nil, data, 0
 	}
+	end += 3
+	node := &CustomNode{}
+	return node, data[4:end], end + 3
+}
+
+func blockTitleHook2(data []byte) (ast.Node, []byte, int) {
+	sep := []byte(`%%%`)
+	// parse text between %%% and %%% and return it as a blockQuote.
+	if !bytes.HasPrefix(data, sep) {
+		return nil, data, 0
+	}
+
+	end := bytes.Index(data[3:], sep)
+	if end < 0 {
+		return nil, data, 0
+	}
+	end += 3
+	node := &CustomNode2{}
+	return node, data[4:end], end + 3
+}
+
+func astPrint(root ast.Node) string {
+	buf := &bytes.Buffer{}
+	ast.Print(buf, root)
+	return buf.String()
+}
+
+func astPretty(s string) string {
+	s = strings.Replace(s, " ", "_", -1)
+	s = strings.Replace(s, "\t", "__", -1)
+	s = strings.Replace(s, "\n", "+", -1)
+	return s
+}
+
+func TestCustomNode(t *testing.T) {
+	tests := []struct {
+		data string
+		want string
+	}{
+		{
+			data: `
+%%%
+hallo
+%%%
+`,
+			want: `CustomNode
+Paragraph 'hallo'
+`,
+		},
+	}
 
-	i += 3
-	// search for end.
-	for i < len(data) {
-		if data[i] == '%' && data[i+1] == '%' && data[i+2] == '%' {
-			break
+	p := New()
+	p.Opts = Options{ParserHook: blockTitleHook}
+
+	for _, test := range tests {
+		p.Block([]byte(test.data))
+		data := astPrint(p.Doc)
+		got := astPretty(data)
+		want := astPretty(test.want)
+
+		if got != want {
+			t.Errorf("want: %s, got: %s", want, got)
 		}
-		i++
 	}
-	node := &ast.BlockQuote{}
-	return node, data[4:i], i + 3
 }
 
-func TestOptions(t *testing.T) {
+func TestCustomNode2(t *testing.T) {
 	tests := []struct {
-		data []byte
-		want []byte
+		data string
+		want string
 	}{
 		{
-			data: []byte(`
+			data: `
 %%%
 hallo
 %%%
-`),
-			want: []byte(`BlockQuote
+`,
+			want: `CustomNode2
   Paragraph 'hallo'
-`),
+`,
 		},
 	}
 
 	p := New()
-	p.Opts = Options{ParserHook: blockTitleHook}
-	buf := &bytes.Buffer{}
+	p.Opts = Options{ParserHook: blockTitleHook2}
 
 	for _, test := range tests {
-		p.block(test.data)
-		ast.Print(buf, p.Doc)
-		data := buf.Bytes()
-		data = bytes.Replace(data, []byte(" "), []byte("_"), -1)
-		test.want = bytes.Replace(test.want, []byte(" "), []byte("_"), -1)
-
-		if !bytes.Equal(data, test.want) {
-			t.Errorf("want ast %s, got %s", test.want, data)
+		p.Block([]byte(test.data))
+		data := astPrint(p.Doc)
+		got := astPretty(data)
+		want := astPretty(test.want)
+
+		if got != want {
+			t.Errorf("want: %s, got: %s", want, got)
 		}
 	}
 }
diff --git a/parser/parser.go b/parser/parser.go
index 19d1f70..91123e1 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -42,7 +42,7 @@ const (
 	SuperSubscript                                // Super- and subscript support: 2^10^, H~2~O.
 	EmptyLinesBreakList                           // 2 empty lines break out of list
 	Includes                                      // Support including other files.
-	Mmark                                         // Support Mmark syntax, see https://mmark.nl/syntax
+	Mmark                                         // Support Mmark syntax, see https://mmark.miek.nl/post/syntax/
 
 	CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode |
 		Autolink | Strikethrough | SpaceHeadings | HeadingIDs |
@@ -84,7 +84,9 @@ type Parser struct {
 	// the bottom will be used to fill in the link details.
 	ReferenceOverride ReferenceOverrideFunc
 
-	// TODO: documentation
+	// IsSafeURLOverride allows overriding the default URL matcher. URL is
+	// safe if the overriding function returns true. Can be used to extend
+	// the default list of safe URLs.
 	IsSafeURLOverride func(url []byte) bool
 
 	Opts Options
@@ -204,13 +206,13 @@ func (p *Parser) isFootnote(ref *reference) bool {
 	return ok
 }
 
-func (p *Parser) finalize(block ast.Node) {
+func (p *Parser) Finalize(block ast.Node) {
 	p.tip = block.GetParent()
 }
 
 func (p *Parser) addChild(node ast.Node) ast.Node {
 	for !canNodeContain(p.tip, node) {
-		p.finalize(p.tip)
+		p.Finalize(p.tip)
 	}
 	ast.AppendChild(p.tip, node)
 	p.tip = node
@@ -237,6 +239,18 @@ func canNodeContain(n ast.Node, v ast.Node) bool {
 		_, ok := v.(*ast.TableCell)
 		return ok
 	}
+	// for nodes implemented outside of ast package, allow them
+	// to implement this logic via CanContain interface
+	if o, ok := n.(ast.CanContain); ok {
+		return o.CanContain(v)
+	}
+	// for container nodes outside of ast package default to true
+	// because false is a bad default
+	typ := fmt.Sprintf("%T", n)
+	customNode := !strings.HasPrefix(typ, "*ast.")
+	if customNode {
+		return n.AsLeaf() == nil
+	}
 	return false
 }
 
@@ -246,7 +260,7 @@ func (p *Parser) closeUnmatchedBlocks() {
 	}
 	for p.oldTip != p.lastMatchedContainer {
 		parent := p.oldTip.GetParent()
-		p.finalize(p.oldTip)
+		p.Finalize(p.oldTip)
 		p.oldTip = parent
 	}
 	p.allClosed = true
@@ -271,10 +285,14 @@ type Reference struct {
 // You can then convert AST to html using html.Renderer, to some other format
 // using a custom renderer or transform the tree.
 func (p *Parser) Parse(input []byte) ast.Node {
-	p.block(input)
+	// the code only works with Unix CR newlines so to make life easy for
+	// callers normalize newlines
+	input = NormalizeNewlines(input)
+
+	p.Block(input)
 	// Walk the tree and finish up some of unfinished blocks
 	for p.tip != nil {
-		p.finalize(p.tip)
+		p.Finalize(p.tip)
 	}
 	// Walk the tree again and process inline markdown in each block
 	ast.WalkFunc(p.Doc, func(node ast.Node, entering bool) ast.WalkStatus {
@@ -320,8 +338,8 @@ func (p *Parser) parseRefsToAST() {
 		IsFootnotesList: true,
 		ListFlags:       ast.ListTypeOrdered,
 	}
-	p.addBlock(&ast.Footnotes{})
-	block := p.addBlock(list)
+	p.AddBlock(&ast.Footnotes{})
+	block := p.AddBlock(list)
 	flags := ast.ListItemBeginningOfList
 	// Note: this loop is intentionally explicit, not range-form. This is
 	// because the body of the loop will append nested footnotes to p.notes and
@@ -336,7 +354,7 @@ func (p *Parser) parseRefsToAST() {
 		listItem.RefLink = ref.link
 		if ref.hasBlock {
 			flags |= ast.ListItemContainsBlock
-			p.block(ref.title)
+			p.Block(ref.title)
 		} else {
 			p.Inline(block, ref.title)
 		}
@@ -390,35 +408,35 @@ func (p *Parser) parseRefsToAST() {
 //
 // Consider this markdown with reference-style links:
 //
-//     [link][ref]
+//	[link][ref]
 //
-//     [ref]: /url/ "tooltip title"
+//	[ref]: /url/ "tooltip title"
 //
 // It will be ultimately converted to this HTML:
 //
-//     <p><a href=\"/url/\" title=\"title\">link</a></p>
+//	<p><a href=\"/url/\" title=\"title\">link</a></p>
 //
 // And a reference structure will be populated as follows:
 //
-//     p.refs["ref"] = &reference{
-//         link: "/url/",
-//         title: "tooltip title",
-//     }
+//	p.refs["ref"] = &reference{
+//	    link: "/url/",
+//	    title: "tooltip title",
+//	}
 //
 // Alternatively, reference can contain information about a footnote. Consider
 // this markdown:
 //
-//     Text needing a footnote.[^a]
+//	Text needing a footnote.[^a]
 //
-//     [^a]: This is the note
+//	[^a]: This is the note
 //
 // A reference structure will be populated as follows:
 //
-//     p.refs["a"] = &reference{
-//         link: "a",
-//         title: "This is the note",
-//         noteID: <some positive int>,
-//     }
+//	p.refs["a"] = &reference{
+//	    link: "a",
+//	    title: "This is the note",
+//	    noteID: <some positive int>,
+//	}
 //
 // TODO: As you can see, it begs for splitting into two dedicated structures
 // for refs and for footnotes.
@@ -658,7 +676,7 @@ gatherLines:
 
 		// if it is an empty line, guess that it is part of this item
 		// and move on to the next line
-		if p.isEmpty(data[blockEnd:i]) > 0 {
+		if IsEmpty(data[blockEnd:i]) > 0 {
 			containsBlankLine = true
 			blockEnd = i
 			continue
@@ -693,8 +711,8 @@ gatherLines:
 	return
 }
 
-// isPunctuation returns true if c is a punctuation symbol.
-func isPunctuation(c byte) bool {
+// IsPunctuation returns true if c is a punctuation symbol.
+func IsPunctuation(c byte) bool {
 	for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
 		if c == r {
 			return true
@@ -703,20 +721,63 @@ func isPunctuation(c byte) bool {
 	return false
 }
 
-// isSpace returns true if c is a white-space charactr
-func isSpace(c byte) bool {
+// IsSpace returns true if c is a white-space charactr
+func IsSpace(c byte) bool {
 	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
 }
 
-// isLetter returns true if c is ascii letter
-func isLetter(c byte) bool {
+// IsLetter returns true if c is ascii letter
+func IsLetter(c byte) bool {
 	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
 }
 
-// isAlnum returns true if c is a digit or letter
+// IsAlnum returns true if c is a digit or letter
 // TODO: check when this is looking for ASCII alnum and when it should use unicode
-func isAlnum(c byte) bool {
-	return (c >= '0' && c <= '9') || isLetter(c)
+func IsAlnum(c byte) bool {
+	return (c >= '0' && c <= '9') || IsLetter(c)
+}
+
+var URIs = [][]byte{
+	[]byte("http://"),
+	[]byte("https://"),
+	[]byte("ftp://"),
+	[]byte("mailto:"),
+}
+
+var Paths = [][]byte{
+	[]byte("/"),
+	[]byte("./"),
+	[]byte("../"),
+}
+
+// IsSafeURL returns true if url starts with one of the valid schemes or is a relative path.
+func IsSafeURL(url []byte) bool {
+	nLink := len(url)
+	for _, path := range Paths {
+		nPath := len(path)
+		linkPrefix := url[:nPath]
+		if nLink >= nPath && bytes.Equal(linkPrefix, path) {
+			if nLink == nPath {
+				return true
+			} else if IsAlnum(url[nPath]) {
+				return true
+			}
+		}
+	}
+
+	for _, prefix := range URIs {
+		// TODO: handle unicode here
+		// case-insensitive prefix test
+		nPrefix := len(prefix)
+		if nLink > nPrefix {
+			linkPrefix := bytes.ToLower(url[:nPrefix])
+			if bytes.Equal(linkPrefix, prefix) && IsAlnum(url[nPrefix]) {
+				return true
+			}
+		}
+	}
+
+	return false
 }
 
 // TODO: this is not used
@@ -809,7 +870,7 @@ func slugify(in []byte) []byte {
 	sym := false
 
 	for _, ch := range in {
-		if isAlnum(ch) {
+		if IsAlnum(ch) {
 			sym = false
 			out = append(out, ch)
 		} else if sym {
@@ -838,3 +899,26 @@ func isListItem(d ast.Node) bool {
 	_, ok := d.(*ast.ListItem)
 	return ok
 }
+
+func NormalizeNewlines(d []byte) []byte {
+	wi := 0
+	n := len(d)
+	for i := 0; i < n; i++ {
+		c := d[i]
+		// 13 is CR
+		if c != 13 {
+			d[wi] = c
+			wi++
+			continue
+		}
+		// replace CR (mac / win) with LF (unix)
+		d[wi] = 10
+		wi++
+		if i < n-1 && d[i+1] == 10 {
+			// this was CRLF, so skip the LF
+			i++
+		}
+
+	}
+	return d[:wi]
+}
diff --git a/parser/ref.go b/parser/ref.go
index 0b59a19..c1e0534 100644
--- a/parser/ref.go
+++ b/parser/ref.go
@@ -7,8 +7,8 @@ import (
 	"github.com/gomarkdown/markdown/ast"
 )
 
-// parse '(#r)', where r does not contain spaces. Or.
-// (!item) (!item, subitem), for an index, (!!item) signals primary.
+// parse '(#r, text)', where r does not contain spaces, but text may (similar to a citation). Or. (!item) (!item,
+// subitem), for an index, (!!item) signals primary.
 func maybeShortRefOrIndex(p *Parser, data []byte, offset int) (int, ast.Node) {
 	if len(data[offset:]) < 4 {
 		return 0, nil
@@ -25,8 +25,8 @@ func maybeShortRefOrIndex(p *Parser, data []byte, offset int) (int, ast.Node) {
 			switch {
 			case c == ')':
 				break Loop
-			case !isAlnum(c):
-				if c == '_' || c == '-' || c == ':' {
+			case !IsAlnum(c):
+				if c == '_' || c == '-' || c == ':' || c == ' ' || c == ',' {
 					i++
 					continue
 				}
@@ -45,6 +45,21 @@ func maybeShortRefOrIndex(p *Parser, data []byte, offset int) (int, ast.Node) {
 		id := data[2:i]
 		node := &ast.CrossReference{}
 		node.Destination = id
+		if c := bytes.Index(id, []byte(",")); c > 0 {
+			idpart := id[:c]
+			suff := id[c+1:]
+			suff = bytes.TrimSpace(suff)
+			node.Destination = idpart
+			node.Suffix = suff
+		}
+		if bytes.Index(node.Destination, []byte(" ")) > 0 {
+			// no spaces allowed in id
+			return 0, nil
+		}
+		if bytes.Index(node.Destination, []byte(",")) > 0 {
+			// nor comma
+			return 0, nil
+		}
 
 		return i + 1, node
 
diff --git a/parser/ref_test.go b/parser/ref_test.go
index 4738f1c..19f26e4 100644
--- a/parser/ref_test.go
+++ b/parser/ref_test.go
@@ -19,14 +19,18 @@ func TestCrossReference(t *testing.T) {
 			data: []byte("(#yes)"),
 			r:    &ast.CrossReference{Destination: []byte("yes")},
 		},
-		// ok
 		{
 			data: []byte("(#y:es)"),
 			r:    &ast.CrossReference{Destination: []byte("y:es")},
 		},
+		{
+			data: []byte("(#id, random text)"),
+			r:    &ast.CrossReference{Destination: []byte("id"), Suffix: []byte("random text")},
+		},
 		// fails
 		{data: []byte("(#y es)"), r: nil, fail: true},
 		{data: []byte("(#yes"), r: nil, fail: true},
+		{data: []byte("(#y es, random text"), r: nil, fail: true},
 	}
 
 	for i, test := range tests {
diff --git a/ref_test.go b/ref_test.go
index 0f011ab..44ef145 100644
--- a/ref_test.go
+++ b/ref_test.go
@@ -92,6 +92,10 @@ func BenchmarkReferenceCodeSpans(b *testing.B) {
 	benchFile(b, "Code Spans")
 }
 
+func BenchmarkIssue265(b *testing.B) {
+	benchFile(b, "issue265-slow-binary")
+}
+
 func BenchmarkReferenceHardWrappedPara(b *testing.B) {
 	benchFile(b, "Hard-wrapped paragraphs with list-like lines")
 }
diff --git a/s/run_tests.ps1 b/s/run_tests.ps1
index cd3aef7..d5573c8 100644
--- a/s/run_tests.ps1
+++ b/s/run_tests.ps1
@@ -1,2 +1,5 @@
 go clean -testcache
-go test ./...
+go test -race -v .
+go test -race -v ./ast
+go test -race -v ./html
+go test -race -v ./parser
diff --git a/s/run_tests.sh b/s/run_tests.sh
index 2819ddc..dcd2b2e 100755
--- a/s/run_tests.sh
+++ b/s/run_tests.sh
@@ -2,4 +2,7 @@
 set -u -e -o pipefail -o verbose
 
 go clean -testcache
-go test -race -v ./...
+go test -race -v .
+go test -race -v ./ast
+go test -race -v ./html
+go test -race -v ./parser
diff --git a/s/test_with_codecoverage.sh b/s/test_with_codecoverage.sh
index 88e32bf..3a1fb0a 100755
--- a/s/test_with_codecoverage.sh
+++ b/s/test_with_codecoverage.sh
@@ -5,7 +5,7 @@
 set -e
 
 # List of packages to test
-pkgs=$(go list ./... | grep -v /cmd)
+pkgs=$(go list ./... | grep -v /cmd | grep -v /examples)
 # list of packages in a format suitable for -coverpkg
 cover_pkgs=$(go list ./... | grep -v /cmd | tr "\n" ",")
 
diff --git a/testdata/DefinitionListWithFencedCodeBlock.tests b/testdata/DefinitionListWithFencedCodeBlock.tests
index 96c8b34..a12b2cc 100644
--- a/testdata/DefinitionListWithFencedCodeBlock.tests
+++ b/testdata/DefinitionListWithFencedCodeBlock.tests
@@ -17,3 +17,31 @@ two:
 <pre><code>code
 </code></pre></dd>
 </dl>
++++
+Paragraph
+
+one:
+: def1
+
+two:
+: def2
+
+~~~
+ <some-code>Code Example</some-code>
+~~~
+
+One more paragraph
++++
+<p>Paragraph</p>
+
+<dl>
+<dt>one:</dt>
+<dd>def1</dd>
+<dt>two:</dt>
+<dd>def2</dd>
+</dl>
+
+<pre><code> &lt;some-code&gt;Code Example&lt;/some-code&gt;
+</code></pre>
+
+<p>One more paragraph</p>
diff --git a/testdata/FencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK.tests b/testdata/FencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK.tests
index 006d94e..846fee3 100644
--- a/testdata/FencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK.tests
+++ b/testdata/FencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK.tests
@@ -37,7 +37,7 @@ language in braces
 <pre><code class="language-ocaml">language in braces
 </code></pre>
 +++
-```    {ocaml}      
+```    {ocaml}
 with extra whitespace
 ```
 +++
@@ -166,3 +166,40 @@ leading spaces
 
 <pre><code>```
 </code></pre>
++++
+1. Ordered list item above it without new line:
+```go
+ fmt.Println("some code")
+
+ fmt.Println("some more code")
+```
++++
+<ol>
+<li>Ordered list item above it without new line:</li>
+</ol>
+
+<pre><code class="language-go"> fmt.Println(&quot;some code&quot;)
+
+ fmt.Println(&quot;some more code&quot;)
+</code></pre>
++++
+* Unordered list item above it without new line
+```go
+ fmt.Println("some code")
+
+ fmt.Println("some more code")
+```
+* Another unordered list
++++
+<ul>
+<li>Unordered list item above it without new line</li>
+</ul>
+
+<pre><code class="language-go"> fmt.Println(&quot;some code&quot;)
+
+ fmt.Println(&quot;some more code&quot;)
+</code></pre>
+
+<ul>
+<li>Another unordered list</li>
+</ul>
diff --git a/testdata/Links, inline style.html b/testdata/Links, inline style.html
index 2b6bf4e..3b22ce3 100644
--- a/testdata/Links, inline style.html	
+++ b/testdata/Links, inline style.html	
@@ -30,4 +30,6 @@
 
 <p><a href="-" title="title saying meh ¯\_(ツ)_/¯">hyphen and title</a>.</p>
 
-<p>[Empty]().</p>
+<p><a href="empty"></a>.</p>
+
+<p><a href="">Empty</a>.</p>
diff --git a/testdata/Links, inline style.text b/testdata/Links, inline style.text
index 4a32ec2..cf293c4 100644
--- a/testdata/Links, inline style.text	
+++ b/testdata/Links, inline style.text	
@@ -30,4 +30,6 @@ Just a [URL](/url/).
 
 [hyphen and title](- 'title saying meh ¯\_(ツ)_/¯').
 
+[](empty).
+
 [Empty]().
diff --git a/testdata/Table.tests b/testdata/Table.tests
index c663f1b..21a9ab5 100644
--- a/testdata/Table.tests
+++ b/testdata/Table.tests
@@ -22,9 +22,21 @@ a | b
 ---|--
 c | d
 +++
-<p>a | b
----|--
-c | d</p>
+<table>
+<thead>
+<tr>
+<th>a</th>
+<th>b</th>
+</tr>
+</thead>
+
+<tbody>
+<tr>
+<td>c</td>
+<td>d</td>
+</tr>
+</tbody>
+</table>
 +++
 |a|b|c|d|
 |----|----|----|---|
diff --git a/testdata/issue265-slow-binary.text b/testdata/issue265-slow-binary.text
new file mode 100644
index 0000000..563ead8
Binary files /dev/null and b/testdata/issue265-slow-binary.text differ
diff --git a/todo.md b/todo.md
deleted file mode 100644
index be8bb55..0000000
--- a/todo.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Things to do
-
-[ ] docs: add examples like https://godoc.org/github.com/dgrijalva/jwt-go (put in foo_example_test.go). Or see https://github.com/garyburd/redigo/blob/master/redis/zpop_example_test.go#L5 / https://godoc.org/github.com/garyburd/redigo/redis or https://godoc.org/github.com/go-redis/redis
-
-[ ] figure out expandTabs and parser.TabSizeEight. Are those used?
-
-[ ] SoftbreakData is not used

More details

Full run details

Historical runs