From c370d35778ee892a9ed9344d50933cebdad02f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= <hugo@lysator.liu.se> Date: Tue, 23 May 2023 01:07:17 +0200 Subject: [PATCH] Change from print-based to data-based output. --- .flake8 | 1 + highlight.css | 45 ++ main.py | 1251 ++++++++++++++++++++++--------------- style.css | 45 +- tests/test_intersperse.py | 7 + 5 files changed, 799 insertions(+), 550 deletions(-) create mode 100644 highlight.css create mode 100644 tests/test_intersperse.py diff --git a/.flake8 b/.flake8 index 4dbcc0f..cc60e25 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,5 @@ [flake8] +extend-ignore = D105 per-file-ignores = tests/*: D103 max-line-length = 99 diff --git a/highlight.css b/highlight.css new file mode 100644 index 0000000..4949f3b --- /dev/null +++ b/highlight.css @@ -0,0 +1,45 @@ +/* +.case { color: ; } +.splat { color: ; } +.array { color: ; } +.parse-error { color: ; } +.parameter { color: ; } +.string { color: ; } +.regex { color: ; } +.invoke { color: ; } +.default { color: ; } +.call { color: ; } +.qr { color: ; } +.lambda { color: ; } +.number { color: ; } +.regex-body { color: ; } +.call-method { color: ; } +*/ + +.literal { color: green; } +.true {} +.false {} +.undef {} + +.keyword { color: orange; } +.class {} +.or {} +.define {} +.unless {} +.if {} +.else {} +.function {} +.and {} +.in {} + +.type { color: darkgreen; } +.qn { color: darkgreen; } + +.var { color: blue; } +.str-var { color: blue; } + +.name { color: red; } + +.string { + color: olive; +} diff --git a/main.py b/main.py index 5e8ca79..baec3d6 100644 --- a/main.py +++ b/main.py @@ -14,18 +14,26 @@ import html import json import sys from typing import ( - Union, - Literal, Any, - TypeAlias, + Literal, Tuple, + TypeAlias, + TypeVar, + Union, ) +from collections.abc import Sequence, Generator +from dataclasses import dataclass HashEntry: TypeAlias = Union[Tuple[Literal['=>'], str, Any], Tuple[Literal['+>'], str, Any], Tuple[Literal['splat-hash'], Any]] +T = TypeVar('T') +U = TypeVar('U') + +Context: TypeAlias = list['str'] + match sys.argv: case [_, d, *_]: filename = d @@ -40,28 +48,71 @@ data = info param_doc: dict[str, str] = {} +@dataclass +class Tag: + """An item with basic metadata.""" + + # item: Any # str | 'Tag' | Sequence[str | 'Tag'] + # item: str | 'Tag' | Sequence[str | 'Tag'] + item: Any + tags: Sequence[str] + + def __str__(self) -> str: + inner: str + if isinstance(self.item, str): + inner = self.item + elif isinstance(self.item, Tag): + inner = str(self.item) + else: + inner = ''.join(str(i) for i in self.item) + + tags = ' '.join(self.tags) + return f'<span class="{tags}">{inner}</span>' + + +all_tags: set[str] = set() + + +def tag(item: str | Tag | Sequence[str | Tag], *tags: str) -> Tag: + """Tag item with tags.""" + global all_tags + all_tags |= set(tags) + return Tag(item, tags=tags) + + +def ind(level: int) -> str: + """Returnu a string for indentation.""" + return ' '*level*2 + + def print_hash(hash: list[HashEntry], indent: int, - context: list[str]) -> None: + context: Context) -> Tag: """Print the contents of a puppet hash literal.""" if not hash: - return + return tag('') # namelen = 0 + items: list[str | Tag] = [] for item in hash: match item: case ['=>', key, value]: - print(' '*indent*2, end='') - parse(key, indent, context) - # print(' => ', end='') - print(' ⇒ ', end='') - parse(value, indent, context) + items += [ + ind(indent), + parse(key, indent, context), + ' ⇒ ', + parse(value, indent, context), + ] case ['splat-hash', value]: - print(' '*indent*2, end='') - print('* => ', end='') - parse(value, indent, context) + items += [ + ind(indent), + '* ⇒ ', + parse(value, indent, context), + ] case _: - print(f'<span class="parse-error">[|[{item}]|]</span>') - print(',') + items += [tag(f'[|[{item}]|]', 'parse-error'), '\n'] + items += [',', '\n'] + + return tag(items) def ops_namelen(ops: list[HashEntry]) -> int: @@ -82,9 +133,6 @@ def ops_namelen(ops: list[HashEntry]) -> int: def print_array(arr: list[Any], indent: int, context: list[str]) -> None: """Print a puppet array literal.""" - if not arr: - print('[]', end='') - return print('[') for item in arr: print(' '*(indent+1)*2, end='') @@ -93,7 +141,7 @@ def print_array(arr: list[Any], indent: int, context: list[str]) -> None: print(' '*indent*2 + ']', end='') -def print_var(x: str, dollar: bool = True) -> None: +def print_var(x: str, dollar: bool = True) -> Tag: """ Print the given variable. @@ -105,10 +153,10 @@ def print_var(x: str, dollar: bool = True) -> None: """ dol = '$' if dollar else '' if doc := param_doc.get(x): - print(f'<span class="var">{dol}{x}<div class="documentation">{doc}</div></span>', - end='') + s = f'{dol}{x}<div class="documentation">{doc}</div>' + return tag(s, 'var') else: - print(f'<span class="var">{dol}{x}</span>', end='') + return tag(f'{dol}{x}', 'var') # TODO strip leading colons when looking up documentation @@ -127,7 +175,40 @@ symbols: dict[str, str] = { } -def parse(form: Any, indent: int, context: list[str]) -> None: +def intersperse(inset: U, sequence: Sequence[T]) -> Generator[U | T, None, None]: + """Intersperse the inset between each element in sequence.""" + if not sequence: + return + + yield sequence[0] + for item in sequence[1:]: + yield inset + yield item + + +def handle_case_body(forms: list[dict[str, Any]], + indent: int, context: Context) -> Tag: + """Handle case body when parsing AST.""" + ret: list[Tag | str] = [] + for form in forms: + when = form['when'] + then = form['then'] + ret += [ind(indent+1)] + # cases = [] + + ret += list(intersperse(', ', list(parse(item, indent+1, context) + for item in when))) + + ret += [':', ' ', '{', '\n'] + + for item in then: + ret += [ind(indent+2), parse(item, indent+2, context), '\n'] + ret += [ind(indent+1), '},', '\n'] + + return tag(ret) + + +def parse(form: Any, indent: int, context: list[str]) -> Tag: """ Print everything from a puppet parse tree. @@ -140,227 +221,240 @@ def parse(form: Any, indent: int, context: list[str]) -> None: # Sorted per `sort -V` match form: case None: - print('<span class="undef">undef</span>', end='') + return tag('undef', 'literal', 'undef') case True: - print('<span class="true">true</span>', end='') + return tag('true', 'literal', 'true') case False: - print('<span class="false">false</span>', end='') + return tag('false', 'literal', 'false') case ['access', how, *args]: - print('<span class="compound-type">', end='') - parse(how, indent, context) - print('[', end='') - first = True - for arg in args: - if not first: - print(', ', end='') - # TODO newlines? - parse(arg, indent, context) - first = False - print(']', end='') - print('</span>', end='') + # TODO newlines? + t = [parse(arg, indent, context) for arg in args] + return tag([ + parse(how, indent, context), + '[', + *intersperse(', ', t), + ']', + ], 'access') case ['and', a, b]: - parse(a, indent, context) - print(' and ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' ', + tag('and', 'keyword', 'and'), + ' ', + parse(b, indent, context), + ]) + + case ['array']: + return tag('[]', 'array') case ['array', *items]: - print_array(items, indent+1, context) + return tag([ + '[', + *([ind(indent+2), + parse(item, indent+1, context), + ','] for item in items), + ind(indent), + ']', + ], 'array') case ['call', {'functor': func, 'args': args}]: - print('<span class="call">', end='') - parse(func, indent, context) - print('(', end='') - first = True - for arg in args: - if not first: - print(', ', end='') - first = False - parse(arg, indent, context) - print(')', end='') - print('</span>', end='') + return tag([ + parse(func, indent, context), + '(', + *intersperse(', ', [parse(arg, indent, context) + for arg in args]), + ')', + ], 'call') case ['call-method', func]: - print('<span class="call-method">', end='') - parse(func['functor'], indent, context) + items = [ + parse(func['functor'], indent, context) + ] - first = True if not ('block' in func and func['args'] == []): - print('(', end='') - for x in func['args']: - if not first: - print(', ', end='') - first = False - parse(x, indent, context) - # print(', '.join(parse(x, indent, context) for x in func['args']), end='') - print(')', end='') + items += [ + '(', + *intersperse(', ', [parse(x, indent, context) + for x in func['args']]), + ')', + ] if 'block' in func: - parse(func['block'], indent+1, context) + items += [parse(func['block'], indent+1, context)] - print('</span>', end='') - - # print() + return tag(items, 'call-method') case ['case', test, forms]: - print('case ', end='') - parse(test, indent, context) - print(' {') - for form in forms: - when = form['when'] - then = form['then'] - print(' '*(indent+1)*2, end='') - # print('{', end='') - cases = [] - first = True - for item in when: - if not first: - print(', ', end='') - first = False - parse(item, indent+1, context) - print(': {') - for item in then: - print(' '*(indent+2)*2, end='') - parse(item, indent+2, context) - print() - print(' '*(indent+1)*2+'},') - print(' '*indent*2+'}', end='') + items = [ + tag('case', 'keyword', 'case'), + ' ', + parse(test, indent, context), + ' ', '{', '\n', + handle_case_body(forms, indent, context), + ind(indent), + '}', + ] + + return tag(items) case ['class', {'name': name, 'body': body, **rest}]: - print(' '*indent*2 + - f'<span class="class">class</span> <span class="name">{name}</span> ', end='') + items = [] + items += [ + ind(indent), + tag('class', 'keyword', 'class'), + ' ', + tag(name, 'name'), + ' ', + ] + if 'params' in rest: - print('(') + items += ['(', '\n'] for name, data in rest['params'].items(): - print(' '*(indent+1)*2, end='') + items += [ind(indent+1)] if 'type' in data: - print('<span class="type">', end='') - parse(data['type'], indent, context) - print('</span> ', end='') - print(f'<span class="var">${name}</span>', end='') + tt = parse(data['type'], indent, context) + # print('type =', tt, file=sys.stderr) + items += [tag(tt, 'type'), + ' '] + items += [tag(f'${name}', 'var')] if 'value' in data: - print(' = ', end='') - parse(data.get('value'), indent, context) - print(',', end='') - print() - print(' '*indent*2 + ') {') + items += [ + ' = ', + parse(data.get('value'), indent, context), + ] + items += [',', '\n'] + items += [ind(indent), ')', ' ', '{', '\n'] else: - print('{') + items += ['{', '\n'] for entry in body: - print(' '*(indent+1)*2, end='') - parse(entry, indent+1, context) - print() - - print(' '*indent*2+'}') + items += [ind(indent+1), + parse(entry, indent+1, context), + '\n'] + items += [ind(indent), '}' + '\n'] + return tag(items) case ['concat', *args]: - print('<span class="string">"', end='') + items = ['"'] for item in args: match item: case ['str', thingy]: - print('<span class="str-var">${', end='') - # print_var(x, dollar=False) - parse(thingy, indent, ['str'] + context) - print('}</span>', end='') + content = parse(thingy, indent, ['str'] + context) + items += [tag(f'${content}', 'str-var')] case s: - # print(s, file=sys.stderr) - print(s - .replace('"', '\\"') - .replace('\n', '\\n'), - end='') - - print('"</span>', end='') + items += [s + .replace('"', '\\"') + .replace('\n', '\\n')] + items += '"' + return tag(items, 'string') case ['collect', {'type': t, 'query': q}]: - parse(t, indent, context) - print(' ', end='') - parse(q, indent, context) + return tag([parse(t, indent, context), + ' ', + parse(q, indent, context)]) case ['default']: - print('<span class="default">default</span>', end='') + return tag('default', 'keyword', 'default') case ['define', {'name': name, 'body': body, **rest}]: - print(' '*indent*2 + - f'<span class="define">define</span> <span class="name">{name}</span> ', end='') + items = [ind(indent), + tag('define', 'keyword', 'define'), + ' ', + tag(name, 'name'), + ' '] if params := rest.get('params'): - print('(') + items += ['(', '\n'] for name, data in params.items(): - print(' '*(indent+1)*2, end='') + items += [ind(indent+1)] if 'type' in data: - print('<span class="type">', end='') - parse(data['type'], indent, context) - print('</span> ', end='') + items += [tag(parse(data['type'], indent, context), + 'type'), + ' '] # print(f'<span class="var">${name}</span>', end='') - print_var(name) + items += [print_var(name)] if 'value' in data: - print(' = ', end='') - parse(data.get('value'), indent, context) - print(',', end='') - print() + items += [ + ' = ', + parse(data.get('value'), indent, context), + ',', + ] + items += ['\n'] - print(' '*indent*2 + ') ', end='') - print('{') + items += [ind(indent), ')'] + + items += ['{', '\n'] for entry in body: - print(' '*(indent+1)*2, end='') - parse(entry, indent+1, context) - print() + items += [ind(indent+1), + parse(entry, indent+1, context), + '\n'] + + items += [ind(indent), '}'] - print(' '*indent*2 + '}', end='') + return tag(items) case ['exported-query']: - print('<<| |>>', end='') + return tag(['<<|', ' ', '|>>']) case ['exported-query', arg]: - print('<<| ', end='') - parse(arg, indent, context) - print(' |>>', end='') + return tag(['<<|', ' ', + parse(arg, indent, context), + ' ', '|>>']) case ['function', {'name': name, 'body': body, **rest}]: - print(f'function {name} (') + items = [] + items += [tag('function', 'keyword', 'function'), + ' ', name, ' ', '(', '\n'] if 'params' in rest: for name, attributes in rest['params'].items(): - print(' '*(indent+1)*2, end='') + items += [ind(indent+1)] if 'type' in attributes: - parse(attributes['type'], indent, context) - print(' ', end='') - print(f'${name}', end='') + items += [parse(attributes['type'], indent, context), + ' '] + items += [f'${name}'] if 'value' in attributes: - print(' = ', end='') - parse(attributes['value'], indent, context) - print(',') - print(')', end='') + items += [ + ' = ', + parse(attributes['value'], indent, context), + ] + items += [',', '\n'] + items += [')'] if 'returns' in rest: - print(' >> ', end='') - parse(rest['returns'], indent, context) - print(' {') + items += [' >> ', + parse(rest['returns'], indent, context)] + items += [' ', '{', '\n'] for item in body: - print(' '*(indent+1)*2, end='') - parse(item, indent+1, context) - print() - print('}') + items += [ + ind(indent+1), + parse(item, indent+1, context), + '\n', + ] + items += ['}', '\n'] + return tag(items) + + case ['hash']: + return tag('{}') case ['hash', *hash]: - if not hash: - print('{}', end='') - else: - print('{') - print_hash(hash, indent+1, context) - print(' '*indent*2, end='') - print('}', end='') + return tag([ + '{', '\n', + print_hash(hash, indent+1, context), + ind(indent), + '}', + ]) case ['heredoc', {'text': text}]: # TODO Should variables be interploated? @@ -372,110 +466,136 @@ def parse(form: Any, indent: int, context: list[str]) -> None: # NOTE text can be a number of types # It can be an explicit "concat", # it can be an untagged string - print('@("EOF")') - parse(text, indent + 1, ['heredoc'] + context) - print(' '*(indent+1)*2 + '| EOF', end='') + tag(['@("EOF")', '\n', + parse(text, indent + 1, ['heredoc'] + context), + ind(indent+1), + '|', ' ', 'EOF', + ]) case ['if', {'test': test, **rest}]: - print('if ', end='') - parse(test, indent, context) - print(' {') + items = [] + items += [ + tag('if', 'keyword', 'if'), + ' ', + parse(test, indent, context), + ' ', '{', '\n', + ] if 'then' in rest: for item in rest['then']: - print(' '*(indent+1)*2, end='') - parse(item, indent+1, context) - print() - print(' '*indent*2 + '} ', end='') + items += [ + ind(indent+1), + parse(item, indent+1, context), + '\n', + ] + items += [ind(indent), '}', ' '] if 'else' in rest: + items = [] match rest['else']: case [['if', *rest]]: - print('els', end='') - parse(['if', *rest], indent, context) + # TODO propper tagging + items += ['els', + parse(['if', *rest], indent, context)] case el: - print('else {') + items += [tag('else', 'keyword', 'else'), + ' ', '{', '\n'] for item in el: - print(' '*(indent+1)*2, end='') - parse(item, indent+1, context) - print() - print(' '*indent*2+'}', end='') + items += [ + ind(indent+1), + parse(item, indent+1, context), + '\n', + ] + items += [ + ind(indent), + '}', + ] + return tag(items) case ['in', needle, stack]: - parse(needle, indent, context) - print(' in ', end='') - parse(stack, indent, context) + return tag([ + parse(needle, indent, context), + ' ', tag('in', 'keyword', 'in'), ' ', + parse(stack, indent, context), + ]) case ['invoke', {'functor': func, 'args': args}]: - print('<span class="invoke">', end='') - parse(func, indent, context) - print(' ', end='') + items = [ + parse(func, indent, context), + ' ', + ] if len(args) == 1: - parse(args[0], indent+1, context) + items += [parse(args[0], indent+1, context)] else: - print(args) - print('(', end='') - first = True - for arg in args: - if not first: - print(', ', end='') - first = False - parse(arg, indent+1, context) - # print(' '*indent*2, end='') - print(')', end='') - # print() - print('</span>', end='') + items += [ + args, + '\n', + '(', + *intersperse(', ', [parse(arg, indent+1, context) + for arg in args]), + ')', + ] + return tag(items, 'invoke') case ['nop']: - # print() - pass + return tag('') case ['lambda', {'params': params, 'body': body}]: - print('<span class="lambda">', end='') - args = ', '.join(f'${x}' for x in params.keys()) - print(f' |{args}| {{') + items = [] + args = [', '.join(f'${x}' for x in params.keys())] + items += [f' |{args}| {{', '\n'] for entry in body: - print(' '*indent*2, end='') - parse(entry, indent, context) - print() - print(' '*(indent-1)*2 + '}', end='') - print('</span>', end='') + items += [ + ind(indent), + parse(entry, indent, context), + '\n', + ] + items += [ind(indent-1), '}'] + return tag(items, 'lambda') + case ['and', a, b]: - parse(a, indent, context) - print(' and ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' ', tag('and', 'keyword', 'and'), ' ', + parse(b, indent, context), + ]) case ['or', a, b]: - parse(a, indent, context) - print(' or ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' ', tag('or', 'keyword', 'or'), ' ', + parse(b, indent, context), + ]) case ['paren', *forms]: - print('(', end='') - for form in forms: - parse(form, indent+1, context) - print(')', end='') + return tag([ + '(', + *(parse(form, indent+1, context) + for form in forms), + ')', + ]) # Qualified name? case ['qn', x]: - print(f'<span class="qn">{x}</span>', end='') + return tag(x, 'qn') # Qualified resource? case ['qr', x]: - print(f'<span class="qr">{x}</span>', end='') + return tag(x, 'qr') case ['regexp', s]: - print(f'<span class="regex">/<span class="regex-body">{s}</span>/</span>', - end='') + return tag(['/', tag(s, 'regex-body'), '/'], 'regex') case ['resource', {'type': t, 'bodies': [body]}]: - parse(t, indent, context) - print(' { ', end='') - parse(body['title'], indent, context) - print(':') + items = [ + parse(t, indent, context), + ' ', '{', ' ', + parse(body['title'], indent, context), + ':', '\n', + ] ops = body['ops'] namelen = ops_namelen(ops) @@ -483,298 +603,401 @@ def parse(form: Any, indent: int, context: list[str]) -> None: for item in ops: match item: case ['=>', key, value]: - print(' '*(indent+1)*2, end='') pad = namelen - len(key) - print(f'<span class="parameter">{key}</span>', end='') - # print(' '*pad + ' => ', end='') - print(' '*pad + ' ⇒ ', end='') - parse(value, indent+1, context) - print(',') + items += [ + ind(indent+1), + tag(key, 'parameter'), + ' '*pad, ' ⇒ ', + parse(value, indent+1, context), + ',', '\n', + ] case ['splat-hash', value]: - print(' '*(indent+1)*2, end='') - print('<span class="parameter splat">*</span>', end='') - # print(' '*(namelen - 1) + ' => ', end='') - print(' '*(namelen - 1) + ' ⇒ ', end='') - parse(value, indent+1, context) - print(',') + items += [ + ind(indent+1), + tag('*', 'parameter', 'splat'), + ' '*(namelen-1), + ' ⇒ ', + parse(value, indent+1, context), + ',', '\n', + ] case _: raise Exception("Unexpected item in resource:", item) - print(' '*indent*2+'}', end='') - case ['resource', {'type': t, - 'bodies': bodies}]: - parse(t, indent, context) - print(' {') - for body in bodies: - print(' '*(indent+1)*2, end='') - parse(body['title'], indent, context) - print(':') - ops = body['ops'] - - namelen = ops_namelen(ops) - - for item in ops: - match item: - case ['=>', key, value]: - print(' '*(indent+2)*2, end='') - pad = namelen - len(key) - print(f'<span class="parameter">{key}</span>', end='') - # print(' '*pad + ' => ', end='') - print(' '*pad + ' ⇒ ', end='') - parse(value, indent+2, context) - print(',') - - case ['splat-hash', value]: - print(' '*(indent+2)*2, end='') - print('<span class="parameter splat">*</span>', end='') - # print(' '*(namelen - 1) + ' => ', end='') - print(' '*(namelen - 1) + ' ⇒ ', end='') - parse(value, indent+2, context) - print(',') - - case _: - raise Exception("Unexpected item in resource:", item) - - print(' '*(indent+1)*2 + ';') - print(' '*indent*2+'}', end='') + items += [ + ind(indent), + '}', + ] + + return tag(items) + + # case ['resource', {'type': t, + # 'bodies': bodies}]: + # items = [] + # items += [ + # parse(t, indent, context), + # ' {', + # ] + # for body in bodies: + # items += [ + # ind(indent+1), + # parse(body['title'], indent, context), + # ':', '\n', + # ] + + # ops = body['ops'] + # namelen = ops_namelen(ops) + + # for item in ops: + # match item: + # case ['=>', key, value]: + # pad = namelen - len(key) + # items += [ + # ind(indent+2), + # tag(key, 'parameter'), + # ' '*pad, + # ' ⇒ ', + # parse(value, indent+2, context), + # ',', '\n', + # ] + + # case ['splat-hash', value]: + # items += [ + # ind(indent+2), + # tag('*', 'parameter', 'splat'), + # ' '*(namelen - 1), + # ' ⇒ ', + # parse(value, indent+2, context), + # ',', '\n', + # ] + + # case _: + # raise Exception("Unexpected item in resource:", item) + + # items += [ind(indent+1), ';', '\n'] + # items += [ind(indent), '}'] + # return tag(items) case ['resource-defaults', {'type': t, 'ops': ops}]: - parse(t, indent, context) - print(' {') + items = [ + parse(t, indent, context), + ' ', '{', '\n', + ] namelen = ops_namelen(ops) for op in ops: match op: case ['=>', key, value]: - print(' '*(indent+1)*2, end='') pad = namelen - len(key) - print(f'<span class="parameter">{key}</span>', end='') - # print(' '*pad + ' => ', end='') - print(' '*pad + ' ⇒ ', end='') - parse(value, indent+3, context) - print(',') + items += [ + ind(indent+1), + tag(key, 'parameter'), + ' '*pad, + ' ⇒ ', + parse(value, indent+3, context), + ',', '\n', + ] case ['splat-hash', value]: - print(' '*(indent+1)*2, end='') pad = namelen - 1 - print('<span class="parameter splat">*</span>', end=' '*pad) - print(' '*(namelen - 1) + ' ⇒ ', end='') - parse(value, indent+2, context) - print(',') + items += [ + ind(indent+1), + tag('*', 'parameter', 'splat'), + ' '*pad, + ' '*(namelen-1), + ' ⇒ ', + parse(value, indent+2, context), + ',', '\n', + ] case x: raise Exception('Unexpected item in resource defaults:', x) - print(' '*indent*2 + '}', end='') + + items += [ind(indent), + '}'] + + return tag(items) case ['resource-override', {'resources': resources, 'ops': ops}]: - parse(resources, indent, context) - print(' {') + items = [ + parse(resources, indent, context), + ' ', '{', '\n', + ] namelen = ops_namelen(ops) for op in ops: match op: case ['=>', key, value]: - print(' '*(indent+1)*2, end='') pad = namelen - len(key) - print(f'<span class="parameter">{key}</span>', end='') - # print(' '*pad + ' => ', end='') - print(' '*pad + ' ⇒ ', end='') - parse(value, indent+3, context) - print(',') + items += [ + ind(indent+1), + tag(key, 'parameter'), + ' '*pad, + ' ⇒ ', + parse(value, indent+3, context), + ',', '\n', + ] case ['+>', key, value]: - print(' '*(indent+2)*2, end='') pad = namelen - len(key) - print(f'<span class="parameter">{key}</span>', end='') - # print(' '*pad + ' => ', end='') - print(' '*pad + ' +> ', end='') - parse(value, indent+2, context) - print(',') + items += [ + ind(indent+2), + tag(key, 'parameter'), + ' '*pad, + ' +> ', + parse(value, indent+2, context), + ',', '\n', + ] case ['splat-hash', value]: - print(' '*(indent+1)*2, end='') pad = namelen - 1 - print('<span class="parameter splat">*</span>', end=' '*pad) - print(' '*(namelen - 1) + ' ⇒ ', end='') - parse(value, indent+2, context) - print(',') + items += [ + ind(indent+1), + tag('*', 'parameter', 'splat'), + ' '*pad, + ' '*(namelen-1), + ' ⇒ ', + parse(value, indent+2, context), + ',', '\n', + ] case _: raise Exception('Unexpected item in resource override:', op) - print(' '*indent*2 + '}', end='') + + items += [ + ind(indent), + '}', + ] + + return tag(items) case ['unless', {'test': test, 'then': then}]: - print('unless ', end='') - parse(test, indent, context) - print(' {') + items = [ + tag('unless', 'keyword', 'unless'), + ' ', + parse(test, indent, context), + ' ', '{', '\n', + ] for item in then: - print(' '*(indent+1)*2, end='') - parse(item, indent+1, context) - print() + items += [ + ind(indent+1), + parse(item, indent+1, context), + '\n', + ] - print(' '*indent*2 + '}', end='') + items += [ + ind(indent), + '}', + ] case ['var', x]: # TODO how does this work with deeply nested expressions # in strings? - print_var(x, context[0] != 'str') + return print_var(x, context[0] != 'str') case ['virtual-query', q]: - print('<| ', end='') - parse(q, indent, context) - print(' |>', end='') + return tag([ + '<|', ' ', + parse(q, indent, context), + ' ', '|>', + ]) case ['virtual-query']: - print('<| |>', end='') + return tag(['<|', ' ', '|>']) # TODO unary splat case ['!', x]: - # print('! ', end='') - print('¬ ', end='') - parse(x, indent, context) + return tag([ + '¬', ' ', + parse(x, indent, context), + ]) case ['!=', a, b]: - parse(a, indent, context) - # print(' != ', end='') - print(' ≠ ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' ≠ ', + parse(b, indent, context), + ]) case ['+', a, b]: - parse(a, indent, context) - print(' + ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' + ', + parse(b, indent, context), + ]) case ['-', a, b]: - parse(a, indent, context) - print(' - ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' - ', + parse(b, indent, context), + ]) case ['-', a]: - print('- ', end='') - parse(a) + return tag([ + '- ', + parse(a), + ]) case ['*', a, b]: - parse(a, indent, context) - print(' × ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' × ', + parse(b, indent, context), + ]) case ['%', a, b]: - parse(a, indent, context) - print(' % ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' % ', + parse(b, indent, context), + ]) case ['<<', a, b]: - parse(a, indent, context) - print(' << ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' << ', + parse(b, indent, context), + ]) case ['>>', a, b]: - parse(a, indent, context) - print(' >> ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' >> ', + parse(b, indent, context), + ]) case ['>=', a, b]: - parse(a, indent, context) - print(' ≥ ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' ≥ ', + parse(b, indent, context), + ]) case ['<=', a, b]: - parse(a, indent, context) - print(' ≤ ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' ≤ ', + parse(b, indent, context), + ]) case ['>', a, b]: - parse(a, indent, context) - print(' > ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' > ', + parse(b, indent, context), + ]) case ['<', a, b]: - parse(a, indent, context) - print(' < ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' < ', + parse(b, indent, context), + ]) case ['~>', left, right]: - parse(left, indent, context) - print(f'\n{" "*indent*2}⤳ ', end='') - # print(f'\n{" "*indent*2}~> ', end='') - parse(right, indent, context) + return tag([ + parse(left, indent, context), + '\n', + ind(indent), + '⤳', ' ', + parse(right, indent, context) + ]) case ['->', left, right]: - parse(left, indent, context) - # print(f'\n{" "*indent*2}-> ', end='') - print(f'\n{" "*indent*2}→ ', end='') - parse(right, indent, context) + return tag([ + parse(left, indent, context), + '\n', + ind(indent), + '→ ', + parse(right, indent, context), + ]) case ['.', left, right]: - parse(left, indent, context) - print() - print(' '*indent*2, end='.') - parse(right, indent+1, context) + return tag([ + parse(left, indent, context), + '\n', + ind(indent), + '.', + parse(right, indent+1, context), + ]) case ['/', a, b]: - parse(a, indent, context) - print(' / ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' / ', + parse(b, indent, context), + ]) case ['=', field, value]: - # print(' ', end='') - parse(field, indent, context) - print(' = ', end='') - parse(value, indent, context) + return tag([ + parse(field, indent, context), + ' = ', + parse(value, indent, context), + ]) case ['==', a, b]: - parse(a, indent, context) - # print(' == ', end='') - print(' ≡ ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' ≡ ', + parse(b, indent, context), + ]) case ['=~', a, b]: - parse(a, indent, context) - print(' =~ ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' =~ ', + parse(b, indent, context), + ]) case ['!~', a, b]: - parse(a, indent, context) - print(' ≁ ', end='') - parse(b, indent, context) + return tag([ + parse(a, indent, context), + ' ≁ ', + parse(b, indent, context), + ]) case ['?', condition, cases]: - print('<span class="case">', end='') - parse(condition, indent, context) - print(' ? {') - print_hash(cases, indent+1, context) - print(' '*indent*2 + '}', end='') - print('</span>', end='') + return tag([ + parse(condition, indent, context), + ' ? {', + '\n', + print_hash(cases, indent+1, context), + ind(indent), + '}', + ], 'case') case form: - if type(form) == str: - print('<span class="string">', end='') + if isinstance(form, str): if context[0] == 'heredoc': - ind = ' '*indent*2 lines: list[str] match form.split('\n'): case [*_lines, '']: lines = _lines case _lines: lines = _lines + + items = [] for line in lines: - print(ind + line) + items += [ind(indent), line, '\n'] + + return tag(items, 'literal', 'string') else: s = form.replace('\n', r'\n') - print(f"'{s}'", end='') - print('</span>', end='') - elif type(form) == int or type(form) == float: - print(f'<span class="number">{form}</span>', end='') + s = f"'{s}'" + return tag(s, 'literal', 'string') + + elif isinstance(form, int) or isinstance(form, float): + return tag(str(form), 'literal', 'number') else: - print(f'<span class="parse-error">[|[{form}]|]</span>', end='') + return tag(f'[|[{form}]|]', 'parse-error') -def print_docstring(docstring: dict[str, Any]) -> None: +def print_docstring(name: str, docstring: dict[str, Any]) -> None: """ Format docstrings as they appear in some puppet types. @@ -819,119 +1042,125 @@ def print_docstring(docstring: dict[str, Any]) -> None: print('</div>') -print('''<!doctype html> -<html> - <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <link type="text/css" rel="stylesheet" href="style.css"/> - </head> - <body> -''') +def main() -> None: + """Entry point of program.""" + print('''<!doctype html> + <html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link type="text/css" rel="stylesheet" href="style.css"/> + <link type="text/css" rel="stylesheet" href="highlight.css"/> + </head> + <body> + ''') + + print('<h1>Puppet Classes</h1>') + for d_type in data['puppet_classes']: + name = d_type['name'] + # print(name, file=sys.stderr) + print_docstring(name, d_type['docstring']) -print('<h1>Puppet Classes</h1>') -for d_type in data['puppet_classes']: - name = d_type['name'] - print(name, file=sys.stderr) - print_docstring(d_type['docstring']) + print('<pre><code class="puppet">') + tree = parse_puppet(d_type['source']) + t = traverse(tree) + print(parse(t, 0, ['root'])) + print('</code></pre>') - print('<pre><code class="puppet">') - tree = parse_puppet(d_type['source']) - t = traverse(tree) - parse(t, 0, ['root']) - print('</code></pre>') + print('<hr/>') - print('<hr/>') + print('<h1>Data Types</h1>') +# TODO -print('<h1>Data Types</h1>') + print('<h1>Data Type Aliases</h1>') + for d_type in data['data_type_aliases']: + name = d_type['name'] + # print(name, file=sys.stderr) + print_docstring(name, d_type['docstring']) + print('<pre><code class="puppet">') + tree = parse_puppet(d_type['alias_of']) + t = traverse(tree) + print(parse(t, 0, ['root'])) + print('</code></pre>') -# TODO + print('<hr/>') -print('<h1>Data Type Aliases</h1>') -for d_type in data['data_type_aliases']: - name = d_type['name'] - print(name, file=sys.stderr) - print_docstring(d_type['docstring']) - print('<pre><code class="puppet">') - tree = parse_puppet(d_type['alias_of']) - t = traverse(tree) - parse(t, 0, ['root']) - print('</code></pre>') - - print('<hr/>') - - -print('<h1>Defined Types</h1>') -for d_type in data['defined_types']: - name = d_type['name'] - print(name, file=sys.stderr) - print_docstring(d_type['docstring']) - - print('<pre><code class="puppet">') - tree = parse_puppet(d_type['source']) - t = traverse(tree) - parse(t, 0, ['root']) - print('</code></pre>') - - print('<hr/>') - - -print('<h1>Resource Types</h1>') -for r_type in data['resource_types']: - name = r_type['name'] - print(f'<h2>{name}</h2>') - print(r_type['docstring']) - if 'properties' in r_type: - print('<h3>Properties</h3>') - print('<ul>') - for property in r_type['properties']: - print(f'<li>{property["name"]}</li>') - # description, values, default - print('</ul>') + print('<h1>Defined Types</h1>') + for d_type in data['defined_types']: + name = d_type['name'] + # print(name, file=sys.stderr) + print_docstring(name, d_type['docstring']) - print('<h3>Parameters</h3>') - print('<ul>') - for parameter in r_type['parameters']: - print(f'<li>{parameter["name"]}</li>') - # description - # Optional[isnamevar] - print('</ul>') - - if 'providers' in r_type: - print('<h3>Providers</h3>') - for provider in r_type['providers']: - print(f'<h4>{provider["name"]}</h4>') - # TODO - -print('<h1>Puppet Functions</h1>') -for function in data['puppet_functions']: - name = function['name'] - print(f'<h2>{name}</h2>') - t = function['type'] - # docstring = function['docstring'] - for signature in function['signatures']: - signature['signature'] - signature['docstring'] - if t in ['ruby3x', 'ruby4x']: - print(f'<pre><code class="ruby">{function["source"]}</code></pre>') - elif t == 'puppet': print('<pre><code class="puppet">') - try: - tree = parse_puppet(function['source']) - t = traverse(tree) - parse(t, 0, ['root']) - except CalledProcessError as e: - print(e) + tree = parse_puppet(d_type['source']) + t = traverse(tree) + print(parse(t, 0, ['root'])) print('</code></pre>') -print('<h1>Puppet Tasks</h1>') + print('<hr/>') + + print('<h1>Resource Types</h1>') + for r_type in data['resource_types']: + name = r_type['name'] + print(f'<h2>{name}</h2>') + print(r_type['docstring']) + if 'properties' in r_type: + print('<h3>Properties</h3>') + print('<ul>') + for property in r_type['properties']: + print(f'<li>{property["name"]}</li>') + # description, values, default + print('</ul>') + + print('<h3>Parameters</h3>') + print('<ul>') + for parameter in r_type['parameters']: + print(f'<li>{parameter["name"]}</li>') + # description + # Optional[isnamevar] + print('</ul>') + + if 'providers' in r_type: + print('<h3>Providers</h3>') + for provider in r_type['providers']: + print(f'<h4>{provider["name"]}</h4>') + # TODO + + print('<h1>Puppet Functions</h1>') + for function in data['puppet_functions']: + name = function['name'] + print(f'<h2>{name}</h2>') + t = function['type'] + # docstring = function['docstring'] + for signature in function['signatures']: + signature['signature'] + signature['docstring'] + if t in ['ruby3x', 'ruby4x']: + print(f'<pre><code class="ruby">{function["source"]}</code></pre>') + elif t == 'puppet': + print('<pre><code class="puppet">') + try: + tree = parse_puppet(function['source']) + t = traverse(tree) + print(parse(t, 0, ['root'])) + except CalledProcessError as e: + print(e) + print('</code></pre>') + + print('<h1>Puppet Tasks</h1>') # TODO -print('<h1>Puppet Plans</h1>') + print('<h1>Puppet Plans</h1>') # TODO + print('</body></html>') -print('</body></html>') + +# for t in all_tags: +# print(t, file=sys.stderr) # TODO apache::* + +if __name__ == '__main__': + main() diff --git a/style.css b/style.css index a693aeb..089626f 100644 --- a/style.css +++ b/style.css @@ -1,42 +1,3 @@ -.qn { - color: green; -} - -.var { - color: blue; -} - -.define { - color: orange; -} - -.name { - color: red; -} - -.string { - color: olive; -} - -.str-var { - color: pink; -} - -.qr { - color: darkgreen; -} - -.compound-type { - color: lightblue; -} - -.undef { - color: lightgray; -} - -.number { - color: red; -} /* -------------------------------------------------- */ @@ -57,6 +18,11 @@ h2 { /* -------------------------------------------------- */ +.documentation { + display: none; +} + +/* .var { position: relative; } @@ -74,3 +40,4 @@ h2 { .var:hover .documentation { display: block; } +*/ diff --git a/tests/test_intersperse.py b/tests/test_intersperse.py new file mode 100644 index 0000000..a8811db --- /dev/null +++ b/tests/test_intersperse.py @@ -0,0 +1,7 @@ +from main import intersperse + +def test_intersperse(): + assert list(intersperse(1, [2, 3, 4])) == [2, 1, 3, 1, 4] + +def test_intersperse_empty(): + assert list(intersperse(1, [])) == [] -- GitLab