This repository has been archived on 2022-02-16. You can view files and clone it, but cannot push or open issues or pull requests.
gowiki/template.go
Michele M. Franceschini 93aa7513fb Adding library code.
2018-01-03 16:21:59 -05:00

661 lines
17 KiB
Go

/*
Copyright (C) IBM Corporation 2015, Michele Franceschini <franceschini@us.ibm.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gowiki
import (
"fmt"
"os"
"regexp"
"sort"
"strings"
)
type Template struct {
Typ string `json:"type"` //magic,normal,ext,param
Name string `json:"name"`
Attr string `json:"attr"` //text after the ':' in magic templates
Parameters map[string]string `json:"parameters"`
}
func (a *Article) parseTemplateEtc(l string) []Template {
return nil
}
type streak struct {
opening bool
length int
b int
e int
}
type template struct {
b int
e int
isparam bool
children []*template
rt string
rendered bool
}
type byStart []*template
func (a byStart) Len() int { return len(a) }
func (a byStart) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byStart) Less(i, j int) bool { return a[i].b < a[j].b }
var templateStreaksRe = regexp.MustCompile(`(?:\{\{+)|(?:\}\}+)`)
func findCurlyStreaks(mw string) [][]int {
out := [][]int{}
found := '.'
beg := 0
// count :=0
for i, r := range mw {
switch r {
case found:
default:
if i-beg > 1 && (found == '{' || found == '}') {
out = append(out, []int{beg, i})
}
beg = i
found = r
}
}
if beg < len(mw)-1 && (found == '{' || found == '}') {
out = append(out, []int{beg, len(mw)})
}
return out
}
func findTemplates(mw string) []*template {
// tsl := templateStreaksRe.FindAllStringSubmatchIndex(mw, -1)
tsl := findCurlyStreaks(mw)
// fmt.Println(tsl)
streaks := make([]streak, 0, len(tsl))
for _, pair := range tsl {
streaks = append(streaks, streak{
opening: (mw[pair[0]] == '{'),
length: pair[1] - pair[0],
b: pair[0],
e: pair[1],
})
}
// fmt.Println(streaks)
tl := make([]*template, 0, 8)
i := 0
for i < len(streaks) {
if !streaks[i].opening && streaks[i].length > 1 { // found a closing set: search for the opening
found := false
for j := i - 1; j >= 0; j-- {
if streaks[j].opening && streaks[j].length > 1 {
found = true
n := 2
isparam := false
if streaks[i].length > 2 && streaks[j].length > 2 {
n = 3
isparam = true
}
tl = append(tl, &template{
isparam: isparam,
b: streaks[j].e - n,
e: streaks[i].b + n,
})
streaks[i].length -= n
streaks[i].b += n
streaks[j].length -= n
streaks[j].e -= n
break
}
}
if found {
continue
}
}
i++
}
sort.Sort(byStart(tl))
/* fmt.Println("Templates found:")
for i := range tl {
fmt.Println(tl[i])
} */
out := make([]*template, 0, 4)
cur_end := 0
for i := range tl {
tl[i].children = []*template{}
if tl[i].b >= cur_end {
cur_end = tl[i].e
out = append(out, tl[i])
} else {
for j := i - 1; j >= 0; j-- {
if tl[j].e > tl[i].e {
tl[j].children = append(tl[j].children, tl[i])
break
}
}
}
}
/* fmt.Println("Templates out:")
for i := range out {
fmt.Println(out[i])
}*/
/* fmt.Println("Templates found:")
for i := range tl {
fmt.Println(mw[tl[i].b:tl[i].e])
}
*/
return out
}
func findTemplateParamPos(mw string, t *template) [][]int { //first is position of pipe, second is position of first equal
out := make([][]int, 0, 1)
inChildTemplate := false
inlink := false
lastopen := false
lastclosed := false
for i, rv := range mw[t.b:t.e] {
inChildTemplate = false
open := false
closed := false
for _, ct := range t.children {
if i+t.b >= ct.b && i+t.b < ct.e {
inChildTemplate = true
break
}
}
if !inChildTemplate {
switch {
case rv == '[':
if lastopen {
inlink = true
}
open = true
case rv == ']':
if lastclosed {
inlink = false
}
closed = true
case rv == '|' && !inlink:
out = append(out, []int{i + t.b})
case rv == '=' && len(out) > 0 && len(out[len(out)-1]) == 1 && !inlink:
out[len(out)-1] = append(out[len(out)-1], i+t.b)
}
}
lastopen = open
lastclosed = closed
}
return out
}
/*func (a *Article) processTemplates(mw string, tokens map[string]*Token) (string, map[string]*Token) {
mlt := findTemplates(mw)
last := 0
out := make([]byte, 0, len(mw))
// tokens := make(map[string]*Token, len(mlt))
for i, t := range mlt {
sb := fmt.Sprintf("\x07tb%05d", i)
se := fmt.Sprintf("\x07te%05d", i)
out = append(out, []byte(mw[last:t.b])...)
out = append(out, []byte(sb+a.renderTemplate(mw, t)+se)...)
last = t.e
tokens[sb] = &Token{
TText: fmt.Sprintf("%d", i),
TType: "tb",
}
tokens[se] = &Token{
TText: fmt.Sprintf("%d", i),
TType: "te",
}
}
out = append(out, []byte(mw[last:])...)
return string(out), tokens
} */
func (a *Article) processTemplates(mws string, tokens map[string]*Token, g PageGetter) (string, map[string]*Token) {
//strip nowiki noinclude etc here
// mws := a.stripComments(mw)
// mws = a.stripNoinclude(mws)
// fmt.Println(mws)
mlt := findTemplates(mws)
last := 0
out := make([]byte, 0, len(mws))
for i, t := range mlt {
// fmt.Println("Process templates:", *t)
sb := fmt.Sprintf("\x07tb%05d", i)
se := fmt.Sprintf("\x07te%05d", i)
tn, pm := a.renderInnerTemplates(mws, t, nil, g, 0)
a.addTemplate(tn, pm)
out = append(out, []byte(mws[last:t.b])...)
out = append(out, []byte(sb+t.rt+se)...)
last = t.e
tokens[sb] = &Token{
TText: fmt.Sprintf("%d", i),
TType: "tb",
}
tokens[se] = &Token{
TText: fmt.Sprintf("%d", i),
TType: "te",
}
}
out = append(out, []byte(mws[last:])...)
//unstrip here
return string(out), tokens
}
func (a *Article) addTemplate(tn string, pm map[string]string) {
outT := Template{Parameters: pm}
base, attr, typ, _ := detectTemplateType(tn)
outT.Typ = typ
outT.Name = base
outT.Attr = attr
a.Templates = append(a.Templates, &outT)
return
}
func (a *Article) renderTemplate(mw string, t *template) string {
pp := findTemplateParamPos(mw, t)
n := 2
if t.isparam {
n = 3
}
var tn string
if len(pp) > 0 {
tn = fmt.Sprint(strings.TrimSpace(mw[t.b+n : pp[0][0]]))
} else {
tn = fmt.Sprint(strings.TrimSpace(mw[t.b+n : t.e-n]))
}
pm := make(map[string]string, len(pp))
pp = append(pp, []int{t.e - n})
for i := 0; i < len(pp)-1; i++ {
var name string
var param string
if len(pp[i]) > 1 { //named param
name = fmt.Sprint(strings.TrimSpace(mw[pp[i][0]+1 : pp[i][1]]))
param = fmt.Sprint(strings.TrimSpace(mw[pp[i][1]+1 : pp[i+1][0]]))
} else {
name = fmt.Sprint(i + 1)
param = fmt.Sprint(strings.TrimSpace(mw[pp[i][0]+1 : pp[i+1][0]]))
}
pm[name] = param
}
outT := Template{Parameters: pm}
base, attr, typ, text := detectTemplateType(tn)
switch {
case t.isparam:
outT.Typ = "param"
outT.Name = tn
text = ""
default:
outT.Typ = typ
outT.Name = base
outT.Attr = attr
}
a.Templates = append(a.Templates, &outT)
return text
}
func detectTemplateType(tn string) (string, string, string, string) {
index := strings.Index(tn, ":")
var base string
var attr string
if index > 0 {
base = strings.TrimSpace(tn[:index])
attr = strings.TrimSpace(tn[index+1:])
} else {
base = tn
}
_, ok := MagicMap[base]
if ok {
return base, attr, "magic", ""
}
return tn, "", "normal", ""
}
type TemplateRenderer func(name, mw string, params map[string]string) string
var MagicMap map[string]TemplateRenderer = map[string]TemplateRenderer{
"DISPLAYTITLE": nil,
}
var noHashFunctionsMap map[string]bool = map[string]bool{
"displaytitle": true,
"formatdate": true,
"int": true,
"namespace": true,
"pagesinnamespace": true,
"speciale": true,
"special": true,
"tag": true,
"anchorencode": true, "basepagenamee": true, "basepagename": true, "canonicalurle": true,
"canonicalurl": true, "cascadingsources": true, "defaultsort": true, "filepath": true,
"formatnum": true, "fullpagenamee": true, "fullpagename": true, "fullurle": true,
"fullurl": true, "gender": true, "grammar": true, "language": true,
"lcfirst": true, "lc": true, "localurle": true, "localurl": true,
"namespacee": true, "namespacenumber": true, "nse": true, "ns": true,
"numberingroup": true, "numberofactiveusers": true, "numberofadmins": true, "numberofarticles": true,
"numberofedits": true, "numberoffiles": true, "numberofpages": true, "numberofusers": true,
"numberofviews": true, "padleft": true, "padright": true, "pageid": true,
"pagenamee": true, "pagename": true, "pagesincategory": true, "pagesize": true,
"plural": true, "protectionlevel": true, "revisionday2": true, "revisionday": true,
"revisionid": true, "revisionmonth1": true, "revisionmonth": true, "revisiontimestamp": true,
"revisionuser": true, "revisionyear": true, "rootpagenamee": true, "rootpagename": true,
"subjectpagenamee": true, "subjectpagename": true, "subjectspacee": true, "subjectspace": true,
"subpagenamee": true, "subpagename": true, "talkpagenamee": true, "talkpagename": true,
"talkspacee": true, "talkspace": true, "ucfirst": true, "uc": true,
"urlencode": true,
}
var variablesMap map[string]bool = map[string]bool{
"articlepath": true,
"basepagenamee": true,
"basepagename": true,
"cascadingsources": true,
"contentlanguage": true,
"currentday2": true,
"currentdayname": true,
"currentday": true,
"currentdow": true,
"currenthour": true,
"currentmonth1": true,
"currentmonthabbrev": true,
"currentmonthnamegen": true,
"currentmonthname": true,
"currentmonth": true,
"currenttimestamp": true,
"currenttime": true,
"currentversion": true,
"currentweek": true,
"currentyear": true,
"directionmark": true,
"fullpagenamee": true,
"fullpagename": true,
"localday2": true,
"localdayname": true,
"localday": true,
"localdow": true,
"localhour": true,
"localmonth1": true,
"localmonthabbrev": true,
"localmonthnamegen": true,
"localmonthname": true,
"localmonth": true,
"localtimestamp": true,
"localtime": true,
"localweek": true,
"localyear": true,
"namespacee": true,
"namespacenumber": true,
"namespace": true,
"numberofactiveusers": true,
"numberofadmins": true,
"numberofarticles": true,
"numberofedits": true,
"numberoffiles": true,
"numberofpages": true,
"numberofusers": true,
"numberofviews": true,
"pageid": true,
"pagenamee": true,
"pagename": true,
"revisionday2": true,
"revisionday": true,
"revisionid": true,
"revisionmonth1": true,
"revisionmonth": true,
"revisionsize": true,
"revisiontimestamp": true,
"revisionuser": true,
"revisionyear": true,
"rootpagenamee": true,
"rootpagename": true,
"scriptpath": true,
"servername": true,
"server": true,
"sitename": true,
"stylepath": true,
"subjectpagenamee": true,
"subjectpagename": true,
"subjectspacee": true,
"subjectspace": true,
"subpagenamee": true,
"subpagename": true,
"talkpagenamee": true,
"talkpagename": true,
"talkspacee": true,
"talkspace": true,
}
func (a *Article) renderTemplateMagic(name string, params map[string]string) string {
return ""
}
func (a *Article) renderTemplateExt(name string, params map[string]string) string {
return ""
}
func (a *Article) renderTemplateRecursive(name string, params map[string]string, g PageGetter, depth int) string {
if depth > 4 {
return ""
}
//name and parameters have already been substituted so they are guarranteed not to contain any template
//establish the type of template
switch templateType(name) {
case "magic":
return a.renderTemplateMagic(name, params)
case "ext":
return a.renderTemplateExt(name, params)
}
//case "normal"
//based on the type of template
//for the name and each parameter, find templates and substite them in the proper order
mw, err := g.Get(WikiCanonicalFormNamespace(name, "Template"))
if err != nil {
fmt.Fprintln(os.Stderr, "Title:", a.Title, " Error retrieving:", name, " ->", err)
return ""
}
return a.TranscludeTemplatesRecursive(mw, params, g, depth)
}
func (a *Article) TranscludeTemplatesRecursive(mw string, params map[string]string, g PageGetter, depth int) string {
var mws string
followed := 0
for {
if followed > 4 {
return ""
}
//strip nowiki noinclude etc here
mws := a.stripComments(mw)
isRedirect, redirect := a.checkRedirect(mws)
if !isRedirect {
break
}
var err error
mw, err = g.Get(*redirect)
if err != nil {
return ""
}
followed++
}
mws = a.stripNoinclude(mws)
// fmt.Println(ds[depth], "TranscludeTemplatesRecursive", mws)
mlt := findTemplates(mws)
last := 0
out := make([]byte, 0, len(mws))
for _, t := range mlt {
a.renderInnerTemplates(mws, t, params, g, depth)
out = append(out, []byte(mws[last:t.b])...)
out = append(out, []byte(t.rt)...)
last = t.e
}
out = append(out, []byte(mws[last:])...)
//unstrip here
return string(out)
}
var ds []string = []string{" ", " ", " ", " ", " ", " "}
func (a *Article) renderInnerTemplates(mws string, t *template, params map[string]string, g PageGetter, depth int) (string, map[string]string) {
// render inner templates first
// fmt.Println(ds[depth], *t, "\n", ds[depth], "Template:\n", ds[depth], mws[t.b:t.e])
for _, it := range t.children {
if !it.rendered {
a.renderInnerTemplates(mws, it, params, g, depth)
}
}
// fmt.Println(ds[depth], "Working on", mws[t.b:t.e])
pp := findTemplateParamPos(mws, t) //position of the pipes for this template
// fmt.Println(ds[depth], "pp:", pp)
n := 2
if t.isparam {
n = 3
}
pp = append(pp, []int{t.e - n})
var mw string
var tb int
// var te int
if len(t.children) == 0 {
// fmt.Println(ds[depth], "No nested templates in", mws[t.b:t.e])
mw = mws
tb = t.b
// te = t.e
} else {
// fmt.Println(ds[depth], "Nested templates: fixing pp")
//substitute the strings and update pp
tci := 0
ioff := t.children[tci].b
tb = 0
mw = mws[t.b:ioff]
// fmt.Println(*t)
ooff := -t.b
ppi0 := 0
ppi1 := 0
for ppi0 < len(pp) {
// fmt.Println(mws)
// fmt.Println(len(mws), tci, ioff, ooff, ppi0, ppi1, pp)
if pp[ppi0][ppi1] <= ioff {
pp[ppi0][ppi1] += ooff
ppi1++
if ppi1 >= len(pp[ppi0]) {
ppi0++
ppi1 = 0
}
} else {
mw += t.children[tci].rt
ooff += len(t.children[tci].rt) - (t.children[tci].e - t.children[tci].b)
teoff := t.children[tci].e
tci++
if tci >= len(t.children) {
ioff = t.e
} else {
ioff = t.children[tci].b
}
// fmt.Println(ds[depth], tci, teoff, ioff)
mw += mws[teoff:ioff]
}
}
// te = len(mw)
}
// fmt.Println("len(mw):", len(mw), "mw:", mw, "\npp:", pp)
var tn string
if len(pp) > 1 {
tn = fmt.Sprint(strings.TrimSpace(mw[tb+n : pp[0][0]]))
} else {
tn = fmt.Sprint(strings.TrimSpace(mw[tb+n : pp[len(pp)-1][0]]))
}
t.rendered = true
if t.isparam { //it's a parameter substitution
text, ok := params[tn]
if ok {
t.rt = text
return "", nil
}
if len(pp) == 1 { //no default
t.rt = "{{{" + tn + "}}}"
return "", nil
}
t.rt = mw[pp[0][0]+1 : pp[len(pp)-1][0]]
return "", nil
}
pm := make(map[string]string, len(pp))
for i := 0; i < len(pp)-1; i++ {
var name string
var param string
if len(pp[i]) > 1 { //named param
name = fmt.Sprint(strings.TrimSpace(mw[pp[i][0]+1 : pp[i][1]]))
param = fmt.Sprint(strings.TrimSpace(mw[pp[i][1]+1 : pp[i+1][0]]))
} else {
name = fmt.Sprint(i + 1)
param = fmt.Sprint(strings.TrimSpace(mw[pp[i][0]+1 : pp[i+1][0]]))
}
pm[name] = param
}
t.rt = a.renderTemplateRecursive(tn, pm, g, depth+1)
return tn, pm
}
func templateType(tn string) string {
index := strings.Index(tn, ":")
tns := strings.TrimSpace(tn)
var base string
// var attr string
if index > 0 {
base = strings.TrimSpace(tn[:index])
// attr = strings.TrimSpace(tn[index+1:])
} else {
base = tns
}
base = strings.ToLower(base)
_, ok1 := noHashFunctionsMap[base]
_, ok2 := variablesMap[base]
if ok1 || ok2 {
return "magic"
}
if strings.HasPrefix(tns, "#") {
return "ext"
}
return "normal"
}
var noincludeRe = regexp.MustCompile(`(?isU)<noinclude>.*(?:</noinclude>|\z)`)
var includeonlyRe = regexp.MustCompile(`(?isU)<includeonly>(.*)(?:</includeonly>|\z)`)
func (a *Article) stripNoinclude(mw string) string {
mwni := noincludeRe.ReplaceAllLiteralString(mw, "")
ssl := includeonlyRe.FindAllStringSubmatch(mwni, -1)
if len(ssl) == 0 {
return mwni
}
sl := make([]string, 0, len(ssl))
for _, s := range ssl {
sl = append(sl, s[1])
}
return strings.Join(sl, "")
}