Files
lvgl/docs/example_list.py
Victor Wheeler dd2b0550e2
Some checks failed
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 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
Test API JSON generator / Test API JSON (push) Has been cancelled
Install LVGL using CMake / build-examples (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
Hardware Performance Test / Hardware Performance Benchmark (push) Has been cancelled
Hardware Performance Test / HW 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
Verify Font License / verify-font-license (push) Has been cancelled
Verify Kconfig / verify-kconfig (push) Has been cancelled
docs(examples): add back show c code button in widget pages (#9354)
2025-12-04 19:06:43 +01:00

562 lines
19 KiB
Python
Executable File

#!/usr/bin/env python3
""" example_list.py -- Build `examples.rst`
`examples.rst` is built by gathering all examples recursively under
`lvgl/examples/`. The recognized directives in that file are then
processed by Sphinx using `lvgl/docs/src/_ext/lv_example.py`.
Subsection names within `examples.rst` are made up of the capitalized
directory names ('_' and '-' characters are converted to spaces) or
are spelled out in `index.rst` files if the subsection names should be
spelled differently. See "index.rst Format" section below for details.
An "example" is defined as:
- being anywhere under the `lvgl/examples/` directory,
- the presence of an `index.rst` in a directory with examples, and
- example C code in file names typically starting with "lv_example_".
That C code and a live, interactive running example, is pulled into each
example in the EXAMPLES HTML page, as well as every doc where such
examples are included, by a directive like this:
.. code::
.. include:: /examples/layouts/flex/index.rst
Note that in the intermediate dir (from which LVGL user docs are generated),
``./examples/`` is a top-level subdirectory, so no relative "../../.." etc.
is required in the path. Using a path starting at "root" tells Sphinx to
start at the top-level directory where the docs are being generated,
typically ``lvgl/docs/intermediate/``.
See `build.py` for more information about the intermediate directory.
If a subdirectory is included in the `avoid_dirs` list herein, it will
not generate any output in the resulting `examples.rst` file.
Example: `lvgl/examples/assets/`.
Usage
*****
.. code::
import example_list
example_list.make_warnings_into_errors() # Optional
example_list.DEBUG_MODE = True # Optional
example_list.exec(intermediate_dir)
or run it directly:
.. code:: bash
python3 example_list.py "/home/lvgl/docs/intermediate"
In both cases, the `intermediate_dir` passed is expected to have
an `./examples/` directory in it (copy of `lvgl/examples/`) as a
top-level subdirectory.
Examples Directory Requirements
*******************************
.. code:: text
lvgl/examples/
index.rst (.. dir_order: pseudo-directive since sub-dirs
are not presented in alphabetical order)
anim/
index.rst (see below for expected contents)
lv_example_anim_1.c
lv_example_anim_2.c
lv_example_anim_3.c
lv_example_anim_4.c
lv_example_anim_timeline_1.c
...
layouts/
flex/
index.rst (see below for expected contents)
lv_example_flex_1.c
lv_example_flex_2.c
lv_example_flex_3.c
etc.
grid/
etc.
libs/
index.rst (section-heading name: "3rd-Party Libraries"
[since it is different than parent directory name])
barcode/
index.rst (see below for expected contents)
lv_example_barcode_1.c
lv_example_barcode_1.h
bmp/
etc.
etc.
etc.
.. note:: The above shows only 2 levels of directories but deeper
levels are supported (up to 5 below document title).
index.rst Format
****************
Examples
--------
Examples are included in `index.rst` files when they are sibling files with
the example C code.
.. code::
Example 1 Title <-- required for each example
--------------- <-- required for each example
<-- blank lines are ignored
.. lv_example:: anim/lv_example_anim_1 <-- path relative to the `lvgl/examples/` dir
:language: c
Repeat the above pattern for each example in the current directory. That number
may be zero (0) for directories like `libs/` in which all examples are in directories
below that level. See directory structure above.
For paths outside the current directory, simply provide the path to the code example
relative to the `lvgl/examples/` directory. Example from
`lvgl/examples/widgets/scale/index.rst`:
.. code::
...
A round scale style simulating a compass
----------------------------------------
.. lv_example:: widgets/scale/lv_example_scale_12
:language: c
Axis ticks and labels with scrolling on a chart
-----------------------------------------------
.. lv_example:: widgets/chart/lv_example_chart_2 <-- path is outside scale/ dir
:language: c
.. note::
Starting the example code filename with `lv_example_` is not a requirement of the
this script, but does make it clear that it is an example, so this pattern should
be preserved for new and changed examples.
Custom Section Headings
-----------------------
If a section heading needs to be spelled differently than the capitalized name of the
parent directory, then an `index.rst` file in that directory may contain the desired
section-heading name in an ``.. example_heading`` pseudo-directive. Example from
`lvgl/examples/libs/index.rst`:
.. code::
.. example_heading: 3rd-Party Libraries
Directory Reordering
--------------------
There are cases where it is not appropriate to present the contents of a
set of subdirectories in alphabetical order. When this is the case, a
pseudo-directive in the `index.rst` file in the parent directory can be specified
to govern the sequence its subdirectories are processed. The example below
is from `lvgl/examples/widgets/index.rst`. It is provided in order to
cause the "Base Widget" (obj) directory to be processed first (and thus
included in the output first).
.. code::
.. dir_order:
obj
animimg
arc
arclabel
bar
button
buttonmatrix
etc.
.. note::
A warning is issued if either:
- a subdirectory is named that does not exist, or
- a subdirectory exists that is not in the list and not in the `avoid_dirs` list.
Making Warnings into Errors
***************************
If `make_warnings_into_errors()` was called, if there were any warnings
issued, after the output is completely generated, this script will exit
with a non-zero exit code. This can be done by client modules like this:
.. code::
import example_list
example_list.make_warnings_into_errors()
example_list.exec(intermediate_dir)
To turn it off:
.. code::
example_list.make_warnings_into_errors(False)
Debug Output
************
Enable debug output like this:
.. code::
example_list.DEBUG_MODE = True
Suppressing DEBUG_MODE is the default.
"""
import os
import sys
from io import TextIOWrapper
from announce import *
# -------------------------------------------------------------------------
# This is the order that LVGL documentation uses for the section heading
# levels. header_defs[0] is the highest and header_defs[5] is the lowest.
# If this order is not kept in the reST files Sphinx will complain, and
# have difficulty formatting the TOC correctly.
# -------------------------------------------------------------------------
TITLE = '='
CHAPTER = '*'
SECTION = '-'
SUBSECTION = '~'
SUBSUBSECTION = '^'
SUBSUBSUBSECTION = "'"
header_defs = [
TITLE,
CHAPTER,
SECTION,
SUBSECTION,
SUBSUBSECTION,
SUBSUBSUBSECTION,
]
LV_EXAMPLE_DIRECTIVE = '.. lv_example::'
EXAMPLE_HEADING_DIRECTIVE = '.. example_heading:'
DIR_ORDER_DIRECTIVE = '.. dir_order:'
DIR_SEP = os.sep
INDEX_FILENAME = 'index.rst'
MAKE_WARNINGS_INTO_ERRORS = False
DEBUG_MODE = False
THIS_FILE = os.path.basename(__file__)
_warning_issued = False
_top_level_heading_count = 0
avoid_dirs = [
os.path.join('examples', 'arduino'),
os.path.join('examples', 'assets'),
]
def make_warnings_into_errors(val: bool = True):
global MAKE_WARNINGS_INTO_ERRORS
MAKE_WARNINGS_INTO_ERRORS = val
def _default_section_heading(level: int, path: str, is_file: bool) -> str:
if is_file:
dir_path = os.path.dirname(path)
else:
dir_path = path
parent_dir = os.path.basename(dir_path)
# Compose default section heading based on capitalized words in `parent_dir`.
word_list = parent_dir.replace('_', ' ').replace('-', ' ').split(' ')
result = ''
for word in word_list:
result += ' ' + word.capitalize()
# Remove leading space.
if result:
result = result[1:]
return result
def _warn(msg: str):
global _warning_issued
if MAKE_WARNINGS_INTO_ERRORS:
warning_type = 'Error'
else:
warning_type = 'Warning'
if is_silent_mode():
print(f'{THIS_FILE}: ' + '\x1b[31m' + f'>>> {warning_type}: ' + msg + '\x1b[0m')
else:
announce_colored(THIS_FILE, 'red', f'>>> {warning_type}: ' + msg)
_warning_issued = True
def _in_avoid_dirs_list(dir_bep: str) -> bool:
result = False
for avoid_dir in avoid_dirs:
if avoid_dir in dir_bep:
result = True
break
return result
def _validate_sub_dirs(sub_dirs: list[str], index_rst_path: str) -> bool:
""" Validate sub-dirs that come from an `index.rst` `.. dir_order:` directive.
1. Check that that each one is an existing directory.
2. Check that there are none missing except those in `avoid_dirs`.
Issue warning if either fails and set `_warning_issued`.
If MAKE_WARNINGS_INTO_ERRORS, this will make script exit with an error
code at the end.
:param sub_dirs: List of sub-dirs to validate
:param index_rst_path: Path to index.rst file
:return: Whether `sub_dirs` is valid per 1 and 2 above
"""
result = True
# Check that each sub-dir is an existing directory.
for sub_dir in sub_dirs:
if not os.path.isdir(sub_dir):
result = False
_warn(f'Dir-order directive in {index_rst_path} contains dir [{sub_dir}] that does not exist.')
# We won't break here so that all such dirs can be listed.
# Check that there are none missing except those in `avoid_dirs`.
dir_path = os.path.dirname(index_rst_path)
actual_dirs = []
for dir_item in os.listdir(dir_path):
path_bep = os.path.join(dir_path, dir_item)
if os.path.isdir(path_bep):
actual_dirs.append(path_bep)
# Are there any missing that are not in `avoid_dirs` list?
# If so, issue warning about each directory missing.
for sub_dir in actual_dirs:
if sub_dir not in sub_dirs:
if not _in_avoid_dirs_list(sub_dir):
_warn(f'Dir-order directive in {index_rst_path} is missing dir [{sub_dir}].')
result = False
return result
def _emit_heading(level: int, hdg: str, f: TextIOWrapper):
"""
Emit reST heading using `header_defs`.
:param level: Directory depth below the top directory in tree. [0-5]
:param hdg: Heading text
:param f: Output file (examples.rst)
:return:
"""
assert 0 <= level < len(header_defs), "level out of range"
global _top_level_heading_count
underline = header_defs[level] * len(hdg)
announce(__file__, f'Section heading [{hdg}] at level [{level}].')
if level == 0:
_top_level_heading_count += 1
if _top_level_heading_count > 1:
f.write('\n\n\n') # 3 extra lines above (doc) root titles, except 1st
f.write(underline)
f.write('\n')
elif level == 1:
f.write('\n\n') # 2 extra lines above chapters
elif level == 2:
f.write('\n') # 1 extra line above sections
f.write(hdg)
f.write('\n')
f.write(underline)
f.write('\n\n')
def _generate_output_from_dir(level: int,
root_len: int,
file_or_dir: str,
is_file: bool,
orig_dir_list: list[str],
f: TextIOWrapper) -> list[str]:
""" Output to `f` based on contents of `file_or_dir`.
:param level: Directory depth below the top directory in tree. [0-4]
:param root_len: Length of root path -- is deleted from paths so that
paths are relative to the `lvgl/examples/` dir.
:param file_or_dir: Path to "thing" being processed.
:param is_file: True if `file_or_dir` is an `index.rst` file.
:param orig_dir_list: List of sub-dirs below dir being processed.
Will be returned from this function if a
dir-order directive is NOT found in index file.
:param f: Output file (examples.rst)
:return: `orig_dir_list` or `dir_order_override` if a
dir-order directive is found in index file.
"""
dir_order_override = []
result_dir_list = orig_dir_list
# It is an error to proceed with `level` out of range. Clamping it to be in
# range is also an error because it would cause the output to be corrupted with
# an invalid section-heading underscore, which would generate an error later.
if 0 <= level and level + 1 < len(header_defs):
section_heading = _default_section_heading(level, file_or_dir, is_file)
example_tuples = []
relative_dir = ''
if is_file:
announce(__file__, f'Processing file [{file_or_dir}]...')
# We are processing an index.rst file.
with open(file_or_dir, 'r', encoding='utf-8') as fidx:
# It is important that this is NOT fidx.readlines() because
# it leaves blank lines containing '\n' instead of ''.
lines = fidx.read().split('\n')
example_title = ''
prev_line = ''
dir_path = os.path.dirname(file_or_dir)
in_dir_order_directive = False
# Accumulate data from `index.rst`. This needs to be done
# first in case it overrides the default section heading.
for line in lines:
if in_dir_order_directive:
leading_non_blank = (len(line) > 0) and not (line[0] == ' ' or line[0] == '\t')
if leading_non_blank:
# Leading non-blank character ends dir-order directive.
in_dir_order_directive = False
else:
# Still in dir-order directive.
stripped_line = line.strip()
if stripped_line:
dir_order_override.append(os.path.join(dir_path, stripped_line))
continue
stripped_line = line.strip()
if not stripped_line:
continue # Skip blank line.
elif stripped_line.startswith(EXAMPLE_HEADING_DIRECTIVE):
new_heading = stripped_line.replace(EXAMPLE_HEADING_DIRECTIVE, '').strip()
announce(__file__, f'Default section heading [{section_heading}] replaced with [{new_heading}]...')
section_heading = new_heading
elif stripped_line.startswith('---'):
example_title = prev_line
announce(__file__, f'Found example [{example_title}]...')
elif stripped_line.startswith(LV_EXAMPLE_DIRECTIVE):
rel_path_from_examples_dir = stripped_line.replace(LV_EXAMPLE_DIRECTIVE, '').strip()
announce(__file__, f' [{rel_path_from_examples_dir}]')
example_tuples.append( (example_title, rel_path_from_examples_dir) )
elif not in_dir_order_directive and stripped_line.startswith(DIR_ORDER_DIRECTIVE):
in_dir_order_directive = True
announce(__file__, f'Found DIR-ORDER directive.')
prev_line = stripped_line
# Output section heading. This occurs even when we have descended into an
# empty directory with example sub-dirs below it.
_emit_heading(level, section_heading, f)
# Output examples, if any. Will be empty when we are in an empty directory
# with example sub-dirs below it.
for example_tuple in example_tuples:
example_title = example_tuple[0]
rel_path_from_examples_dir = example_tuple[1]
example_hdg_underline = header_defs[level + 1] * len(example_title)
announce(__file__, f'Writing example [{rel_path_from_examples_dir}]...')
_emit_heading(level + 1, example_title, f)
f.write(LV_EXAMPLE_DIRECTIVE + ' ' + rel_path_from_examples_dir)
f.write('\n\n')
if dir_order_override:
_validate_sub_dirs(dir_order_override, file_or_dir)
result_dir_list = dir_order_override
return result_dir_list
def process_dir_recursively(level: int, root_len: int, dir_bep: str, f: TextIOWrapper):
""" Process dir `dir_bep` recursively, avoiding sub-dirs in `avoid_dirs`.
:param level: Directory depth below the top directory in tree.
:param root_len: Length of root path (gets removed from full path)
:param dir_bep: Directory *being processed*
:param f: Output file (examples.rst)
"""
announce(__file__, f'Processing dir [{dir_bep}]...')
sub_dirs = []
idx_files = []
if not os.path.isdir(dir_bep):
_warn(f'process_dir_recursively: `dir_bep` [{dir_bep}] does not exist.')
return
# For each "thing" found in `dir_bep`, build lists: sub_dirs and idx_files.
for dir_item in os.listdir(dir_bep):
path_bep = os.path.join(dir_bep, dir_item)
if os.path.isdir(path_bep):
sub_dirs.append(path_bep)
elif path_bep.endswith(INDEX_FILENAME):
idx_files.append(path_bep)
if idx_files:
sub_dirs = _generate_output_from_dir(level, root_len, idx_files[0], True, sub_dirs, f)
# `sub_dirs` can be replaced if `index.rst` contains a dir-order directive.
else:
_generate_output_from_dir(level, root_len, dir_bep, False, sub_dirs, f)
# Now recursively process sub_dirs.
for subdir in sub_dirs:
if not _in_avoid_dirs_list(subdir):
process_dir_recursively(level + 1, root_len, subdir, f)
def exec(intermediate_dir):
announce_set_silent_mode(not DEBUG_MODE)
output_path = os.path.join(intermediate_dir, 'examples.rst')
input_paths = [
os.path.join('..', 'examples'),
# os.path.join('..', 'demos')
]
with open(output_path, 'w', encoding='utf-8') as f:
f.write('.. _examples:\n')
f.write('\n')
# Recursively walk the directories in `input_paths` array for
# ``index.rst`` files.
for root_path in input_paths:
root_len = len(root_path) + 1
process_dir_recursively(0, root_len, root_path, f)
if MAKE_WARNINGS_INTO_ERRORS and _warning_issued:
exit(1)
if __name__ == '__main__':
"""Make module run-able as well as importable."""
base_dir = os.path.abspath(os.path.dirname(__file__))
os.chdir(base_dir)
exec(sys.argv[1])