diff --git a/_datafiles/world/default/ansi-aliases.yaml b/_datafiles/world/default/ansi-aliases.yaml index f60c17fc..e58b518e 100644 --- a/_datafiles/world/default/ansi-aliases.yaml +++ b/_datafiles/world/default/ansi-aliases.yaml @@ -153,4 +153,13 @@ colors: mob-corpse: 67 user-corpse: 143 tip-text: 219 - character-joined: 120 \ No newline at end of file + character-joined: 120 + md-h1-prefix: black-bold + md-h1: magenta + md-h2: yellow-bold + md-li: white-bold + md-sp1: command + md-bold: 9 + md-em: 208 + md-hr1: 4 + md-hr2: 4 \ No newline at end of file diff --git a/_datafiles/world/default/templates/help/attack.md b/_datafiles/world/default/templates/help/attack.md new file mode 100644 index 00000000..7d422586 --- /dev/null +++ b/_datafiles/world/default/templates/help/attack.md @@ -0,0 +1,20 @@ +# Help for ~attack~ + +The ~attack~ command engages in combat with a player or NPC. +Once started, combat continues until someone flees or someone dies. + +## Usage: + + ~attack goblin~ + This would start combat with the goblin. + +*Chance to Hit* is calculated as follows: + +- **attackersSpeed** / (**atackerSpeed** + **defenderSpeed**) * **70** + **30** + +You always have a minimum 5% chance to miss, and a minimum 5% chance to hit. + +*Crit Chance* is calculated as follows: + +- (**Strength** + **Speed**) / (**attackerLevel** - **defenderLevel**) + **5** + diff --git a/_datafiles/world/default/templates/help/attack.template b/_datafiles/world/default/templates/help/attack.template index 3ba59e2e..a96a7e8b 100644 --- a/_datafiles/world/default/templates/help/attack.template +++ b/_datafiles/world/default/templates/help/attack.template @@ -5,10 +5,11 @@ until someone flees or someone dies. Usage: - attack goblin + attack goblin This would start combat with the goblin When in combat your chance to hit is: attackersSpeed / (atackerSpeed+defenderSpeed) * 70 + 30 You always have a minimum 5% chance to miss and a minimum 5% chance to hit Crits Chance is calculated as follows: (Strength+Speed) / (attackerLevel-defenderLevel) + 5 + diff --git a/_datafiles/world/empty/ansi-aliases.yaml b/_datafiles/world/empty/ansi-aliases.yaml index f60c17fc..e58b518e 100644 --- a/_datafiles/world/empty/ansi-aliases.yaml +++ b/_datafiles/world/empty/ansi-aliases.yaml @@ -153,4 +153,13 @@ colors: mob-corpse: 67 user-corpse: 143 tip-text: 219 - character-joined: 120 \ No newline at end of file + character-joined: 120 + md-h1-prefix: black-bold + md-h1: magenta + md-h2: yellow-bold + md-li: white-bold + md-sp1: command + md-bold: 9 + md-em: 208 + md-hr1: 4 + md-hr2: 4 \ No newline at end of file diff --git a/_datafiles/world/empty/templates/help/attack.md b/_datafiles/world/empty/templates/help/attack.md new file mode 100644 index 00000000..7d422586 --- /dev/null +++ b/_datafiles/world/empty/templates/help/attack.md @@ -0,0 +1,20 @@ +# Help for ~attack~ + +The ~attack~ command engages in combat with a player or NPC. +Once started, combat continues until someone flees or someone dies. + +## Usage: + + ~attack goblin~ + This would start combat with the goblin. + +*Chance to Hit* is calculated as follows: + +- **attackersSpeed** / (**atackerSpeed** + **defenderSpeed**) * **70** + **30** + +You always have a minimum 5% chance to miss, and a minimum 5% chance to hit. + +*Crit Chance* is calculated as follows: + +- (**Strength** + **Speed**) / (**attackerLevel** - **defenderLevel**) + **5** + diff --git a/internal/markdown/README.md b/internal/markdown/README.md new file mode 100644 index 00000000..47b15db1 --- /dev/null +++ b/internal/markdown/README.md @@ -0,0 +1,143 @@ +## Supported Markdown + +### Rules + +* Two line breaks starts a new paragraph of text. +* Single line breaks collapse into a single line, UNLESS the previous line ended with a double space (Not my convention!) +* Most wrapping markdown can be nested, bold within emphasis inside of a Heading, etc. + +### Headings + +Markdown: + +`# Heading` + +Html: + +``` +Html tag output:

Heading

+``` + +Ansitags: + +``` +.: This is a HEADING +``` + +Note: Adding additional #'s will increment the `` +Note: Ansitags only add the prefix for h1 + +### Horizontal Lines + +Markdown: + +``` +--- +=== +::: +``` + +Html: + +``` +
+``` + +Ansitags: + +``` +-------------------------------------------------------------------------------- +================================================================================ +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +``` + +### Lists + +**Markdown:** + +``` +- List Item 1 + - List Sub 1 +- List Item 2 +- List Item 3 +``` + +**Html:** + +``` + +``` + +**Ansitags:** + +``` +- List Item 1 + - List Sub 1 +- List Item 2 +- List Item 3 +``` + +### Emphasis + +**Markdown:** + +`*Emphasize me*` + +**Html:** + +``` +Emphasize me +``` + +**Ansitags:** + +``` +Emphasize me +``` + +### Bold + +**Markdown:** + +`**Bold me**` + +**Html:** + +``` +Bold me +``` + +**Ansitags:** + +``` +Bold me +``` + +### Special + +**Markdown:** + +`~I'm Special~` + +**Html:** + +``` +I'm Special +``` + +**Ansitags:** + +``` +I'm Special +``` + +**Notes:** Additional wrapping ~'s increment the number: `~~I'm Special~~`, `~~~I'm Special~~~` and so on. +**Notes:** `~~` is typically treated as a strikethrough in markdown. diff --git a/internal/markdown/ast.go b/internal/markdown/ast.go new file mode 100644 index 00000000..7c0f7394 --- /dev/null +++ b/internal/markdown/ast.go @@ -0,0 +1,86 @@ +package markdown + +import ( + "fmt" +) + +// NodeType identifies the kind of AST node. +type NodeType string + +const ( + DocumentNode NodeType = "Document" + HeadingNode NodeType = "Heading" + ParagraphNode NodeType = "Paragraph" + HorizontalLineNode NodeType = "HorizontalLine" + HardBreakNode NodeType = "HardBreak" + ListNode NodeType = "List" + ListItemNode NodeType = "ListItem" + TextNode NodeType = "Text" + StrongNode NodeType = "Strong" + EmphasisNode NodeType = "Emphasis" + SpecialNode NodeType = "Special" +) + +var ( + activeFormatter Formatter = ReMarkdown{} +) + +func SetFormatter(newFormatter Formatter) { + activeFormatter = newFormatter +} + +// Node is an element in the AST. +type Node interface { + Type() NodeType + Children() []Node + String(int) string +} + +// baseNode provides common fields. +type baseNode struct { + nodeType NodeType + nodeChildren []Node + level int + content string +} + +func (n *baseNode) Type() NodeType { return n.nodeType } +func (n *baseNode) Children() []Node { return n.nodeChildren } +func (n *baseNode) String(depth int) string { + ret := `` + for _, c := range n.Children() { + if n.Type() == ListNode { + ret += c.String(depth - 1) + } else { + ret += c.String(depth + 1) + } + + } + + switch n.Type() { + case DocumentNode: + return activeFormatter.Document(ret, depth) + case HeadingNode: + return activeFormatter.Heading(ret, n.level) + case ParagraphNode: + return activeFormatter.Paragraph(ret, depth) + case HorizontalLineNode: + return activeFormatter.HorizontalLine(n.content, depth) + case HardBreakNode: + return activeFormatter.HardBreak(ret, depth) + case ListNode: + return activeFormatter.List(ret, depth) + case ListItemNode: + return activeFormatter.ListItem(ret, depth) + case TextNode: + return activeFormatter.Text(n.content+ret, depth) + case StrongNode: + return activeFormatter.Strong(ret, depth) + case EmphasisNode: + return activeFormatter.Emphasis(ret, depth) + case SpecialNode: + return activeFormatter.Special(ret, n.level) + default: + return fmt.Sprintf(`[INVALID Node: type=%s depth=%d text=%s]`, n.Type(), depth, n.content+"/"+ret) + } +} diff --git a/internal/markdown/ast_test.go b/internal/markdown/ast_test.go new file mode 100644 index 00000000..2359b476 --- /dev/null +++ b/internal/markdown/ast_test.go @@ -0,0 +1,94 @@ +package markdown + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func init() { + // Use ReMarkdown for consistent output + SetFormatter(ReMarkdown{}) +} + +func TestParser(t *testing.T) { + t.Run("Heading", func(t *testing.T) { + doc := NewParser("# Hello").Parse() + require.Equal(t, DocumentNode, doc.Type()) + require.Len(t, doc.Children(), 1) + heading := doc.Children()[0] + require.Equal(t, HeadingNode, heading.Type()) + require.Equal(t, "# Hello", doc.String(0)) + }) + + t.Run("Paragraph", func(t *testing.T) { + doc := NewParser("Hello world").Parse() + require.Len(t, doc.Children(), 1) + para := doc.Children()[0] + require.Equal(t, ParagraphNode, para.Type()) + require.Equal(t, "Hello world", doc.String(0)) + }) + + t.Run("HorizontalLine", func(t *testing.T) { + doc := NewParser("---").Parse() + require.Len(t, doc.Children(), 1) + sep := doc.Children()[0] + require.Equal(t, HorizontalLineNode, sep.Type()) + require.Equal(t, "---", doc.String(0)) + }) + + t.Run("HardBreak", func(t *testing.T) { + doc := NewParser("Line one \nLine two").Parse() + require.Equal(t, "Line one\nLine two", doc.String(0)) + }) + + t.Run("List", func(t *testing.T) { + doc := NewParser("- one\n- two").Parse() + require.Len(t, doc.Children(), 1) + list := doc.Children()[0] + require.Equal(t, ListNode, list.Type()) + require.Equal(t, "- one\n- two", doc.String(0)) + }) + + t.Run("InlineFormatting", func(t *testing.T) { + t.Run("Emphasis", func(t *testing.T) { + doc := NewParser("*em*").Parse() + require.Len(t, doc.Children(), 1) + para := doc.Children()[0].(*baseNode) + require.Equal(t, ParagraphNode, para.Type()) + children := para.Children() + require.Len(t, children, 1) + require.Equal(t, EmphasisNode, children[0].Type()) + require.Equal(t, "*em*", doc.String(0)) + }) + + t.Run("Strong", func(t *testing.T) { + doc := NewParser("**bold**").Parse() + require.Len(t, doc.Children(), 1) + para := doc.Children()[0].(*baseNode) + require.Equal(t, ParagraphNode, para.Type()) + children := para.Children() + require.Len(t, children, 1) + require.Equal(t, StrongNode, children[0].Type()) + require.Equal(t, "**bold**", doc.String(0)) + }) + + t.Run("Special", func(t *testing.T) { + doc := NewParser("~sp~").Parse() + require.Len(t, doc.Children(), 1) + para := doc.Children()[0].(*baseNode) + require.Equal(t, ParagraphNode, para.Type()) + children := para.Children() + require.Len(t, children, 1) + require.Equal(t, SpecialNode, children[0].Type()) + require.Equal(t, "~sp~", doc.String(0)) + }) + }) + + t.Run("InvalidNodeType", func(t *testing.T) { + n := &baseNode{nodeType: NodeType("Unknown"), content: "xyz"} + str := n.String(5) + require.Contains(t, str, "INVALID Node") + require.Contains(t, str, "xyz") + }) +} diff --git a/internal/markdown/formatter.go b/internal/markdown/formatter.go new file mode 100644 index 00000000..6f2fcb49 --- /dev/null +++ b/internal/markdown/formatter.go @@ -0,0 +1,15 @@ +package markdown + +type Formatter interface { + Document(string, int) string + Paragraph(string, int) string + HorizontalLine(string, int) string + HardBreak(string, int) string + Heading(string, int) string + List(string, int) string + ListItem(string, int) string + Text(string, int) string + Strong(string, int) string + Emphasis(string, int) string + Special(string, int) string +} diff --git a/internal/markdown/formatter_ansitags.go b/internal/markdown/formatter_ansitags.go new file mode 100644 index 00000000..12f467f9 --- /dev/null +++ b/internal/markdown/formatter_ansitags.go @@ -0,0 +1,75 @@ +package markdown + +import ( + "strconv" + "strings" +) + +// Formats into HTML tags +// +// Expected ansitags color aliases: +// md +// md-p +// md-h1-prefix +// md-h1, md-h2, md-h3 etc. +// md-li +// md-bold +// md-em +// md-sp1, md-sp2, md-sp3, etc. +// md-tbl-hdr +// md-tbl-row +// md-tbl-cell +// md-hr1 +// md-hr2 +// +// All have bg classes named the same with "-bg" at the end. +// Example: md-li-bg + +var dividers = map[string]string{ + "---": "\n--------------------------------------------------------------------------------", + "===": "\n================================================================================", + ":::": "\n .--. .-'. .--. .--. .--. .--. .`-. .--. \n" + + ":::::.\\::::::::.\\::::::::.\\::::::::.\\::::::::.\\::::::::.\\::::::::.\\::::::::.\\:::\n" + + "' `--' `.-' `--' `--' `--' `-.' `--' `--", +} + +type ANSITags struct{} + +func (ANSITags) Document(contents string, depth int) string { + return "" + strings.TrimLeft(contents, "\n ") + "" +} +func (ANSITags) Paragraph(contents string, depth int) string { + return "\n\n" + contents + "" +} +func (ANSITags) HorizontalLine(contents string, depth int) string { + return "\n" + dividers[contents] +} +func (ANSITags) HardBreak(contents string, depth int) string { return "\n" } +func (ANSITags) Heading(contents string, depth int) string { + if depth == 1 { + contents = ".: " + contents + } + return "\n\n" + contents + "" +} +func (ANSITags) List(contents string, depth int) string { + if depth == 0 { + return "\n\n" + contents + } + return strings.Repeat(` `, depth) + contents +} +func (ANSITags) ListItem(contents string, depth int) string { + return "\n" + strings.Repeat(` `, depth) + "- " + contents + "" +} +func (ANSITags) Text(contents string, depth int) string { + //return strings.TrimSpace(contents) + return contents +} +func (ANSITags) Strong(contents string, depth int) string { + return "" + contents + "" +} +func (ANSITags) Emphasis(contents string, depth int) string { + return "" + contents + "" +} +func (ANSITags) Special(contents string, depth int) string { + return "" + contents + "" +} diff --git a/internal/markdown/formatter_html.go b/internal/markdown/formatter_html.go new file mode 100644 index 00000000..3522059d --- /dev/null +++ b/internal/markdown/formatter_html.go @@ -0,0 +1,36 @@ +package markdown + +import ( + "strconv" + "strings" +) + +// +// Formats into HTML tags +// + +type HTML struct{} + +func (HTML) Document(contents string, depth int) string { + return strings.TrimLeft(contents, "\n ") +} +func (HTML) Paragraph(contents string, depth int) string { return "\n

\n" + contents + "\n

" } +func (HTML) HorizontalLine(contents string, depth int) string { return "\n
\n" } +func (HTML) HardBreak(contents string, depth int) string { return "\n
\n" } +func (HTML) Heading(contents string, depth int) string { + return "\n" + contents + "" +} +func (HTML) List(contents string, depth int) string { + return "\n" + strings.Repeat("\t", depth) + "" +} +func (HTML) ListItem(contents string, depth int) string { + return "\n" + strings.Repeat("\t", depth) + "
  • " + contents + "\n" + strings.Repeat("\t", depth) + "
  • " +} +func (HTML) Text(contents string, depth int) string { + return contents +} +func (HTML) Strong(contents string, depth int) string { return "" + contents + "" } +func (HTML) Emphasis(contents string, depth int) string { return "" + contents + "" } +func (HTML) Special(contents string, depth int) string { + return "" + contents + "" +} diff --git a/internal/markdown/formatter_remarkdown.go b/internal/markdown/formatter_remarkdown.go new file mode 100644 index 00000000..61879ac6 --- /dev/null +++ b/internal/markdown/formatter_remarkdown.go @@ -0,0 +1,37 @@ +package markdown + +import "strings" + +// +// Formats into a clean version of supported markdown +// + +type ReMarkdown struct{} + +func (ReMarkdown) Document(contents string, depth int) string { + return strings.TrimLeft(contents, "\n ") +} +func (ReMarkdown) Paragraph(contents string, depth int) string { return "\n\n" + contents } +func (ReMarkdown) HardBreak(contents string, depth int) string { return "\n" } +func (ReMarkdown) HorizontalLine(contents string, depth int) string { return "\n\n---" } +func (ReMarkdown) Heading(contents string, depth int) string { + return "\n\n" + strings.Repeat(`#`, depth) + " " + contents +} +func (ReMarkdown) List(contents string, depth int) string { + if depth == 0 { + return "\n\n" + contents + } + return strings.Repeat(` `, depth) + contents +} +func (ReMarkdown) ListItem(contents string, depth int) string { + return "\n" + strings.Repeat(` `, depth) + "- " + contents +} +func (ReMarkdown) Text(contents string, depth int) string { + //return strings.TrimSpace(contents) + return contents +} +func (ReMarkdown) Strong(contents string, depth int) string { return "**" + contents + "**" } +func (ReMarkdown) Emphasis(contents string, depth int) string { return "*" + contents + "*" } +func (ReMarkdown) Special(contents string, depth int) string { + return strings.Repeat(`~`, depth) + contents + strings.Repeat(`~`, depth) +} diff --git a/internal/markdown/markdown_test.go b/internal/markdown/markdown_test.go new file mode 100644 index 00000000..133b7610 --- /dev/null +++ b/internal/markdown/markdown_test.go @@ -0,0 +1,97 @@ +package markdown + +import ( + "fmt" + "strings" + "testing" +) + +// Output oriented tests, for development + +// This test should be ran just as a way to verify content visually. +// Prefixed with "x" when not being used. +func xTest_Printing(t *testing.T) { + src := `# This is a **HEADING** + + +This is a *NEW PARAGRAPH*. +Paragraph a preceded by two new lines. +This is a **line break**. +Line breaks happen when the previous line ended. +with two spaces: " ". +They are preceded by only a single new line. + +This is another paragraph. + +- item one + - item one >> sub one + - item one >> sub two + - item one >> sub two >> sub one +- **bold** item two + +## This is a ~~SUB HEADING~~ + + That ~is~ all. + +Some text +--- +=== +::: +Some more text + + That ~is~ all. +` + + parser := NewParser(src) + ast := parser.Parse() + + fmt.Println("------------------- DUMP -------------------") + fmt.Println() + Dump(ast, 3) + fmt.Println() + fmt.Println("----------------- REFORMAT -----------------") + fmt.Println() + fmt.Println(ast.String(0)) + fmt.Println() + + fmt.Println("------------------- HTML -------------------") + fmt.Println() + SetFormatter(HTML{}) + fmt.Println(ast.String(0)) + fmt.Println() + fmt.Println("------------------- ANSI -------------------") + fmt.Println() + SetFormatter(ANSITags{}) + fmt.Println(ast.String(0)) + fmt.Println() + + fmt.Println("------------------- DONE -------------------") +} + +// Useful for printing out stuff +func Dump(n Node, indentSpaces int, currentIndent ...int) { + + if len(currentIndent) == 0 { + currentIndent = []int{0} + } + + if indentSpaces == 0 { + indentSpaces = 1 + } + + fmt.Printf("%s- %s", strings.Repeat(" ", currentIndent[0]*indentSpaces), n.Type()) + + bNode := n.(*baseNode) + switch n.Type() { + case HeadingNode: + fmt.Printf(" (level=%d)\n", bNode.level) + case TextNode: + fmt.Printf(": %q\n", bNode.content) + default: + fmt.Printf(" (%d)", len(n.Children())) + fmt.Println() + } + for _, c := range n.Children() { + Dump(c, indentSpaces, currentIndent[0]+1) + } +} diff --git a/internal/markdown/parser.go b/internal/markdown/parser.go new file mode 100644 index 00000000..10a7e9e7 --- /dev/null +++ b/internal/markdown/parser.go @@ -0,0 +1,256 @@ +package markdown + +import ( + "regexp" + "strings" +) + +const ( + lineBreakString = " " +) + +var tableSep = regexp.MustCompile(`^\s*\|?[-: ]+\|?([-: ]*\|?)*\s*$`) + +type Parser struct { + lines []string + pos int +} + +func NewParser(input string) *Parser { + return &Parser{ + lines: strings.Split(input, "\n"), + } +} + +func (p *Parser) Parse() Node { + doc := &baseNode{nodeType: DocumentNode} + for p.pos < len(p.lines) { + line := p.lines[p.pos] + + switch { + case strings.HasPrefix(line, "---"), strings.HasPrefix(line, "==="), strings.HasPrefix(line, ":::"): + doc.nodeChildren = append(doc.nodeChildren, p.parseHorizontalLine()) + case strings.HasPrefix(line, "#"): + doc.nodeChildren = append(doc.nodeChildren, p.parseHeading()) + case strings.HasPrefix(strings.TrimSpace(line), "- "): + // compute leading-space indent + indent := len(line) - len(strings.TrimLeft(line, " ")) + doc.nodeChildren = append(doc.nodeChildren, p.parseList(indent)) + case strings.TrimSpace(line) == "": + p.pos++ // skip blank + default: + // instead of a single node, grab a slice + for _, node := range p.parseParagraphNodes() { + doc.nodeChildren = append(doc.nodeChildren, node) + } + } + } + return doc +} + +func (p *Parser) parseHorizontalLine() *baseNode { + line := p.lines[p.pos] + level := 0 + + lineType := line[level] + for level < len(line) && line[level] == lineType { + level++ + } + // skip "# " prefix + content := "" + if len(line) > level+1 { + content = line[level+1:] + } + p.pos++ + + h := &baseNode{ + nodeType: HorizontalLineNode, + nodeChildren: p.parseInline(content), + level: level, + content: strings.Repeat(string(lineType), 3), + } + return h +} + +func (p *Parser) parseHeading() *baseNode { + line := p.lines[p.pos] + level := 0 + for level < len(line) && line[level] == '#' { + level++ + } + // skip "# " prefix + content := "" + if len(line) > level+1 { + content = line[level+1:] + } + p.pos++ + + h := &baseNode{ + nodeType: HeadingNode, + nodeChildren: p.parseInline(content), + level: level, + } + return h +} + +// parseList now takes the indent level of its bullets +func (p *Parser) parseList(baseIndent int) *baseNode { + list := &baseNode{nodeType: ListNode} + + for p.pos < len(p.lines) { + line := p.lines[p.pos] + // count leading spaces + currIndent := len(line) - len(strings.TrimLeft(line, " ")) + trimmed := strings.TrimSpace(line) + + // if it’s not a bullet or we’ve de-indented, this list is done + if !strings.HasPrefix(trimmed, "- ") || currIndent < baseIndent { + break + } + + if currIndent > baseIndent { + // nested list: recurse, attach to last ListItem + nested := p.parseList(currIndent) + if len(list.nodeChildren) > 0 { + lastItem := list.nodeChildren[len(list.nodeChildren)-1].(*baseNode) + lastItem.nodeChildren = append(lastItem.nodeChildren, nested) + } + continue + } + + // same-level item + itemText := trimmed[2:] // drop "- " + item := &baseNode{nodeType: ListItemNode} + item.nodeChildren = p.parseInline(itemText) + list.nodeChildren = append(list.nodeChildren, item) + p.pos++ + } + + return list +} + +func (p *Parser) parseParagraphNodes() []Node { + // 1) collect until blank line + var lines []string + for p.pos < len(p.lines) && strings.TrimSpace(p.lines[p.pos]) != "" && !strings.HasPrefix(strings.TrimSpace(p.lines[p.pos]), `---`) { + lines = append(lines, p.lines[p.pos]) + p.pos++ + } + + var nodes []Node + start := 0 + + // 2) whenever we see a line ending in " ", + // that's a hard break point. + para := &baseNode{nodeType: ParagraphNode} + + for i, line := range lines { + if strings.HasSuffix(line, lineBreakString) { + // lines[start..i] form one paragraph + seg := append([]string{}, lines[start:i+1]...) + seg[len(seg)-1] = strings.TrimSuffix(seg[len(seg)-1], lineBreakString) + + // Parse the contents thus far, converting new lines into spaces + newChildren := p.parseInline(strings.Join(seg, " ")) + // add to paragraph + para.nodeChildren = append(para.nodeChildren, newChildren...) + // now add a line break + para.nodeChildren = append(para.nodeChildren, &baseNode{nodeType: HardBreakNode}) + + // now skip ahead + start = i + 1 + continue + } + } + + // 3) whatever remains after the last hard-break + if start < len(lines) { + seg := lines[start:] + newChildren := p.parseInline(strings.Join(seg, " ")) + para.nodeChildren = append(para.nodeChildren, newChildren...) + } + + nodes = append(nodes, para) + return nodes +} + +func (p *Parser) parseInline(text string) []Node { + var nodes []Node + for i := 0; i < len(text); { + // —— special: ~…~ + if text[i] == '~' { + start := i + for i < len(text) && text[i] == '~' { + i++ + } + count := i - start + delim := strings.Repeat("~", count) + + if j := strings.Index(text[i:], delim); j >= 0 { + inner := text[i : i+j] + childNodes := p.parseInline(inner) + n := &baseNode{ + nodeType: SpecialNode, + nodeChildren: childNodes, + level: count, + } + nodes = append(nodes, n) + i += j + count + continue + } + + // no closing run → literal dollars + nodes = append(nodes, &baseNode{ + nodeType: TextNode, + content: text[start:i], + }) + continue + } + + // —— strong: **bold** + if strings.HasPrefix(text[i:], "**") && i+2 < len(text) && text[i+2] != ' ' { + if j := strings.Index(text[i+2:], "**"); j >= 0 { + inner := text[i+2 : i+2+j] + n := &baseNode{nodeType: StrongNode} + n.nodeChildren = p.parseInline(inner) + nodes = append(nodes, n) + i += 4 + j + continue + } + } + + // —— emphasis: *em* + if text[i] == '*' && i+1 < len(text) && text[i+1] != ' ' { + if j := strings.Index(text[i+1:], "*"); j >= 0 { + inner := text[i+1 : i+1+j] + n := &baseNode{nodeType: EmphasisNode} + n.nodeChildren = p.parseInline(inner) + nodes = append(nodes, n) + i += 2 + j + continue + } + } + + // —— plain text fallback + j := i + for j < len(text) && text[j] != '*' && text[j] != '~' { + j++ + } + if j == i { + // unmatched '*' or '~', consume one char + nodes = append(nodes, &baseNode{ + nodeType: TextNode, + content: text[i : i+1], + }) + i++ + } else { + // real plain text + nodes = append(nodes, &baseNode{ + nodeType: TextNode, + content: text[i:j], + }) + i = j + } + } + return nodes +} diff --git a/internal/templates/templates.go b/internal/templates/templates.go index 88ca4ead..ab1c3cf2 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -14,6 +14,7 @@ import ( "github.com/GoMudEngine/GoMud/internal/colorpatterns" "github.com/GoMudEngine/GoMud/internal/configs" "github.com/GoMudEngine/GoMud/internal/fileloader" + "github.com/GoMudEngine/GoMud/internal/markdown" "github.com/GoMudEngine/GoMud/internal/mudlog" "github.com/GoMudEngine/GoMud/internal/users" "github.com/GoMudEngine/ansitags" @@ -33,6 +34,10 @@ const ( AnsiTagsNone = AnsiTagsDefault // alias to default ForceScreenReaderUserId = -1 + + divider = "\n .--. .-'. .--. .--. .--. .--. .`-. .--.\n" + + ":::::.\\::::::::.\\::::::::.\\::::::::.\\::::::::.\\::::::::.\\::::::::.\\::::::::.\\\n" + + "' `--' `.-' `--' `--' `--' `-.' `--' `\n" ) type cacheEntry struct { @@ -100,14 +105,21 @@ type templateConfig struct { } type templateDetails struct { - name string - path string + name string + path string + preProcess func(string) string } func ClearTemplateConfigCache(userId int) { delete(templateConfigCache, userId) } +func processMarkdown(in string) string { + markdown.SetFormatter(markdown.ANSITags{}) + p := markdown.NewParser(in) + return "\n" + divider + "\n" + p.Parse().String(0) + "\n" +} + func Process(fname string, data any, receivingUserId ...int) (string, error) { ansiLock.RLock() defer ansiLock.RUnlock() @@ -157,6 +169,14 @@ func Process(fname string, data any, receivingUserId ...int) (string, error) { ) } + filesToAttempt = append(filesToAttempt, + templateDetails{ + name: fname, + path: util.FilePath(`templates/`, fname+`.md`), // All templates must end with .template + preProcess: processMarkdown, + }, + ) + filesToAttempt = append(filesToAttempt, templateDetails{ name: fname, @@ -183,6 +203,10 @@ func Process(fname string, data any, receivingUserId ...int) (string, error) { if parseAnsiTags { return ansitags.Parse(buf.String(), ansitagsParseBehavior...), nil } + + if tplInfo.preProcess != nil { + return tplInfo.preProcess(buf.String()), nil + } return buf.String(), nil } @@ -226,6 +250,10 @@ func Process(fname string, data any, receivingUserId ...int) (string, error) { if parseAnsiTags { return ansitags.Parse(buf.String(), ansitagsParseBehavior...), nil } + + if tplInfo.preProcess != nil { + return tplInfo.preProcess(buf.String()), nil + } return buf.String(), nil }