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 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
|
# File: voom_mode_asciidoc.py
# Last Modified: 2017-01-07
# Description: VOoM -- two-pane outliner plugin for Python-enabled Vim
# Website: http://www.vim.org/scripts/script.php?script_id=2657
# Author: Vlad Irnov (vlad DOT irnov AT gmail DOT com)
# License: CC0, see http://creativecommons.org/publicdomain/zero/1.0/
"""
VOoM markup mode for AsciiDoc document and section titles.
See |voom-mode-asciidoc|, ../../../doc/voom.txt#*voom-mode-asciidoc*
"""
### NOTES
#
# When outline operation changes level, it has to deal with two ambiguities:
# a) Level 1-5 headline can use 2-style (underline) or 1-style (=).
# b) 1-style can have or not have closing ='s.
# To determine current preferences: check first headline at level <6 and check
# first headline with =. This must be done in hook_makeOutline().
# (Save in VO, similar to reST mode.) Cannot be done during outline operation,
# that is in hook_doBodyAfterOop().
# Defaults: use underline, use closing ='s.
try:
import vim
if vim.eval('exists("g:voom_asciidoc_do_blanks")')=='1' and vim.eval("g:voom_asciidoc_do_blanks")=='0':
DO_BLANKS = False
else:
DO_BLANKS = True
except ImportError:
DO_BLANKS = True
import sys
if sys.version_info[0] > 2:
xrange = range
def len_u(s, enc):
return len(s)
else:
def len_u(s, enc):
return len(unicode(s, enc, 'replace'))
import re
# regex for 1-style headline, assumes there is no trailing whitespace
HEAD_MATCH = re.compile(r'^(=+)(\s+\S.*?)(\s+\1)?$').match
#---------------------------------------------------------------------
# Characters used as underlines in two-line headlines.
ADS_LEVELS = {'=' : 1, '-' : 2, '~' : 3, '^' : 4, '+' : 5}
# Characters for Delimited Blocks. Headines are ignored inside such blocks.
BLOCK_CHARS = {'/' : 0, '+' : 0, '-' : 0, '.' : 0, '*' : 0, '_' : 0, '=' : 0}
#LEVELS_ADS = {1:'=', 2:'-', 3:'~', 4:'^', 5:'+'}
LEVELS_ADS = {}
for k in ADS_LEVELS:
LEVELS_ADS[ADS_LEVELS[k]] = k
# Combine all signficant chars. Need one of these at start of line for a headline or DelimitedBlock to occur.
CHARS = {}
for k in ADS_LEVELS:
CHARS[k] = 0
for k in BLOCK_CHARS:
CHARS[k] = 0
#---------------------------------------------------------------------
def hook_makeOutline(VO, blines):
"""Return (tlines, bnodes, levels) for Body lines blines.
blines is either Vim buffer object (Body) or list of buffer lines.
"""
ENC = VO.enc
Z = len(blines)
tlines, bnodes, levels = [], [], []
tlines_add, bnodes_add, levels_add = tlines.append, bnodes.append, levels.append
# trailing whitespace is always removed with rstrip()
# if headline is precedeed by [AAA] and/or [[AAA]], bnode is set to their lnum
#
# 1-style, overrides 2-style
# [[AAA]] L3, blines[i-2]
# [yyy] L2, blines[i-1]
# == head == L1, blines[i] -- current line, closing = are optional
#
# 2-style (underline)
# [[AAA]] L4, blines[i-3]
# [yyy] L3, blines[i-2]
# head L2, blines[i-1] -- title line, many restrictions on the format
# ---- L1, blines[i] -- current line
# Set this the first time a headline with level 1-5 is encountered.
# 0 or 1 -- False, use 2-style (default); 2 -- True, use 1-style
useOne = 0
# Set this the first time headline in 1-style is encountered.
# 0 or 1 -- True, use closing ='s (default); 2 -- False, do not use closing ='s
useOneClose = 0
isHead = False
isFenced = False # True if inside DelimitedBlock, the value is the char
headI = -2 # idx of the last line that is part of a headline
blockI = -2 # idx of the last line where a DelimitedBlock ended
m = None # match object for 1-style regex
for i in xrange(Z):
L1 = blines[i].rstrip()
if not L1 or not L1[0] in CHARS:
continue
ch = L1[0]
if isFenced:
if isFenced==ch and len(L1)>3 and L1.lstrip(ch)=='':
isFenced = False
blockI = i
continue
# 1-style headline
if ch == '=' and L1.strip('='):
m = HEAD_MATCH(L1)
if m:
isHead = True
headI_ = headI
headI = i
lev = len(m.group(1))
head = m.group(2).strip()
bnode = i+1
# current line is an underline
# the previous, underlined line (L2) is not a headline if it:
# is not exactly the length of underline +/- 2
# is already part of in the previous headline
# looks like an underline or a delimited block line
# is [[AAA]] or [AAA] (BlockID or Attribute List)
# starts with . (Block Title, they have no level)
# starts with // (comment line)
# starts with tab (don't know why, spaces are ok)
# is only 1 chars (avoids confusion with --, as in Vim syntax, not as in AsciiDoc)
if not isHead and ch in ADS_LEVELS and L1.lstrip(ch)=='' and i > 0:
L2 = blines[i-1].rstrip()
z2 = len_u(L2, ENC)
z1 = len(L1)
if (L2 and
(-3 < z2 - z1 < 3) and z1 > 1 and z2 > 1 and
headI != i-1 and
not ((L2[0] in CHARS) and L2.lstrip(L2[0])=='') and
not (L2.startswith('[') and L2.endswith(']')) and
not L2.startswith('.') and
not L2.startswith('\t') and
not (L2.startswith('//') and not L2.startswith('///'))
):
isHead = True
headI_ = headI
headI = i
lev = ADS_LEVELS[ch]
head = L2.strip()
bnode = i # lnum of previous line (L2)
if isHead and bnode > 1:
# decrement bnode if preceding lines are [[AAA]] or [AAA] lines
# that is set bnode to the topmost [[AAA]] or [AAA] line number
j_ = bnode-2 # idx of line before the title line
L3 = blines[bnode-2].rstrip()
while L3.startswith('[') and L3.endswith(']'):
bnode -= 1
if bnode > 1:
L3 = blines[bnode-2].rstrip()
else:
break
# headline must be preceded by a blank line unless:
# it's line 1 (j == -1)
# headline is preceded by [AAA] or [[AAA]] lines (j != j_)
# previous line is a headline (headI_ == j)
# previous line is the end of a DelimitedBlock (blockI == j)
j = bnode-2
if DO_BLANKS and j==j_ and j > -1:
L3 = blines[j].rstrip()
if L3 and headI_ != j and blockI != j:
# skip over any adjacent comment lines
while L3.startswith('//') and not L3.startswith('///'):
j -= 1
if j > -1:
L3 = blines[j].rstrip()
else:
L3 = ''
if L3 and headI_ != j and blockI != j:
isHead = False
headI = headI_
# start of DelimitedBlock
if not isHead and ch in BLOCK_CHARS and len(L1)>3 and L1.lstrip(ch)=='':
isFenced = ch
continue
if isHead:
isHead = False
# save style info for first headline and first 1-style headline
if not useOne and lev < 6:
if m:
useOne = 2
else:
useOne = 1
if not useOneClose and m:
if m.group(3):
useOneClose = 1
else:
useOneClose = 2
# make outline
tline = ' %s|%s' %('. '*(lev-1), head)
tlines_add(tline)
bnodes_add(bnode)
levels_add(lev)
# don't clobber these when parsing clipboard during Paste
# which is the only time blines is not Body
if blines is VO.Body:
VO.useOne = useOne == 2
VO.useOneClose = useOneClose < 2
return (tlines, bnodes, levels)
def hook_newHeadline(VO, level, blnum, tlnum):
"""Return (tree_head, bodyLines).
tree_head is new headline string in Tree buffer (text after |).
bodyLines is list of lines to insert in Body buffer.
"""
tree_head = 'NewHeadline'
if level < 6 and not VO.useOne:
bodyLines = [tree_head, LEVELS_ADS[level]*11, '']
else:
lev = '='*level
if VO.useOneClose:
bodyLines = ['%s %s %s' %(lev, tree_head, lev), '']
else:
bodyLines = ['%s %s' %(lev, tree_head), '']
# Add blank line when inserting after non-blank Body line.
if VO.Body[blnum-1].strip():
bodyLines[0:0] = ['']
return (tree_head, bodyLines)
#def hook_changeLevBodyHead(VO, h, levDelta):
# DO NOT CREATE THIS HOOK
def hook_doBodyAfterOop(VO, oop, levDelta, blnum1, tlnum1, blnum2, tlnum2, blnumCut, tlnumCut):
# this is instead of hook_changeLevBodyHead()
# Based on Markdown mode function.
# Inserts blank separator lines if missing.
#print('oop=%s levDelta=%s blnum1=%s tlnum1=%s blnum2=%s tlnum2=%s tlnumCut=%s blnumCut=%s' % (oop, levDelta, blnum1, tlnum1, blnum2, tlnum2, tlnumCut, blnumCut))
Body = VO.Body
Z = len(Body)
bnodes, levels = VO.bnodes, VO.levels
ENC = VO.enc
# blnum1 blnum2 is first and last lnums of Body region pasted, inserted
# during up/down, or promoted/demoted.
if blnum1:
assert blnum1 == bnodes[tlnum1-1]
if tlnum2 < len(bnodes):
assert blnum2 == bnodes[tlnum2]-1
else:
assert blnum2 == Z
# blnumCut is Body lnum after which a region was removed during 'cut',
# 'up', 'down'. Need this to check if there is blank line between nodes
# used to be separated by the cut/moved region.
if blnumCut:
if tlnumCut < len(bnodes):
assert blnumCut == bnodes[tlnumCut]-1
else:
assert blnumCut == Z
# Total number of added lines minus number of deleted lines.
b_delta = 0
### After 'cut' or 'up': insert blank line if there is none
# between the nodes used to be separated by the cut/moved region.
if DO_BLANKS and (oop=='cut' or oop=='up') and (0 < blnumCut < Z) and Body[blnumCut-1].strip():
Body[blnumCut:blnumCut] = ['']
update_bnodes(VO, tlnumCut+1 ,1)
b_delta+=1
if oop=='cut':
return
### Make sure there is blank line after the last node in the region:
# insert blank line after blnum2 if blnum2 is not blank, that is insert
# blank line before bnode at tlnum2+1.
if DO_BLANKS and blnum2 < Z and Body[blnum2-1].strip():
Body[blnum2:blnum2] = ['']
update_bnodes(VO, tlnum2+1 ,1)
b_delta+=1
### Change levels and/or formats of headlines in the affected region.
# Always do this after Paste, even if level is unchanged -- format can
# be different when pasting from other outlines.
# Examine each headline, from bottom to top, and change level and/or format.
# To change from 1-style to 2-style:
# strip ='s, strip whitespace;
# insert underline.
# To change from 2-style to 1-style:
# delete underline;
# insert ='s.
# Update bnodes after inserting or deleting a line.
#
# NOTE: bnode can be [[AAA]] or [AAA] line, we check for that and adjust it
# to point to the headline text line
#
# 1-style 2-style
#
# L0 L0 Body[bln-2]
# == head L1 head L1 <--bnode Body[bln-1] (not always the actual bnode)
# L2 ---- L2 Body[bln]
# L3 L3 Body[bln+1]
if levDelta or oop=='paste':
for i in xrange(tlnum2, tlnum1-1, -1):
# required level (VO.levels has been updated)
lev = levels[i-1]
# current level from which to change to lev
lev_ = lev - levDelta
# Body headline (bnode) and the next line
bln = bnodes[i-1]
L1 = Body[bln-1].rstrip()
# bnode can point to the tompost [AAA] or [[AAA]] line
# increment bln until the actual headline (title line) is found
while L1.startswith('[') and L1.endswith(']'):
bln += 1
L1 = Body[bln-1].rstrip()
# the underline line
if bln < len(Body):
L2 = Body[bln].rstrip()
else:
L2 = ''
# get current headline format
hasOne, hasOneClose = False, VO.useOneClose
theHead = L1
if L1.startswith('='):
m = HEAD_MATCH(L1)
if m:
hasOne = True
# headline without ='s but with whitespace around it preserved
theHead = m.group(2)
theclose = m.group(3)
if theclose:
hasOneClose = True
theHead += theclose.rstrip('=')
else:
hasOneClose = False
# get desired headline format
if oop=='paste':
if lev > 5:
useOne = True
else:
useOne = VO.useOne
useOneClose = VO.useOneClose
elif lev < 6 and lev_ < 6:
useOne = hasOne
useOneClose = hasOneClose
elif lev > 5 and lev_ > 5:
useOne = True
useOneClose = hasOneClose
elif lev < 6 and lev_ > 5:
useOne = VO.useOne
useOneClose = VO.useOneClose
elif lev > 5 and lev_ < 6:
useOne = True
useOneClose = hasOneClose
else:
assert False
#print('useOne=%s hasOne=%s useOneClose=%s hasOneClose=%s' %(useOne, hasOne, useOneClose, hasOneClose))
### change headline level and/or format
# 2-style unchanged, only adjust level of underline
if not useOne and not hasOne:
if not levDelta: continue
Body[bln] = LEVELS_ADS[lev]*len(L2)
# 1-style unchanged, adjust level of ='s and add/remove closing ='s
elif useOne and hasOne:
# no format change, there are closing ='s
if useOneClose and hasOneClose:
if not levDelta: continue
Body[bln-1] = '%s%s%s' %('='*lev, theHead, '='*lev)
# no format change, there are no closing ='s
elif not useOneClose and not hasOneClose:
if not levDelta: continue
Body[bln-1] = '%s%s' %('='*lev, theHead)
# add closing ='s
elif useOneClose and not hasOneClose:
Body[bln-1] = '%s%s %s' %('='*lev, theHead.rstrip(), '='*lev)
# remove closing ='s
elif not useOneClose and hasOneClose:
Body[bln-1] = '%s%s' %('='*lev, theHead.rstrip())
# insert underline, remove ='s
elif not useOne and hasOne:
L1 = theHead.strip()
Body[bln-1] = L1
# insert underline
Body[bln:bln] = [LEVELS_ADS[lev] * len_u(L1, ENC)]
update_bnodes(VO, i+1, 1)
b_delta+=1
# remove underline, insert ='s
elif useOne and not hasOne:
if useOneClose:
Body[bln-1] = '%s %s %s' %('='*lev, theHead.strip(), '='*lev)
else:
Body[bln-1] = '%s %s' %('='*lev, theHead.strip())
# delete underline
Body[bln:bln+1] = []
update_bnodes(VO, i+1, -1)
b_delta-=1
### Make sure first headline is preceded by a blank line.
blnum1 = bnodes[tlnum1-1]
if DO_BLANKS and blnum1 > 1 and Body[blnum1-2].strip():
Body[blnum1-1:blnum1-1] = ['']
update_bnodes(VO, tlnum1 ,1)
b_delta+=1
### After 'down' : insert blank line if there is none
# between the nodes used to be separated by the moved region.
if DO_BLANKS and oop=='down' and (0 < blnumCut < Z) and Body[blnumCut-1].strip():
Body[blnumCut:blnumCut] = ['']
update_bnodes(VO, tlnumCut+1 ,1)
b_delta+=1
assert len(Body) == Z + b_delta
def update_bnodes(VO, tlnum, delta):
"""Update VO.bnodes by adding/substracting delta to each bnode
starting with bnode at tlnum and to the end.
"""
bnodes = VO.bnodes
for i in xrange(tlnum, len(bnodes)+1):
bnodes[i-1] += delta
|