264 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Some utility functions for working with headline of Markdown.
 | |
| 
 | |
| Terminologies
 | |
| - Headline :: The headline entity OR the text of the headline
 | |
| - Content :: The content under the current headline. It stops after
 | |
|   encountering a headline with the same or higher level OR EOF.
 | |
| """
 | |
| # Author: Muchenxuan Tong <demon386@gmail.com>
 | |
| 
 | |
| import re
 | |
| import sublime
 | |
| 
 | |
| try:
 | |
|     from .utilities import is_region_void
 | |
| except ValueError:
 | |
|     from utilities import is_region_void
 | |
| 
 | |
| MATCH_PARENT = 1   # Match headlines at the same or higher level
 | |
| MATCH_CHILD = 2    # Match headlines at the same or lower level
 | |
| MATCH_SILBING = 3  # Only Match headlines at the same level.
 | |
| MATCH_ANY = 4      # Any headlines would be matched.
 | |
| ANY_LEVEL = -1     # level used when MATCH_ANY is used as match type
 | |
| 
 | |
| 
 | |
| def region_of_content_of_headline_at_point(view, from_point):
 | |
|     """Extract the region of the content of under current headline."""
 | |
|     _, level = headline_and_level_at_point(view, from_point)
 | |
|     if level == None:
 | |
|         return None
 | |
| 
 | |
|     if is_content_empty_at_point(view, from_point):
 | |
|         return None
 | |
| 
 | |
|     line_num, _ = view.rowcol(from_point)
 | |
|     content_line_start_point = view.text_point(line_num + 1, 0)
 | |
| 
 | |
|     next_headline, _ = find_headline(view, \
 | |
|                                      content_line_start_point, \
 | |
|                                      level, \
 | |
|                                      True, \
 | |
|                                      MATCH_PARENT)
 | |
|     if not is_region_void(next_headline):
 | |
|         end_pos = next_headline.a - 1
 | |
|     else:
 | |
|         end_pos = view.size()
 | |
|     return sublime.Region(content_line_start_point, end_pos)
 | |
| 
 | |
| 
 | |
| def headline_and_level_at_point(view, from_point, search_above_and_down=False):
 | |
|     """Return the current headline and level.
 | |
| 
 | |
|     If from_point is inside a headline, then return the headline and level.
 | |
|     Otherwise depends on the argument it might search above and down.
 | |
|     """
 | |
|     line_region = view.line(from_point)
 | |
|     line_content = view.substr(line_region)
 | |
|     # Update the level in case it's headline.ANY_LEVEL
 | |
|     level = _extract_level_from_headline(line_content)
 | |
| 
 | |
|     # Search above and down
 | |
|     if level is None and search_above_and_down:
 | |
|         # Search above
 | |
|         headline_region, _ = find_headline(view,\
 | |
|                                            from_point,\
 | |
|                                            ANY_LEVEL,
 | |
|                                            False,
 | |
|                                            skip_folded=True)
 | |
|         if not is_region_void(headline_region):
 | |
|             line_content, level = headline_and_level_at_point(view,\
 | |
|                                                               headline_region.a)
 | |
|         # Search down
 | |
|         if level is None:
 | |
|             headline_region, _ = find_headline(view,\
 | |
|                                                from_point,\
 | |
|                                                ANY_LEVEL,
 | |
|                                                True,
 | |
|                                                skip_folded=True)
 | |
|             if not is_region_void(headline_region):
 | |
|                 line_content, level = headline_and_level_at_point(view, headline_region.a)
 | |
| 
 | |
|     return line_content, level
 | |
| 
 | |
| 
 | |
| def _extract_level_from_headline(headline):
 | |
|     """Extract the level of headline, None if not found.
 | |
| 
 | |
|     """
 | |
|     re_string = _get_re_string(ANY_LEVEL, MATCH_ANY)
 | |
|     match = re.match(re_string, headline)
 | |
| 
 | |
|     if match:
 | |
|         return len(match.group(1))
 | |
|     else:
 | |
|         return None
 | |
| 
 | |
| 
 | |
| def is_content_empty_at_point(view, from_point):
 | |
|     """Check if the content under the current headline is empty.
 | |
| 
 | |
|     For implementation, check if next line is a headline a the same
 | |
|     or higher level.
 | |
| 
 | |
|     """
 | |
|     _, level = headline_and_level_at_point(view, from_point)
 | |
|     if level is None:
 | |
|         raise ValueError("from_point must be inside a valid headline.")
 | |
| 
 | |
|     line_num, _ = view.rowcol(from_point)
 | |
|     next_line_region = view.line(view.text_point(line_num + 1, 0))
 | |
|     next_line_content = view.substr(next_line_region)
 | |
|     next_line_level = _extract_level_from_headline(next_line_content)
 | |
| 
 | |
|     # Note that EOF works too in this case.
 | |
|     if next_line_level and next_line_level <= level:
 | |
|         return True
 | |
|     else:
 | |
|         return False
 | |
| 
 | |
| 
 | |
| def find_headline(view, from_point, level, forward=True, \
 | |
|                   match_type=MATCH_ANY, skip_headline_at_point=False, \
 | |
|                   skip_folded=False):
 | |
|     """Return the region of the next headline or EOF.
 | |
| 
 | |
|     Parameters
 | |
|     ----------
 | |
|     view: sublime.view
 | |
| 
 | |
|     from_point: int
 | |
|         From which to find.
 | |
| 
 | |
|     level: int
 | |
|         The headline level to match.
 | |
| 
 | |
|     forward: boolean
 | |
|         Search forward or backward
 | |
| 
 | |
|     match_type: int
 | |
|         MATCH_SILBING, MATCH_PARENT, MATCH_CHILD or MATCH_ANY.
 | |
| 
 | |
|     skip_headline_at_point: boolean
 | |
|         When searching whether skip the headline at point
 | |
| 
 | |
|     skip_folded: boolean
 | |
|         Whether to skip the folded region
 | |
| 
 | |
|     Returns
 | |
|     -------
 | |
|     match_region: int
 | |
|         Matched region, or None if not found.
 | |
| 
 | |
|     match_level: int
 | |
|         The level of matched headline, or None if not found.
 | |
| 
 | |
|     """
 | |
|     if skip_headline_at_point:
 | |
|         # Move the point to the next line if we are
 | |
|         # current in a headline already.
 | |
|         from_point = _get_new_point_if_already_in_headline(view, from_point,
 | |
|                                                            forward)
 | |
| 
 | |
|     re_string = _get_re_string(level, match_type)
 | |
|     if forward:
 | |
|         match_region = view.find(re_string, from_point)
 | |
|     else:
 | |
|         all_match_regions = view.find_all(re_string)
 | |
|         match_region = _nearest_region_among_matches_from_point(view, \
 | |
|                                                                 all_match_regions, \
 | |
|                                                                 from_point, \
 | |
|                                                                 False, \
 | |
|                                                                 skip_folded)
 | |
| 
 | |
|     if skip_folded:
 | |
|         while (_is_region_folded(match_region, view)):
 | |
|             from_point = match_region.b
 | |
|             match_region = view.find(re_string, from_point)
 | |
| 
 | |
|     if not is_region_void(match_region):
 | |
|         if not is_scope_headline(view, match_region.a):
 | |
|             return find_headline(view, match_region.a, level, forward, \
 | |
|                                  match_type, True, skip_folded)
 | |
|         else:
 | |
|             ## Extract the level of matched headlines according to the region
 | |
|             headline = view.substr(match_region)
 | |
|             match_level = _extract_level_from_headline(headline)
 | |
|     else:
 | |
|         match_level = None
 | |
|     return (match_region, match_level)
 | |
| 
 | |
| def _get_re_string(level, match_type=MATCH_ANY):
 | |
|     """Get regular expression string according to match type.
 | |
| 
 | |
|     Return regular expression string, rather than compiled string. Since
 | |
|     sublime's view.find function needs string.
 | |
| 
 | |
|     Parameters
 | |
|     ----------
 | |
|     match_type: int
 | |
|         MATCH_SILBING, MATCH_PARENT, MATCH_CHILD or ANY_LEVEL.
 | |
| 
 | |
|     """
 | |
|     if match_type == MATCH_ANY:
 | |
|         re_string = r'^(#+)\s.*'
 | |
|     else:
 | |
|         try:
 | |
|             if match_type == MATCH_PARENT:
 | |
|                 re_string = r'^(#{1,%d})\s.*' % level
 | |
|             elif match_type == MATCH_CHILD:
 | |
|                 re_string = r'^(#{%d,})\s.*' % level
 | |
|             elif match_type == MATCH_SILBING:
 | |
|                 re_string = r'^(#{%d,%d})\s.*' % (level, level)
 | |
|         except ValueError:
 | |
|             print("match_type has to be specified if level isn't ANY_LEVE")
 | |
|     return re_string
 | |
| 
 | |
| 
 | |
| def _get_new_point_if_already_in_headline(view, from_point, forward=True):
 | |
|     line_content = view.substr(view.line(from_point))
 | |
|     if _extract_level_from_headline(line_content):
 | |
|         line_num, _ = view.rowcol(from_point)
 | |
|         if forward:
 | |
|             return view.text_point(line_num + 1, 0)
 | |
|         else:
 | |
|             return view.text_point(line_num, 0) - 1
 | |
|     else:
 | |
|         return from_point
 | |
| 
 | |
| 
 | |
| def is_scope_headline(view, from_point):
 | |
|     return view.score_selector(from_point, "markup.heading") > 0 or \
 | |
|         view.score_selector(from_point, "meta.block-level.markdown") > 0
 | |
| 
 | |
| 
 | |
| def _nearest_region_among_matches_from_point(view, all_match_regions, \
 | |
|                                              from_point, forward=False,
 | |
|                                              skip_folded=True):
 | |
|     """Find the nearest matched region among all matched regions.
 | |
| 
 | |
|     None if not found.
 | |
| 
 | |
|     """
 | |
|     nearest_region = None
 | |
| 
 | |
|     for r in all_match_regions:
 | |
|         if not forward and r.b <= from_point and \
 | |
|             (not nearest_region or r.a > nearest_region.a):
 | |
|             candidate = r
 | |
|         elif forward and r.a >= from_point and \
 | |
|             (not nearest_region or r.b < nearest_region.b):
 | |
|             candidate = r
 | |
|         else:
 | |
|             continue
 | |
|         if skip_folded and not _is_region_folded(candidate, view):
 | |
|             nearest_region = candidate
 | |
| 
 | |
|     return nearest_region
 | |
| 
 | |
| 
 | |
| def _is_region_folded(region, view):
 | |
|     for i in view.folded_regions():
 | |
|         if i.contains(region):
 | |
|             return True
 | |
|     return False
 |