Skip to content

Commit

Permalink
web: Add simple C/C++ style preprocessor for module-available defines
Browse files Browse the repository at this point in the history
Supported directives are #include, #define, #undef, #ifdef, #if, #else
and #endif.

In contrast to C/C++ a not defined symbol in an #if directive is an
error and does not evaluate to false.

For compatibilty with TypeScript all directives have to be prefixed
with //. Code removal by #if/#endif blocks is realized by commeting
out the TypeScript code using //. This preserves line numbers as no
lines are added or removed.

To make this work the base directory for the TypeScript import path
got moved from web/ to web/src/. This breaks absolute imports starting
with src/. They need to be changed to drop the src/.
  • Loading branch information
photron committed Nov 1, 2024
1 parent f235737 commit 7fb1431
Show file tree
Hide file tree
Showing 15 changed files with 397 additions and 100 deletions.
2 changes: 2 additions & 0 deletions software/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ web/src/ts/branding.ts
web/src/ts/translation.tsx
web/src/ts/translation.json
web/src/modules/*/*.enum.ts
web/src/modules/*/module_available.inc
web/src_tfpp/
web/**/*.embedded.ts
coredump_py_gdb_cmds
api_info.json
Expand Down
177 changes: 116 additions & 61 deletions software/pio_hooks.py

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions software/tfpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#!/usr/bin/env python3

import sys
import argparse
import pathlib
import re
from dataclasses import dataclass

@dataclass
class Define:
value: int
location: str

@dataclass
class If:
value: int
location: str

@dataclass
class Else:
value: int
location: str

def any_zero(ifs_elses):
return any([if_else.value == 0 for if_else in ifs_elses])

def parse_file(input_path, defines, ifs_elses):
output_lines = []

with input_path.open(encoding='utf-8') as input_file:
for i, line in enumerate(input_file.readlines()):
m = re.match(r'^[/\s]*//\s*#\s*(.*)$', line.strip())

if m != None:
directive = m.group(1)
m = re.match(r'^(include |define |undef |ifdef |if |else|endif)\s*(.*)$', directive)

if m == None:
raise Exception(f'Malformed directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

verb = m.group(1).strip()
arguments = m.group(2)

if verb == 'include':
m = re.match(r'^(?:"([^"]+)"|\'([^\']+)\')$', arguments)

if m == None:
raise Exception(f'Malformed path in #include directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

include_path = input_path.parent / pathlib.Path(m.group(1))

if not any_zero(ifs_elses):
if not include_path.exists():
raise Exception(f'File in #include directive at {input_path}:{i + 1} is missing: {include_path}')

parse_file(include_path, defines, ifs_elses)
elif verb == 'define':
m = re.match(r'^([A-Za-z_-][A-Za-z0-9_-]*)\s+(0|1)$', arguments)

if m == None:
raise Exception(f'Malformed arguments in #define directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

if not any_zero(ifs_elses):
symbol = m.group(1)
value = int(m.group(2))
define = defines.get(symbol)

if define != None:
raise Exception(f'Symbol {symbol} in #define directive at {input_path}:{i + 1} is already defined as {define.value} at {define.location}: {line.rstrip('\r\n')}')
else:
defines[symbol] = Define(value, f'{input_path}:{i + 1}')
elif verb == 'undef':
m = re.match(r'^([A-Za-z_-][A-Za-z0-9_-]*)$', arguments)

if m == None:
raise Exception(f'Malformed arguments in #undef directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

if not any_zero(ifs_elses):
symbol = m.group(1)
define = defines.get(symbol)

if define == None:
raise Exception(f'Symbol {symbol} in #undef directive at {input_path}:{i + 1} is not defined: {line.rstrip('\r\n')}')

line = line.rstrip() + f' [defined at {define.location}]\n'
defines.pop(symbol)
elif verb == 'ifdef':
m = re.match(r'^([A-Za-z_-][A-Za-z0-9_-]*)$', arguments)

if m == None:
raise Exception(f'Malformed arguments in #ifdef directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

symbol = m.group(1)
value = None

if not any_zero(ifs_elses):
define = defines.get(symbol)

if define != None:
value = 1
line = line.rstrip() + f' [defined as {define.value} at {define.location}]\n'
else:
value = 0
line = line.rstrip() + f' [not defined]\n'

ifs_elses.append(If(value, f'{input_path}:{i + 1}'))
elif verb == 'if':
m = re.match(r'^(1|0|[A-Za-z_-][A-Za-z0-9_-]*)$', arguments)

if m == None:
raise Exception(f'Malformed arguments in #if directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

value_or_symbol = m.group(1)
value = None

if not any_zero(ifs_elses):
if value_or_symbol in ['1', '0']:
value = int(value_or_symbol)
else:
symbol = value_or_symbol
define = defines.get(symbol)

if define == None:
raise Exception(f'Symbol {symbol} in #if directive at {input_path}:{i + 1} is not defined: {line.rstrip('\r\n')}')

value = define.value
line = line.rstrip() + f' [defined as {value} at {define.location}]\n'

ifs_elses.append(If(value, f'{input_path}:{i + 1}'))
elif verb == 'else':
if len(arguments) > 0:
raise Exception(f'Unexpected arguments in #else directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

if len(ifs_elses) == 0:
raise Exception(f'Missing #if directive for #else directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

if_else = ifs_elses.pop()

if isinstance(if_else, Else):
raise Exception(f'Duplicate #else directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

ifs_elses.append(Else(1 - if_else.value, f'{input_path}:{i + 1}'))
elif verb == 'endif':
if len(arguments) > 0:
raise Exception(f'Unexpected arguments in #endif directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

if len(ifs_elses) == 0:
raise Exception(f'Missing #if directive for #endif directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')

ifs_elses.pop()

if any_zero(ifs_elses) and not line.lstrip().startswith('//'):
line = '//' + line

output_lines.append(line)

return output_lines

def tfpp(input_path, output_path, overwrite=False):
input_path = pathlib.Path(input_path)
output_path = pathlib.Path(output_path)

if not input_path.exists():
raise Exception(f"Input file {input_path} is missing")

if output_path.exists() and not overwrite:
raise Exception(f"Output file {output_path} already exists")

if input_path == output_path:
raise Exception(f"Input file {input_path} and output file {output_path} are the same")

defines = {}
ifs_elses = []
output_lines = parse_file(input_path, defines, ifs_elses)

if len(ifs_elses) > 0:
raise Exception(f'Missing #endif directive for #{"if" if isinstance(ifs_elses[0], If) else "else"} directive at {ifs_elses[0].location}')

output_path.parent.mkdir(parents=True, exist_ok=True)
output_path_tmp = output_path.with_suffix('.tfpptmp')

with output_path_tmp.open(mode='w', encoding='utf-8') as output_file:
output_file.writelines(output_lines)

output_path_tmp.replace(output_path)

def main():
parser = argparse.ArgumentParser()
parser.add_argument('input_path')
parser.add_argument('output_path')
parser.add_argument('--overwrite', action='store_true')

args = parser.parse_args()

tfpp(input_path=args.input_path, output_path=args.output_path, overwrite=args.overwrite)

if __name__ == '__main__':
try:
main()
except Exception as e:
print(f'Error: {e}')
sys.exit(1)
41 changes: 34 additions & 7 deletions software/web/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import shutil
import subprocess
from base64 import b64encode

import argparse
import tinkerforge_util as tfutil

tfutil.create_parent_module(__file__, 'software')

from software.tfpp import tfpp

JS_ANALYZE = False

BUILD_DIR = 'build'
BUILD_DIR = '../build'

HTML_MINIFIER_TERSER_OPTIONS = [
'--collapse-boolean-attributes',
Expand Down Expand Up @@ -40,6 +44,29 @@ def main():
parser.add_argument('--no-minify', action='store_true')
build_args = parser.parse_args()

try:
shutil.rmtree('src_tfpp')
except FileNotFoundError:
pass

print('tfpp...')
for root, dirs, files in os.walk('./src'):
for name in files:
src_path = os.path.join(root, name)
src_tfpp_path = src_path.replace('./src/', './src_tfpp/')

if src_path.endswith('.ts') or src_path.endswith('.tsx'):
try:
tfpp(src_path, src_tfpp_path)
except Exception as e:
print(f'Error: {e}', file=sys.stderr)
exit(42)
else:
os.makedirs(os.path.split(src_tfpp_path)[0], exist_ok=True)
shutil.copy2(src_path, src_tfpp_path)

os.chdir('src_tfpp')

try:
shutil.rmtree(BUILD_DIR)
except FileNotFoundError:
Expand All @@ -59,12 +86,12 @@ def main():
args = [
'npx',
'esbuild',
'src/main.tsx',
'main.tsx',
'--metafile={}'.format(os.path.join(BUILD_DIR, 'meta.json')),
'--bundle',
'--target=es6',
'--alias:argon2-browser=./node_modules/argon2-browser/dist/argon2-bundled.min.js',
'--alias:jquery=./node_modules/jquery/dist/jquery.slim.min',
'--alias:argon2-browser=../node_modules/argon2-browser/dist/argon2-bundled.min.js',
'--alias:jquery=../node_modules/jquery/dist/jquery.slim.min',
'--outfile={0}'.format(os.path.join(BUILD_DIR, 'bundle.min.js'))
]

Expand All @@ -89,7 +116,7 @@ def main():
args += ['--no-source-map']

args += [
'src/main.scss',
'main.scss',
os.path.join(BUILD_DIR, 'main.css')
]

Expand Down Expand Up @@ -125,7 +152,7 @@ def main():
HTML_MINIFIER_TERSER_OPTIONS + [
'-o',
os.path.join(BUILD_DIR, 'index.min.html'),
'src/index.html'
'index.html'
], shell=sys.platform == 'win32')

if __name__ == '__main__':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { InputSelect } from "../../ts/components/input_select";
import { FormRow } from "../../ts/components/form_row";
import * as util from "../../ts/util";
import * as API from "../../ts/api";
import { InputText } from "src/ts/components/input_text";
import { InputText } from "../../ts/components/input_text";

export type CronAutomationTrigger = [
AutomationTriggerID.Cron,
Expand Down
3 changes: 1 addition & 2 deletions software/web/src/modules/charge_manager/chargers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ import { Collapse, ListGroup, ListGroupItem } from "react-bootstrap";
import { InputSelect } from "../../ts/components/input_select";
import { SubPage } from "../../ts/components/sub_page";
import { Table } from "../../ts/components/table";

import type { ChargeManagerStatus } from "./main"
import { InputFloat } from "src/ts/components/input_float";
import { InputFloat } from "../../ts/components/input_float";

type ChargeManagerConfig = API.getType["charge_manager/config"];
type ChargerConfig = ChargeManagerConfig["chargers"][0];
Expand Down
3 changes: 1 addition & 2 deletions software/web/src/modules/charge_manager/debug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ import { Button, Card } from "react-bootstrap";
import { SubPage } from "../../ts/components/sub_page";
import { NavbarItem } from "../../ts/components/navbar_item";
import { Download, Terminal } from "react-feather";
import { InputNumber } from "src/ts/components/input_number";

import { InputNumber } from "../../ts/components/input_number";

const CMDOutFloat = (props: any) => <OutputFloat maxFractionalDigitsOnPage={3} maxUnitLengthOnPage={2} {...props}/>
const CMDCardOutFloat = (props: any) => <OutputFloat maxUnitLengthOnPage={3.5} {...props}/>
Expand Down
8 changes: 3 additions & 5 deletions software/web/src/modules/charge_manager/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,16 @@ import { FormRow } from "../../ts/components/form_row";
import { Button, Collapse } from "react-bootstrap";
import { InputSelect } from "../../ts/components/input_select";
import { InputFloat } from "../../ts/components/input_float";
import { OutputFloat } from "src/ts/components/output_float";
import { OutputFloat } from "../../ts/components/output_float";
import { Switch } from "../../ts/components/switch";
import { InputNumber } from "../../ts/components/input_number";
import { SubPage } from "../../ts/components/sub_page";

import { MeterValueID } from "../meters/meter_value_id";
import { get_noninternal_meter_slots, NoninternalMeterSelector } from "../power_manager/main";
import type { ChargeManagerStatus } from "./main"
import { FormSeparator } from "src/ts/components/form_separator";

import { FormSeparator } from "../../ts/components/form_separator";
import { ChargeManagerDebug } from "./debug";
import { CollapsedSection } from "src/ts/components/collapsed_section";
import { CollapsedSection } from "../../ts/components/collapsed_section";

type ChargeManagerConfig = API.getType["charge_manager/config"];

Expand Down
Loading

0 comments on commit 7fb1431

Please sign in to comment.