From 06e1dcf466cabb6df6c865b370d4d3d0e97ceee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= <hugo@lysator.liu.se> Date: Thu, 25 May 2023 17:26:15 +0200 Subject: [PATCH] Rework entire structure. --- .flake8 | 2 +- Makefile | 4 +- cache.py | 39 --- muppet/__main__.py | 358 ++++++++++++++++++++++++ muppet/cache.py | 55 ++++ main.py => muppet/format.py | 163 +---------- intersperse.py => muppet/intersperse.py | 0 reflow.py => muppet/puppet/parser.py | 43 +-- muppet/puppet/strings.py | 15 + muppet/tabs.py | 73 +++++ highlight.css => static/highlight.css | 0 static/hl1.css | 40 +++ static/script.js | 3 + style.css => static/style.css | 0 static/style2.css | 36 +++ tabs.js => static/tabs.js | 0 templates/base.html | 11 +- templates/code_page.html | 5 + templates/index.html | 28 ++ templates/module_index.html | 20 ++ 20 files changed, 678 insertions(+), 217 deletions(-) delete mode 100644 cache.py create mode 100644 muppet/__main__.py create mode 100644 muppet/cache.py rename main.py => muppet/format.py (89%) rename intersperse.py => muppet/intersperse.py (100%) rename reflow.py => muppet/puppet/parser.py (60%) mode change 100755 => 100644 create mode 100644 muppet/puppet/strings.py create mode 100644 muppet/tabs.py rename highlight.css => static/highlight.css (100%) create mode 100644 static/hl1.css create mode 100644 static/script.js rename style.css => static/style.css (100%) create mode 100644 static/style2.css rename tabs.js => static/tabs.js (100%) create mode 100644 templates/code_page.html create mode 100644 templates/index.html create mode 100644 templates/module_index.html diff --git a/.flake8 b/.flake8 index cc60e25..47be68d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -extend-ignore = D105 +extend-ignore = D105,D107 per-file-ignores = tests/*: D103 max-line-length = 99 diff --git a/Makefile b/Makefile index 0327d59..64bc5e8 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,8 @@ index.html: $(CACHE_DIR)/output.json *.py python3 main.py $< > $@ check: - flake8 *.py - mypy *.py + flake8 muppet + mypy -p muppet test: python -m pytest diff --git a/cache.py b/cache.py deleted file mode 100644 index d46342d..0000000 --- a/cache.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Run puppet parse if files have changed. - -Non-changed file are fetched from the cache. -(Cache currently implemneted on redis). -""" - -import hashlib -import json -import subprocess -import redis -from typing import Any - -r = redis.Redis(host='localhost') - - -def parse_puppet_for_real(s: str) -> bytes: - """Parse the given puppetstring into a json representation.""" - cmd = subprocess.run('puppet parser dump --format json'.split(' '), - input=s.encode('UTF-8'), - check=True, - capture_output=True) - return cmd.stdout - - -def parse_puppet(s: str) -> dict[str, Any]: - """Parse the given puppet string into a json representation, but use a cache.""" - hash = hashlib.sha256(s.encode('UTF-8')) - digest = hash.hexdigest() - if record := r.get(digest): - data = json.loads(record) - else: - almost_data = parse_puppet_for_real(s) - hash = hashlib.sha256(almost_data) - r.set(digest, almost_data) - data = json.loads(almost_data) - if type(data) != dict: - raise Exception("Weird data returned from puppet parse") - return data diff --git a/muppet/__main__.py b/muppet/__main__.py new file mode 100644 index 0000000..21593dd --- /dev/null +++ b/muppet/__main__.py @@ -0,0 +1,358 @@ +"""New, better, entry point.""" + +import argparse +import os +import os.path +from dataclasses import dataclass +import hashlib +import sys +from jinja2 import ( + Environment, + # PackageLoader, + FileSystemLoader, +) +import pathlib +import json +from typing import ( + Any, + TypeVar, + Callable, + TypedDict, +) +from collections.abc import ( + Iterable, + Sequence, +) + +from .cache import Cache +from .puppet.strings import puppet_strings +from .format import format_class + +jinja = Environment( + loader=FileSystemLoader('templates'), + autoescape=False, +) + +parser = argparse.ArgumentParser( + prog='puppet-doc configure', + description='Sets up puppet doc') + +parser.add_argument('--env', action='store') + +args = parser.parse_args() + +env = args.env or '/etc/puppetlabs/code/modules' + +cache = Cache('/home/hugo/.cache/puppet-doc') + + +@dataclass +class ModuleEntry: + """ + One entry in a module. + + Parameters: + name - local name of the module, should always be the basename + of path + path - Absolute path in the filesystem where the module can be + found. + strings_output - output of `puppet strings`. + """ + + name: str + path: str + strings_output: bytes + + def file(self, path: str) -> str: + """Return the absolute path of a path inside the module.""" + return os.path.join(self.path, path) + + +def get_puppet_strings(path: str) -> bytes: + """ + Run puppet string, but check cache first. + + The cache uses the contents of metadata.json as its key, + so any updates without an updated metadata.json wont't be + detected. + + Hashing the entire contents of the module was tested, but was to + slow. + """ + try: + with open(os.path.join(path, 'metadata.json'), 'rb') as f: + data = f.read() + key = 'puppet-strings' + hashlib.sha1(data).hexdigest() + if parsed := cache.get(key): + result = parsed + else: + result = puppet_strings(path) + cache.put(key, result) + return result + except FileNotFoundError: + # TODO actually run puppet strings again. + # This is just since without a metadata.json we always get a + # cache miss, which is slow. + # return puppet_strings(path) + return b'' + + # try: + # with open(module.file('.git/FETCH_HEAD')) as f: + # st = os.stat(f.fileno()) + # st.st_mtime + # except FileNotFoundError: + # pass + + +def get_modules(dir: str) -> list[ModuleEntry]: + """ + Enumerate modules in directory. + + The directory should be the modules subdirectory of an environment, + e.g. /etc/puppetlabs/code/environments/production/modules. + """ + modules: list[ModuleEntry] = [] + + for entry in sorted(list(os.scandir(dir)), key=lambda d: d.name): + # TODO Logging + print('- entry', entry, file=sys.stderr) + name = entry.name + path = os.path.join(env, entry) + strings_data = get_puppet_strings(path) + + modules.append(ModuleEntry(name, path, strings_data)) + + return modules + +# -------------------------------------------------- + + +pathlib.Path('output').mkdir(exist_ok=True) + + +T = TypeVar('T') +U = TypeVar('U') + + +def group_by(proc: Callable[[T], U], seq: Sequence[T]) -> dict[U, list[T]]: + """ + Group elements in seq by proc. + + Return a dictionary mapping the result of proc onto lists of each + element which evaluated to that key. + """ + d: dict[U, list[T]] = {} + for item in seq: + key = proc(item) + d[key] = (d.get(key) or []) + [item] + return d + + +def isprivate(entry: dict[str, Any]) -> bool: + """ + Is the given puppet declaration marked private. + + Assumes input is a dictionary as returned by puppet strings, one + of the entries in (for example) 'puppet_classes'. + + Currently only checks for an "@api private" tag. + """ + if ds := entry.get('docstring'): + if tags := ds.get('tags'): + for tag in tags: + if tag.get('tag_name') == 'api' and \ + tag.get('text') == 'private': + return True + return False + + +def setup_index(base: str) -> None: + """Create the main index.html file.""" + template = jinja.get_template('index.html') + with open(os.path.join(base, 'index.html'), 'w') as f: + f.write(template.render(modules=modules)) + + +class IndexItem(TypedDict): + """A single list entry in a module index page.""" + + name: str + file: str + + +class IndexSubcategory(TypedDict): + """A subheading on an index page.""" + + title: str + list: Iterable[IndexItem] + + +class IndexCategory(TypedDict): + """A top heading on an index page.""" + + title: str + list: Iterable[IndexSubcategory] + + +def class_index(class_list: list) -> IndexCategory: + """Prepage class index list.""" + groups = group_by(isprivate, class_list) + + lst: list[IndexSubcategory] = [] + + if publics := groups.get(False): + lst.append({ + 'title': 'Public Classes', + 'list': ({'name': i['name'], + 'file': os.path.splitext(i['file'])[0]} + for i in publics), + }) + + if privates := groups.get(True): + lst.append({ + 'title': 'Private Classes', + 'list': ({'name': i['name'], + 'file': os.path.splitext(i['file'])[0]} + for i in privates), + }) + + return { + 'title': 'Classes', + 'list': lst + } + + +def defined_types_index(defined_list: list) -> IndexCategory: + """ + Prepare defined types index list. + + These are puppet types introduces by puppet code. + Each only has one implemenattion. + """ + groups = group_by(isprivate, defined_list) + + lst: list[IndexSubcategory] = [] + + if publics := groups.get(False): + lst.append({ + 'title': 'Public Defined Types', + 'list': ({'name': i['name'], + 'file': os.path.splitext(i['file'])[0]} + for i in publics), + }) + + if privates := groups.get(True): + lst.append({ + 'title': 'Private Defined Types', + 'list': ({'name': i['name'], + 'file': os.path.splitext(i['file'])[0]} + for i in privates), + }) + + return { + 'title': 'Defined Types', + 'list': lst + } + + +# def type_aliases_index(alias_list: list) -> IndexCategory: +# """Prepare type alias index list.""" +# # TODO +# +# +# def resource_types_index(resource_list: list) -> IndexCategory: +# """ +# Prepare resource type index list. +# +# These are the resource types introduced through ruby. Each can +# have multiple implementations. +# """ +# return {} + + +def setup_module_index(base: str, module: ModuleEntry, data: dict[str, Any]) -> None: + """Create the index file for a specific module.""" + template = jinja.get_template('module_index.html') + + content = [] + + content.append(class_index(data['puppet_classes'])) + + data['data_types'] + + data['data_type_aliases'] + + # content.append({ + # 'title': 'Type Aliases', + # 'list': [ + # ] + # }) + + content.append(defined_types_index(data['defined_types'])) + + data['resource_types'] + data['providers'] + data['puppet_functions'] + data['puppet_tasks'] + data['puppet_plans'] + + with open(os.path.join(base, 'index.html'), 'w') as f: + f.write(template.render(module_name=module.name, + content=content)) + + +def setup_module(base: str, module: ModuleEntry) -> None: + """ + Create all output files for a puppet module. + + Will generate a directory under base for the module. + """ + path = os.path.join(base, module.name) + pathlib.Path(path).mkdir(exist_ok=True) + if not module.strings_output: + return + data = json.loads(module.strings_output) + + setup_module_index(path, module, data) + + for puppet_class in data['puppet_classes'] + data['defined_types']: + # localpath = puppet_class['name'].split('::') + localpath, _ = os.path.splitext(puppet_class['file']) + # localdir, _ = os.path.splitext(puppet_class['name']) + dir = os.path.join(path, localpath) + pathlib.Path(dir).mkdir(parents=True, exist_ok=True) + # puppet_class['docstring'] + # puppet_class['defaults'] + + with open(os.path.join(dir, 'source.pp'), 'w') as f: + f.write(puppet_class['source']) + + with open(os.path.join(dir, 'source.json'), 'w') as f: + json.dump(puppet_class, f, indent=2) + + with open(os.path.join(dir, 'source.pp.html'), 'w') as f: + f.write(format_class(puppet_class)) + + with open(os.path.join(dir, 'index.html'), 'w') as f: + template = jinja.get_template('code_page.html') + f.write(template.render(content=format_class(puppet_class))) + + # puppet_class['file'] + # puppet_class['line'] + + for type_alias in data['data_type_aliases']: + ... + + os.system("cp -r static output") + + # data['data_type_aliases'] + # data['defined_types'] + # data['resource_types'] + + +modules = get_modules(env) + +setup_index('output') +for module in modules: + # print(module) + setup_module('output', module) diff --git a/muppet/cache.py b/muppet/cache.py new file mode 100644 index 0000000..a9279d1 --- /dev/null +++ b/muppet/cache.py @@ -0,0 +1,55 @@ +"""A simple cache.""" + +from typing import ( + Callable, + Optional, +) +import os.path +import pathlib +import hashlib + + +class Cache: + """ + A simple cache. + + This implementation is file system-backed, but its interface + should allow any backend. + """ + + def __init__(self, path: str, *, mkdir: bool = True): + self.base = path + if mkdir: + pathlib.Path(path).mkdir(parents=True, exist_ok=True) + + def put(self, key: str, value: bytes) -> None: + """Put content into cache.""" + with open(os.path.join(self.base, key), 'wb') as f: + f.write(value) + + def get(self, key: str) -> Optional[bytes]: + """Get item from cache if it exists, or Nothing otherwise.""" + try: + with open(os.path.join(self.base, key), 'rb') as f: + return f.read() + except FileNotFoundError: + return None + + def memoize_function(self, + prefix: str, + func: Callable[[bytes], bytes]) -> Callable[[bytes], bytes]: + """Return a new function identical to the one given, but memoized.""" + def inner(data: bytes) -> bytes: + key = prefix + hashlib.sha1(data).hexdigest() + if value := self.get(key): + return value + else: + value = func(data) + self.put(key, value) + return value + return inner + + def memoize(self, prefix: str) -> Callable[[Callable[[bytes], bytes]], + Callable[[bytes], bytes]]: + """memoize_function, but as a decorator.""" + return lambda func: self.memoize_function(prefix, func) diff --git a/main.py b/muppet/format.py similarity index 89% rename from main.py rename to muppet/format.py index 942db73..1b190b2 100644 --- a/main.py +++ b/muppet/format.py @@ -6,12 +6,9 @@ provided as the first element. This program goes through every definition in it, and outputs a complete index.html. """ -from cache import parse_puppet from commonmark import commonmark -from reflow import traverse from subprocess import CalledProcessError import html -import json import sys from typing import ( Any, @@ -22,13 +19,11 @@ from typing import ( ) from collections.abc import Sequence from dataclasses import dataclass -from jinja2 import Environment, PackageLoader -from intersperse import intersperse -env = Environment( - loader=PackageLoader('main'), - autoescape=False, -) +from .puppet.parser import puppet_parser +from .intersperse import intersperse + +parse_puppet = puppet_parser HashEntry: TypeAlias = Union[Tuple[Literal['=>'], str, Any], Tuple[Literal['+>'], str, Any], @@ -36,17 +31,6 @@ HashEntry: TypeAlias = Union[Tuple[Literal['=>'], str, Any], Context: TypeAlias = list['str'] -match sys.argv: - case [_, d, *_]: - filename = d - case [_]: - filename = 'output.json' - -with open(filename) as f: - info = json.load(f) - -data = info - param_doc: dict[str, str] = {} @@ -167,7 +151,6 @@ symbols: dict[str, str] = { } - def handle_case_body(forms: list[dict[str, Any]], indent: int, context: Context) -> Tag: """Handle case body when parsing AST.""" @@ -1038,65 +1021,6 @@ def print_docstring(name: str, docstring: dict[str, Any]) -> str: return out -@dataclass -class Tab: - """ - A single tab part of a tab group. - - :parameters: - title - Display name of the tab - id - Internal reference ID, must be globaly unique - content - contents of the tab - """ - - title: str - id: str - content: str - - -@dataclass -class TabGroup: - """A collection of tabs.""" - - id: str - tabs: list[Tab] - - -def tab_widget(tabgroup: TabGroup) -> str: - """ - Render a HTML tab widget. - - The argument is the list of tabs, nothing is returned, but instead - written to stdout. - """ - template = env.get_template('tabset.html') - return template.render(tabset=tabgroup) - - -__counter = 0 - - -def next_id(prefix: str = 'id') -> str: - """Return a new unique id.""" - global __counter - __counter += 1 - return f'{prefix}-{__counter}' - - -def tabs(panes: dict[str, str]) -> str: - """ - Build a tab widget from given dictionary. - - Keys are used as tab names, values as tab content. - Id's are generated. - """ - tabs = [] - for title, content in panes.items(): - tabs.append(Tab(title=title, content=content, id=next_id('tab'))) - - return tab_widget(TabGroup(id=next_id('tabgroup'), tabs=tabs)) - - def format_class(d_type: dict[str, Any]) -> str: """Format Puppet class.""" out = '' @@ -1105,8 +1029,7 @@ def format_class(d_type: dict[str, Any]) -> str: print_docstring(name, d_type['docstring']) out += '<pre><code class="puppet">' - tree = parse_puppet(d_type['source']) - t = traverse(tree) + t = parse_puppet(d_type['source']) out += str(parse(t, 0, ['root'])) out += '</code></pre>' return out @@ -1125,8 +1048,7 @@ def format_type_alias(d_type: dict[str, Any]) -> str: out += print_docstring(name, d_type['docstring']) out += '\n' out += '<pre><code class="puppet">' - tree = parse_puppet(d_type['alias_of']) - t = traverse(tree) + t = parse_puppet(d_type['alias_of']) out += str(parse(t, 0, ['root'])) out += '</code></pre>\n' return out @@ -1140,8 +1062,7 @@ def format_defined_type(d_type: dict[str, Any]) -> str: out += print_docstring(name, d_type['docstring']) out += '<pre><code class="puppet">' - tree = parse_puppet(d_type['source']) - t = traverse(tree) + t = parse_puppet(d_type['source']) out += str(parse(t, 0, ['root'])) out += '</code></pre>\n' return out @@ -1193,8 +1114,7 @@ def format_puppet_function(function: dict[str, Any]) -> str: elif t == 'puppet': out += '<pre><code class="puppet">' try: - tree = parse_puppet(function['source']) - t = traverse(tree) + t = parse_puppet(function['source']) out += str(parse(t, 0, ['root'])) except CalledProcessError as e: print(e, file=sys.stderr) @@ -1213,70 +1133,3 @@ def format_puppet_task() -> str: def format_puppet_plan() -> str: """Format Puppet plan.""" return 'TODO format_puppet_plan not implemented' - - -def main() -> None: - """Entry point of program.""" - out = '' - - out += '<h1>Puppet Classes</h1>' - for d_type in data['puppet_classes']: - out += f'<h2>{d_type["name"]}</h2>' - out += tabs({ - 'Formatted': format_class(d_type), - 'JSON': '<pre><code class="json">' + json.dumps(d_type, indent=2) + '</code></pre>', - 'Source': '<pre><code class="puppet">' + d_type['source'] + '</code></pre>', - }) - out += '<hr/>' - - out += '<h1>Data Types</h1>' - -# TODO - - out += '<h1>Data Type Aliases</h1>' - for d_type in data['data_type_aliases']: - out += tabs({ - 'Formatted': format_type_alias(d_type), - 'JSON': '<pre><code class="json">' + json.dumps(d_type, indent=2) + '</code></pre>', - }) - - out += '<hr/>' - - out += '<h1>Defined Types</h1>' - for d_type in data['defined_types']: - out += tabs({ - 'Formatted': format_defined_type(d_type), - 'JSON': '<pre><code class="json">' + json.dumps(d_type, indent=2) + '</code></pre>', - 'Source': '<pre><code class="puppet">' + d_type['source'] + '</code></pre>', - }) - - out += '<hr/>' - - out += '<h1>Resource Types</h1>' - for r_type in data['resource_types']: - out += tabs({ - 'Formatted': format_resource_type(r_type), - 'JSON': '<pre><code class="json">' + json.dumps(r_type, indent=2) + '</code></pre>', - }) - - out += '<h1>Puppet Functions</h1>' - for function in data['puppet_functions']: - out += tabs({ - 'Formatted': format_puppet_function(function), - 'JSON': '<pre><code class="json">' + json.dumps(function, indent=2) + '</code></pre>', - 'Source': '<pre><code class="puppet">' + function['source'] + '</code></pre>', - }) - - out += '<h1>Puppet Tasks</h1>' -# TODO - out += '<h1>Puppet Plans</h1>' -# TODO - - template = env.get_template('base.html') - print(template.render(content=out)) - -# TODO apache::* - - -if __name__ == '__main__': - main() diff --git a/intersperse.py b/muppet/intersperse.py similarity index 100% rename from intersperse.py rename to muppet/intersperse.py diff --git a/reflow.py b/muppet/puppet/parser.py old mode 100755 new mode 100644 similarity index 60% rename from reflow.py rename to muppet/puppet/parser.py index 2d39f23..d446743 --- a/reflow.py +++ b/muppet/puppet/parser.py @@ -1,16 +1,19 @@ -#!/usr/bin/env python3 - """ -Reword the output of puppet parser to something managable. - -Traverse takes a tree as returned by -`puppet parser dump --format json` (as a python object). +A Python wrapper around `puppet parser dump`. -It returns a new python object representing the same data, but in a -more managable format. +puppet_parser_raw simply runs `puppet parser dump --format json` on +the given output. `puppet_parser` also reflows the output into +something managable. """ +import subprocess +import json + from typing import Any +from ..cache import Cache + + +cache = Cache('/home/hugo/.cache/puppet-doc') def tagged_list_to_dict(lst: list[Any]) -> dict[Any, Any]: @@ -61,13 +64,21 @@ def traverse(tree: Any) -> Any: return tree -def __main() -> None: - import json - import sys - - json.dump(traverse(json.load(sys.stdin)), sys.stdout) - print() +@cache.memoize('puppet-parser') +def puppet_parser_raw(code: bytes) -> bytes: + """Parse the given puppetstring into a json representation.""" + cmd = subprocess.run('puppet parser dump --format json'.split(' '), + input=code, + check=True, + capture_output=True) + return cmd.stdout -if __name__ == '__main__': - __main() +def puppet_parser(code: str) -> list: + """Parse the given puppet string, and reflow it.""" + data = traverse(json.loads(puppet_parser_raw(code.encode('UTF-8')))) + # TODO log output here? + if isinstance(data, list): + return data + else: + raise ValueError('Expected well formed tree, got %s', data) diff --git a/muppet/puppet/strings.py b/muppet/puppet/strings.py new file mode 100644 index 0000000..f308985 --- /dev/null +++ b/muppet/puppet/strings.py @@ -0,0 +1,15 @@ +"""Python wrapper around puppet strings.""" + +import subprocess + + +def puppet_strings(path: str) -> bytes: + """Run `puppet strings` on puppet module at path.""" + # TODO adding an --out flag (to not stdout) causes warnings to be + # printed to stdout. Warnings + + cmd = subprocess.run('puppet strings generate --format json'.split(' '), + cwd=path, + check=True, + stdout=subprocess.PIPE) + return cmd.stdout diff --git a/muppet/tabs.py b/muppet/tabs.py new file mode 100644 index 0000000..702a491 --- /dev/null +++ b/muppet/tabs.py @@ -0,0 +1,73 @@ +"""Thingies for an HTML tab widget.""" + +from dataclasses import dataclass +from jinja2 import ( + Environment, + # PackageLoader, + FileSystemLoader, +) + + +env = Environment( + loader=FileSystemLoader('templates'), + autoescape=False, +) + + +@dataclass +class Tab: + """ + A single tab part of a tab group. + + :parameters: + title - Display name of the tab + id - Internal reference ID, must be globaly unique + content - contents of the tab + """ + + title: str + id: str + content: str + + +@dataclass +class TabGroup: + """A collection of tabs.""" + + id: str + tabs: list[Tab] + + +def tab_widget(tabgroup: TabGroup) -> str: + """ + Render a HTML tab widget. + + The argument is the list of tabs, nothing is returned, but instead + written to stdout. + """ + template = env.get_template('tabset.html') + return template.render(tabset=tabgroup) + + +__counter = 0 + + +def next_id(prefix: str = 'id') -> str: + """Return a new unique id.""" + global __counter + __counter += 1 + return f'{prefix}-{__counter}' + + +def tabs(panes: dict[str, str]) -> str: + """ + Build a tab widget from given dictionary. + + Keys are used as tab names, values as tab content. + Id's are generated. + """ + tabs = [] + for title, content in panes.items(): + tabs.append(Tab(title=title, content=content, id=next_id('tab'))) + + return tab_widget(TabGroup(id=next_id('tabgroup'), tabs=tabs)) diff --git a/highlight.css b/static/highlight.css similarity index 100% rename from highlight.css rename to static/highlight.css diff --git a/static/hl1.css b/static/hl1.css new file mode 100644 index 0000000..29b8111 --- /dev/null +++ b/static/hl1.css @@ -0,0 +1,40 @@ +.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; +} + diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..a81098c --- /dev/null +++ b/static/script.js @@ -0,0 +1,3 @@ +document.addEventListener('load', function () { + let tab_labels = document.querySelectorAll('[data-tab]') +}); diff --git a/style.css b/static/style.css similarity index 100% rename from style.css rename to static/style.css diff --git a/static/style2.css b/static/style2.css new file mode 100644 index 0000000..163bfdc --- /dev/null +++ b/static/style2.css @@ -0,0 +1,36 @@ +/* Style for tabgroups */ +.tabs { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + border: 1px solid red; +} + +.tabs menu { + display: flex; + flex-direction: row; + padding: 0; + margin: 0; +} + +.tabs menu li { + display: block; +} + + +/* +.tabs menu li { + display: block; + background-color: lightgray; + border: 1px solid gray; +} +*/ + +.tab { + display: none; + border: 1px solid green; +} + +.tab.selected { + display: block !important; +} diff --git a/tabs.js b/static/tabs.js similarity index 100% rename from tabs.js rename to static/tabs.js diff --git a/templates/base.html b/templates/base.html index 3ee0ef5..e29d88e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,9 +3,9 @@ <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"/> - <link type="text/css" rel="stylesheet" href="style2.css"/> + <link type="text/css" rel="stylesheet" href="/static/style.css"/> + <link type="text/css" rel="stylesheet" href="/static/highlight.css"/> + <link type="text/css" rel="stylesheet" href="/static/style2.css"/> <script src="tabs.js"></script> <noscript> <style> @@ -19,6 +19,9 @@ </style> </noscript> </head> - <body>{{ content }}</body> + <body> + {% block content %} + {% endblock %} + </body> </html> {# ft: jinja #} diff --git a/templates/code_page.html b/templates/code_page.html new file mode 100644 index 0000000..1bf0fc1 --- /dev/null +++ b/templates/code_page.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block content %} +{{ content }} +{% endblock %} +{# ft: jinja #} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..693c6f3 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,28 @@ +<!doctype html> +<html> +<head> + <meta charset="UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <title></title> + <!-- + <link type="text/css" rel="stylesheet" href="style.css"/> + --> +<style> +.error { + background-color: red; +} +</style> +</head> +<body> + <ul> + {% for module in modules %} + <li> + <a href="/{{ module.name }}" + class="{{ 'error' if module.strings_output == None }}" + >{{ module.name }}</a> + </li> + {% endfor %} + </ul> +</body> +</html> + diff --git a/templates/module_index.html b/templates/module_index.html new file mode 100644 index 0000000..41119ee --- /dev/null +++ b/templates/module_index.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block content %} +<h1>{{ module_name }}</h1> + +{% for entry in content %} + <h2>{{ entry['title'] }}</h2> + {% for subentry in entry['list'] %} + <h3>{{ subentry['title'] }}</h3> + <ul> + {% for item in subentry['list'] %} + <li> + <a href="{{ item['file'] }}">{{ item['name'] }}</a> + </li> + {% endfor %} + </ul> + {% endfor %} +{% endfor %} + +{% endblock %} +{# ft: jinja #} -- GitLab