##// END OF EJS Templates
Fixed multiline quote. Simplified the quote source retrieving
neko259 -
r2133:0208042a default
parent child Browse files
Show More
@@ -1,299 +1,296 b''
1 # coding=utf-8
1 # coding=utf-8
2 from xml import etree
2 from xml import etree
3
3
4 import re
4 import re
5 import random
5 import random
6 import bbcode
6 import bbcode
7
7
8 from urllib.parse import unquote
8 from urllib.parse import unquote
9
9
10 from django.core.exceptions import ObjectDoesNotExist
10 from django.core.exceptions import ObjectDoesNotExist
11 from django.urls import reverse
11 from django.urls import reverse
12 from django.utils.translation import ugettext_lazy as _
12 from django.utils.translation import ugettext_lazy as _
13
13
14 import boards
14 import boards
15 from boards import settings
15 from boards import settings
16 from neboard.settings import ALLOWED_HOSTS
16 from neboard.settings import ALLOWED_HOSTS
17
17
18
18
19 __author__ = 'neko259'
19 __author__ = 'neko259'
20
20
21
21
22 REFLINK_PATTERN = re.compile(r'^\d+$')
22 REFLINK_PATTERN = re.compile(r'^\d+$')
23 GLOBAL_REFLINK_PATTERN = re.compile(r'(\w+)::([^:]+)::(\d+)')
23 GLOBAL_REFLINK_PATTERN = re.compile(r'(\w+)::([^:]+)::(\d+)')
24 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
24 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
25 ONE_NEWLINE = '\n'
25 ONE_NEWLINE = '\n'
26 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
26 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
27 LINE_BREAK_HTML = '<div class="br"></div>'
27 LINE_BREAK_HTML = '<div class="br"></div>'
28 SPOILER_SPACE = '&nbsp;'
28 SPOILER_SPACE = '&nbsp;'
29 REGEX_ANY_LINE_BREAK = re.compile(r'(' + LINE_BREAK_HTML + '|\r|\n)')
30 QUOTE_REPLACEMENT = '\g<1>&gt;'
29
31
30 MAX_SPOILER_MULTIPLIER = 2
32 MAX_SPOILER_MULTIPLIER = 2
31 MAX_SPOILER_SPACE_COUNT = 20
33 MAX_SPOILER_SPACE_COUNT = 20
32
34
33
35
34 class TextFormatter:
36 class TextFormatter:
35 """
37 """
36 An interface for formatter that can be used in the text format panel
38 An interface for formatter that can be used in the text format panel
37 """
39 """
38
40
39 def __init__(self):
41 def __init__(self):
40 pass
42 pass
41
43
42 name = ''
44 name = ''
43
45
44 # Left and right tags for the button preview
46 # Left and right tags for the button preview
45 preview_left = ''
47 preview_left = ''
46 preview_right = ''
48 preview_right = ''
47
49
48 # Tag name to enclose the text in the form
50 # Tag name to enclose the text in the form
49 tag_name = ''
51 tag_name = ''
50
52
51 has_input = 'false'
53 has_input = 'false'
52 input_prompt = ''
54 input_prompt = ''
53
55
54
56
55 class AutolinkPattern:
57 class AutolinkPattern:
56 def handleMatch(self, m):
58 def handleMatch(self, m):
57 link_element = etree.Element('a')
59 link_element = etree.Element('a')
58 href = m.group(2)
60 href = m.group(2)
59 link_element.set('href', href)
61 link_element.set('href', href)
60 link_element.text = href
62 link_element.text = href
61
63
62 return link_element
64 return link_element
63
65
64
66
65 class QuotePattern(TextFormatter):
67 class QuotePattern(TextFormatter):
66 name = '>q'
68 name = '>q'
67 preview_left = '<span class="quote">'
69 preview_left = '<span class="quote">'
68 preview_right = '</span>'
70 preview_right = '</span>'
69
71
70 tag_name = 'quote'
72 tag_name = 'quote'
71
73
72
74
73 class SpoilerPattern(TextFormatter):
75 class SpoilerPattern(TextFormatter):
74 name = 'spoiler'
76 name = 'spoiler'
75 preview_left = '<span class="spoiler">'
77 preview_left = '<span class="spoiler">'
76 preview_right = '</span>'
78 preview_right = '</span>'
77
79
78 tag_name = 'spoiler'
80 tag_name = 'spoiler'
79
81
80
82
81 class CommentPattern(TextFormatter):
83 class CommentPattern(TextFormatter):
82 name = ''
84 name = ''
83 preview_left = '<span class="comment">// '
85 preview_left = '<span class="comment">// '
84 preview_right = '</span>'
86 preview_right = '</span>'
85
87
86 tag_name = 'comment'
88 tag_name = 'comment'
87
89
88
90
89 # TODO Use <s> tag here
91 # TODO Use <s> tag here
90 class StrikeThroughPattern(TextFormatter):
92 class StrikeThroughPattern(TextFormatter):
91 name = 's'
93 name = 's'
92 preview_left = '<span class="strikethrough">'
94 preview_left = '<span class="strikethrough">'
93 preview_right = '</span>'
95 preview_right = '</span>'
94
96
95 tag_name = 's'
97 tag_name = 's'
96
98
97
99
98 class ItalicPattern(TextFormatter):
100 class ItalicPattern(TextFormatter):
99 name = 'i'
101 name = 'i'
100 preview_left = '<i>'
102 preview_left = '<i>'
101 preview_right = '</i>'
103 preview_right = '</i>'
102
104
103 tag_name = 'i'
105 tag_name = 'i'
104
106
105
107
106 class BoldPattern(TextFormatter):
108 class BoldPattern(TextFormatter):
107 name = 'b'
109 name = 'b'
108 preview_left = '<b>'
110 preview_left = '<b>'
109 preview_right = '</b>'
111 preview_right = '</b>'
110
112
111 tag_name = 'b'
113 tag_name = 'b'
112
114
113
115
114 class CodePattern(TextFormatter):
116 class CodePattern(TextFormatter):
115 name = 'code'
117 name = 'code'
116 preview_left = '<code>'
118 preview_left = '<code>'
117 preview_right = '</code>'
119 preview_right = '</code>'
118
120
119 tag_name = 'code'
121 tag_name = 'code'
120
122
121
123
122 class HintPattern(TextFormatter):
124 class HintPattern(TextFormatter):
123 name = 'hint'
125 name = 'hint'
124 preview_left = '<span class="hint">'
126 preview_left = '<span class="hint">'
125 preview_right = '</span>'
127 preview_right = '</span>'
126
128
127 tag_name = 'hint'
129 tag_name = 'hint'
128
130
129
131
130 class ColorPattern(TextFormatter):
132 class ColorPattern(TextFormatter):
131 name = 'color'
133 name = 'color'
132 preview_left = '<span style="color:yellow;">'
134 preview_left = '<span style="color:yellow;">'
133 preview_right = '</span>'
135 preview_right = '</span>'
134
136
135 tag_name = 'color'
137 tag_name = 'color'
136
138
137 has_input = 'true'
139 has_input = 'true'
138 input_prompt = _('Enter color (name or #ccc code)')
140 input_prompt = _('Enter color (name or #ccc code)')
139
141
140
142
141 def render_reflink(tag_name, value, options, parent, context):
143 def render_reflink(tag_name, value, options, parent, context):
142 result = '>>%s' % value
144 result = '>>%s' % value
143
145
144 post = None
146 post = None
145 if REFLINK_PATTERN.match(value):
147 if REFLINK_PATTERN.match(value):
146 post_id = int(value)
148 post_id = int(value)
147
149
148 try:
150 try:
149 post = boards.models.Post.objects.get(id=post_id)
151 post = boards.models.Post.objects.get(id=post_id)
150
152
151 except ObjectDoesNotExist:
153 except ObjectDoesNotExist:
152 pass
154 pass
153 elif GLOBAL_REFLINK_PATTERN.match(value):
155 elif GLOBAL_REFLINK_PATTERN.match(value):
154 match = GLOBAL_REFLINK_PATTERN.search(value)
156 match = GLOBAL_REFLINK_PATTERN.search(value)
155 try:
157 try:
156 global_id = boards.models.GlobalId.objects.get(
158 global_id = boards.models.GlobalId.objects.get(
157 key_type=match.group(1), key=match.group(2),
159 key_type=match.group(1), key=match.group(2),
158 local_id=match.group(3))
160 local_id=match.group(3))
159 post = global_id.post
161 post = global_id.post
160 except ObjectDoesNotExist:
162 except ObjectDoesNotExist:
161 pass
163 pass
162
164
163 if post is not None:
165 if post is not None:
164 result = post.get_link_view()
166 result = post.get_link_view()
165
167
166 return result
168 return result
167
169
168
170
169 def render_quote(tag_name, value, options, parent, context):
171 def render_quote(tag_name, value, options, parent, context):
170 source = ''
172 source = options.get('quote') or options.get('source')
171 if 'source' in options:
172 source = options['source']
173 elif 'quote' in options:
174 source = options['quote']
175
173
176 if source:
174 if source:
177 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
175 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
178 else:
176 else:
179 # Insert a ">" at the start of every line
177 # Insert a ">" at the start of every line
180 result = '<span class="quote">&gt;{}</span>'.format(
178 result = '<span class="quote">&gt;{}</span>'.format(
181 value.replace(LINE_BREAK_HTML,
179 REGEX_ANY_LINE_BREAK.sub(QUOTE_REPLACEMENT, value))
182 '{}&gt;'.format(LINE_BREAK_HTML)))
183
180
184 return result
181 return result
185
182
186
183
187 def render_hint(tag_name, value, options, parent, context):
184 def render_hint(tag_name, value, options, parent, context):
188 if 'hint' in options:
185 if 'hint' in options:
189 hint = options['hint']
186 hint = options['hint']
190 result = '<span class="hint" title="{}">{}</span>'.format(hint, value)
187 result = '<span class="hint" title="{}">{}</span>'.format(hint, value)
191 else:
188 else:
192 result = value
189 result = value
193 return result
190 return result
194
191
195
192
196 def render_notification(tag_name, value, options, parent, content):
193 def render_notification(tag_name, value, options, parent, content):
197 username = value.lower()
194 username = value.lower()
198
195
199 return '<a href="{}" class="user-cast">@{}</a>'.format(
196 return '<a href="{}" class="user-cast">@{}</a>'.format(
200 reverse('notifications', kwargs={'username': username}), username)
197 reverse('notifications', kwargs={'username': username}), username)
201
198
202
199
203 def render_tag(tag_name, value, options, parent, context):
200 def render_tag(tag_name, value, options, parent, context):
204 tag_name = value.lower()
201 tag_name = value.lower()
205
202
206 tag = boards.models.Tag.objects.get_by_alias(tag_name)
203 tag = boards.models.Tag.objects.get_by_alias(tag_name)
207 if tag:
204 if tag:
208 url = tag.get_view()
205 url = tag.get_view()
209 else:
206 else:
210 url = tag_name
207 url = tag_name
211
208
212 return url
209 return url
213
210
214
211
215 def render_spoiler(tag_name, value, options, parent, context):
212 def render_spoiler(tag_name, value, options, parent, context):
216 if settings.get_bool('Forms', 'AdditionalSpoilerSpaces'):
213 if settings.get_bool('Forms', 'AdditionalSpoilerSpaces'):
217 text_len = len(value)
214 text_len = len(value)
218 space_count = min(random.randint(0, text_len * MAX_SPOILER_MULTIPLIER),
215 space_count = min(random.randint(0, text_len * MAX_SPOILER_MULTIPLIER),
219 MAX_SPOILER_SPACE_COUNT)
216 MAX_SPOILER_SPACE_COUNT)
220 side_spaces = SPOILER_SPACE * (space_count // 2)
217 side_spaces = SPOILER_SPACE * (space_count // 2)
221 else:
218 else:
222 side_spaces = ''
219 side_spaces = ''
223 return '<span class="spoiler">{}{}{}</span>'.format(side_spaces,
220 return '<span class="spoiler">{}{}{}</span>'.format(side_spaces,
224 value, side_spaces)
221 value, side_spaces)
225
222
226
223
227 formatters = [
224 formatters = [
228 QuotePattern,
225 QuotePattern,
229 SpoilerPattern,
226 SpoilerPattern,
230 ItalicPattern,
227 ItalicPattern,
231 BoldPattern,
228 BoldPattern,
232 CommentPattern,
229 CommentPattern,
233 StrikeThroughPattern,
230 StrikeThroughPattern,
234 CodePattern,
231 CodePattern,
235 HintPattern,
232 HintPattern,
236 ColorPattern,
233 ColorPattern,
237 ]
234 ]
238
235
239
236
240 PREPARSE_PATTERNS = {
237 PREPARSE_PATTERNS = {
241 r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
238 r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
242 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
239 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
243 r'^//\s?(.+)': r'[comment]\1[/comment]', # Comment "//text"
240 r'^//\s?(.+)': r'[comment]\1[/comment]', # Comment "//text"
244 r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user"
241 r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user"
245 }
242 }
246
243
247 for hostname in ALLOWED_HOSTS:
244 for hostname in ALLOWED_HOSTS:
248 if hostname != '*':
245 if hostname != '*':
249 PREPARSE_PATTERNS[r'https?://{}/thread/\d+/#(\d+)/?'.format(hostname)] = r'[post]\1[/post]'
246 PREPARSE_PATTERNS[r'https?://{}/thread/\d+/#(\d+)/?'.format(hostname)] = r'[post]\1[/post]'
250 PREPARSE_PATTERNS[r'https?://{}/thread/(\d+)/?'.format(hostname)] = r'[post]\1[/post]'
247 PREPARSE_PATTERNS[r'https?://{}/thread/(\d+)/?'.format(hostname)] = r'[post]\1[/post]'
251
248
252
249
253 class Parser:
250 class Parser:
254 def __init__(self):
251 def __init__(self):
255 # The newline hack is added because br's margin does not work in all
252 # The newline hack is added because br's margin does not work in all
256 # browsers except firefox, when the div's does.
253 # browsers except firefox, when the div's does.
257 self.parser = bbcode.Parser(newline=LINE_BREAK_HTML)
254 self.parser = bbcode.Parser(newline=LINE_BREAK_HTML)
258
255
259 self.parser.add_formatter('post', render_reflink, strip=True)
256 self.parser.add_formatter('post', render_reflink, strip=True)
260 self.parser.add_formatter('quote', render_quote, strip=True)
257 self.parser.add_formatter('quote', render_quote, strip=True)
261 self.parser.add_formatter('hint', render_hint, strip=True)
258 self.parser.add_formatter('hint', render_hint, strip=True)
262 self.parser.add_formatter('user', render_notification, strip=True)
259 self.parser.add_formatter('user', render_notification, strip=True)
263 self.parser.add_formatter('tag', render_tag, strip=True)
260 self.parser.add_formatter('tag', render_tag, strip=True)
264 self.parser.add_formatter('spoiler', render_spoiler, strip=True)
261 self.parser.add_formatter('spoiler', render_spoiler, strip=True)
265 self.parser.add_simple_formatter(
262 self.parser.add_simple_formatter(
266 'comment', '<span class="comment">// %(value)s</span>', strip=True)
263 'comment', '<span class="comment">// %(value)s</span>', strip=True)
267 self.parser.add_simple_formatter(
264 self.parser.add_simple_formatter(
268 's', '<span class="strikethrough">%(value)s</span>')
265 's', '<span class="strikethrough">%(value)s</span>')
269 self.parser.add_simple_formatter('code',
266 self.parser.add_simple_formatter('code',
270 '<pre><code>%(value)s</pre></code>',
267 '<pre><code>%(value)s</pre></code>',
271 render_embedded=False,
268 render_embedded=False,
272 escape_html=True,
269 escape_html=True,
273 replace_links=False,
270 replace_links=False,
274 replace_cosmetic=False)
271 replace_cosmetic=False)
275
272
276 def preparse(self, text):
273 def preparse(self, text):
277 """
274 """
278 Performs manual parsing before the bbcode parser is used.
275 Performs manual parsing before the bbcode parser is used.
279 Preparsed text is saved as raw and the text before preparsing is lost.
276 Preparsed text is saved as raw and the text before preparsing is lost.
280 """
277 """
281 new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
278 new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
282
279
283 for key, value in PREPARSE_PATTERNS.items():
280 for key, value in PREPARSE_PATTERNS.items():
284 new_text = re.sub(key, value, new_text, flags=re.MULTILINE)
281 new_text = re.sub(key, value, new_text, flags=re.MULTILINE)
285
282
286 for link in REGEX_URL.findall(text):
283 for link in REGEX_URL.findall(text):
287 new_text = new_text.replace(link, unquote(link))
284 new_text = new_text.replace(link, unquote(link))
288
285
289 return new_text
286 return new_text
290
287
291 def parse(self, text):
288 def parse(self, text):
292 return self.parser.format(text)
289 return self.parser.format(text)
293
290
294
291
295 parser = Parser()
292 parser = Parser()
296
293
297
294
298 def get_parser():
295 def get_parser():
299 return parser
296 return parser
General Comments 0
You need to be logged in to leave comments. Login now