-
Notifications
You must be signed in to change notification settings - Fork 3
/
markdown.go
315 lines (270 loc) · 7.64 KB
/
markdown.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
package txt
import (
"math"
"github.com/jakubDoka/mlok/mat"
"github.com/jakubDoka/gogen/str"
)
// key markdown sintax is stored in variable so it can be customized
var (
MarkdownIdent = '!'
ColorIdent = '#'
BlockStart = '['
BlockEnd = ']'
NullIdent = string([]rune{1})
DefaultFont = "default"
noConstructor = "markdown is missing default font, use use constructor that adds it"
)
// Markdown handles text markdown parsing, markdown sintax is as follows:
//
// #FF0000[hello] // hello will appear ugly red
// #FF000099[hello] // hello will appear ugly red and slightly transparent
//
// Now writing effects like this is maybe flexible but not always convenient to user
// thats why you can define your own effects by adding effects to Markdown.Effects. Name
// of the effect is a key in map thus:
//
// m.Effects["red"] = ColorEffect{Color: mat.RGB(1, 0, 0)}
//
// will do the trick. user then can use this effect like:
//
// !red[hello] // witch does the same
//
// now you can make any kind of triangle mutator you like, it even allows dinamic effects
// so something like fading text or exploding text is possible. You can also use multiple
// fonts in one paragraph by adding more atlases to your markdown. Pattern is same as for
// adding custom effects:
//
// m.Effects["italic"] = NDrawer(italicFontAtlas)
//
// User then can use font like:
//
// !italic[hello] // hello will be italic, syntax will not appear
//
// Last feature of markdown are shortcuts. After you have added all effects you wanted you can call
// GenerateShortcuts method. This will map all effect names to its starting rune thus user can be very
// lazy:
//
// !i[hello] // hello will be italic with low effort
//
// Markdown is only compatible with paragraph, mind that parsing markdown is slow and grows linearly with text
// length, O(n), if course if you want effects to even display you have to set DisplayEffects to true in paragraph
type Markdown struct {
Shortcuts map[rune]string
Fonts map[string]*Drawer
Effects map[string]Effect
buff, stack FEffs
stack2 Effs
}
// NMarkdown initializes inner maps and adds default drawer
func NMarkdown() *Markdown {
m := &Markdown{
Shortcuts: map[rune]string{},
Effects: map[string]Effect{
"red": &ColorEffect{Color: mat.Red},
"green": &ColorEffect{Color: mat.Green},
"blue": &ColorEffect{Color: mat.Blue},
},
Fonts: map[string]*Drawer{DefaultFont: NDrawer(Atlas7x13)},
}
m.GenerateShortcuts()
return m
}
// GenerateShortcuts creates shortcuts for all effects, if names overlap random one is bind
func (m *Markdown) GenerateShortcuts() {
for k := range m.Fonts {
if k == "" {
continue
}
m.Shortcuts[rune(k[0])] = k
}
for k := range m.Effects {
if k == "" {
continue
}
m.Shortcuts[rune(k[0])] = k
}
}
// Parse turns markdown stored in p.Content into final text with effects, for markdown syntax see
// struct documentation
func (m *Markdown) Parse(p *Paragraph) {
if _, ok := m.Fonts[p.Font]; !ok {
p.Font = DefaultFont
if _, ok := m.Fonts[p.Font]; !ok {
panic(noConstructor)
}
}
p.Compiled = append(p.Compiled[:0], p.Content...)
p.changing.Clear()
p.instant.Clear()
p.chunks.Clear()
if !p.NoEffects {
m.CollectEffects(p)
p.Sort()
}
m.ResolveChunks(p)
m.MakeTriangles(p)
return
}
// CollectEffects removes all valid effect syntax and stores parsed effects in paragraph
func (m *Markdown) CollectEffects(p *Paragraph) {
var (
mv, i int
ident string
ok bool
)
m.stack2 = m.stack2[:0]
push := func() {
ef := m.stack2.Pop()
ef.Close(i)
p.AddEff(ef)
}
o:
for ; i < len(p.Compiled); i += mv {
b := p.Compiled[i]
mv = 1
switch b {
case BlockEnd: // fond text that should be skipped
if len(m.stack2) != 0 {
p.Compiled.Remove(i)
if i < len(p.Compiled) && p.Compiled[i] == ']' {
continue
}
mv = 0
push()
continue
}
case ColorIdent, MarkdownIdent:
// ingoreing
default:
continue
}
if i+2 >= len(p.Compiled) || !str.IsIdent(byte(p.Compiled[i+1])) { //shortcut can't fit there or space right after explanation mark
continue
}
if p.Compiled[i+2] == BlockStart { // in case of shortcut - shortcut is always just one rune
ident, ok = m.Shortcuts[p.Compiled[i+1]]
if !ok { // invalid shortcut so ignore it
continue
}
p.Compiled.RemoveSlice(i, i+3)
mv = 0
} else { // find out full identifier
k := i + 1
for {
if k >= len(p.Compiled) {
continue o //out of bounds and we haven't even found non ident byte, ignoring
}
if !str.IsIdent(byte(p.Compiled[k])) {
if p.Compiled[k] != BlockStart {
continue o //ident should end with BlockStart, ignoring
}
break
}
k++
}
ident = string(p.Compiled[i+1 : k]) // i+1 because we are not including ident
if b == ColorIdent { // this can also be color ident so handle it
ce, err := NColorEffect(ident, i)
if err != nil {
continue
}
m.stack2 = append(m.stack2, ce)
ident = NullIdent // we don't want to handle ident twice
}
p.Compiled.RemoveSlice(i, k+1)
mv = 0
}
if ident == NullIdent {
continue
}
if _, ok := m.Fonts[ident]; ok {
m.stack2 = append(m.stack2, NFontEffect(ident, i, 0))
} else if val, ok := m.Effects[ident]; ok {
m.stack2 = append(m.stack2, val.Copy(i))
}
}
for len(m.stack2) != 0 { // close all reminding effects
push()
}
}
// MakeTriangles creates triangles, these are not drawn to screen as only
// instant effects are applied on them and you have to call Update on paragraph
// for anything to show up
func (m *Markdown) MakeTriangles(p *Paragraph) {
p.data.Clear()
p.dot = mat.V(0, -p.Ascent)
p.bounds = mat.A(0, -p.LineHeight, 0, 0)
p.dots = append(p.dots[:0], p.dot)
p.lines = append(p.lines[:0], line{p.dot.Y, 0, -1})
for _, c := range p.chunks {
m.Fonts[c.Font].DrawParagraph(p, c.start, c.End)
}
end := &p.lines[len(p.lines)-1]
if end.end == -1 {
end.end = len(p.dots)
}
// do aligning
if p.Width != 0 && p.Align != Left {
if p.lines[0].end == 1 { // no content case
p.dots[0].X += p.Width * float64(p.Align)
} else {
for _, l := range p.lines {
end := l.end*4 - 4
shift := (p.Width - p.data.Vertexes[end-1].Pos.X) * float64(p.Align)
for i := l.start * 4; i < end; i++ {
p.data.Vertexes[i].Pos.X += shift
}
for i := l.start; i < l.end; i++ {
p.dots[i].X += shift
}
}
}
}
for _, e := range p.instant { //instant effects are applied to base data
e.Apply(p.data.Vertexes, 0)
}
}
// ResolveChunks gets rid of nested FontEffects as nesting of then does not make sense
// it turns ranges like 0-10 3-7 to 0-3 3-7 7-10 so no ranges overlap.
func (m *Markdown) ResolveChunks(p *Paragraph) {
fef := NFontEffect(p.Font, 0, len(p.Compiled))
p.chunks = p.chunks[:0]
if p.NoEffects {
p.chunks = append(p.chunks, fef)
return
}
m.buff = m.buff[:0]
m.stack = m.stack[:0]
m.stack = append(m.stack, fef)
if !p.CustomLineheight {
f := m.Fonts[p.Font]
p.LineHeight = f.LineHeight()
p.Ascent = f.Ascent()
p.Descent = f.Descent()
}
for _, c := range p.chunks {
if !p.CustomLineheight {
f := m.Fonts[c.Font]
p.LineHeight = math.Max(p.LineHeight, f.LineHeight())
p.Ascent = math.Max(p.Ascent, f.Ascent())
p.Descent = math.Max(p.Descent, f.Descent())
}
for len(m.stack) != 0 {
l := m.stack.Last()
if l.End > c.start {
m.buff = append(m.buff, NFontEffect(l.Font, l.start, c.start))
l.start = c.End
break
} else {
m.buff = append(m.buff, m.stack.Pop())
}
}
m.stack = append(m.stack, c)
}
m.stack.Reverse()
m.buff = append(m.buff, m.stack...)
m.buff.Filter(func(e *FontEffect) bool {
return !e.Redundant()
})
p.chunks = append(p.chunks, m.buff...)
}