|
| 1 | +#!/usr/bin/env python |
| 2 | +# coding: utf-8 |
| 3 | + |
| 4 | +from os import path |
| 5 | +from textwrap import dedent |
| 6 | +from operator import itemgetter |
| 7 | +import re |
| 8 | +import panflute as pf |
| 9 | +from comment_parser import comment_parser |
| 10 | + |
| 11 | + |
| 12 | +def is_include_line(elem): |
| 13 | + if len(elem.content) < 3: |
| 14 | + return False |
| 15 | + elif not all(isinstance(x, (pf.Str, pf.Space)) for x in elem.content): |
| 16 | + return False |
| 17 | + elif elem.content[0].text != '<<<': |
| 18 | + return False |
| 19 | + elif type(elem.content[1]) != pf.Space: |
| 20 | + return False |
| 21 | + else: |
| 22 | + return True |
| 23 | + |
| 24 | + |
| 25 | +DEFAULT_PARAMS = {'filename': None, 'region': 'snippet'} |
| 26 | + |
| 27 | + |
| 28 | +def get_args(elem): |
| 29 | + fn, region = itemgetter('filename', 'region')(DEFAULT_PARAMS) |
| 30 | + params = pf.stringify(elem, newlines=False).split(maxsplit=2) |
| 31 | + if params[1][0:2] == '@/': |
| 32 | + fn = './' + params[1][2:] |
| 33 | + else: |
| 34 | + raise ValueError(f'[code import] {params} should begin with @/') |
| 35 | + if len(params) > 2: |
| 36 | + region = params[2] |
| 37 | + return fn, region |
| 38 | + |
| 39 | + |
| 40 | +SUPPORTED_SUBEXT = { |
| 41 | + 'sh': ('session'), |
| 42 | + 'conf': ('apache', 'nginx') |
| 43 | +} |
| 44 | + |
| 45 | + |
| 46 | +def get_code_type(extension, subextension): |
| 47 | + """convert a file extension to a prism.js language type alias |
| 48 | +
|
| 49 | + see https://github.com/PrismJS/prism/issues/178 |
| 50 | +
|
| 51 | + Args: |
| 52 | + extension (str): The file extension (without dot) |
| 53 | + subtension (str): "sub" extension |
| 54 | + e.g. "session" in ".session.sh" for shell-session instead of bash |
| 55 | +
|
| 56 | + Returns: |
| 57 | + str: prism.js language type alias (see https://github.com/PrismJS/prism/issues/178) |
| 58 | + """ |
| 59 | + valid_subexts = SUPPORTED_SUBEXT.get(extension) |
| 60 | + if valid_subexts is None or subextension not in valid_subexts: |
| 61 | + subextension = '' |
| 62 | + |
| 63 | + return { |
| 64 | + # js, jsx, ts, tsx, html, md, py |
| 65 | + 'sh': 'bash', |
| 66 | + 'sessionsh': 'shell-session', |
| 67 | + 'apacheconf': 'apacheconf', |
| 68 | + 'nginxconf': 'nginx' |
| 69 | + }.get(subextension + extension, extension) |
| 70 | + |
| 71 | + |
| 72 | +REGION_REGEXPS = ( |
| 73 | + r'^\/\/ ?#?((?:end)?region) ([\w*-]+)$', # javascript, typescript, java |
| 74 | + r'^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$', # css, less, scss |
| 75 | + r'^#pragma ((?:end)?region) ([\w*-]+)$', # C, C++ |
| 76 | + r'^<!-- #?((?:end)?region) ([\w*-]+) -->$', # HTML, markdown |
| 77 | + r'^#((?:End )Region) ([\w*-]+)$', # Visual Basic |
| 78 | + r'^::#((?:end)region) ([\w*-]+)$', # Bat |
| 79 | + # Csharp, PHP, Powershell, Python, perl & misc |
| 80 | + r'^# ?((?:end)?region) ([\w*-]+)$' |
| 81 | +) |
| 82 | + |
| 83 | +def test_line(line, regexp, regionName, end=False): |
| 84 | + search = re.search(regexp, line.strip()) |
| 85 | + if search is None: |
| 86 | + return |
| 87 | + (tag, name) = search.groups() |
| 88 | + tag_exist = tag is not None and len(tag) > 0 |
| 89 | + name_exist = name is not None and len(name) > 0 |
| 90 | + name_is_valid = name == regionName |
| 91 | + start_or_close = re.compile( |
| 92 | + r'^[Ee]nd ?[rR]egion$' if end else r'^[rR]egion$') |
| 93 | + tag_is_valid = start_or_close.match(tag) is not None |
| 94 | + return tag_exist and name_exist and name_is_valid and tag_is_valid |
| 95 | + |
| 96 | + |
| 97 | +def find_region(lines, regionName): |
| 98 | + regexp = None |
| 99 | + start = -1 |
| 100 | + |
| 101 | + for lineId, line in enumerate(lines): |
| 102 | + if regexp is None: |
| 103 | + for reg in REGION_REGEXPS: |
| 104 | + if test_line(line, reg, regionName): |
| 105 | + start = lineId + 1 |
| 106 | + regexp = reg |
| 107 | + break |
| 108 | + elif test_line(line, regexp, regionName, True): |
| 109 | + return {'start': start, 'end': lineId, 'regexp': regexp} |
| 110 | + |
| 111 | + return None |
| 112 | + |
| 113 | + |
| 114 | +def extract_region(code, key, filepath): |
| 115 | + lines = code.splitlines() |
| 116 | + region_limits = find_region(lines, key) |
| 117 | + |
| 118 | + if region_limits is not None: |
| 119 | + regexp = re.compile(region_limits['regexp']) |
| 120 | + subset = lines[region_limits['start']:region_limits['end']] |
| 121 | + return '\n'.join(filter(lambda x: not regexp.match(x.strip()), subset)) |
| 122 | + |
| 123 | + if key is not None and region_limits is None: |
| 124 | + raise ValueError(f'[code import] {filepath}#{key} not found') |
| 125 | + return code |
| 126 | + |
| 127 | + |
| 128 | +def action(elem, doc): |
| 129 | + if isinstance(elem, pf.Para) and is_include_line(elem): |
| 130 | + raw_path = pf.stringify(elem, newlines=False).split( |
| 131 | + maxsplit=1)[1].strip() |
| 132 | + if raw_path[0:2] == '@/': |
| 133 | + raw_path = './' + raw_path[2:] |
| 134 | + else: |
| 135 | + raise ValueError(f'[code import] {raw_path} should begin with @/') |
| 136 | + |
| 137 | + rawPathRegexp = r'^(.+(?:\.([a-z]+)))(?:#([\w-]+))?(?: ?({\d(?:[,-]\d)?}))?$' |
| 138 | + search = re.search(rawPathRegexp, raw_path) |
| 139 | + |
| 140 | + if search is None: |
| 141 | + raise ValueError(f'[code import] invalid parameter {raw_path}') |
| 142 | + |
| 143 | + (filepath, extension, region_name, meta) = search.groups() |
| 144 | + |
| 145 | + if not path.isfile(filepath): |
| 146 | + raise ValueError(f'[code import] file not found: {filepath}') |
| 147 | + |
| 148 | + basename = path.basename(filepath).split('.') |
| 149 | + extension = basename[-1] |
| 150 | + subextension = '' |
| 151 | + if len(basename) > 2: |
| 152 | + subextension = basename[-2] |
| 153 | + |
| 154 | + with open(filepath) as f: |
| 155 | + raw = f.read() |
| 156 | + |
| 157 | + region = extract_region(raw, region_name, filepath) |
| 158 | + return pf.CodeBlock(dedent(region), '', [get_code_type(extension, subextension)]) |
| 159 | + |
| 160 | + |
| 161 | +def main(doc=None): |
| 162 | + return pf.run_filter(action, doc=doc) |
| 163 | + |
| 164 | + |
| 165 | +if __name__ == '__main__': |
| 166 | + main() |
0 commit comments