mirror of
https://github.com/lvgl/lvgl.git
synced 2026-08-17 09:42:19 +08:00
0241174e50
Arduino Lint / lint (push) Has been cancelled
Build Examples with C++ Compiler / build-examples (push) Has been cancelled
MicroPython CI / Build esp32 port (push) Has been cancelled
MicroPython CI / Build rp2 port (push) Has been cancelled
MicroPython CI / Build stm32 port (push) Has been cancelled
MicroPython CI / Build unix port (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_NORMAL_8BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_SDL - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build ESP IDF ESP32S3 (push) Has been cancelled
C/C++ CI / Run tests with 32bit build (push) Has been cancelled
C/C++ CI / Run tests with 64bit build (push) Has been cancelled
BOM Check / bom-check (push) Has been cancelled
Verify that lv_conf_internal.h matches repository state / verify-conf-internal (push) Has been cancelled
Verify GDB constants are up-to-date / verify-gdb-consts (push) Has been cancelled
Verify the widget property name / verify-property-name (push) Has been cancelled
Verify code formatting / verify-formatting (push) Has been cancelled
Compare file templates with file names / template-check (push) Has been cancelled
Build Docs / build-and-deploy (push) Has been cancelled
Build .deb packages / build (push) Has been cancelled
Test API JSON generator / Test API JSON (push) Has been cancelled
Install LVGL using CMake / build-examples (private) (push) Has been cancelled
Install LVGL using CMake / build-examples (public) (push) Has been cancelled
Check Makefile / Build using Makefile (push) Has been cancelled
Check Makefile for UEFI / Build using Makefile for UEFI (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/benchmark_results_comment/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/filter_docker_logs/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/serialize_results/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 32b - lv_conf_perf32b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 64b - lv_conf_perf64b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Save PR Number (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_32B - Ubuntu (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_64B - Ubuntu (push) Has been cancelled
Port repo release update / run-release-branch-updater (push) Has been cancelled
Static Checks / Static Checks (push) Has been cancelled
Verify Font License / verify-font-license (push) Has been cancelled
Verify Kconfig / verify-kconfig (push) Has been cancelled
Hardware Performance Test / Hardware Performance Benchmark (push) Has been cancelled
Hardware Performance Test / HW Benchmark - Save PR Number (push) Has been cancelled
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1195 lines
45 KiB
Python
1195 lines
45 KiB
Python
#!/usr/bin/env python3
|
|
"""Clean up generated LVGL example C files.
|
|
|
|
Generator output (`screenN_gen.c`) is verbose — section-banner comments, an
|
|
empty `style_inited` block, `LV_TRACE_OBJ_CREATE` traces, redundant
|
|
`lv_obj_set_name` calls, etc. — and this script post-processes those files
|
|
into the hand-written-looking examples we ship in `examples/`.
|
|
|
|
Each `.c` file that has a sibling `.xml` of the same basename is run through
|
|
`TRANSFORMATIONS` in order; each transform is `(source, path) -> str` and
|
|
pure, so the runner only writes back when the final text differs. Every
|
|
transform is idempotent — safe to re-run on already-processed output without
|
|
duplicating content.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
|
|
# Repo root = parent of `scripts/`. Used to (a) locate the `examples/` tree
|
|
# and (b) compute the relative path to `examples/lv_examples.h` for include
|
|
# rewriting.
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
# Subjects declared at the project level (one source of truth) so each example
|
|
# can pull the names/types/initial-values it actually references.
|
|
GLOBALS_XML_PATH = REPO_ROOT / "examples" / "xml_project" / "globals.xml"
|
|
|
|
# Fixed buffer size used for string subjects when promoted into example-local
|
|
# inits. The project's gen header uses `UI_SUBJECT_STRING_LENGTH = 256`; the
|
|
# example files don't include that header so we inline the literal here.
|
|
SUBJECT_STRING_BUF_SIZE = 256
|
|
|
|
|
|
# =============================================================================
|
|
# Generator boilerplate removal
|
|
# =============================================================================
|
|
|
|
# Matches the empty style-init scaffold the generator always emits:
|
|
#
|
|
# static bool style_inited = false;
|
|
#
|
|
# if (!style_inited) {
|
|
# <only whitespace>
|
|
# style_inited = true;
|
|
# }
|
|
#
|
|
# When a real style-init block has body content we leave it alone; the inner
|
|
# `(?:[ \t]*\n)*` only consumes blank lines between brace and `style_inited =
|
|
# true;`. The trailing `(?:[ \t]*\n)?` eats one blank line below so we don't
|
|
# leave a yawning gap after removal.
|
|
EMPTY_STYLE_INITED_RE = re.compile(
|
|
r"""
|
|
[ \t]*static\ bool\ style_inited\ =\ false;[ \t]*\n
|
|
(?:[ \t]*\n)*
|
|
[ \t]*if\ \(!style_inited\)\ \{[ \t]*\n
|
|
(?:[ \t]*\n)*
|
|
[ \t]*style_inited\ =\ true;[ \t]*\n
|
|
[ \t]*\}[ \t]*\n
|
|
(?:[ \t]*\n)?
|
|
""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
|
|
def remove_empty_style_inited(source: str) -> str:
|
|
return EMPTY_STYLE_INITED_RE.sub("", source)
|
|
|
|
|
|
# Trace calls like `LV_TRACE_OBJ_CREATE("begin");` — pure debug noise in the
|
|
# generated files. Optional trailing blank line is consumed too.
|
|
LV_TRACE_OBJ_CREATE_RE = re.compile(
|
|
r"[ \t]*LV_TRACE_OBJ_CREATE\([^)]*\);[ \t]*\n(?:[ \t]*\n)?"
|
|
)
|
|
|
|
|
|
def remove_lv_trace_obj_create(source: str) -> str:
|
|
return LV_TRACE_OBJ_CREATE_RE.sub("", source)
|
|
|
|
|
|
# The generator emits `lv_obj_set_name(obj, "...")` (and `_static` variant)
|
|
# for debugging-friendly object naming. Hand-written examples don't carry it.
|
|
LV_OBJ_SET_NAME_RE = re.compile(
|
|
r"[ \t]*lv_obj_set_name(?:_static)?\([^)]*\);[ \t]*\n"
|
|
)
|
|
|
|
|
|
def remove_lv_obj_set_name(source: str) -> str:
|
|
return LV_OBJ_SET_NAME_RE.sub("", source)
|
|
|
|
|
|
# Three-line section banner comments such as:
|
|
#
|
|
# /*********************
|
|
# * INCLUDES
|
|
# *********************/
|
|
#
|
|
# The opening sequence requires 3+ asterisks (`/\*{3,}`), which deliberately
|
|
# excludes doxygen file headers (`/**`).
|
|
SECTION_BANNER_RE = re.compile(
|
|
r"[ \t]*/\*{3,}[ \t]*\n"
|
|
r"[ \t]*\*[^\n]*\n"
|
|
r"[ \t]*\*{3,}/[ \t]*\n"
|
|
r"(?:[ \t]*\n)?"
|
|
)
|
|
|
|
|
|
def remove_section_banners(source: str) -> str:
|
|
return SECTION_BANNER_RE.sub("", source)
|
|
|
|
|
|
# Drop the ` * @brief ...` line inside the generator's file-header doxygen
|
|
# block. The file directive (`@file`) is updated later by `fix_file_directive`.
|
|
BRIEF_LINE_RE = re.compile(r"[ \t]*\*[ \t]*@brief[^\n]*\n")
|
|
|
|
|
|
def remove_brief_line(source: str) -> str:
|
|
return BRIEF_LINE_RE.sub("", source)
|
|
|
|
|
|
# =============================================================================
|
|
# Code modernization
|
|
# =============================================================================
|
|
|
|
# Replace the generator's `lv_obj_create(NULL)` root with `lv_screen_active()`
|
|
# and rename all references to that root (default name `lv_obj_0`) to `screen`.
|
|
ROOT_OBJ_CREATE_RE = re.compile(
|
|
r"lv_obj_t\s*\*\s*lv_obj_0\s*=\s*lv_obj_create\s*\(\s*NULL\s*\)\s*;"
|
|
)
|
|
LV_OBJ_0_REF_RE = re.compile(r"\blv_obj_0\b")
|
|
|
|
|
|
def root_obj_to_screen(source: str) -> str:
|
|
source = ROOT_OBJ_CREATE_RE.sub("lv_obj_t * screen = lv_screen_active();", source)
|
|
return LV_OBJ_0_REF_RE.sub("screen", source)
|
|
|
|
|
|
# Argument-content pattern that tolerates one level of nested parens, so we
|
|
# can match values like `lv_pct(100)` without truncating at the inner `)`.
|
|
# Without this, `[^)]+` would stop at the first `)`.
|
|
_ARG_PAT = r"(?:[^()]|\([^)]*\))*"
|
|
|
|
# Adjacent width/height pair on the same object → `lv_obj_set_size(obj, W, H)`.
|
|
# Group 1 = indentation, group 2 = object name (must match on both lines via
|
|
# the backreference `\2`), groups 3/4 = the width/height values.
|
|
WIDTH_HEIGHT_PAIR_RE = re.compile(
|
|
rf"([ \t]*)lv_obj_set_width\(\s*([A-Za-z_]\w*)\s*,\s*({_ARG_PAT})\)\s*;[ \t]*\n"
|
|
rf"\1lv_obj_set_height\(\s*\2\s*,\s*({_ARG_PAT})\)\s*;"
|
|
)
|
|
|
|
# Same shape, for x/y → `lv_obj_set_pos`. Note: we don't handle the reverse
|
|
# order (height-then-width or y-then-x) because the generator's output is
|
|
# consistent. Add a sibling regex if that assumption breaks.
|
|
X_Y_PAIR_RE = re.compile(
|
|
rf"([ \t]*)lv_obj_set_x\(\s*([A-Za-z_]\w*)\s*,\s*({_ARG_PAT})\)\s*;[ \t]*\n"
|
|
rf"\1lv_obj_set_y\(\s*\2\s*,\s*({_ARG_PAT})\)\s*;"
|
|
)
|
|
|
|
|
|
def combine_size_and_pos(source: str) -> str:
|
|
source = WIDTH_HEIGHT_PAIR_RE.sub(r"\1lv_obj_set_size(\2, \3, \4);", source)
|
|
source = X_Y_PAIR_RE.sub(r"\1lv_obj_set_pos(\2, \3, \4);", source)
|
|
return source
|
|
|
|
|
|
# Drop the trailing `return screen;` — the root is now `lv_screen_active()`,
|
|
# the function is `void`, so returning is meaningless. We consume one leading
|
|
# blank line so the function body doesn't end on dead whitespace.
|
|
RETURN_SCREEN_RE = re.compile(r"(?:[ \t]*\n)?[ \t]*return\s+screen\s*;[ \t]*\n")
|
|
|
|
|
|
def remove_return_screen(source: str, path: Path) -> str:
|
|
return RETURN_SCREEN_RE.sub("", source)
|
|
|
|
|
|
# =============================================================================
|
|
# Identity / naming
|
|
# =============================================================================
|
|
|
|
# `lv_obj_t * <name>_create(void)` → `void <file_stem>(void)`.
|
|
# The generator names the entry function either by screen index
|
|
# (`screenN_create`, older builds) or by the screen XML's filename
|
|
# (`<file_stem>_create`, current builds). The broader `\w+_create` match
|
|
# handles both. We drop the `_create` suffix entirely so example functions
|
|
# read as `void lv_example_<feature>(void)` — matching the legacy C-only
|
|
# examples. The new return type is `void` because `remove_return_screen`
|
|
# already dropped the return statement — leaving `lv_obj_t *` would emit a
|
|
# function declared to return a pointer with no return statement.
|
|
CREATE_FUNC_DECL_RE = re.compile(
|
|
r"lv_obj_t\s*\*\s*\w+_create\s*\(\s*void\s*\)"
|
|
)
|
|
|
|
|
|
def rename_create_function(source: str, path: Path) -> str:
|
|
return CREATE_FUNC_DECL_RE.sub(f"void {path.stem}(void)", source)
|
|
|
|
|
|
# Collapse the contiguous `#include` block at the top of the file into a
|
|
# single relative include of `examples/lv_examples.h` (the shared example
|
|
# header that pulls in `lvgl.h` plus the per-topic prototypes). `count=1`
|
|
# ensures we only touch the first such block, leaving any deeper `#include`
|
|
# (rare) untouched.
|
|
INCLUDE_BLOCK_RE = re.compile(
|
|
r"(?:#include[ \t]+[\"<][^\">\n]+[\">][ \t]*\n)+"
|
|
)
|
|
|
|
|
|
def replace_includes_with_lv_examples(source: str, path: Path) -> str:
|
|
# Relative path is recomputed per file so this works for any directory depth.
|
|
rel = os.path.relpath(REPO_ROOT / "examples" / "lv_examples.h", path.parent)
|
|
return INCLUDE_BLOCK_RE.sub(f'#include "{rel}"\n', source, count=1)
|
|
|
|
|
|
# Update the ` * @file <whatever>` line inside the existing file-header
|
|
# doxygen block to use the actual filename. Does nothing if no `@file` exists.
|
|
FILE_DIRECTIVE_RE = re.compile(r"(\*[ \t]*@file)[ \t]+[^\n]*")
|
|
|
|
|
|
def fix_file_directive(source: str, path: Path) -> str:
|
|
return FILE_DIRECTIVE_RE.sub(lambda m: f"{m.group(1)} {path.name}", source)
|
|
|
|
|
|
# =============================================================================
|
|
# XML doc mapping
|
|
# =============================================================================
|
|
#
|
|
# The sibling `.xml` carries semantic comments that the generator drops:
|
|
#
|
|
# * A top-level `<!-- ... -->` block (typically with `@title`/`@brief`)
|
|
# before `<screen>` — becomes a doxygen `/** ... */` block above the
|
|
# create function.
|
|
# * Inline `<!-- ... -->` before each element under `<view>` — becomes a
|
|
# `/* ... */` comment above the matching `lv_<type>_create(...)` line in
|
|
# the C file.
|
|
|
|
# Match a `lv_obj_t * <name> = lv_<type>_create(...)` line so we can map
|
|
# XML annotations to the right C creation by tag/order. Group 1 = indent,
|
|
# group 2 = the `lv_<type>` token (e.g. `lv_arc`, `lv_label`).
|
|
CREATE_LINE_RE = re.compile(
|
|
r"([ \t]*)lv_obj_t\s*\*\s*\w+\s*=\s*(lv_\w+)_create\s*\("
|
|
)
|
|
|
|
|
|
def _xml_elements_with_comments(xml_path: Path) -> list[tuple[str, str | None]]:
|
|
"""Return (tag, preceding_comment) for each XML element in document order.
|
|
|
|
A comment binds to the next element that follows it, regardless of
|
|
nesting depth (a comment inside `<view>` followed by `<lv_arc>` annotates
|
|
that arc; a comment inside `<lv_arc>` followed by `<lv_label>` annotates
|
|
that label).
|
|
|
|
Top-level comments outside the root (e.g. before `<screen>`) are NOT
|
|
returned here — those are handled separately by
|
|
`add_top_level_doc_comment` via a regex on the raw text, because
|
|
`ET.parse` discards everything outside the root.
|
|
"""
|
|
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
|
|
tree = ET.parse(xml_path, parser=parser)
|
|
root = tree.getroot()
|
|
|
|
annotations: list[tuple[str, str | None]] = []
|
|
pending: str | None = None
|
|
|
|
def walk(elem):
|
|
nonlocal pending
|
|
for child in elem:
|
|
if child.tag is ET.Comment:
|
|
# Comments get stored, awaiting the next non-comment element.
|
|
pending = (child.text or "").strip()
|
|
else:
|
|
annotations.append((child.tag, pending))
|
|
pending = None
|
|
# Recurse so nested elements are emitted in document order.
|
|
walk(child)
|
|
|
|
walk(root)
|
|
return annotations
|
|
|
|
|
|
# A comment block at the very start of the XML file (before any element).
|
|
# Used as the source for the per-function doxygen block.
|
|
TOP_LEVEL_XML_COMMENT_RE = re.compile(r"\A\s*<!--(.*?)-->", re.DOTALL)
|
|
|
|
# Locates the `void <name>_create(void)` line so we can insert the doxygen
|
|
# block immediately above it.
|
|
FUNCTION_DECL_RE = re.compile(
|
|
r"^void\s+lv_example_\w+\s*\(\s*void\s*\)", re.MULTILINE
|
|
)
|
|
|
|
# Matches a single `/** ... */` block at the very end of a substring (we slice
|
|
# `source[:fn_start]` and search this against it). Important nuance: the inner
|
|
# pattern `(?:[^*]|\*(?!/))*` refuses to cross a closing `*/`. Without that
|
|
# guard, the more obvious `[\s\S]*?\*/` combined with the trailing `\Z`
|
|
# anchor will silently expand across multiple adjacent doc blocks (because
|
|
# non-greedy backtracks until the `\Z` anchor is satisfied) — which used to
|
|
# eat the `@file` header along with the `@title` block above the function.
|
|
EXISTING_DOC_BLOCK_RE = re.compile(
|
|
r"/\*\*(?:[^*]|\*(?!/))*\*/[ \t]*\n\s*\Z"
|
|
)
|
|
|
|
|
|
def _format_doxygen_block(body: str) -> str:
|
|
"""Wrap a multi-line plain-text body into `/** * ... */` form.
|
|
|
|
Blank lines become bare ` *` (so doxygen renders them as paragraph
|
|
breaks); non-blank lines are reindented under ` * `.
|
|
"""
|
|
out = ["/**"]
|
|
for line in body.splitlines():
|
|
stripped = line.strip()
|
|
out.append(f" * {stripped}" if stripped else " *")
|
|
out.append(" */")
|
|
return "\n".join(out)
|
|
|
|
|
|
def add_top_level_doc_comment(source: str, path: Path) -> str:
|
|
"""Lift the XML's top-level comment into a doxygen block above the function.
|
|
|
|
Idempotent: if a `@title`/`@brief`-bearing block already sits right above
|
|
the function, it gets replaced rather than stacked. The (separate) `@file`
|
|
header at the top of the file is preserved because `EXISTING_DOC_BLOCK_RE`
|
|
can't span across multiple doc blocks (see the note on that regex).
|
|
"""
|
|
xml_path = path.with_suffix(".xml")
|
|
if not xml_path.exists():
|
|
return source
|
|
|
|
m = TOP_LEVEL_XML_COMMENT_RE.match(xml_path.read_text())
|
|
if not m or not m.group(1).strip():
|
|
return source
|
|
|
|
comment_block = _format_doxygen_block(m.group(1).strip()) + "\n"
|
|
|
|
fn_match = FUNCTION_DECL_RE.search(source)
|
|
if not fn_match:
|
|
return source
|
|
fn_start = fn_match.start()
|
|
|
|
# If a pre-existing @title/@brief doc block sits right above the function,
|
|
# strip it before inserting the fresh one (so re-runs replace, not stack).
|
|
# The "@title"/"@brief" content check ensures we only touch *our* block —
|
|
# not, say, a legit doxygen comment the user wrote about the function.
|
|
before_fn = source[:fn_start]
|
|
existing = EXISTING_DOC_BLOCK_RE.search(before_fn)
|
|
if existing and ("@title" in existing.group(0) or "@brief" in existing.group(0)):
|
|
source = source[: existing.start()] + source[fn_start:]
|
|
fn_start = existing.start()
|
|
|
|
return source[:fn_start] + comment_block + source[fn_start:]
|
|
|
|
|
|
def _format_inline_comment(comment: str, indent: str) -> list[str]:
|
|
"""Format an XML comment as 1+ properly indented C block-comment lines.
|
|
|
|
The XML source preserves whatever whitespace sat between `<!--` and `-->`
|
|
(typically tabs from nested XML indentation). Emitting `/* {comment} */`
|
|
verbatim leaves continuation lines stuck with XML indentation, which
|
|
looks broken in C source. We strip each line and re-indent continuations
|
|
under the start of the first comment word (3 spaces past `indent` to
|
|
clear the opening `/* `).
|
|
"""
|
|
lines = [ln.strip() for ln in comment.splitlines()]
|
|
# Drop blank leading/trailing lines so re-indent doesn't create stray
|
|
# empty comment rows.
|
|
while lines and not lines[0]:
|
|
lines.pop(0)
|
|
while lines and not lines[-1]:
|
|
lines.pop()
|
|
if not lines:
|
|
return []
|
|
if len(lines) == 1:
|
|
return [f"{indent}/* {lines[0]} */\n"]
|
|
cont = indent + " "
|
|
out = [f"{indent}/* {lines[0]}\n"]
|
|
for ln in lines[1:-1]:
|
|
out.append(f"{cont}{ln}\n")
|
|
out.append(f"{cont}{lines[-1]} */\n")
|
|
return out
|
|
|
|
|
|
def _strip_preceding_comment(out_lines: list[str]) -> None:
|
|
"""Pop any single-line `//` or `/* */` block comment at the tail of out_lines.
|
|
|
|
Used by `map_xml_comments` for idempotency: re-running the cleanup
|
|
should replace a stale comment block (e.g. an earlier misindented copy,
|
|
or the legacy `// ...` form), not stack a new one on top of it.
|
|
"""
|
|
if not out_lines:
|
|
return
|
|
last = out_lines[-1].rstrip()
|
|
if last.lstrip().startswith("//"):
|
|
out_lines.pop()
|
|
return
|
|
if last.endswith("*/"):
|
|
# Walk back to the line that opens the block; both single-line and
|
|
# multi-line `/* */` shapes collapse to this.
|
|
i = len(out_lines) - 1
|
|
while i >= 0 and "/*" not in out_lines[i]:
|
|
i -= 1
|
|
if i >= 0:
|
|
del out_lines[i:]
|
|
|
|
|
|
def map_xml_comments(source: str, path: Path) -> str:
|
|
"""Map per-element XML comments to `/* */` comments above C creations.
|
|
|
|
Pairing is by sequential order: the Nth `lv_<type>_create(...)` line in
|
|
the C file is associated with the Nth `lv_*` element in the XML's
|
|
document order (after filtering out wrapper tags like `<view>`).
|
|
|
|
The tag is verified to match (e.g. an `lv_arc` create line only consumes
|
|
an `lv_arc` annotation) — a mismatch likely means the C file diverged
|
|
from the XML, in which case we'd want to bail rather than misattribute.
|
|
Currently we silently skip mismatches; tighten this if the script starts
|
|
inserting comments in wrong places.
|
|
|
|
Idempotent across comment styles and shapes: any existing `//` or
|
|
`/* */` block immediately above the create line is stripped and replaced
|
|
so re-runs converge instead of accumulating. (Generated files don't have
|
|
hand-written comments above create calls — every such comment is
|
|
something this function previously inserted.)
|
|
"""
|
|
xml_path = path.with_suffix(".xml")
|
|
if not xml_path.exists():
|
|
return source
|
|
|
|
# Drop wrapper tags (e.g. `<view>`) that have no C counterpart — without
|
|
# this filter the pairing-by-index gets shifted by one.
|
|
annotations = [
|
|
(tag, comment)
|
|
for tag, comment in _xml_elements_with_comments(xml_path)
|
|
if tag.startswith("lv_")
|
|
]
|
|
|
|
out_lines: list[str] = []
|
|
idx = 0
|
|
for line in source.splitlines(keepends=True):
|
|
m = CREATE_LINE_RE.match(line)
|
|
if m and idx < len(annotations):
|
|
tag, comment = annotations[idx]
|
|
if tag == m.group(2) and comment:
|
|
indent = m.group(1)
|
|
_strip_preceding_comment(out_lines)
|
|
out_lines.extend(_format_inline_comment(comment, indent))
|
|
# Always advance the annotation pointer for any create line so
|
|
# one missing comment doesn't desync the whole mapping.
|
|
idx += 1
|
|
out_lines.append(line)
|
|
|
|
return "".join(out_lines)
|
|
|
|
|
|
# =============================================================================
|
|
# Local subject inits
|
|
# =============================================================================
|
|
#
|
|
# Subjects are declared once in `examples/xml_project/globals.xml`. The
|
|
# generator emits `&subject_X` references but assumes those subjects are
|
|
# externs defined by project init code. For standalone example files we
|
|
# inject the decl + init for each referenced subject so a reader can
|
|
# copy-paste the example and have it work.
|
|
#
|
|
# Note the label `style_inited` survives this section because
|
|
# `remove_empty_style_inited` downstream matches that literal name; the
|
|
# rename to `inited` happens after empty-block removal.
|
|
|
|
|
|
def _load_globals_subjects() -> dict[str, dict]:
|
|
"""Return a mapping `name -> {type, value, min_value, max_value}` from
|
|
`globals.xml`.
|
|
|
|
Cached on first call. Subjects keep their declaration order (Python 3.7+
|
|
dict preserves insertion order), which we lean on when emitting init
|
|
blocks so the example files stay diff-stable run-to-run.
|
|
"""
|
|
cache = getattr(_load_globals_subjects, "_cache", None)
|
|
if cache is not None:
|
|
return cache
|
|
|
|
subjects: dict[str, dict] = {}
|
|
if GLOBALS_XML_PATH.exists():
|
|
tree = ET.parse(GLOBALS_XML_PATH)
|
|
subjects_elem = tree.getroot().find("subjects")
|
|
if subjects_elem is not None:
|
|
for child in subjects_elem:
|
|
# ElementTree gives us comment nodes too when comments are
|
|
# captured; filter those out by checking the tag is a real
|
|
# string.
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
name = child.get("name")
|
|
if not name:
|
|
continue
|
|
subjects[name] = {
|
|
"type": child.tag, # "int", "string", "float"
|
|
"value": child.get("value", "0"),
|
|
"min_value": child.get("min_value"),
|
|
"max_value": child.get("max_value"),
|
|
}
|
|
|
|
_load_globals_subjects._cache = subjects # type: ignore[attr-defined]
|
|
return subjects
|
|
|
|
|
|
def _subject_decl_lines(name: str, meta: dict) -> list[str]:
|
|
"""Top-of-function static declarations for a subject (4-space indent).
|
|
|
|
`string` subjects need two backing buffers — value and previous-value —
|
|
plus the subject itself.
|
|
"""
|
|
out = [f" static lv_subject_t {name};"]
|
|
if meta["type"] == "string":
|
|
out.append(f" static char {name}_buf[{SUBJECT_STRING_BUF_SIZE}];")
|
|
out.append(f" static char {name}_prev_buf[{SUBJECT_STRING_BUF_SIZE}];")
|
|
return out
|
|
|
|
|
|
def _subject_init_lines(name: str, meta: dict) -> list[str]:
|
|
"""Init-block calls for a subject (8-space indent, inside `if (!inited)`)."""
|
|
typ = meta["type"]
|
|
value = meta["value"]
|
|
out: list[str] = []
|
|
if typ == "int":
|
|
out.append(f" lv_subject_init_int(&{name}, {value});")
|
|
if meta.get("min_value") is not None:
|
|
out.append(
|
|
f" lv_subject_set_min_value_int(&{name}, {meta['min_value']});"
|
|
)
|
|
if meta.get("max_value") is not None:
|
|
out.append(
|
|
f" lv_subject_set_max_value_int(&{name}, {meta['max_value']});"
|
|
)
|
|
elif typ == "float":
|
|
out.append(f" lv_subject_init_float(&{name}, {value});")
|
|
elif typ == "string":
|
|
# Multi-line call to match the canonical formatting of generated
|
|
# project code; the args are too long to fit comfortably on one line.
|
|
out.extend([
|
|
f" lv_subject_init_string(&{name},",
|
|
f" {name}_buf,",
|
|
f" {name}_prev_buf,",
|
|
f" {SUBJECT_STRING_BUF_SIZE},",
|
|
f' "{value}");',
|
|
])
|
|
return out
|
|
|
|
|
|
# Locates the function body's opening brace. We pair it with FUNCTION_DECL_RE
|
|
# (defined earlier) when we need to insert a fresh `style_inited` block in a
|
|
# file that didn't have one.
|
|
FUNCTION_OPEN_BRACE_RE = re.compile(
|
|
r"^(void\s+lv_example_\w+\s*\(\s*void\s*\))\s*\n\{\s*\n", re.MULTILINE
|
|
)
|
|
|
|
# Captures the body of the existing `static bool style_inited` block so we
|
|
# can splice subject inits in just before the `style_inited = true;` line.
|
|
INITED_BLOCK_RE = re.compile(
|
|
r"""
|
|
(?P<head>[ \t]*static\ bool\ style_inited\ =\ false;[ \t]*\n
|
|
(?:[ \t]*\n)*
|
|
[ \t]*if\ \(!style_inited\)\ \{[ \t]*\n)
|
|
(?P<body>(?:.*?\n)*?)
|
|
(?P<tail>[ \t]*style_inited\ =\ true;[ \t]*\n
|
|
[ \t]*\}[ \t]*\n)
|
|
""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
|
|
def _used_subjects(source: str) -> list[str]:
|
|
"""Return subjects from globals.xml referenced as `&name` in `source`,
|
|
keeping `globals.xml` order (for stable output)."""
|
|
meta = _load_globals_subjects()
|
|
return [n for n in meta if re.search(rf"&{re.escape(n)}\b", source)]
|
|
|
|
|
|
def init_subjects(source: str, path: Path) -> str:
|
|
"""Add `static lv_subject_t` declarations + init calls for any subject
|
|
the example references, so the file is self-contained.
|
|
|
|
Idempotent: a subject that already has a `static lv_subject_t <name>`
|
|
declaration isn't re-declared, and one that already has an
|
|
`lv_subject_init_*(&<name>` call isn't re-initialised.
|
|
"""
|
|
meta = _load_globals_subjects()
|
|
if not meta:
|
|
return source
|
|
|
|
used = _used_subjects(source)
|
|
if not used:
|
|
return source
|
|
|
|
new_decls: list[str] = []
|
|
new_inits: list[str] = []
|
|
for name in used:
|
|
if not re.search(rf"\bstatic\s+lv_subject_t\s+{re.escape(name)}\b", source):
|
|
new_decls.extend(_subject_decl_lines(name, meta[name]))
|
|
if not re.search(
|
|
rf"lv_subject_init_(?:int|float|string)\s*\(\s*&{re.escape(name)}\b",
|
|
source,
|
|
):
|
|
new_inits.extend(_subject_init_lines(name, meta[name]))
|
|
|
|
if not new_decls and not new_inits:
|
|
return source
|
|
|
|
block_match = INITED_BLOCK_RE.search(source)
|
|
if block_match:
|
|
# The example already has a `style_inited` block (typically because
|
|
# it has styles). Splice the subject decls in just before the block
|
|
# and the init lines in just before `style_inited = true;`.
|
|
block_start = block_match.start()
|
|
head = block_match.group("head")
|
|
body = block_match.group("body")
|
|
tail = block_match.group("tail")
|
|
|
|
if new_inits:
|
|
extra = "\n".join(new_inits) + "\n"
|
|
body = body + extra
|
|
|
|
new_block = head + body + tail
|
|
|
|
before_block = source[:block_start]
|
|
after_block = source[block_match.end():]
|
|
|
|
if new_decls:
|
|
# Insert decls just before the block, separated by a blank line
|
|
# from any earlier static declarations.
|
|
decl_text = "\n".join(new_decls) + "\n\n"
|
|
before_block = before_block + decl_text
|
|
|
|
return before_block + new_block + after_block
|
|
|
|
# No existing block — synthesise one right after the function's `{`.
|
|
fn_match = FUNCTION_OPEN_BRACE_RE.search(source)
|
|
if not fn_match:
|
|
return source
|
|
|
|
insert_pos = fn_match.end()
|
|
lines: list[str] = []
|
|
if new_decls:
|
|
lines.extend(new_decls)
|
|
lines.append("")
|
|
lines.append(" static bool style_inited = false;")
|
|
lines.append("")
|
|
lines.append(" if (!style_inited) {")
|
|
lines.extend(new_inits)
|
|
lines.append(" style_inited = true;")
|
|
lines.append(" }")
|
|
lines.append("")
|
|
block_text = "\n".join(lines) + "\n"
|
|
return source[:insert_pos] + block_text + source[insert_pos:]
|
|
|
|
|
|
# =============================================================================
|
|
# Image declarations
|
|
# =============================================================================
|
|
#
|
|
# The generator emits `lv_image_set_src(img, my_image)` for registered image
|
|
# names. To compile against a C-array image the call needs to pass `&my_image`
|
|
# (an `lv_image_dsc_t *`) and the example must declare the symbol via
|
|
# `LV_IMAGE_DECLARE(my_image)` so the linker can resolve the extern.
|
|
#
|
|
# Image names that look like LVGL constants (start with `LV_`) — e.g.
|
|
# `LV_SYMBOL_OK` — are skipped because those are string macros, not C-array
|
|
# image descriptors.
|
|
|
|
# Captures the `lv_image_set_src(obj, [&]name)` call. Groups:
|
|
# 1 = call prefix up to and including the `,`/whitespace before the arg
|
|
# 2 = optional `&` already present (idempotency)
|
|
# 3 = the image identifier
|
|
LV_IMAGE_SET_SRC_RE = re.compile(
|
|
r"(lv_image_set_src\s*\(\s*[A-Za-z_]\w*\s*,\s*)(&?)([A-Za-z_]\w*)"
|
|
)
|
|
|
|
# A registered image can also reach a style by name through the image-src
|
|
# style setters — `lv_style_set_bg_image_src(&style, name)`,
|
|
# `lv_style_set_arc_image_src(...)`, `lv_obj_set_style_bg_image_src(obj,
|
|
# name, sel)`, … — which need the same `&name` + `LV_IMAGE_DECLARE` fix. The
|
|
# first argument is a `&style`/`obj`, so it is matched loosely as `[^,]+`.
|
|
LV_STYLE_IMAGE_SRC_RE = re.compile(
|
|
r"(lv_(?:style_set|obj_set_style)_\w*image_src\s*\(\s*[^,]+,\s*)(&?)([A-Za-z_]\w*)"
|
|
)
|
|
|
|
|
|
def declare_and_ref_images(source: str, path: Path) -> str:
|
|
images: list[str] = [] # preserve first-seen order for stable output
|
|
|
|
def repl(m: re.Match) -> str:
|
|
prefix, amp, name = m.group(1), m.group(2), m.group(3)
|
|
if name.startswith("LV_"):
|
|
# `LV_SYMBOL_*` etc. — symbol macros, not C-array images.
|
|
return m.group(0)
|
|
if name not in images:
|
|
images.append(name)
|
|
return f"{prefix}&{name}" if not amp else m.group(0)
|
|
|
|
new_source = LV_IMAGE_SET_SRC_RE.sub(repl, source)
|
|
new_source = LV_STYLE_IMAGE_SRC_RE.sub(repl, new_source)
|
|
if not images:
|
|
return new_source
|
|
|
|
# Add `LV_IMAGE_DECLARE(name);` once per image at the top of the function
|
|
# body. Idempotent: skip names that already have a declare line.
|
|
fn_match = FUNCTION_OPEN_BRACE_RE.search(new_source)
|
|
if not fn_match:
|
|
return new_source
|
|
|
|
insert_pos = fn_match.end()
|
|
decls: list[str] = []
|
|
for img in images:
|
|
if not re.search(rf"LV_IMAGE_DECLARE\s*\(\s*{re.escape(img)}\s*\)", new_source):
|
|
decls.append(f" LV_IMAGE_DECLARE({img});")
|
|
if not decls:
|
|
return new_source
|
|
|
|
block = "\n".join(decls) + "\n\n"
|
|
return new_source[:insert_pos] + block + new_source[insert_pos:]
|
|
|
|
|
|
# =============================================================================
|
|
# Image declarations from globals.xml
|
|
# =============================================================================
|
|
#
|
|
# `declare_and_ref_images` above only covers images that appear as the direct
|
|
# argument to `lv_image_set_src`. This companion transform reads the image
|
|
# registry from `globals.xml` and inserts `LV_IMAGE_DECLARE(name)` for every
|
|
# registered image whose name appears anywhere in the source as a bare C
|
|
# identifier — covering setters like `lv_style_set_arc_image_src` that the
|
|
# narrower regex above doesn't see.
|
|
|
|
|
|
def _load_globals_images() -> dict[str, dict]:
|
|
"""Return a mapping `name -> {src_path}` from the `<images>` section of `globals.xml`.
|
|
|
|
Cached on first call. Keeps declaration order (Python 3.7+ dicts) for
|
|
stable output across runs.
|
|
"""
|
|
cache = getattr(_load_globals_images, "_cache", None)
|
|
if cache is not None:
|
|
return cache
|
|
|
|
images: dict[str, dict] = {}
|
|
if GLOBALS_XML_PATH.exists():
|
|
tree = ET.parse(GLOBALS_XML_PATH)
|
|
images_elem = tree.getroot().find("images")
|
|
if images_elem is not None:
|
|
for child in images_elem:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
name = child.get("name")
|
|
if not name:
|
|
continue
|
|
images[name] = {
|
|
"src_path": child.get("src_path", ""),
|
|
}
|
|
|
|
_load_globals_images._cache = images # type: ignore[attr-defined]
|
|
return images
|
|
|
|
|
|
def _used_global_images(source: str) -> list[str]:
|
|
"""Return image names from globals.xml referenced as identifiers in `source`.
|
|
|
|
Keeps globals.xml declaration order for stable output.
|
|
"""
|
|
meta = _load_globals_images()
|
|
return [n for n in meta if re.search(rf"\b{re.escape(n)}\b", source)]
|
|
|
|
|
|
def declare_global_images(source: str, path: Path) -> str:
|
|
"""Insert `LV_IMAGE_DECLARE(name)` for every globals.xml image the example references.
|
|
|
|
Idempotent: images that already have a declare line are skipped.
|
|
"""
|
|
meta = _load_globals_images()
|
|
if not meta:
|
|
return source
|
|
|
|
used = _used_global_images(source)
|
|
if not used:
|
|
return source
|
|
|
|
fn_match = FUNCTION_OPEN_BRACE_RE.search(source)
|
|
if not fn_match:
|
|
return source
|
|
|
|
insert_pos = fn_match.end()
|
|
decls: list[str] = []
|
|
for img in used:
|
|
if not re.search(rf"LV_IMAGE_DECLARE\s*\(\s*{re.escape(img)}\s*\)", source):
|
|
decls.append(f" LV_IMAGE_DECLARE({img});")
|
|
|
|
if not decls:
|
|
return source
|
|
|
|
block = "\n".join(decls) + "\n\n"
|
|
return source[:insert_pos] + block + source[insert_pos:]
|
|
|
|
|
|
# =============================================================================
|
|
# Font declarations from globals.xml
|
|
# =============================================================================
|
|
#
|
|
# The generator passes a `globals.xml`-registered font to a font setter by
|
|
# bare name: `lv_obj_set_style_text_font(label, font_example_large, 0)` or
|
|
# `lv_style_set_text_font(&style, font_example_large)`. To compile against a
|
|
# C-array font the call needs `&font_example_large` and the example must
|
|
# `LV_FONT_DECLARE(font_example_large)` so the linker resolves the extern.
|
|
#
|
|
# Scoped strictly to fonts declared in `globals.xml`'s `<fonts>` section, so
|
|
# built-in fonts (`&lv_font_montserrat_*`, `LV_FONT_DEFAULT`), theme getters
|
|
# (`lv_theme_get_font_*(...)`), and unrelated local `lv_font_t *` variables in
|
|
# C-only examples are left untouched.
|
|
|
|
|
|
def _load_globals_fonts() -> list[str]:
|
|
"""Return font names from the `<fonts>` section of `globals.xml`.
|
|
|
|
Cached on first call. Keeps declaration order for stable output.
|
|
"""
|
|
cache = getattr(_load_globals_fonts, "_cache", None)
|
|
if cache is not None:
|
|
return cache
|
|
|
|
fonts: list[str] = []
|
|
if GLOBALS_XML_PATH.exists():
|
|
tree = ET.parse(GLOBALS_XML_PATH)
|
|
fonts_elem = tree.getroot().find("fonts")
|
|
if fonts_elem is not None:
|
|
for child in fonts_elem:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
name = child.get("name")
|
|
if name and name not in fonts:
|
|
fonts.append(name)
|
|
|
|
_load_globals_fonts._cache = fonts # type: ignore[attr-defined]
|
|
return fonts
|
|
|
|
|
|
def declare_and_ref_fonts(source: str, path: Path) -> str:
|
|
"""`&`-prefix + `LV_FONT_DECLARE` every globals.xml font the example uses.
|
|
|
|
Idempotent: an already-`&`'d reference is left as-is, and a font that
|
|
already has a declare line is not declared again.
|
|
"""
|
|
names = _load_globals_fonts()
|
|
if not names:
|
|
return source
|
|
|
|
used: list[str] = [] # globals order — stable output
|
|
new_source = source
|
|
for name in names:
|
|
# The font name must be the *whole* argument of a text-font setter:
|
|
# the trailing `[,)]` guard prevents matching a substring of e.g.
|
|
# `lv_theme_get_<name>(...)`.
|
|
ref_re = re.compile(
|
|
r"(lv_(?:obj_set_style|style_set)_text_font\s*\(\s*[^,]+,\s*)"
|
|
r"(&?)(" + re.escape(name) + r")(\s*[,)])"
|
|
)
|
|
|
|
def repl(m: re.Match) -> str:
|
|
prefix, amp, fname, tail = m.groups()
|
|
return m.group(0) if amp else f"{prefix}&{fname}{tail}"
|
|
|
|
new_source, count = ref_re.subn(repl, new_source)
|
|
if count:
|
|
used.append(name)
|
|
|
|
if not used:
|
|
return new_source
|
|
|
|
fn_match = FUNCTION_OPEN_BRACE_RE.search(new_source)
|
|
if not fn_match:
|
|
return new_source
|
|
|
|
insert_pos = fn_match.end()
|
|
decls = [
|
|
f" LV_FONT_DECLARE({f});"
|
|
for f in used
|
|
if not re.search(rf"LV_FONT_DECLARE\s*\(\s*{re.escape(f)}\s*\)", new_source)
|
|
]
|
|
if not decls:
|
|
return new_source
|
|
|
|
block = "\n".join(decls) + "\n\n"
|
|
return new_source[:insert_pos] + block + new_source[insert_pos:]
|
|
|
|
|
|
# =============================================================================
|
|
# Coalesce the declaration block
|
|
# =============================================================================
|
|
#
|
|
# `declare_global_images`, `declare_and_ref_images` and `declare_and_ref_fonts`
|
|
# each splice their own `\n\n`-terminated block in at the function's opening
|
|
# brace, so a file using both ends up with the font block, a blank line, then
|
|
# the image block — two stray groups in insertion order. Gather every
|
|
# `LV_*_DECLARE` line that leads the function body and re-emit them as a single
|
|
# contiguous block, images before fonts, followed by exactly one blank line —
|
|
# the hand-written convention the example tree already uses.
|
|
|
|
DECLARE_LINE_RE = re.compile(r"[ \t]*LV_(IMAGE|FONT)_DECLARE\([^)]*\);[ \t]*\Z")
|
|
|
|
|
|
def coalesce_declares(source: str, path: Path) -> str:
|
|
fn_match = FUNCTION_OPEN_BRACE_RE.search(source)
|
|
if not fn_match:
|
|
return source
|
|
|
|
start = fn_match.end()
|
|
lines = source[start:].split("\n")
|
|
|
|
images: list[str] = []
|
|
fonts: list[str] = []
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
if line.strip() == "":
|
|
i += 1
|
|
continue
|
|
m = DECLARE_LINE_RE.fullmatch(line)
|
|
if not m:
|
|
break
|
|
(images if m.group(1) == "IMAGE" else fonts).append(line.strip())
|
|
i += 1
|
|
|
|
if not images and not fonts:
|
|
return source
|
|
|
|
block = "\n".join(f" {d}" for d in images + fonts) + "\n\n"
|
|
return source[:start] + block + "\n".join(lines[i:])
|
|
|
|
|
|
# =============================================================================
|
|
# Rename `style_inited` → `inited`
|
|
# =============================================================================
|
|
#
|
|
# After empty-block removal has done its job (which matches the literal name
|
|
# `style_inited`), rename any surviving guards. The block now also gates
|
|
# subject inits, so the more generic `inited` reads cleaner than
|
|
# `style_inited`. Whole-word match so we don't touch unrelated identifiers
|
|
# that happen to contain the substring.
|
|
|
|
STYLE_INITED_NAME_RE = re.compile(r"\bstyle_inited\b")
|
|
|
|
|
|
def rename_style_inited(source: str) -> str:
|
|
return STYLE_INITED_NAME_RE.sub("inited", source)
|
|
|
|
|
|
# =============================================================================
|
|
# Whitespace cleanup
|
|
# =============================================================================
|
|
|
|
# Strip blank lines that sit directly after an opening `{`. After earlier
|
|
# transformations remove `LV_TRACE_OBJ_CREATE("begin");` and the
|
|
# `style_inited` scaffold, the function body often starts with a stray blank
|
|
# line — this cleans that up.
|
|
BLANK_AFTER_OPEN_BRACE_RE = re.compile(r"(\{[ \t]*\n)(?:[ \t]*\n)+")
|
|
|
|
|
|
def remove_blank_after_open_brace(source: str) -> str:
|
|
return BLANK_AFTER_OPEN_BRACE_RE.sub(r"\1", source)
|
|
|
|
|
|
# Strip trailing spaces/tabs on every line. The generator occasionally emits
|
|
# ` ` (a "blank" line that's actually 4 spaces) between blocks, which is
|
|
# both unsightly and tends to upset code-format tools.
|
|
TRAILING_WS_RE = re.compile(r"[ \t]+$", re.MULTILINE)
|
|
|
|
|
|
def strip_trailing_whitespace(source: str) -> str:
|
|
return TRAILING_WS_RE.sub("", source)
|
|
|
|
|
|
# =============================================================================
|
|
# Drop unused pointer variables
|
|
# =============================================================================
|
|
#
|
|
# The generator assigns every created/added object to a local even when the
|
|
# example never touches it again, and not always an `lv_obj_t *` — e.g.
|
|
#
|
|
# lv_obj_t * lv_calendar_header_arrow_0 = lv_calendar_add_header_arrow(cal);
|
|
# lv_subject_increment_dsc_t * subject_increment_event_0 =
|
|
# lv_obj_add_subject_increment_event(btn, &s, LV_EVENT_CLICKED, -1);
|
|
#
|
|
# The call has the side effect we want but the variable is dead. Hand-written
|
|
# examples just write the bare call. Strip the `<type> * <name> = ` prefix
|
|
# for any single-token-typed pointer local when `<name>` occurs exactly once
|
|
# in the whole file (i.e. only at its own declaration); a variable referenced
|
|
# anywhere else is kept as-is. The optional-`const` + single type token
|
|
# matches the generator's actual output without reaching for arbitrary
|
|
# multi-word C types. `static` decls can't match (the `*` must follow the
|
|
# type token directly), so subject/style scaffolding is untouched.
|
|
# Idempotent: the rewritten bare call no longer matches the pattern.
|
|
|
|
PTR_DECL_ASSIGN_RE = re.compile(
|
|
r"^([ \t]*)(?:const\s+)?[A-Za-z_]\w*\s*\*\s*([A-Za-z_]\w*)\s*=\s*([^\n]+;)[ \t]*$",
|
|
re.MULTILINE,
|
|
)
|
|
|
|
|
|
def drop_unused_ptr_vars(source: str) -> str:
|
|
def repl(m: re.Match) -> str:
|
|
indent, name, rhs = m.group(1), m.group(2), m.group(3)
|
|
# A used variable appears at least twice (declaration + >=1 use).
|
|
# `\b` stops `lv_calendar_0` matching inside
|
|
# `lv_calendar_header_arrow_0`, and still counts `&name` uses.
|
|
if len(re.findall(rf"\b{re.escape(name)}\b", source)) > 1:
|
|
return m.group(0)
|
|
return f"{indent}{rhs}"
|
|
|
|
return PTR_DECL_ASSIGN_RE.sub(repl, source)
|
|
|
|
|
|
# =============================================================================
|
|
# Cast OR-ed enum flag expressions
|
|
# =============================================================================
|
|
#
|
|
# The generator emits OR-ed enum constants both in array initializers and as
|
|
# call arguments, e.g.
|
|
#
|
|
# static const lv_buttonmatrix_ctrl_t m[] = {A, B | C, D};
|
|
# lv_chart_add_series(c, col, LV_CHART_AXIS_PRIMARY_Y | LV_CHART_AXIS_PRIMARY_X);
|
|
#
|
|
# In C `B | C` is an `int`, which is fine; in C++ an `int` won't implicitly
|
|
# convert to the expected `enum` type, so g++ fails with "invalid conversion
|
|
# from 'int' to 'lv_..._t'". Wrap each `|`-run of constants from a *single*
|
|
# known enum family in an explicit `(lv_..._t)(...)` cast so the same source
|
|
# compiles as both C and C++.
|
|
#
|
|
# Only true `enum` types are listed. Deliberately excluded:
|
|
# * LV_PART_* | LV_STATE_* — combine into `lv_style_selector_t`, which is
|
|
# `typedef uint32_t` (no enum, `int` converts cleanly). Mixed-family runs
|
|
# are skipped anyway.
|
|
# * LV_SCALE_LABEL_ROTATE_* — plain `#define` int macros, no enum type.
|
|
ENUM_FLAG_TYPES = {
|
|
"LV_BUTTONMATRIX_CTRL_": "lv_buttonmatrix_ctrl_t",
|
|
"LV_CHART_AXIS_": "lv_chart_axis_t",
|
|
"LV_DIR_": "lv_dir_t",
|
|
"LV_FS_MODE_": "lv_fs_mode_t",
|
|
"LV_GRIDNAV_CTRL_": "lv_gridnav_ctrl_t",
|
|
}
|
|
|
|
ORED_CONSTS_RE = re.compile(r"LV_[A-Z0-9_]+(?:\s*\|\s*LV_[A-Z0-9_]+)+")
|
|
|
|
|
|
def _enum_flag_type(token: str) -> str | None:
|
|
for pfx, typ in ENUM_FLAG_TYPES.items():
|
|
if token.startswith(pfx):
|
|
return typ
|
|
return None
|
|
|
|
|
|
def cast_ored_enum_flags(source: str) -> str:
|
|
def repl(m: re.Match) -> str:
|
|
run = m.group(0)
|
|
types = {_enum_flag_type(t.strip()) for t in run.split("|")}
|
|
# Cast only when every operand is from the same known enum family;
|
|
# a mixed run (e.g. LV_PART_*|LV_STATE_*) or any unknown operand
|
|
# leaves `types` with >1 entry or a `None`.
|
|
if len(types) != 1 or None in types:
|
|
return run
|
|
typ = types.pop()
|
|
# Idempotent: don't re-wrap a run already inside its cast.
|
|
if source[: m.start()].endswith(f"({typ})("):
|
|
return run
|
|
return f"({typ})({run})"
|
|
|
|
return ORED_CONSTS_RE.sub(repl, source)
|
|
|
|
|
|
# =============================================================================
|
|
# Pipeline
|
|
# =============================================================================
|
|
#
|
|
# Every transformation has signature `(source: str, path: Path) -> str`.
|
|
# Functions that don't need `path` are wrapped in a `lambda` so the runner
|
|
# can call them uniformly. The order matters in a few places:
|
|
#
|
|
# * `remove_return_screen` and `rename_create_function` must both run
|
|
# before any reader assumes the function is `void` — together they make
|
|
# it so.
|
|
# * `replace_includes_with_lv_examples` must run before `fix_file_directive`
|
|
# would be meaningless either way, but conceptually the include block
|
|
# belongs to the "file identity" group.
|
|
# * `add_top_level_doc_comment` reads from the XML and writes above the
|
|
# function, so it must run after `rename_create_function` (so the
|
|
# function decl is already in its final shape).
|
|
# * `remove_blank_after_open_brace` runs last so it can clean up blanks
|
|
# left by everything else.
|
|
TRANSFORMATIONS = [
|
|
# Subject decls/inits — must run before the empty-block removal so the
|
|
# block isn't yet "empty" when we want to fill it.
|
|
init_subjects,
|
|
# Generator boilerplate.
|
|
lambda s, p: remove_empty_style_inited(s),
|
|
lambda s, p: remove_lv_trace_obj_create(s),
|
|
lambda s, p: remove_lv_obj_set_name(s),
|
|
# Code modernization.
|
|
lambda s, p: root_obj_to_screen(s),
|
|
lambda s, p: combine_size_and_pos(s),
|
|
remove_return_screen,
|
|
# Identity / naming.
|
|
rename_create_function,
|
|
# Image/font declarations + `&` prefix. These insert `LV_*_DECLARE`
|
|
# lines at the function's opening brace, which `FUNCTION_OPEN_BRACE_RE`
|
|
# only locates once the signature is the final `void <name>_create(void)`
|
|
# — so they must run *after* `remove_return_screen` +
|
|
# `rename_create_function`, not before.
|
|
declare_global_images,
|
|
declare_and_ref_images,
|
|
declare_and_ref_fonts,
|
|
# Merge the blocks the three transforms above each spliced in separately
|
|
# into one contiguous, images-then-fonts declare group.
|
|
coalesce_declares,
|
|
lambda s, p: remove_section_banners(s),
|
|
lambda s, p: remove_brief_line(s),
|
|
replace_includes_with_lv_examples,
|
|
fix_file_directive,
|
|
# XML doc mapping.
|
|
add_top_level_doc_comment,
|
|
map_xml_comments,
|
|
# Rename the guard variable now that the block has potentially gained
|
|
# subject inits — `inited` reads better than the legacy `style_inited`.
|
|
# Must follow `remove_empty_style_inited` which matches the literal name.
|
|
lambda s, p: rename_style_inited(s),
|
|
# Strip dead `<type> * x = ...;` declarations (x referenced only at
|
|
# its own declaration). Must run after `map_xml_comments`: removing a
|
|
# `... = lv_<type>_create(...)` LHS would otherwise desync the
|
|
# comment-to-create pairing, which keys off `CREATE_LINE_RE`.
|
|
lambda s, p: drop_unused_ptr_vars(s),
|
|
# Cast OR-ed enum flag runs so examples compile under g++ too.
|
|
lambda s, p: cast_ored_enum_flags(s),
|
|
# Whitespace cleanup (always last so it can mop up).
|
|
lambda s, p: remove_blank_after_open_brace(s),
|
|
lambda s, p: strip_trailing_whitespace(s),
|
|
]
|
|
|
|
|
|
# =============================================================================
|
|
# Runner
|
|
# =============================================================================
|
|
|
|
|
|
def find_target_files(examples_dir: Path) -> list[Path]:
|
|
"""Yield every `.c` under `examples_dir` that has a sibling `.xml`."""
|
|
targets = []
|
|
for c_file in examples_dir.rglob("*.c"):
|
|
if c_file.with_suffix(".xml").exists():
|
|
targets.append(c_file)
|
|
return sorted(targets)
|
|
|
|
|
|
def process(path: Path) -> bool:
|
|
"""Run all transformations on a file. Return True if it changed on disk."""
|
|
original = path.read_text()
|
|
updated = original
|
|
for fn in TRANSFORMATIONS:
|
|
updated = fn(updated, path)
|
|
if updated != original:
|
|
path.write_text(updated)
|
|
return True
|
|
return False
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
examples_dir = REPO_ROOT / "examples"
|
|
if not examples_dir.is_dir():
|
|
print(f"examples directory not found: {examples_dir}", file=sys.stderr)
|
|
return 1
|
|
|
|
targets = find_target_files(examples_dir)
|
|
if not targets:
|
|
print("No .c files with matching .xml siblings found.")
|
|
return 0
|
|
|
|
changed = 0
|
|
for path in targets:
|
|
if process(path):
|
|
changed += 1
|
|
print(f"updated: {path.relative_to(REPO_ROOT)}")
|
|
|
|
print(f"\n{changed} of {len(targets)} files changed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|