commit c755452cd2bc8a426bbdf9d7f08c91fd992589fe
parent 48b31077616c8d1e72fd4916a0a335d55cc463fc
Author: Alex Balgavy <a.balgavy@gmail.com>
Date: Tue, 5 Jun 2018 01:59:04 +0200
New markdown viewer script
Light markdown viewer (mdvl) along with an alias to pipe the result to less. A nice little script.
Diffstat:
A | bin/mdvl | | | 651 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | commonprofile | | | 1 | + |
2 files changed, 652 insertions(+), 0 deletions(-)
diff --git a/bin/mdvl b/bin/mdvl
@@ -0,0 +1,651 @@
+#!/usr/bin/env python -Ss
+# coding: utf-8
+# Some systems do not accept shebang with args. Use python -Ss mdvl.py then.
+'''
+# Lightweight Simple Markdown Renderer for the Terminal
+
+## Usage
+
+ mdvl <markdown source | markdown file>
+ cat <markdown file> | mdvl
+
+## Config
+
+```
+%s
+```
+
+### Colors
+
+```
+%%s
+```
+
+## Debugging Parsing Errors
+
+ export mdvl_debug=1
+
+See also https://github.com/axiros/mdvl
+
+'''
+__version__ = "2017.07.16.7" # count up for new pip versions
+__author__ = "Gunther Klessinger"
+
+from textwrap import fill
+from operator import setitem as set
+import re, os
+
+debug=os.environ.get('mdvl_debug')
+
+# check environ for value and cast into bools if necessary:
+_b = {'True': True, 'False': False}
+env = lambda k, d=None: _b.get(k, os.environ.get(k, d))
+
+# ----------------------------------------------------------------- Config Mgmt
+class Cfg:
+ '''
+ Base class for osenv and kw configurable instances.
+ - We have defaults, overridable by environ keys, overridable by **kws
+ - problem is that color codes should be givabable as ints - but on the
+ terminal we usually have them as full ansi escape str.
+ Thats why `get_val` is present and adapts for that in the Colors cls.
+ '''
+ _parms = None # our (relevant) keys and values
+
+ def setup(self, kw):
+ ''' find all our key value defaults and override with env and **kw'''
+ self._parms = []
+ kv = [(k, getattr(self, k))
+ for k in dir(self) if not k.startswith('_')]
+ self._parms = [(k, v) for k, v in kv if not hasattr(v, '__code__')]
+ [setattr(self, k, self.get_val(k, v, kw)) for k, v in self._parms]
+
+ def get_val(self, k, dflt, kw):
+ try:
+ return type(dflt)(kw.get(k, env(k, dflt)))
+ except Exception as ex:
+ # show reason clearer:
+ raise Exception(
+ 'Could not cast type %s - have %s %s %s %s' % (
+ type(dflt), k, dflt, kw, env(k, dflt)))
+
+
+class Colors(Cfg):
+ '''
+ Color namespae with efault color scheme (greenish).
+ The 'C' in the main method.
+ x_ -> x with ansi escapes in __init__, with env precedence
+ '''
+ O = '\x1B[0m'
+ GRAY = 240
+ CODE = 245
+ L = env('L', 66)
+ H1 = env('I', 158)
+ H2 = env('G', 115)
+ H3 = env('M', 72)
+ H4 = env('CODE', 66)
+ emph = env('I', 158)
+ ital = env('M', 72)
+
+ def get_val(self, k, dflt, kw):
+ # see Cfg for expl.
+ v = kw.get(k, env(k, dflt))
+ v = str(v)
+ if '\x1B' in v:
+ pass
+ elif '[' in v:
+ v = '\x1B' + v[2:]
+ else:
+ v = '\x1B[1;38;5;%sm' % v
+ return v
+
+ def H(s, lev):
+ return getattr(s, 'H%s' % lev, s.L)
+
+
+class Facts(Cfg):
+ ''' features config '''
+ debug = False
+ term_width = 80
+ no_print = False
+ bq_mark = '┃'
+ code_mark = '│'
+ light_bg = False
+ no_smart_indent = False
+ horiz_rule = '─'
+ single_line_mode = False
+ # left and right global indents:
+ indent = 0
+ rindent = 0
+ width = 0 # if set > 0 we set rindent accordingly
+ header_numbering = 50 # -1: off, min number of lines to do autonumbering
+ header_numb_level_min= 1 # min header level to show the numbers
+ header_numb_level_max= 6 # max header level to show the numbers
+ header_underlining = '*' # e.g. '*-' to underline H1 with *** and H2 with ---
+ opts_tbl_start = '-'
+ opts_tbl_end = ':'
+
+
+ def __init__(f, md, **kw):
+ # first check if the config contains color codes and set to C:
+ # now overriding our defaults with kw then with env
+ if md.split('\n', 1)[0] == md:
+ f.single_line_mode = True
+ f.indent = 0
+ f.setup(kw)
+ f.colr = Colors(); f.colr.setup(kw)
+
+# ------------------------------------------------ end config - begin rendering
+# helper funcs:
+def get_subseq_light_table_indent(l0):
+ p = '**' if l0.startswith('**') else '*'
+ keywrd, l1 = l0[2:].split(p, 1)
+ keywrd = l0[:2] + keywrd + p
+ l1 = l1
+ offs = 1 if l1 and l1[0] == ' ' else 2
+ return len(l0) - len(l1[offs:].lstrip()) - (2 * len(p))
+
+
+def block_quote_status(l, g):
+ 'blockquote'
+ if not l.startswith('>'):
+ return 0, l, ''
+ _ = l.split(' ', 1)
+ lev = len(_[0])
+ g['max_bq_depth'] = max(lev, g['max_bq_depth'])
+ return lev, _[1], _[0]
+
+
+h_rules_col = {'-': 'L', '_': 'H3', '*': 'H1'} # different colors
+list_markup = {'- ': ('\x03 ', 'L', '❖ '), '* ': ('\x04 ', 'H2', '▪ ')}
+h_rules = '---', '___', '***'
+def _main(md, f):
+ C, cur_colr = f.colr, 'cur_colr'
+ cols = int(f.term_width)
+ if f.width:
+ f.rindent = cols - f.indent - f.width + f.rindent
+ cols = cols - f.indent - f.rindent
+
+ g = {} # glob parsing state (current color, code blocks)
+
+
+
+ # ------------ line tools requiring facts instance, possible ctx g as well:
+ def is_opts_tbl(l, b=f.opts_tbl_start, e=f.opts_tbl_end):
+ fw = first_word(l)
+ if fw and fw.startswith(b) and fw.endswith(e):
+ return l.replace(fw, '*%s*' % fw[:-len(e)]), len(fw)
+ return l, None
+
+ def is_rule(l):
+ if not l[:3] in h_rules:
+ return
+ ll = len(l)
+ return True if l in (ll * '-', ll * '*', ll * '_') else False
+
+
+
+ # Line Tools:
+ first_word = lambda l: l.split(' ', 1)[0]
+ is_header = lambda l: l.startswith('#')
+ is_list = lambda l: l.lstrip()[:2] in list_markup
+ is_empty = lambda l: l.strip() == ''
+ is_md_link = lambda l: l[0] == '[' and 'http' in l and ']' in l
+
+ is_new_block = lambda l: (
+ is_header(l) or
+ is_list(l) or
+ is_opts_tbl(l)[1] or
+ is_empty(l) or
+ is_md_link(l) or
+ l[0] in ('\x02', ) or
+ is_rule(l)
+ )
+ # -------------------------------------------------------------------------
+
+
+ md = md.strip()
+
+ # FENCED CODE BLOCKS:
+ # we take them out before all parsing,see http://stackoverflow.com/a/587518
+ apo, apos = chr(96), chr(96) * 3 # chr 96 is backtick.
+ _ = r'^({apos}[^\n]+)\n((?:[^{apo}]+\n{apo}{apo})+)'.format(apos=apos, apo=apo)
+ fncd = re.compile(_, re.MULTILINE) # finds fenced code
+ md = md.replace('\n~~~', apos) # alternative markup for fenced
+ # remembering the blocks by their occurance number (len(g))
+ [set(g, len(g), '\n'.join(m.groups()) + apo ) for m in fncd.finditer(md)]
+ blocks = len(g)
+ for i in range(blocks):
+ md = md.replace(g[i], '\x02%s' % i)
+
+ g['max_bq_depth'] = 0
+
+
+ # LINESPROCESSOR:
+ lines, out = md.splitlines(), []
+
+ g['header_numbering'] = False
+ if f.header_numbering > -1 and len(lines) > f.header_numbering:
+ g['header_numbering'] = True
+ g['header_level'] = {} # storing the current header numberings
+
+ # remove boundary effects:
+ lines.insert(0, '')
+ lines.append('')
+
+ while lines:
+
+ line = lines.pop(0)
+ if is_empty(line):
+ out.append('')
+ continue
+ if debug:
+ print('procesing: ', line)
+ if is_rule(line):
+ out.append(getattr(C, h_rules_col[line[0]])+ (cols * f.horiz_rule))
+ continue
+
+ cb = None # indentd code blocks:
+ while line.startswith(' '):
+ cb = cb or []
+ cb.append(line[4:])
+ line = lines.pop(0)
+ if cb:
+ if out[-1] == '':
+ out.pop()
+ g[blocks] = '\n%s\n' % '\n'.join(cb)
+ out.append('\x02%s' % blocks)
+ blocks += 1
+ lines.insert(0, line)
+ continue
+
+ ssi = None # subseq indent for textwrap
+
+ # TEXTBLOCKS: Concat lines which must be wrapped:
+ bqm = '' # blockquote mark. e.g. '>>'.
+ bq_lev, line, bqm = block_quote_status(line, g)
+
+ src_line_nr = 0
+
+ # we derive the (static) opts table ssi for a new textblox:
+ line, opts_tbl_ssi = is_opts_tbl(line)
+ # now we find all other lines belonging to that text block and
+ # concat (pop from lines) all of them:
+ while ( lines and not line.endswith(' ')
+ and not is_header(line) ):
+
+ src_line_nr += 1
+ nl, l0 = lines[0], line.lstrip() # next line, this line
+
+ bqnl = block_quote_status(nl, g)
+ if bqnl[0] == bq_lev:
+ lines[0] = nl = bqnl[1] # remove redundant '>'
+
+ elif bqnl[0] != bq_lev and bqnl[0] > 0:
+ break # next line different blockquote level -> new text block
+
+ # finding subseq. indent for textwrap.fill:
+
+ # Little md violation: If first word is starred, we set a ssi to
+ # position: first line second word start.
+ # Gives easy 2 col wrappable tables when first col is hilited.
+
+ #if 'xyz' in line:
+ # import pdb; pdb.set_trace()
+ if ssi == None:
+ if is_list(l0):
+ # replace "- " and "* " with tags:
+ line = list_markup[l0[:2]][0] + l0[2:]
+ ssi = 2
+ elif opts_tbl_ssi:
+ ssi = opts_tbl_ssi
+ elif ( l0.startswith('*') and
+ not f.no_smart_indent and
+ src_line_nr == 1 ):
+ ssi = get_subseq_light_table_indent(l0)
+
+ if is_new_block(nl):
+ # line is now one wrapable textblock
+ if bqnl[0]: # block quote new line
+ # adapt next line to parse:
+ lines[0] = (bqnl[2] + ' ') + lines[0]
+ break
+ else:
+ line = line.rstrip() + ' ' + lines.pop(0).lstrip()
+
+ ssi = 0 if ssi is None else ssi
+ # lines are now blocks
+
+ g[cur_colr] = C.O # reset color
+ ind = len(line) - len(line.lstrip())
+ if bqm:
+ bqm += ' '
+ line = bqm + line
+
+
+ if is_header(line):
+ cont = line.lstrip('#')
+ level = len(line) - len(cont)
+ line = cont.lstrip()
+
+ u = getattr(f, 'header_underlining', '')
+ if len(u) >= level:
+ lines.insert(0, 3 * u[level-1])
+
+ if g['header_numbering']:
+ hl = g['header_level']
+ hl[level] = hl.get(level, 0) + 1
+ [set(hl, i, 0) for i in hl if i > level]
+ nr = '.'.join([str(hl[ll]) for ll in range(1, level + 1)])
+ if f.header_numb_level_max > level - 1:
+ if f.header_numb_level_min > 1:
+ nr = nr.split('.')[f.header_numb_level_min-1:]
+ nr = '.'.join(nr)
+ if nr:
+ line = nr + ' ' + line
+
+ g[cur_colr] = C.H(level)
+
+ # WRAP:
+ if len(line) > cols:
+ s = (bqm + ' ' * (ind + ssi))
+ line = fill(line, subsequent_indent=s, width=cols)
+ if is_md_link(line):
+ g[cur_colr] = C.GRAY
+ out.append(g[cur_colr] + line)
+
+
+ # --------------- Leaving line/block scanning, reWork complete document now
+ g[cur_colr] = C.O
+ out = '\n'.join(out)
+
+ # INLINE MARKUP, *, **, backticks
+ # Alternating replacements, e.g. code, emph. requires a first space char:
+ altern = lambda s, c, r: re.sub(
+ r'([^{c}]+){c}([^{c}]+){c}?'.format(c=c),
+ r'\1%s\2%s' % (r, g[cur_colr]), ' ' + s)[1:] # removing space again
+
+ # Star must be replaced, else the re would not work :((
+ # currently no way to find single stars and not process them..
+ out = out.replace('*', '\x01')
+ out = altern(out, apo , C.CODE) # code
+ out = altern(out, '\x01\x01', C.emph) # **
+ out = altern(out, '\x01' , C.ital) # *
+
+ # rearrange resets, to be *before* the line breaks, not after...
+ out = out.replace('\n' + C.O, C.O + '\n')
+ # ... so that we can look for blockquotes:
+ for i in range(g['max_bq_depth'], 0, -1):
+ # coloring, take header levels. bq_mark is "|":
+ m = ''
+ for j in range(1, i + 1):
+ m += C.H(j) + f.bq_mark
+ m += C.O
+ out = out.replace('\n' + '>' * i, '\n' + m)
+
+ # Insert back the stored code blocks:
+ code_fmt = lambda c: c.replace('\n', '\n%s%s %s' % (C.L, f.code_mark, C.CODE)
+ ).rsplit('\n', 1)[0]
+ for i in range(blocks):
+ out = out.replace('\x02%s' % i,
+ '%s%s%s' % (C.CODE, code_fmt(g[i]), C.O))
+ out = out.replace(apos + '\n', '') # before
+ out = out.replace(apos, '') # after
+
+ for k, v in list_markup.items():
+ out = out.replace(v[0], getattr(C, v[1]) + v[2] + C.O)
+
+ out = strip_it(out, C.O)
+ if not f.single_line_mode:
+ out = '\n' + out + '\n'
+ li, ri = f.indent * ' ', f.rindent * ' '
+ if li or ri:
+ out = li + out.replace('\n', '%s\n%s' % (ri, li))
+ out += C.O # reset
+ if not f.no_print:
+ print (out)
+ return out
+
+def strip_it(out, rst):
+ 'clumsy way to strip at start at end, including color resets'
+ sc = {' ': 1, rst: len(rst), '\n': 1}
+ while 1:
+ m = False
+ for k in sc:
+ if out.startswith(k):
+ out = out[sc[k]:]
+ m = True
+ if out.endswith(k):
+ out = out[:-sc[k]]
+ m = True
+ if m:
+ break
+ if not m:
+ break
+ return out
+
+
+def main(md, **kw):
+ f = Facts(md, **kw)
+ #return _main(md, f), f # we also return to the client the config
+ if debug or f.debug:
+ return _main(md, f), f # we also return to the client the config
+ try:
+ return _main(md, f), f # we also return to the client the config
+ except Exception as ex:
+ print (md) # clear text
+ print ('md error: %s %s ' % (f.colr.CODE, ex))
+
+def render(md, cols, **kw):
+ kw['term_width'] = cols
+ return main(md, **kw)[0]
+
+def get_help(cols, PY2):
+ ff = Facts('\n', term_width=cols)
+ md, C = __doc__, ff.colr
+ for o in ff, C:
+ mmd = ()
+ for k, d in sorted(o._parms):
+ v = getattr(o, k)
+ if o == ff: # need the perceived len here:
+ v = C.H2 + (str(u'%5s' % str(v)) if PY2 else '%5s' % v) + C.O
+ mmd += ('%s %s [%s]' % (v, k, d),)
+ md = md % ('\n'.join(mmd))
+ return md
+
+# allow to adapt $COLUMNS by setting $term_width:
+get_cols = lambda: (env('term_width') or
+ os.popen('tput cols 2>/dev/null').read().strip() or '80' )
+
+def sys_main():
+ import sys
+ PY2 = sys.version_info[0] == 2
+ if PY2:
+ reload(sys); sys.setdefaultencoding('utf-8')
+ import os
+ from stat import S_ISFIFO
+ err = None
+ try:
+ cols = get_cols()
+ except Exception as ex:
+ err = str(ex)
+ cols = 80
+ if S_ISFIFO(os.fstat(0).st_mode): # pipe mode
+ md = sys.stdin.read()
+ else:
+ if not len(sys.argv) > 1 or '-h' in sys.argv:
+ md = get_help(cols, PY2)
+ else:
+ md = sys.argv[1]
+ if os.path.exists(md):
+ with open(md) as fd:
+ md = fd.read()
+ if err:
+ print(err)
+ print md
+ else:
+ main(md, term_width=cols)
+
+# ============================================== Script Formatters ===========
+
+def format_bash(dev_help, cols, lines, script, *args):
+ '''
+ Renders help for a bash script nicely, given it follows some conventions.
+
+ These are:
+
+ 1. An `md_doc` function is required, returning general docu as markdown,
+ containing the string "<auto_command_doc>"
+
+ 2. All public functions must be in this format:
+
+ : 'optional doculines before...'
+ function myfunc {
+ : 'optional inner doculines (params...)'
+ <code lines>
+ }
+
+ 3. In the command parsing part then this: `show_help $sourced $0 $*`
+ with that function elsewhere in your tools:
+
+ show_help () {
+ local sourced=$1; shift
+ local dev_help=false
+ test "${2:-}x" == "make_docx" && { md_doc; exit 0; }
+ test "${@: -1}" == "-hh" 2>/dev/null && dev_help=true || {
+ test "${@: -1}" == "-h" 2>/dev/null || return 0
+ }
+ local cols=`stty size | cut -d ' ' -f 2`
+ mdvl -f $dev_help $cols "$*"; $sourced && return 1 || exit
+ }
+
+ '''
+ dev_help = True if str(dev_help) in ('True', 'true', '1') else False
+ single_func_doc=False; l = lines; funcs = []
+ start = ": '"
+ is_func = lambda l: l.startswith('function ')
+ is_cmt_end = lambda l: l.rstrip().endswith("'")
+ is_cmt_start= lambda l: l.lstrip().startswith(start)
+
+ def clean(s, head_sub):
+ s = s.strip()
+ s = s[len(start):] if s.startswith(start) else s
+ s = s[:-1] if s.endswith("'") else s
+ s = (('\n' + s).replace('\n#', '\n%s#' % head_sub))[1:]
+ return s
+
+ def render_func(m, single_func_doc):
+ fn = m.keys()[0]
+ hf, hs = ('# `Function` **%s**', '##') if single_func_doc else (
+ '### %s', '###')
+ nr, pre, post, code = m.values()[0]
+ md = [hf % fn]
+ pre and md.append(clean('\n'.join(pre), hs))
+ post and md.append(clean('\n'.join(post), hs))
+ if code:
+ code = '\n'.join(code)
+ if post or pre:
+ md.append('')
+ md.append(code)
+ md.extend(['---', ''])
+ md = '\n'.join(md)
+ return md
+
+ fm = {}
+ for i in range(len(l)):
+ if is_func(l[i]):
+ fn = (l[i] + ' ').split(' ', 2)[1]
+ funcs.append({fn: [i, [], [], []]})
+ fm[fn] = len(funcs) - 1
+
+ if not '-h' in args[0]:
+ match = args[0]
+ f = []
+ for m in funcs:
+ if match in m.keys()[0]:
+ f.append(m)
+ if f:
+ funcs = f
+ single_func_doc = True
+
+ for m in funcs:
+ nr, pre, post, code = m.values()[0]
+ # pre:
+ if is_cmt_end(l[nr-1]):
+ i = nr
+ while True:
+ i = i -1
+ pre.insert(0, l[i])
+ if is_cmt_start(l[i]):
+ break
+ if i > 1 and l[i-1].rstrip().endswith('}'):
+ pre = [] # err
+ break
+
+ # post:
+ i = nr
+ if is_cmt_start(l[nr + 1]):
+ while True:
+ i += 1
+ post.append(l[i][4:])
+ if is_cmt_end(l[i]) and not l[i].strip() == start:
+ break
+ if l[i+1].rstrip().endswith('}'):
+ post = []; i = nr # err
+ break
+ if dev_help:
+ i += 1
+ while True:
+ if l[i].strip() == '}':
+ break
+ code.append(l[i])
+ i += 1
+
+ Facts.indent = 0
+ if single_func_doc:
+ for m in funcs:
+ md = render_func(m, single_func_doc)
+ main(md, term_width=cols)
+ print
+ return
+
+ # now the full doc. convention is to call with make_doc arg:
+ Facts.no_print = True
+ Facts.header_numbering = 10
+ Facts.header_numb_level_min = 2
+ Facts.header_numb_level_max = 2
+
+ full = os.popen(script.split(' ')[0] + ' make_doc').read()
+ acd = '<auto_command_doc>'
+ full = full.replace(acd, '## Commands\n\n' + acd)
+ md = main(full, term_width=cols)[0]
+
+ Facts.header_numbering = -1
+ rfuncs, sep = '', '\n\n'
+ if dev_help:
+ sep = ''
+ for m in funcs:
+ rfuncs = rfuncs + sep + render_func(m, single_func_doc)
+ mdf = main(rfuncs, term_width=cols)[0]
+ print(md.replace(acd, mdf))
+
+
+
+
+def format_file(dev_help, cols, fn, *args):
+ if not os.path.exists(fn):
+ raise Exception('Not found' + fn)
+ with open(fn) as fd:
+ lines = fd.read().splitlines()
+ if 'bash' in lines[0]:
+ format_bash(dev_help, cols, lines, fn, *args)
+ else:
+ raise Exception('Not supported format')
+
+if __name__ == '__main__':
+ import os, sys
+ if len(sys.argv) > 1 and sys.argv[1] == '-f':
+ format_file(*sys.argv[2:])
+ else:
+ sys_main()
+
diff --git a/commonprofile b/commonprofile
@@ -16,6 +16,7 @@ alias g='git'
alias r='ranger'
### SIMPLE FUNCTIONS ###
+mark() { mdvl $1 | less }
cd() { builtin cd -P "$@"; ls; } # List contents after cding
mkcd() { mkdir -p -- "$1" && cd -P -- "$1" } # Make dir and cd at the same time
procinfo() { ps -aux | grep $1 } # Get info about a process (by name)