Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/9 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
417 lines
11 KiB
Go
417 lines
11 KiB
Go
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", "<br>")), 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 "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", "<br>")
|
|
out.WriteString("<p>")
|
|
out.WriteString(escaped)
|
|
out.WriteString("</p>")
|
|
}
|
|
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 (ctx *wikiContext) renderTemplateReference(value any, dataset string) string {
|
|
return ctx.renderReference(value, ctx.wikiIDMapForDataset(dataset), ctx.wikiNameResolverForDataset(dataset))
|
|
}
|
|
|
|
func (ctx *wikiContext) wikiNameResolverForDataset(dataset string) func(string) string {
|
|
switch dataset {
|
|
case "classes":
|
|
return ctx.resolveClassName
|
|
case "feat":
|
|
return ctx.resolveFeatName
|
|
case "skills":
|
|
return ctx.resolveSkillName
|
|
default:
|
|
return func(string) string { return "" }
|
|
}
|
|
}
|
|
|
|
func (ctx *wikiContext) renderTemplateLink(value any, labelField string, state *wikiTemplateRenderState) string {
|
|
key := stringifyWikiTemplatePlain(value)
|
|
label := labelField
|
|
if state.row != nil {
|
|
if raw, ok := lookupField(state.row, labelField); ok {
|
|
label = stringifyWikiTemplatePlain(raw)
|
|
}
|
|
}
|
|
if label == "" {
|
|
label = key
|
|
}
|
|
if key == "" {
|
|
return label
|
|
}
|
|
if !ctx.canReferenceWikiKey(key) {
|
|
return ""
|
|
}
|
|
if target := ctx.publicWikiTargetForKey(key, label); target != "" {
|
|
key = target
|
|
}
|
|
return "[[" + key + "|" + label + "]]"
|
|
}
|
|
|
|
func stringifyWikiTemplateOutput(value any) string {
|
|
if htmlValue, ok := value.(wikiTemplateHTML); ok {
|
|
return string(htmlValue)
|
|
}
|
|
return html.EscapeString(stringifyWikiTemplatePlain(value))
|
|
}
|
|
|
|
func stringifyWikiTemplatePlain(value any) string {
|
|
if htmlValue, ok := value.(wikiTemplateHTML); ok {
|
|
return string(htmlValue)
|
|
}
|
|
return stringifyWikiTableValue(value)
|
|
}
|