diff --git a/bbcode.go b/bbcode.go
new file mode 100644
index 0000000..807e13d
--- /dev/null
+++ b/bbcode.go
@@ -0,0 +1,188 @@
+package yatwiki3
+
+import (
+ "encoding/json"
+ "html/template"
+ "regexp"
+ "strings"
+)
+
+// An embarassing cascade of half-working hacks follows.
+type BBCodeRenderer struct {
+ baseUrl string
+ CodePresent bool
+ DynamicContentWarning string
+}
+
+func NewBBCodeRenderer(baseUrl string) *BBCodeRenderer {
+ return &BBCodeRenderer{
+ baseUrl: baseUrl,
+ CodePresent: false,
+ DynamicContentWarning: `⚠ run dynamic content`,
+ }
+}
+
+type pregReplaceRule struct {
+ match *regexp.Regexp
+ replace string
+ replaceFunc func([]string) string
+}
+
+// Internal part of BBCode rendering.
+// It handles most leaf-level tags.
+func (this *BBCodeRenderer) bbcode(data string) string {
+
+ s_to_r := []pregReplaceRule{
+ pregReplaceRule{regexp.MustCompile(`(?si)\[h\](.*?)\[/h\]`), `
$1
`, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[b\](.*?)\[/b\]`), `$1`, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[i\](.*?)\[/i\]`), `$1`, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[u\](.*?)\[/u\]`), `$1`, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[s\](.*?)\[/s\]`), `$1`, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[spoiler\](.*?)\[/spoiler\]`), `$1`, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[img\](.*?)\[/img\]`), ``, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[list\](.*?)\[/list\]`), ``, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[\*\]`), ``, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[url=(.*?)\](.*?)\[/url\]`), `$2`, nil},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[article=(.*?)\](.*?)\[/article\]`), "", func(m []string) string {
+ return `` + m[2] + ``
+ }},
+ pregReplaceRule{regexp.MustCompile(`(?si)\[rev=(.*?)\](.*?)\[/rev\]`), "", func(m []string) string {
+ return `` + m[2] + ``
+ }},
+
+ pregReplaceRule{regexp.MustCompile(`(?si)\[imgur\](.*?)\.(...)\[/imgur\]`),
+ ``,
+ nil,
+ },
+
+ pregReplaceRule{regexp.MustCompile(`(?si)\[section=(.*?)](.*?)\[/section\]`), "", func(m []string) string {
+ return `` + strings.TrimSpace(m[2]) + `
`
+ }},
+ }
+
+ for _, prr := range s_to_r {
+
+ for prr.match.MatchString(data) { // repeat until all recursive replacements are consumed
+ if len(prr.replace) > 0 {
+ data = prr.match.ReplaceAllString(data, prr.replace)
+
+ } else {
+ data = PregReplaceCallback(prr.match, prr.replaceFunc, data)
+ }
+ }
+ }
+ return data
+}
+
+// Internal part of BBCode rendering.
+// It extracts tables and then passes the remainder to bbcode().
+func (this *BBCodeRenderer) bbtables(s string) string {
+ lines := strings.Split(s, "\n")
+ lastct := 0
+
+ ret := make([]string, 0)
+ tbl := make([]string, 0)
+ buffer := make([]string, 0)
+
+ maybe_close_previous_table := func() {
+ if lastct > 0 { // Close previous table
+ tbl = append(tbl, "")
+ ret = append(ret, strings.Join(tbl, ""))
+ tbl = []string{}
+ lastct = 0
+ }
+ }
+
+ inner_formatter := func(s string) string {
+ return this.bbcode(template.HTMLEscapeString(s))
+ }
+
+ flush_buffer := func() {
+ if len(buffer) > 0 {
+ ret = append(ret, inner_formatter(strings.Join(buffer, "\n")))
+ buffer = []string{}
+ }
+ }
+
+ for _, line := range lines {
+ if len(line) > 0 && line[0] == '|' {
+ flush_buffer()
+
+ cols := strings.Split(line, "|")
+ if lastct != len(cols) {
+ maybe_close_previous_table()
+
+ // Start new table
+ lastct = len(cols)
+ tbl = append(tbl, "")
+ }
+
+ for i := 0; i < len(cols); i += 1 {
+ cols[i] = inner_formatter(strings.Replace(cols[i], ";", "\n", -1))
+ }
+
+ // Add these columns
+ tbl = append(tbl, ""+strings.Join(cols[1:], " | ")+" |
")
+
+ } else {
+ maybe_close_previous_table()
+ buffer = append(buffer, line)
+ }
+ }
+
+ flush_buffer()
+ maybe_close_previous_table()
+
+ return strings.Join(ret, "\n")
+}
+
+// Internal part of BBCode rendering.
+// It extracts [code] sections and then passes the remainder to bbtables().
+func (this *BBCodeRenderer) bbformat(str string) string {
+ return PregReplaceCallback(
+ regexp.MustCompile(`(?si)\[code\](.*?)\[/code\]`),
+ func(m []string) string {
+ this.CodePresent = true
+ return "" + strings.Replace(m[1], "
\r\n", "\r\n", -1) + "
"
+ },
+ strings.Replace(
+ strings.Replace(this.bbtables(str), "\r\n", "
", -1), //bbtables(bbcode(h($str)))
+ "
\r\n
",
+ "
",
+ -1, // replace all
+ ),
+ )
+}
+
+// Internal part of BBCode rendering.
+// It extracts [html] sections and then passes the remainder to bbformat().
+func (this *BBCodeRenderer) displayfmt(s string) string {
+ hpos := 0
+ ret := ""
+
+ for {
+ spos := strings.Index(s[hpos:], "[html]")
+ if spos == -1 {
+ break
+ }
+
+ ret += this.bbformat(s[hpos : spos-hpos])
+ spos += 6
+
+ epos := strings.Index(s[spos:], "[/html]")
+ if epos == -1 {
+ break // no matching [/html] tag found
+ }
+
+ jsonInnerContent, _ := json.Marshal(s[spos : epos-spos])
+
+ ret += ``
+ hpos = epos + 7
+ }
+ return ret + this.bbformat(s[hpos:])
+}
+
+// RenderHTML converts the input string to HTML.
+func (this *BBCodeRenderer) RenderHTML(s string) template.HTML {
+ return template.HTML(this.displayfmt(s))
+}
diff --git a/regex.go b/regex.go
new file mode 100644
index 0000000..54aca17
--- /dev/null
+++ b/regex.go
@@ -0,0 +1,15 @@
+package yatwiki3
+
+import (
+ "regexp"
+)
+
+func PregReplaceCallback(pattern *regexp.Regexp, cb func([]string) string, content string) string {
+ // FIXME avoid double-matching
+ // Submit to upstream https://github.com/golang/go/issues/5690
+ return pattern.ReplaceAllStringFunc(content, func(fullmatch string) string {
+ parts := pattern.FindStringSubmatch(fullmatch)
+ return cb(parts)
+ })
+
+}