package topdata
import (
"fmt"
"html"
"strconv"
"strings"
"unicode"
)
type wikiTemplateHTML string
func extractWikiTemplateBlock(source string, offset int, blockName, path, pageID string) (string, int, error) {
depth := 1
bodyStart := offset
for offset < len(source) {
start := strings.Index(source[offset:], "{{")
if start < 0 {
break
}
start += offset
end := strings.Index(source[start+2:], "}}")
if end < 0 {
return "", offset, fmt.Errorf("render wiki template %s for %s: unclosed placeholder", path, pageID)
}
end += start + 2
token := strings.TrimSpace(source[start+2 : end])
if token == "#"+blockName || strings.HasPrefix(token, "#"+blockName+" ") {
depth++
} else if token == "/"+blockName {
depth--
if depth == 0 {
return source[bodyStart:start], end + 2, nil
}
}
offset = end + 2
}
return "", offset, fmt.Errorf("render wiki template %s for %s: missing {{/%s}}", path, pageID, blockName)
}
func (ctx *wikiContext) evalWikiTemplateExpression(expr string, state *wikiTemplateRenderState) (any, error) {
parts := splitWikiTemplatePipes(expr)
if len(parts) == 0 {
return "", nil
}
value, err := ctx.evalWikiTemplateBase(strings.TrimSpace(parts[0]), state)
if err != nil {
return nil, err
}
for _, rawFilter := range parts[1:] {
value, err = ctx.applyWikiTemplateFilter(value, strings.TrimSpace(rawFilter), state)
if err != nil {
return nil, err
}
}
return value, nil
}
func splitWikiTemplatePipes(expr string) []string {
parts := []string{}
start := 0
inQuote := false
escaped := false
for i := 0; i < len(expr); i++ {
switch {
case escaped:
escaped = false
case expr[i] == '\\' && inQuote:
escaped = true
case expr[i] == '"':
inQuote = !inQuote
case expr[i] == '|' && !inQuote:
parts = append(parts, expr[start:i])
start = i + 1
}
}
parts = append(parts, expr[start:])
return parts
}
func (ctx *wikiContext) evalWikiTemplateBase(expr string, state *wikiTemplateRenderState) (any, error) {
if expr == "." {
return state.dot, nil
}
if value, ok := ctx.resolveWikiTemplateScopedValue(expr, state); ok {
return value, nil
}
renderCtx := wikiTableRenderContext{Page: state.page, Path: state.path}
row := state.page.Row
if state.row != nil {
row = state.row
}
parser := wikiExprParser{tokens: tokenizeWikiExpr(expr), row: row, ctx: ctx, renderCtx: renderCtx}
value, err := parser.parseComparison()
if err != nil {
return nil, fmt.Errorf("render wiki template %s for %s expression %q: %w", state.path, state.page.PageID, expr, err)
}
if parser.peek().kind != wikiExprEOF {
return nil, fmt.Errorf("render wiki template %s for %s expression %q: unexpected token %q", state.path, state.page.PageID, expr, parser.peek().text)
}
return value, nil
}
func (ctx *wikiContext) resolveWikiTemplateScopedValue(name string, state *wikiTemplateRenderState) (any, bool) {
name = strings.TrimSpace(name)
if strings.HasPrefix(name, "page.") {
field := strings.TrimPrefix(name, "page.")
switch field {
case "title", "Title":
return state.page.Title, true
case "page_id", "PageID":
return state.page.PageID, true
case "status", "Status":
return state.page.Status, true
case "category", "Category":
return state.page.Category, true
case "key", "Key":
return state.page.Key, true
default:
if value, ok := lookupField(state.page.Row, field); ok {
return value, true
}
return nil, false
}
}
if state.row != nil {
if value, ok := lookupField(state.row, name); ok {
return value, true
}
}
if value, ok := lookupField(state.page.Row, name); ok {
return value, true
}
switch name {
case "title":
return state.page.Title, true
case "page_id":
return state.page.PageID, true
case "status":
return state.page.Status, true
}
return nil, false
}
func (ctx *wikiContext) applyWikiTemplateFilter(value any, filter string, state *wikiTemplateRenderState) (any, error) {
name, argText, hasArg := strings.Cut(filter, ":")
name = strings.TrimSpace(name)
args, err := parseWikiTemplateFilterArgs(argText)
if hasArg && err != nil {
return nil, fmt.Errorf("render wiki template %s for %s filter %q: %w", state.path, state.page.PageID, filter, err)
}
switch name {
case "text":
return ctx.resolveTextValue(value, nil), nil
case "trim":
return strings.TrimSpace(stringifyWikiTemplatePlain(value)), nil
case "after":
return afterFirstMarker(stringifyWikiTemplatePlain(value), firstArg(args)), nil
case "before":
return beforeFirstMarker(stringifyWikiTemplatePlain(value), firstArg(args)), nil
case "between":
text := stringifyWikiTemplatePlain(value)
return beforeFirstMarker(afterFirstMarker(text, firstArg(args)), secondArg(args)), nil
case "paragraph":
return selectWikiTemplateParagraphs(stringifyWikiTemplatePlain(value), firstArg(args)), nil
case "paragraphs":
return selectWikiTemplateParagraphs(stringifyWikiTemplatePlain(value), firstArg(args)), nil
case "linebreaks":
return wikiTemplateHTML(strings.ReplaceAll(html.EscapeString(stringifyWikiTemplatePlain(value)), "\n", "
")), nil
case "html":
return wikiTemplateHTML(renderWikiTemplatePlainHTML(stringifyWikiTemplatePlain(value))), nil
case "wiki":
return wikiTemplateHTML(renderWikiLinks(stringifyWikiTemplatePlain(value))), nil
case "lower":
return strings.ToLower(stringifyWikiTemplatePlain(value)), nil
case "title":
return strings.Title(stringifyWikiTemplatePlain(value)), nil
case "sentence":
text := stringifyWikiTemplatePlain(value)
for i, r := range text {
if unicode.IsLetter(r) {
return text[:i] + string(unicode.ToUpper(r)) + text[i+len(string(r)):], nil
}
}
return text, nil
case "join":
return joinWikiTemplateValue(value, firstArg(args)), nil
case "list":
return wikiTemplateHTML(renderWikiTemplateList(value, firstArg(args))), nil
case "ordinal":
return ordinalWikiTableValue(numericWikiTableValue(value)), nil
case "default":
if strings.TrimSpace(stringifyWikiTemplatePlain(value)) == "" {
return firstArg(args), nil
}
return value, nil
case "ref":
return ctx.renderTemplateReference(value, firstArg(args)), nil
case "link":
return ctx.renderTemplateLink(value, firstArg(args), state), nil
default:
return nil, fmt.Errorf("render wiki template %s for %s: unknown filter %q", state.path, state.page.PageID, name)
}
}
func parseWikiTemplateFilterArgs(raw string) ([]string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
args := []string{}
for len(raw) > 0 {
raw = strings.TrimSpace(raw)
if !strings.HasPrefix(raw, `"`) {
if idx := strings.Index(raw, ","); idx >= 0 {
args = append(args, strings.TrimSpace(raw[:idx]))
raw = raw[idx+1:]
continue
}
args = append(args, strings.TrimSpace(raw))
break
}
var b strings.Builder
escaped := false
i := 1
for ; i < len(raw); i++ {
if escaped {
b.WriteByte(raw[i])
escaped = false
continue
}
if raw[i] == '\\' {
escaped = true
continue
}
if raw[i] == '"' {
break
}
b.WriteByte(raw[i])
}
if i >= len(raw) {
return nil, fmt.Errorf("unterminated quoted argument")
}
args = append(args, b.String())
raw = strings.TrimSpace(raw[i+1:])
if strings.HasPrefix(raw, ",") {
raw = raw[1:]
} else if raw != "" {
return nil, fmt.Errorf("unexpected argument text %q", raw)
}
}
return args, nil
}
func firstArg(args []string) string {
if len(args) == 0 {
return ""
}
return args[0]
}
func secondArg(args []string) string {
if len(args) < 2 {
return ""
}
return args[1]
}
func afterFirstMarker(text, marker string) string {
if marker == "" {
return text
}
idx := strings.Index(text, marker)
if idx < 0 {
return ""
}
return text[idx+len(marker):]
}
func beforeFirstMarker(text, marker string) string {
if marker == "" {
return text
}
idx := strings.Index(text, marker)
if idx < 0 {
return text
}
return text[:idx]
}
func selectWikiTemplateParagraphs(text, selector string) string {
paragraphs := splitWikiTemplateParagraphs(text)
if len(paragraphs) == 0 {
return ""
}
switch strings.TrimSpace(selector) {
case "first":
return paragraphs[0]
case "last":
return paragraphs[len(paragraphs)-1]
case "all", "":
return strings.Join(paragraphs, "\n\n")
default:
selected := []string{}
for _, part := range strings.Split(selector, ",") {
switch strings.TrimSpace(part) {
case "first":
selected = append(selected, paragraphs[0])
case "last":
selected = append(selected, paragraphs[len(paragraphs)-1])
default:
if idx, err := strconv.Atoi(strings.TrimSpace(part)); err == nil && idx > 0 && idx <= len(paragraphs) {
selected = append(selected, paragraphs[idx-1])
}
}
}
return strings.Join(selected, "\n\n")
}
}
func splitWikiTemplateParagraphs(text string) []string {
normalized := strings.ReplaceAll(text, "\r\n", "\n")
chunks := strings.Split(normalized, "\n\n")
out := []string{}
for _, chunk := range chunks {
if trimmed := strings.TrimSpace(chunk); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
func renderWikiTemplatePlainHTML(text string) string {
paragraphs := splitWikiTemplateParagraphs(text)
if len(paragraphs) == 0 {
return ""
}
out := strings.Builder{}
for _, paragraph := range paragraphs {
escaped := html.EscapeString(paragraph)
escaped = strings.ReplaceAll(escaped, "\n", "
")
out.WriteString("
") out.WriteString(escaped) out.WriteString("
") } return out.String() } func joinWikiTemplateValue(value any, sep string) string { switch typed := value.(type) { case []string: return strings.Join(typed, sep) case []any: parts := make([]string, 0, len(typed)) for _, item := range typed { if text := stringifyWikiTemplatePlain(item); text != "" { parts = append(parts, text) } } return strings.Join(parts, sep) default: return stringifyWikiTemplatePlain(value) } } func renderWikiTemplateList(value any, class string) string { items := wikiTemplateListItems(value) if len(items) == 0 { return "" } var out strings.Builder out.WriteString("