/
Wikitext.scala
178 lines (148 loc) · 5.96 KB
/
Wikitext.scala
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
package models
import play.api.Logger
import play.api.templates.Html
import language.implicitConversions
import eu.henkelmann.actuarius.{Decorator, Transformer}
import querki.values.DebugRenderable
import querki.util._
case class DisplayText(val str:String) {
override def toString() = str
def +(other:DisplayText) = new DisplayText(str + other.str)
// Since a DisplayText is already HTML-neutered, it is safe to encode as HTML:
def html = Html(str)
def htmlWikitext = HtmlWikitext(html)
}
object DisplayText {
implicit def displayText2String(disp:DisplayText) = disp.str
}
trait Wikitext extends DebugRenderable {
def transform(builder: => Transformer)(str:String):String = {
val transformer = builder
transformer(str)
}
def transformDisplay = transform(new QuerkiTransformer) _
def transformRaw = transform(new RawTransformer) _
def display:DisplayText
/**
* Produces the "raw" string, with minimal markup. Use this for situations where you
* don't want to allow much Wikitext, such as display names.
*/
def raw:DisplayText
/**
* This should only be used internally, never to display to the user!
*
* We do simple substitutions here, that aren't worth coding into the wikitext engine
* itself.
*
* Octal 266 is Hex 182, aka the paragraph character. Enter on the numeric keypad as
* Alt-0182.
*/
def internal:String
/**
* Subclasses need to be clear about this. Iff this is set, then we preserve the exact content of
* this wikitext node, instead of passing it through Wikitexting.
*/
def keepRaw:Boolean
/**
* This is the nearly raw, unprocessed text. It should only be used when we are *not* sending
* to an HTML environment -- generally, when you want to process a text field for QL but not
* for QText. (Or being used relatively directly from Play, when we know that it will be doing
* the escaping.) Note that this does no XML escaping!
*/
def plaintext:String
/**
* Used to build composite wikitexts from smaller ones.
*/
def contents:Seq[Wikitext] = Seq(this)
/**
* Wikitext can be concatenated just like strings.
*/
def +(other:Wikitext, insertNewline:Boolean = false):Wikitext = new CompositeWikitext(this, other, insertNewline)
def debugRender = plaintext
}
case class QWikitext(wiki:String) extends Wikitext {
val keepRaw = false
def display = DisplayText(transformDisplay(internal))
def raw = DisplayText(transformRaw(internal))
/**
* This should only be used internally, never to display to the user!
*
* We do simple substitutions here, that aren't worth coding into the wikitext engine
* itself.
*
* Octal 266 is Hex 182, aka the paragraph character. Enter on the numeric keypad as
* Alt-0182.
*/
def internal = wiki.replace('\266', '\n')
/**
* This is the nearly raw, unprocessed text. It should only be used when we are *not* sending
* to an HTML environment -- generally, when you want to process a text field for QL but not
* for QText. Note that this does no XML escaping!
*/
def plaintext = internal
}
/**
* Internal systems can inject HTML into the stream by creating an HtmlWikitext. This will not be
* processed any further, just inserted directly.
*/
case class HtmlWikitext(html:Html) extends Wikitext {
private def str = html.toString
def display = DisplayText(str)
def raw = DisplayText(str)
def internal = html.toString
def plaintext = str
val keepRaw = true
}
case class CompositeWikitext(left:Wikitext, right:Wikitext, insertNewline:Boolean) extends Wikitext {
/**
* The flattened contents of all the Wikitexts that built this up. This winds up as a flat sequence of
* QWikitexts and HtmlWikitexts.
*/
override def contents:Seq[Wikitext] = {
(left.contents :+ (if (insertNewline) Wikitext.nl else Wikitext.empty)) ++ right.contents
}
case class ProcessState(str:String, map:Map[Int, Wikitext])
def process(processor:String => String):String = {
val indexedContents = contents.zipWithIndex
// To begin with, we process everything where keepRaw == false, and replace the keepRaw == true...
val ProcessState(builtStr, substitutionMap) = (ProcessState("", Map.empty[Int, Wikitext]) /: indexedContents) { (state, textAndIndex) =>
val (text, index) = textAndIndex
if (text.keepRaw)
ProcessState(state.str + "(-+" + index + "+-)", state.map + (index -> text))
else
ProcessState(state.str + text.internal, state.map)
}
val processedStr = processor(builtStr)
// ... and now we substitute in the keepRaw == true entries:
val result = (processedStr /: substitutionMap) { (str, entry) =>
val (index, text) = entry
str.replaceAllLiterally("(-+" + index + "+-)", text.internal)
}
result
}
def display = DisplayText(process(transformDisplay))
def raw = DisplayText(process(transformRaw))
def plaintext = process(str => str)
def internal = throw new Exception("Nothing should be calling CompositeWikitext.internal!")
val keepRaw = false
}
object Wikitext {
def apply(str:String):Wikitext = new QWikitext(str)
val empty = Wikitext("")
val nl = Wikitext("\n")
}
class QuerkiTransformer extends Transformer with Decorator {
override def deco() = this
// We no longer allow XML in QText. However, internal systems can inject HTML by using a
override def allowVerbatimXml():Boolean = false
// We use <div> instead of a real <p>, because it turns out that older versions of IE (specifically IE9)
// do not permit <form>s inside of <p> -- and restructure the HTML to prevent it, breaking our forms.
override def decorateParagraphOpen():String = """<div class="para">"""
override def decorateParagraphClose():String = """</div>"""
}
class RawTransformer extends Transformer with Decorator {
override def deco() = this
override def allowVerbatimXml():Boolean = false
override def decorateParagraphOpen():String = ""
override def decorateParagraphClose():String = ""
}