Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
264 lines (208 sloc) 8.83 KB
"""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
Something went wrong with that request. Please try again.