dedupe help center template partials and reuse the markdown link helper

This commit is contained in:
Abhinav Raut
2026-08-14 01:44:37 +05:30
parent 2476ee2685
commit 1feb3743f6
11 changed files with 122 additions and 52 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ import (
)
// Bump whenever ChunkHTMLContent changes the text it emits for unchanged input; it feeds reindex fingerprints.
const ChunkerVersion = 2
const ChunkerVersion = 3
var (
sentenceRegex = regexp.MustCompile(`[.!?]+[\s]+`)
+1 -15
View File
@@ -12,7 +12,7 @@ func prepareHTMLForEmbedding(htmlContent string) string {
if err != nil {
return htmlContent
}
inlineLinkHrefs(doc)
inlineAnchorMarkdown(doc)
flattenTables(doc)
var b strings.Builder
@@ -22,20 +22,6 @@ func prepareHTMLForEmbedding(htmlContent string) string {
return b.String()
}
// inlineLinkHrefs appends each link's destination after its text so the flattened text keeps the URL.
func inlineLinkHrefs(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
href := strings.TrimSpace(attrValue(n, "href"))
text := strings.TrimSpace(nodeText(n))
if href != "" && !strings.HasPrefix(href, "#") && !strings.HasPrefix(href, "cid:") && text != href {
n.AppendChild(&html.Node{Type: html.TextNode, Data: " (" + href + ")"})
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
inlineLinkHrefs(c)
}
}
// flattenTables turns each table into a <pre> of one header-labelled line per row, which also keeps it unsplittable.
func flattenTables(n *html.Node) {
var tables []*html.Node
+87
View File
@@ -0,0 +1,87 @@
package stringutil
import (
"strings"
"testing"
)
func TestPrepareHTMLForEmbedding(t *testing.T) {
tests := []struct {
name string
in string
contains []string
notContains []string
}{
{
name: "link becomes markdown",
in: `<p>See <a href="https://example.com/guide">the guide</a> for steps.</p>`,
contains: []string{"See [the guide](https://example.com/guide) for steps."},
notContains: []string{"<a "},
},
{
name: "bare url link keeps the url once",
in: `<p><a href="https://example.com">https://example.com</a></p>`,
contains: []string{">https://example.com<"},
notContains: []string{
"[https://example.com](https://example.com)",
"<a ",
},
},
{
name: "anchor and cid links are left alone",
in: `<p><a href="#setup">Setup</a> <a href="cid:img1">inline</a></p>`,
contains: []string{`<a href="#setup">Setup</a>`, `<a href="cid:img1">inline</a>`},
notContains: []string{"[Setup]"},
},
{
name: "table flattens to header labelled lines",
in: `<table>
<tr><th>Plan</th><th>Price</th></tr>
<tr><td>Basic</td><td>$5</td></tr>
<tr><td>Pro</td><td>$10</td></tr>
</table>`,
contains: []string{"<pre>", "Plan: Basic | Price: $5", "Plan: Pro | Price: $10"},
notContains: []string{"<table>"},
},
{
name: "header only table keeps the header line",
in: `<table><tr><th>Plan</th><th>Price</th></tr></table>`,
contains: []string{"Plan | Price"},
notContains: []string{"<table>"},
},
{
name: "caption leads the table text",
in: `<table><caption>Pricing</caption><tr><td>Basic</td><td>$5</td></tr></table>`,
contains: []string{
"Pricing\nBasic | $5",
},
},
{
name: "link inside a table cell survives as markdown",
in: `<table><tr><th>Doc</th></tr><tr><td><a href="https://example.com/x">Pay now</a></td></tr></table>`,
contains: []string{"Doc: [Pay now](https://example.com/x)"},
notContains: []string{"<a "},
},
{
name: "invalid html is returned untouched on parse success path",
in: `plain text with no markup`,
contains: []string{"plain text with no markup"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := prepareHTMLForEmbedding(tt.in)
for _, want := range tt.contains {
if !strings.Contains(got, want) {
t.Errorf("output missing %q\ngot: %s", want, got)
}
}
for _, not := range tt.notContains {
if strings.Contains(got, not) {
t.Errorf("output should not contain %q\ngot: %s", not, got)
}
}
})
}
}
@@ -22,26 +22,13 @@
<span>{{ if $showAuthor }}<span class="hc-author">{{ .Data.Article.AuthorName }}</span> &middot; {{ end }}{{ $.L.T "globals.terms.lastUpdated" }} <time class="hc-date" datetime="{{ .Data.ModifiedTime }}">{{ .Data.Article.UpdatedAt.Format "Jan 2, 2006" }}</time></span>
</div>
<div class="hc-article-body">
{{ if not .Data.HelpCenter.Theme.Article.HideToc }}
<nav class="hc-toc{{ if .Data.TOC }} hc-toc-open{{ end }}" id="hc-toc" aria-label="{{ $.L.T "helpCenter.onThisPage" }}"{{ if not .Data.TOC }} hidden{{ end }}>
<button class="hc-toc-title" type="button" id="hc-toc-toggle" aria-expanded="{{ if .Data.TOC }}true{{ else }}false{{ end }}" aria-controls="hc-toc-list">
{{ $.L.T "helpCenter.onThisPage" }}
<svg class="hc-toc-caret" width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul id="hc-toc-list">{{ range .Data.TOC }}<li><a href="#{{ .ID }}">{{ .Title }}</a></li>{{ end }}</ul>
</nav>
{{ end }}
{{ template "hc-article-toc" . }}
<div class="hc-prose" id="hc-prose">{{ .Data.Content }}</div>
</div>
</article>
<div class="hc-article-foot">
<div class="hc-helpful" id="hc-helpful" role="group" aria-labelledby="hc-helpful-q" data-endpoint="/api/public/help-centers/{{ .Data.HelpCenter.Slug }}/articles/{{ .Data.Article.Slug }}/feedback?locale={{ .Data.Article.Locale }}">
<span class="hc-helpful-q" id="hc-helpful-q">{{ $.L.T "helpCenter.wasHelpful" }}</span>
<button type="button" data-helpful="true">{{ $.L.T "globals.messages.yes" }}</button>
<button type="button" data-helpful="false">{{ $.L.T "globals.messages.no" }}</button>
<span class="hc-helpful-thanks" hidden>{{ $.L.T "helpCenter.feedbackThanks" }}</span>
</div>
{{ template "hc-helpful" . }}
{{ if and .Data.Related (not .Data.HelpCenter.Theme.Article.HideRelated) }}
<aside class="hc-related">
@@ -25,7 +25,7 @@
{{ if .Data.HelpCenter.TaglineHTML }}<p class="hc-hero-tagline">{{ .Data.HelpCenter.TaglineHTML }}</p>{{ end }}
{{ end }}
<form class="hc-search" action="{{ .Data.HelpCenter.BasePath }}/search" method="GET" role="search" data-hc-base="{{ .Data.HelpCenter.BasePath }}" data-hc-slug="{{ .Data.HelpCenter.Slug }}" data-hc-locale="{{ .Data.HelpCenter.CurrentLocale }}" data-hc-empty="{{ $.L.T "helpCenter.noResults" }}" data-hc-results-one="{{ $.L.Tc "helpCenter.resultsCount" 1 }}" data-hc-results="{{ $.L.Tc "helpCenter.resultsCount" 2 }}">
<svg class="hc-search-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="9" cy="9" r="6" /><path d="m18 18-4.5-4.5" stroke-linecap="round" /></svg>
{{ template "hc-search-icon" "hc-search-icon" }}
<input type="search" name="q" value="{{ .Data.Query }}" placeholder="{{ $.L.T "helpCenter.searchPlaceholder" }}" aria-label="{{ $.L.T "helpCenter.searchPlaceholder" }}" autocomplete="off" />
<button type="submit">{{ $.L.T "globals.terms.search" }}</button>
</form>
@@ -14,26 +14,13 @@
<span>{{ if $showAuthor }}<span class="hcd-author">{{ .Data.Article.AuthorName }}</span> &middot; {{ end }}{{ .L.T "globals.terms.lastUpdated" }} <time class="hc-date" datetime="{{ .Data.ModifiedTime }}">{{ .Data.Article.UpdatedAt.Format "Jan 2, 2006" }}</time></span>
</div>
<div class="hc-article-body hcd-article-body">
{{ if not .Data.HelpCenter.Theme.Article.HideToc }}
<nav class="hc-toc{{ if .Data.TOC }} hc-toc-open{{ end }}" id="hc-toc" aria-label="{{ .L.T "helpCenter.onThisPage" }}"{{ if not .Data.TOC }} hidden{{ end }}>
<button class="hc-toc-title" type="button" id="hc-toc-toggle" aria-expanded="{{ if .Data.TOC }}true{{ else }}false{{ end }}" aria-controls="hc-toc-list">
{{ .L.T "helpCenter.onThisPage" }}
<svg class="hc-toc-caret" width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul id="hc-toc-list">{{ range .Data.TOC }}<li><a href="#{{ .ID }}">{{ .Title }}</a></li>{{ end }}</ul>
</nav>
{{ end }}
{{ template "hc-article-toc" . }}
<div class="hc-prose" id="hc-prose">{{ .Data.Content }}</div>
</div>
</article>
<div class="hcd-article-foot">
<div class="hc-helpful" id="hc-helpful" role="group" aria-labelledby="hc-helpful-q" data-endpoint="/api/public/help-centers/{{ .Data.HelpCenter.Slug }}/articles/{{ .Data.Article.Slug }}/feedback?locale={{ .Data.Article.Locale }}">
<span class="hc-helpful-q" id="hc-helpful-q">{{ .L.T "helpCenter.wasHelpful" }}</span>
<button type="button" data-helpful="true">{{ .L.T "globals.messages.yes" }}</button>
<button type="button" data-helpful="false">{{ .L.T "globals.messages.no" }}</button>
<span class="hc-helpful-thanks" hidden>{{ .L.T "helpCenter.feedbackThanks" }}</span>
</div>
{{ template "hc-helpful" . }}
{{ if and .Data.Related (not .Data.HelpCenter.Theme.Article.HideRelated) }}
<aside class="hcd-related">
@@ -6,7 +6,7 @@
<h1>{{ $heroText }}</h1>
{{ if .Data.HelpCenter.TaglineHTML }}<p class="hcd-hero-tagline">{{ .Data.HelpCenter.TaglineHTML }}</p>{{ end }}
<a class="hcd-hero-search" href="{{ .Data.HelpCenter.BasePath }}/search" data-hcd-search-open>
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="9" cy="9" r="6" /><path d="m18 18-4.5-4.5" stroke-linecap="round" /></svg>
{{ template "hc-search-icon" }}
<span>{{ .L.T "helpCenter.searchPlaceholder" }}</span>
<span class="hcd-kbd-combo" aria-hidden="true"><kbd class="hcd-kbd hcd-kbd-mod">Ctrl</kbd><kbd class="hcd-kbd">K</kbd></span>
</a>
@@ -14,7 +14,7 @@
</a>
</div>
<a class="hcd-search-btn" href="{{ .Data.HelpCenter.BasePath }}/search" data-hcd-search-open>
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="9" cy="9" r="6" /><path d="m18 18-4.5-4.5" stroke-linecap="round" /></svg>
{{ template "hc-search-icon" }}
<span class="hcd-search-btn-label">{{ .L.T "globals.terms.search" }}</span>
<span class="hcd-kbd-combo" aria-hidden="true"><kbd class="hcd-kbd hcd-kbd-mod">Ctrl</kbd><kbd class="hcd-kbd">K</kbd></span>
</a>
@@ -33,7 +33,7 @@
<div class="hcd-search-backdrop" data-hcd-search-close></div>
<div class="hcd-search-panel" role="dialog" aria-modal="true" aria-label="{{ .L.T "globals.terms.search" }}">
<form class="hc-search" action="{{ .Data.HelpCenter.BasePath }}/search" method="GET" role="search" data-hc-base="{{ .Data.HelpCenter.BasePath }}" data-hc-slug="{{ .Data.HelpCenter.Slug }}" data-hc-locale="{{ .Data.HelpCenter.CurrentLocale }}" data-hc-empty="{{ .L.T "helpCenter.noResults" }}" data-hc-results-one="{{ .L.Tc "helpCenter.resultsCount" 1 }}" data-hc-results="{{ .L.Tc "helpCenter.resultsCount" 2 }}">
<svg class="hc-search-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="9" cy="9" r="6" /><path d="m18 18-4.5-4.5" stroke-linecap="round" /></svg>
{{ template "hc-search-icon" "hc-search-icon" }}
<input type="search" name="q" value="" placeholder="{{ .L.T "helpCenter.searchPlaceholder" }}" aria-label="{{ .L.T "helpCenter.searchPlaceholder" }}" autocomplete="off" />
<button type="submit" class="hc-sr-only">{{ .L.T "globals.terms.search" }}</button>
</form>
@@ -6,7 +6,7 @@
<h1>{{ .L.T "globals.messages.pageNotFound" }}</h1>
<p>{{ .L.T "helpCenter.notFoundText" }}</p>
<a class="hcd-notfound-search" href="{{ .Data.HelpCenter.BasePath }}/search" data-hcd-search-open>
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="9" cy="9" r="6" /><path d="m18 18-4.5-4.5" stroke-linecap="round" /></svg>
{{ template "hc-search-icon" }}
<span>{{ .L.T "helpCenter.searchPlaceholder" }}</span>
<span class="hcd-kbd-combo" aria-hidden="true"><kbd class="hcd-kbd hcd-kbd-mod">Ctrl</kbd><kbd class="hcd-kbd">K</kbd></span>
</a>
@@ -2,7 +2,7 @@
{{ template "docs-header" . }}
<form class="hcd-page-search" action="{{ .Data.HelpCenter.BasePath }}/search" method="GET" role="search">
<svg class="hc-search-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="9" cy="9" r="6" /><path d="m18 18-4.5-4.5" stroke-linecap="round" /></svg>
{{ template "hc-search-icon" "hc-search-icon" }}
<input type="search" name="q" value="{{ .Data.Query }}" placeholder="{{ .L.T "helpCenter.searchPlaceholder" }}" aria-label="{{ .L.T "helpCenter.searchPlaceholder" }}" autocomplete="off" />
<button type="submit" class="hc-sr-only">{{ .L.T "globals.terms.search" }}</button>
</form>
@@ -26,6 +26,29 @@
</div>
{{ end }}
{{ define "hc-search-icon" }}<svg{{ with . }} class="{{ . }}"{{ end }} viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="9" cy="9" r="6" /><path d="m18 18-4.5-4.5" stroke-linecap="round" /></svg>{{ end }}
{{ define "hc-article-toc" }}
{{ if not .Data.HelpCenter.Theme.Article.HideToc }}
<nav class="hc-toc{{ if .Data.TOC }} hc-toc-open{{ end }}" id="hc-toc" aria-label="{{ .L.T "helpCenter.onThisPage" }}"{{ if not .Data.TOC }} hidden{{ end }}>
<button class="hc-toc-title" type="button" id="hc-toc-toggle" aria-expanded="{{ if .Data.TOC }}true{{ else }}false{{ end }}" aria-controls="hc-toc-list">
{{ .L.T "helpCenter.onThisPage" }}
<svg class="hc-toc-caret" width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul id="hc-toc-list">{{ range .Data.TOC }}<li><a href="#{{ .ID }}">{{ .Title }}</a></li>{{ end }}</ul>
</nav>
{{ end }}
{{ end }}
{{ define "hc-helpful" }}
<div class="hc-helpful" id="hc-helpful" role="group" aria-labelledby="hc-helpful-q" data-endpoint="/api/public/help-centers/{{ .Data.HelpCenter.Slug }}/articles/{{ .Data.Article.Slug }}/feedback?locale={{ .Data.Article.Locale }}">
<span class="hc-helpful-q" id="hc-helpful-q">{{ .L.T "helpCenter.wasHelpful" }}</span>
<button type="button" data-helpful="true">{{ .L.T "globals.messages.yes" }}</button>
<button type="button" data-helpful="false">{{ .L.T "globals.messages.no" }}</button>
<span class="hc-helpful-thanks" hidden>{{ .L.T "helpCenter.feedbackThanks" }}</span>
</div>
{{ end }}
{{ define "hc-doc-icon" }}<svg class="hc-doc" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M4 1.5h5L12.5 5v9.5h-8.5z" stroke-linejoin="round"/><path d="M8.5 1.5V5h4" stroke-linejoin="round"/></svg>{{ end }}
{{ define "hc-arrow-icon" }}<svg class="hc-arrow" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M5 3l5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg>{{ end }}