Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(331)

Side by Side Diff: third_party/markdown/blockprocessors.py

Issue 93743005: Support markdown template for html editor (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fix path without dir Created 6 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/markdown/blockparser.py ('k') | third_party/markdown/extensions/__init__.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 """
2 CORE MARKDOWN BLOCKPARSER
3 ===========================================================================
4
5 This parser handles basic parsing of Markdown blocks. It doesn't concern itself
6 with inline elements such as **bold** or *italics*, but rather just catches
7 blocks, lists, quotes, etc.
8
9 The BlockParser is made up of a bunch of BlockProssors, each handling a
10 different type of block. Extensions may add/replace/remove BlockProcessors
11 as they need to alter how markdown blocks are parsed.
12 """
13
14 from __future__ import absolute_import
15 from __future__ import division
16 from __future__ import unicode_literals
17 import logging
18 import re
19 from . import util
20 from .blockparser import BlockParser
21
22 logger = logging.getLogger('MARKDOWN')
23
24
25 def build_block_parser(md_instance, **kwargs):
26 """ Build the default block parser used by Markdown. """
27 parser = BlockParser(md_instance)
28 parser.blockprocessors['empty'] = EmptyBlockProcessor(parser)
29 parser.blockprocessors['indent'] = ListIndentProcessor(parser)
30 parser.blockprocessors['code'] = CodeBlockProcessor(parser)
31 parser.blockprocessors['hashheader'] = HashHeaderProcessor(parser)
32 parser.blockprocessors['setextheader'] = SetextHeaderProcessor(parser)
33 parser.blockprocessors['hr'] = HRProcessor(parser)
34 parser.blockprocessors['olist'] = OListProcessor(parser)
35 parser.blockprocessors['ulist'] = UListProcessor(parser)
36 parser.blockprocessors['quote'] = BlockQuoteProcessor(parser)
37 parser.blockprocessors['paragraph'] = ParagraphProcessor(parser)
38 return parser
39
40
41 class BlockProcessor:
42 """ Base class for block processors.
43
44 Each subclass will provide the methods below to work with the source and
45 tree. Each processor will need to define it's own ``test`` and ``run``
46 methods. The ``test`` method should return True or False, to indicate
47 whether the current block should be processed by this processor. If the
48 test passes, the parser will call the processors ``run`` method.
49
50 """
51
52 def __init__(self, parser):
53 self.parser = parser
54 self.tab_length = parser.markdown.tab_length
55
56 def lastChild(self, parent):
57 """ Return the last child of an etree element. """
58 if len(parent):
59 return parent[-1]
60 else:
61 return None
62
63 def detab(self, text):
64 """ Remove a tab from the front of each line of the given text. """
65 newtext = []
66 lines = text.split('\n')
67 for line in lines:
68 if line.startswith(' '*self.tab_length):
69 newtext.append(line[self.tab_length:])
70 elif not line.strip():
71 newtext.append('')
72 else:
73 break
74 return '\n'.join(newtext), '\n'.join(lines[len(newtext):])
75
76 def looseDetab(self, text, level=1):
77 """ Remove a tab from front of lines but allowing dedented lines. """
78 lines = text.split('\n')
79 for i in range(len(lines)):
80 if lines[i].startswith(' '*self.tab_length*level):
81 lines[i] = lines[i][self.tab_length*level:]
82 return '\n'.join(lines)
83
84 def test(self, parent, block):
85 """ Test for block type. Must be overridden by subclasses.
86
87 As the parser loops through processors, it will call the ``test`` method
88 on each to determine if the given block of text is of that type. This
89 method must return a boolean ``True`` or ``False``. The actual method of
90 testing is left to the needs of that particular block type. It could
91 be as simple as ``block.startswith(some_string)`` or a complex regular
92 expression. As the block type may be different depending on the parent
93 of the block (i.e. inside a list), the parent etree element is also
94 provided and may be used as part of the test.
95
96 Keywords:
97
98 * ``parent``: A etree element which will be the parent of the block.
99 * ``block``: A block of text from the source which has been split at
100 blank lines.
101 """
102 pass
103
104 def run(self, parent, blocks):
105 """ Run processor. Must be overridden by subclasses.
106
107 When the parser determines the appropriate type of a block, the parser
108 will call the corresponding processor's ``run`` method. This method
109 should parse the individual lines of the block and append them to
110 the etree.
111
112 Note that both the ``parent`` and ``etree`` keywords are pointers
113 to instances of the objects which should be edited in place. Each
114 processor must make changes to the existing objects as there is no
115 mechanism to return new/different objects to replace them.
116
117 This means that this method should be adding SubElements or adding text
118 to the parent, and should remove (``pop``) or add (``insert``) items to
119 the list of blocks.
120
121 Keywords:
122
123 * ``parent``: A etree element which is the parent of the current block.
124 * ``blocks``: A list of all remaining blocks of the document.
125 """
126 pass
127
128
129 class ListIndentProcessor(BlockProcessor):
130 """ Process children of list items.
131
132 Example:
133 * a list item
134 process this part
135
136 or this part
137
138 """
139
140 ITEM_TYPES = ['li']
141 LIST_TYPES = ['ul', 'ol']
142
143 def __init__(self, *args):
144 BlockProcessor.__init__(self, *args)
145 self.INDENT_RE = re.compile(r'^(([ ]{%s})+)'% self.tab_length)
146
147 def test(self, parent, block):
148 return block.startswith(' '*self.tab_length) and \
149 not self.parser.state.isstate('detabbed') and \
150 (parent.tag in self.ITEM_TYPES or \
151 (len(parent) and parent[-1] and \
152 (parent[-1].tag in self.LIST_TYPES)
153 )
154 )
155
156 def run(self, parent, blocks):
157 block = blocks.pop(0)
158 level, sibling = self.get_level(parent, block)
159 block = self.looseDetab(block, level)
160
161 self.parser.state.set('detabbed')
162 if parent.tag in self.ITEM_TYPES:
163 # It's possible that this parent has a 'ul' or 'ol' child list
164 # with a member. If that is the case, then that should be the
165 # parent. This is intended to catch the edge case of an indented
166 # list whose first member was parsed previous to this point
167 # see OListProcessor
168 if len(parent) and parent[-1].tag in self.LIST_TYPES:
169 self.parser.parseBlocks(parent[-1], [block])
170 else:
171 # The parent is already a li. Just parse the child block.
172 self.parser.parseBlocks(parent, [block])
173 elif sibling.tag in self.ITEM_TYPES:
174 # The sibling is a li. Use it as parent.
175 self.parser.parseBlocks(sibling, [block])
176 elif len(sibling) and sibling[-1].tag in self.ITEM_TYPES:
177 # The parent is a list (``ol`` or ``ul``) which has children.
178 # Assume the last child li is the parent of this block.
179 if sibling[-1].text:
180 # If the parent li has text, that text needs to be moved to a p
181 # The p must be 'inserted' at beginning of list in the event
182 # that other children already exist i.e.; a nested sublist.
183 p = util.etree.Element('p')
184 p.text = sibling[-1].text
185 sibling[-1].text = ''
186 sibling[-1].insert(0, p)
187 self.parser.parseChunk(sibling[-1], block)
188 else:
189 self.create_item(sibling, block)
190 self.parser.state.reset()
191
192 def create_item(self, parent, block):
193 """ Create a new li and parse the block with it as the parent. """
194 li = util.etree.SubElement(parent, 'li')
195 self.parser.parseBlocks(li, [block])
196
197 def get_level(self, parent, block):
198 """ Get level of indent based on list level. """
199 # Get indent level
200 m = self.INDENT_RE.match(block)
201 if m:
202 indent_level = len(m.group(1))/self.tab_length
203 else:
204 indent_level = 0
205 if self.parser.state.isstate('list'):
206 # We're in a tightlist - so we already are at correct parent.
207 level = 1
208 else:
209 # We're in a looselist - so we need to find parent.
210 level = 0
211 # Step through children of tree to find matching indent level.
212 while indent_level > level:
213 child = self.lastChild(parent)
214 if child and (child.tag in self.LIST_TYPES or child.tag in self.ITEM _TYPES):
215 if child.tag in self.LIST_TYPES:
216 level += 1
217 parent = child
218 else:
219 # No more child levels. If we're short of indent_level,
220 # we have a code block. So we stop here.
221 break
222 return level, parent
223
224
225 class CodeBlockProcessor(BlockProcessor):
226 """ Process code blocks. """
227
228 def test(self, parent, block):
229 return block.startswith(' '*self.tab_length)
230
231 def run(self, parent, blocks):
232 sibling = self.lastChild(parent)
233 block = blocks.pop(0)
234 theRest = ''
235 if sibling and sibling.tag == "pre" and len(sibling) \
236 and sibling[0].tag == "code":
237 # The previous block was a code block. As blank lines do not start
238 # new code blocks, append this block to the previous, adding back
239 # linebreaks removed from the split into a list.
240 code = sibling[0]
241 block, theRest = self.detab(block)
242 code.text = util.AtomicString('%s\n%s\n' % (code.text, block.rstrip( )))
243 else:
244 # This is a new codeblock. Create the elements and insert text.
245 pre = util.etree.SubElement(parent, 'pre')
246 code = util.etree.SubElement(pre, 'code')
247 block, theRest = self.detab(block)
248 code.text = util.AtomicString('%s\n' % block.rstrip())
249 if theRest:
250 # This block contained unindented line(s) after the first indented
251 # line. Insert these lines as the first block of the master blocks
252 # list for future processing.
253 blocks.insert(0, theRest)
254
255
256 class BlockQuoteProcessor(BlockProcessor):
257
258 RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)')
259
260 def test(self, parent, block):
261 return bool(self.RE.search(block))
262
263 def run(self, parent, blocks):
264 block = blocks.pop(0)
265 m = self.RE.search(block)
266 if m:
267 before = block[:m.start()] # Lines before blockquote
268 # Pass lines before blockquote in recursively for parsing forst.
269 self.parser.parseBlocks(parent, [before])
270 # Remove ``> `` from begining of each line.
271 block = '\n'.join([self.clean(line) for line in
272 block[m.start():].split('\n')])
273 sibling = self.lastChild(parent)
274 if sibling and sibling.tag == "blockquote":
275 # Previous block was a blockquote so set that as this blocks parent
276 quote = sibling
277 else:
278 # This is a new blockquote. Create a new parent element.
279 quote = util.etree.SubElement(parent, 'blockquote')
280 # Recursively parse block with blockquote as parent.
281 # change parser state so blockquotes embedded in lists use p tags
282 self.parser.state.set('blockquote')
283 self.parser.parseChunk(quote, block)
284 self.parser.state.reset()
285
286 def clean(self, line):
287 """ Remove ``>`` from beginning of a line. """
288 m = self.RE.match(line)
289 if line.strip() == ">":
290 return ""
291 elif m:
292 return m.group(2)
293 else:
294 return line
295
296 class OListProcessor(BlockProcessor):
297 """ Process ordered list blocks. """
298
299 TAG = 'ol'
300 # Detect an item (``1. item``). ``group(1)`` contains contents of item.
301 RE = re.compile(r'^[ ]{0,3}\d+\.[ ]+(.*)')
302 # Detect items on secondary lines. they can be of either list type.
303 CHILD_RE = re.compile(r'^[ ]{0,3}((\d+\.)|[*+-])[ ]+(.*)')
304 # Detect indented (nested) items of either type
305 INDENT_RE = re.compile(r'^[ ]{4,7}((\d+\.)|[*+-])[ ]+.*')
306 # The integer (python string) with which the lists starts (default=1)
307 # Eg: If list is intialized as)
308 # 3. Item
309 # The ol tag will get starts="3" attribute
310 STARTSWITH = '1'
311 # List of allowed sibling tags.
312 SIBLING_TAGS = ['ol', 'ul']
313
314 def test(self, parent, block):
315 return bool(self.RE.match(block))
316
317 def run(self, parent, blocks):
318 # Check fr multiple items in one block.
319 items = self.get_items(blocks.pop(0))
320 sibling = self.lastChild(parent)
321
322 if sibling and sibling.tag in self.SIBLING_TAGS:
323 # Previous block was a list item, so set that as parent
324 lst = sibling
325 # make sure previous item is in a p- if the item has text, then it
326 # it isn't in a p
327 if lst[-1].text:
328 # since it's possible there are other children for this sibling,
329 # we can't just SubElement the p, we need to insert it as the
330 # first item
331 p = util.etree.Element('p')
332 p.text = lst[-1].text
333 lst[-1].text = ''
334 lst[-1].insert(0, p)
335 # if the last item has a tail, then the tail needs to be put in a p
336 # likely only when a header is not followed by a blank line
337 lch = self.lastChild(lst[-1])
338 if lch is not None and lch.tail:
339 p = util.etree.SubElement(lst[-1], 'p')
340 p.text = lch.tail.lstrip()
341 lch.tail = ''
342
343 # parse first block differently as it gets wrapped in a p.
344 li = util.etree.SubElement(lst, 'li')
345 self.parser.state.set('looselist')
346 firstitem = items.pop(0)
347 self.parser.parseBlocks(li, [firstitem])
348 self.parser.state.reset()
349 elif parent.tag in ['ol', 'ul']:
350 # this catches the edge case of a multi-item indented list whose
351 # first item is in a blank parent-list item:
352 # * * subitem1
353 # * subitem2
354 # see also ListIndentProcessor
355 lst = parent
356 else:
357 # This is a new list so create parent with appropriate tag.
358 lst = util.etree.SubElement(parent, self.TAG)
359 # Check if a custom start integer is set
360 if not self.parser.markdown.lazy_ol and self.STARTSWITH !='1':
361 lst.attrib['start'] = self.STARTSWITH
362
363 self.parser.state.set('list')
364 # Loop through items in block, recursively parsing each with the
365 # appropriate parent.
366 for item in items:
367 if item.startswith(' '*self.tab_length):
368 # Item is indented. Parse with last item as parent
369 self.parser.parseBlocks(lst[-1], [item])
370 else:
371 # New item. Create li and parse with it as parent
372 li = util.etree.SubElement(lst, 'li')
373 self.parser.parseBlocks(li, [item])
374 self.parser.state.reset()
375
376 def get_items(self, block):
377 """ Break a block into list items. """
378 items = []
379 for line in block.split('\n'):
380 m = self.CHILD_RE.match(line)
381 if m:
382 # This is a new list item
383 # Check first item for the start index
384 if not items and self.TAG=='ol':
385 # Detect the integer value of first list item
386 INTEGER_RE = re.compile('(\d+)')
387 self.STARTSWITH = INTEGER_RE.match(m.group(1)).group()
388 # Append to the list
389 items.append(m.group(3))
390 elif self.INDENT_RE.match(line):
391 # This is an indented (possibly nested) item.
392 if items[-1].startswith(' '*self.tab_length):
393 # Previous item was indented. Append to that item.
394 items[-1] = '%s\n%s' % (items[-1], line)
395 else:
396 items.append(line)
397 else:
398 # This is another line of previous item. Append to that item.
399 items[-1] = '%s\n%s' % (items[-1], line)
400 return items
401
402
403 class UListProcessor(OListProcessor):
404 """ Process unordered list blocks. """
405
406 TAG = 'ul'
407 RE = re.compile(r'^[ ]{0,3}[*+-][ ]+(.*)')
408
409
410 class HashHeaderProcessor(BlockProcessor):
411 """ Process Hash Headers. """
412
413 # Detect a header at start of any line in block
414 RE = re.compile(r'(^|\n)(?P<level>#{1,6})(?P<header>.*?)#*(\n|$)')
415
416 def test(self, parent, block):
417 return bool(self.RE.search(block))
418
419 def run(self, parent, blocks):
420 block = blocks.pop(0)
421 m = self.RE.search(block)
422 if m:
423 before = block[:m.start()] # All lines before header
424 after = block[m.end():] # All lines after header
425 if before:
426 # As the header was not the first line of the block and the
427 # lines before the header must be parsed first,
428 # recursively parse this lines as a block.
429 self.parser.parseBlocks(parent, [before])
430 # Create header using named groups from RE
431 h = util.etree.SubElement(parent, 'h%d' % len(m.group('level')))
432 h.text = m.group('header').strip()
433 if after:
434 # Insert remaining lines as first block for future parsing.
435 blocks.insert(0, after)
436 else:
437 # This should never happen, but just in case...
438 logger.warn("We've got a problem header: %r" % block)
439
440
441 class SetextHeaderProcessor(BlockProcessor):
442 """ Process Setext-style Headers. """
443
444 # Detect Setext-style header. Must be first 2 lines of block.
445 RE = re.compile(r'^.*?\n[=-]+[ ]*(\n|$)', re.MULTILINE)
446
447 def test(self, parent, block):
448 return bool(self.RE.match(block))
449
450 def run(self, parent, blocks):
451 lines = blocks.pop(0).split('\n')
452 # Determine level. ``=`` is 1 and ``-`` is 2.
453 if lines[1].startswith('='):
454 level = 1
455 else:
456 level = 2
457 h = util.etree.SubElement(parent, 'h%d' % level)
458 h.text = lines[0].strip()
459 if len(lines) > 2:
460 # Block contains additional lines. Add to master blocks for later.
461 blocks.insert(0, '\n'.join(lines[2:]))
462
463
464 class HRProcessor(BlockProcessor):
465 """ Process Horizontal Rules. """
466
467 RE = r'^[ ]{0,3}((-+[ ]{0,2}){3,}|(_+[ ]{0,2}){3,}|(\*+[ ]{0,2}){3,})[ ]*'
468 # Detect hr on any line of a block.
469 SEARCH_RE = re.compile(RE, re.MULTILINE)
470
471 def test(self, parent, block):
472 m = self.SEARCH_RE.search(block)
473 # No atomic grouping in python so we simulate it here for performance.
474 # The regex only matches what would be in the atomic group - the HR.
475 # Then check if we are at end of block or if next char is a newline.
476 if m and (m.end() == len(block) or block[m.end()] == '\n'):
477 # Save match object on class instance so we can use it later.
478 self.match = m
479 return True
480 return False
481
482 def run(self, parent, blocks):
483 block = blocks.pop(0)
484 # Check for lines in block before hr.
485 prelines = block[:self.match.start()].rstrip('\n')
486 if prelines:
487 # Recursively parse lines before hr so they get parsed first.
488 self.parser.parseBlocks(parent, [prelines])
489 # create hr
490 util.etree.SubElement(parent, 'hr')
491 # check for lines in block after hr.
492 postlines = block[self.match.end():].lstrip('\n')
493 if postlines:
494 # Add lines after hr to master blocks for later parsing.
495 blocks.insert(0, postlines)
496
497
498
499 class EmptyBlockProcessor(BlockProcessor):
500 """ Process blocks that are empty or start with an empty line. """
501
502 def test(self, parent, block):
503 return not block or block.startswith('\n')
504
505 def run(self, parent, blocks):
506 block = blocks.pop(0)
507 filler = '\n\n'
508 if block:
509 # Starts with empty line
510 # Only replace a single line.
511 filler = '\n'
512 # Save the rest for later.
513 theRest = block[1:]
514 if theRest:
515 # Add remaining lines to master blocks for later.
516 blocks.insert(0, theRest)
517 sibling = self.lastChild(parent)
518 if sibling and sibling.tag == 'pre' and len(sibling) and sibling[0].tag == 'code':
519 # Last block is a codeblock. Append to preserve whitespace.
520 sibling[0].text = util.AtomicString('%s%s' % (sibling[0].text, fille r))
521
522
523 class ParagraphProcessor(BlockProcessor):
524 """ Process Paragraph blocks. """
525
526 def test(self, parent, block):
527 return True
528
529 def run(self, parent, blocks):
530 block = blocks.pop(0)
531 if block.strip():
532 # Not a blank block. Add to parent, otherwise throw it away.
533 if self.parser.state.isstate('list'):
534 # The parent is a tight-list.
535 #
536 # Check for any children. This will likely only happen in a
537 # tight-list when a header isn't followed by a blank line.
538 # For example:
539 #
540 # * # Header
541 # Line 2 of list item - not part of header.
542 sibling = self.lastChild(parent)
543 if sibling is not None:
544 # Insetrt after sibling.
545 if sibling.tail:
546 sibling.tail = '%s\n%s' % (sibling.tail, block)
547 else:
548 sibling.tail = '\n%s' % block
549 else:
550 # Append to parent.text
551 if parent.text:
552 parent.text = '%s\n%s' % (parent.text, block)
553 else:
554 parent.text = block.lstrip()
555 else:
556 # Create a regular paragraph
557 p = util.etree.SubElement(parent, 'p')
558 p.text = block.lstrip()
OLDNEW
« no previous file with comments | « third_party/markdown/blockparser.py ('k') | third_party/markdown/extensions/__init__.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698