Allow configurable header ID prefix/suffixes.

This is specifically driven by the Hugo usecase where multiple documents
are often rendered into the same ultimate HTML page.

When a header ID is written to the output HTML format (either through
`HTML_TOC`, `EXTENSION_HEADER_IDS`, or `EXTENSION_AUTO_HEADER_IDS`), it
is possible that multiple documents will hvae identical header IDs. To
permit validation to pass, it is useful to have a per-document prefix or
suffix (in our case, an MD5 of the content filename, and we will be
using it as a suffix).

That is, two documents (`A` and `B`) that have the same header ID (`#
Reason {#reason}`), will end up having an actual header ID of the form
`#reason-DOCID` (e.g., `#reason-A`, `#reason-B`) with these HTML
parameters.

This is built on top of #126 (more intelligent collision detection for
`EXTENSION_AUTO_HEADER_IDS`).
pull/129/head
Austin Ziegler 2014-11-23 20:37:27 -05:00
parent 40f28ee022
commit 9c061de92b
2 changed files with 142 additions and 3 deletions

View File

@ -17,16 +17,35 @@ import (
"testing"
)
func runMarkdownBlockWithRenderer(input string, extensions int, renderer Renderer) string {
return string(Markdown([]byte(input), renderer, extensions))
}
func runMarkdownBlock(input string, extensions int) string {
htmlFlags := 0
htmlFlags |= HTML_USE_XHTML
renderer := HtmlRenderer(htmlFlags, "", "")
return string(Markdown([]byte(input), renderer, extensions))
return runMarkdownBlockWithRenderer(input, extensions, renderer)
}
func runnerWithRendererParameters(parameters HtmlRendererParameters) func(string, int) string {
return func(input string, extensions int) string {
htmlFlags := 0
htmlFlags |= HTML_USE_XHTML
renderer := HtmlRendererWithParameters(htmlFlags, "", "", parameters)
return runMarkdownBlockWithRenderer(input, extensions, renderer)
}
}
func doTestsBlock(t *testing.T, tests []string, extensions int) {
doTestsBlockWithRunner(t, tests, extensions, runMarkdownBlock)
}
func doTestsBlockWithRunner(t *testing.T, tests []string, extensions int, runner func(string, int) string) {
// catch and report panics
var candidate string
defer func() {
@ -39,7 +58,7 @@ func doTestsBlock(t *testing.T, tests []string, extensions int) {
input := tests[i]
candidate = input
expected := tests[i+1]
actual := runMarkdownBlock(candidate, extensions)
actual := runner(candidate, extensions)
if actual != expected {
t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]",
candidate, expected, actual)
@ -237,6 +256,54 @@ func TestPrefixHeaderIdExtension(t *testing.T) {
doTestsBlock(t, tests, EXTENSION_HEADER_IDS)
}
func TestPrefixHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) {
var tests = []string{
"# header 1 {#someid}\n",
"<h1 id=\"PRE:someid:POST\">header 1</h1>\n",
"## header 2 {#someid}\n",
"<h2 id=\"PRE:someid:POST\">header 2</h2>\n",
"### header 3 {#someid}\n",
"<h3 id=\"PRE:someid:POST\">header 3</h3>\n",
"#### header 4 {#someid}\n",
"<h4 id=\"PRE:someid:POST\">header 4</h4>\n",
"##### header 5 {#someid}\n",
"<h5 id=\"PRE:someid:POST\">header 5</h5>\n",
"###### header 6 {#someid}\n",
"<h6 id=\"PRE:someid:POST\">header 6</h6>\n",
"####### header 7 {#someid}\n",
"<h6 id=\"PRE:someid:POST\"># header 7</h6>\n",
"# header 1 # {#someid}\n",
"<h1 id=\"PRE:someid:POST\">header 1</h1>\n",
"## header 2 ## {#someid}\n",
"<h2 id=\"PRE:someid:POST\">header 2</h2>\n",
"* List\n# Header {#someid}\n* List\n",
"<ul>\n<li><p>List</p>\n\n<h1 id=\"PRE:someid:POST\">Header</h1></li>\n\n<li><p>List</p></li>\n</ul>\n",
"* List\n#Header {#someid}\n* List\n",
"<ul>\n<li><p>List</p>\n\n<h1 id=\"PRE:someid:POST\">Header</h1></li>\n\n<li><p>List</p></li>\n</ul>\n",
"* List\n * Nested list\n # Nested header {#someid}\n",
"<ul>\n<li><p>List</p>\n\n<ul>\n<li><p>Nested list</p>\n\n" +
"<h1 id=\"PRE:someid:POST\">Nested header</h1></li>\n</ul></li>\n</ul>\n",
}
parameters := HtmlRendererParameters{
HeaderIDPrefix: "PRE:",
HeaderIDSuffix: ":POST",
}
doTestsBlockWithRunner(t, tests, EXTENSION_HEADER_IDS, runnerWithRendererParameters(parameters))
}
func TestPrefixAutoHeaderIdExtension(t *testing.T) {
var tests = []string{
"# Header 1\n",
@ -288,6 +355,63 @@ func TestPrefixAutoHeaderIdExtension(t *testing.T) {
doTestsBlock(t, tests, EXTENSION_AUTO_HEADER_IDS)
}
func TestPrefixAutoHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) {
var tests = []string{
"# Header 1\n",
"<h1 id=\"PRE:header-1:POST\">Header 1</h1>\n",
"# Header 1 \n",
"<h1 id=\"PRE:header-1:POST\">Header 1</h1>\n",
"## Header 2\n",
"<h2 id=\"PRE:header-2:POST\">Header 2</h2>\n",
"### Header 3\n",
"<h3 id=\"PRE:header-3:POST\">Header 3</h3>\n",
"#### Header 4\n",
"<h4 id=\"PRE:header-4:POST\">Header 4</h4>\n",
"##### Header 5\n",
"<h5 id=\"PRE:header-5:POST\">Header 5</h5>\n",
"###### Header 6\n",
"<h6 id=\"PRE:header-6:POST\">Header 6</h6>\n",
"####### Header 7\n",
"<h6 id=\"PRE:-header-7:POST\"># Header 7</h6>\n",
"Hello\n# Header 1\nGoodbye\n",
"<p>Hello</p>\n\n<h1 id=\"PRE:header-1:POST\">Header 1</h1>\n\n<p>Goodbye</p>\n",
"* List\n# Header\n* List\n",
"<ul>\n<li><p>List</p>\n\n<h1 id=\"PRE:header:POST\">Header</h1></li>\n\n<li><p>List</p></li>\n</ul>\n",
"* List\n#Header\n* List\n",
"<ul>\n<li><p>List</p>\n\n<h1 id=\"PRE:header:POST\">Header</h1></li>\n\n<li><p>List</p></li>\n</ul>\n",
"* List\n * Nested list\n # Nested header\n",
"<ul>\n<li><p>List</p>\n\n<ul>\n<li><p>Nested list</p>\n\n" +
"<h1 id=\"PRE:nested-header:POST\">Nested header</h1></li>\n</ul></li>\n</ul>\n",
"# Header\n\n# Header\n",
"<h1 id=\"PRE:header:POST\">Header</h1>\n\n<h1 id=\"PRE:header-1:POST\">Header</h1>\n",
"# Header 1\n\n# Header 1",
"<h1 id=\"PRE:header-1:POST\">Header 1</h1>\n\n<h1 id=\"PRE:header-1-1:POST\">Header 1</h1>\n",
"# Header\n\n# Header 1\n\n# Header\n\n# Header",
"<h1 id=\"PRE:header:POST\">Header</h1>\n\n<h1 id=\"PRE:header-1:POST\">Header 1</h1>\n\n<h1 id=\"PRE:header-1-1:POST\">Header</h1>\n\n<h1 id=\"PRE:header-1-2:POST\">Header</h1>\n",
}
parameters := HtmlRendererParameters{
HeaderIDPrefix: "PRE:",
HeaderIDSuffix: ":POST",
}
doTestsBlockWithRunner(t, tests, EXTENSION_AUTO_HEADER_IDS, runnerWithRendererParameters(parameters))
}
func TestPrefixMultipleHeaderExtensions(t *testing.T) {
var tests = []string{
"# Header\n\n# Header {#header}\n\n# Header 1",

17
html.go
View File

@ -62,6 +62,11 @@ type HtmlRendererParameters struct {
// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
// <sup>[return]</sup> is used.
FootnoteReturnLinkContents string
// If set, add this text to the front of each Header ID, to ensure
// uniqueness.
HeaderIDPrefix string
// If set, add this text to the back of each Header ID, to ensure uniqueness.
HeaderIDSuffix string
}
// Html is a type that implements the Renderer interface for HTML output.
@ -200,7 +205,17 @@ func (options *Html) Header(out *bytes.Buffer, text func() bool, level int, id s
}
if id != "" {
out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, options.ensureUniqueHeaderID(id)))
id = options.ensureUniqueHeaderID(id)
if options.parameters.HeaderIDPrefix != "" {
id = options.parameters.HeaderIDPrefix + id
}
if options.parameters.HeaderIDSuffix != "" {
id = id + options.parameters.HeaderIDSuffix
}
out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
} else {
out.WriteString(fmt.Sprintf("<h%d>", level))
}