diff --git a/arcade/resources/assets/images/alien/alienBlue_fall.png b/arcade/resources/assets/images/alien/alienBlue_fall.png new file mode 100644 index 000000000..437a473e4 Binary files /dev/null and b/arcade/resources/assets/images/alien/alienBlue_fall.png differ diff --git a/doc/links.rst b/doc/_includes/links.rst similarity index 100% rename from doc/links.rst rename to doc/_includes/links.rst diff --git a/doc/_includes/resources_Image_Theme_Sets.rst b/doc/_includes/resources_Image_Theme_Sets.rst new file mode 100644 index 000000000..ab629803c --- /dev/null +++ b/doc/_includes/resources_Image_Theme_Sets.rst @@ -0,0 +1,3 @@ +Arcade includes an number of themed image sets to help you get started +making specific types of games. Some of these help you complete the tutorials +while others are general-purpose prototyping tools. diff --git a/doc/_includes/resources_Kenney.rst b/doc/_includes/resources_Kenney.rst new file mode 100644 index 000000000..28c8eab59 --- /dev/null +++ b/doc/_includes/resources_Kenney.rst @@ -0,0 +1,9 @@ +.. figure:: images/fonts_blue.png + :align: center + :alt: The bundled Kenney.nl fonts. + +.. Put the text *after* the CSS, or add
via .. raw:: html blocks +.. since the CSS may be broken. + +Arcade includes the following fonts from `Kenney.nl's font pack `_ +are available using the path and filenames below. diff --git a/doc/_includes/resources_Liberation.rst b/doc/_includes/resources_Liberation.rst new file mode 100644 index 000000000..d1d238aa3 --- /dev/null +++ b/doc/_includes/resources_Liberation.rst @@ -0,0 +1,15 @@ +.. figure:: images/fonts_liberation.png + :alt: The bundled Liberation font family trio. + :align: center + +.. Put the text *after* the CSS, or add
via .. raw:: html blocks +.. since the CSS may be broken. + +Arcade also includes the Liberation font family. This trio is designed and +licensed specifically to be a portable, drop-in set of substitutes for Times, Arial, +and Courier fonts. It uses the proven, commercial-friendly `SIL Open Font License`_. + +To use these fonts, you may use either approach: + +* load files for specific variants via :py:func:`arcade.load_font` +* load all variants at once with :py:func:`arcade.resources.load_liberation_fonts`. diff --git a/doc/_includes/resources_Top-Level_Resources.rst b/doc/_includes/resources_Top-Level_Resources.rst new file mode 100644 index 000000000..b69278445 --- /dev/null +++ b/doc/_includes/resources_Top-Level_Resources.rst @@ -0,0 +1,8 @@ +This logo of the snake doubles as a quick way to test Arcade's resource handles. + +#. Mouse over the copy button (|Example Copy Button|) below +#. It should change color to indicate you've hovered +#. Click to copy + +Paste in your favorite text editor! + diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index 3178585f3..f633e7e9e 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -214,18 +214,120 @@ table.dataTable.display tbody tr.even > .sorting_1 { background-repeat: repeat; } + +/*Test*/ table.resource-table { empty-cells: hide; + white-space: normal; /* table behavior for CSS styling? maybe dupe of below. */ } + /* Make the resource page's table cell contents look neat */ -table.resource-table td { + +/* copy button doesn't need so much margin in here */ +.resource-table div.highlight-python { + margin-bottom: 0; +} + +/* Only center the contents of tiles which have images in them + * NOTE: The * instead of table alloes for non-table resource tiles + */ +*.resource-table td:has(.resource-thumb) { text-align: center; } -table.resource-table td > img { +.resource-table, +.resource-table table + { + width: 100%; + max-width: 100%; +} +.wy-table-responsive table :is(th, td) { white-space: normal; } +/* Prevent huge copyable areas for .. code:: boxes using sphinx-copybutton */ +.resource-table div.highlight > pre { + padding: 8px; +} + +table.resource-table td > .resource-thumb { display: inline-block; +} +table.resource-table td > img.resource-thumb { /* Maximum 20% of the smallest display dimension */ max-width: min(20vw, 20vh); } +table.resource-table td > .resource-thumb.file-icon { + width: 128px; + height: 128px; + max-width: 128px; + max-height: 128px; +} + + +.resource-handle { + display: inline-block; + border-radius: 0.4em; + border: 1px solid rgb(0, 0, 0, 0); + width: fit-content !important; +} +.resource-handle:has(button.arcade-ezcopy:hover) { + border-color: #54c079; + color: #54c079; +} +.resource-table .caption-text > .literal, +.resource-handle > .literal { + font-style: normal; + height: 1.5em; + min-height: 1.5em; + border-radius: 0.4em; + border: 1px solid #1b1f2426; + vertical-align: middle; + /* Not clear why this doesn't work for the .caption-text */ + font-size: 1em !important; +} + + +/* Imitate sphinx-copybutton style */ +.arcade-ezcopy { + width: 1.5em; + height: 1.5em; + min-width: 1.5em; + min-height: 1.5em; + padding: 0; + + /* Imitate sphinx-copybutton minus styling spacing issues */ + border-radius: 0.4em 0.4em 0.4em 0.4em; + border: 1px solid #1b1f2426; + + border-left: 0; + + color: #57606a; + background-color: #f6f8fa; + +} +/* Hide left border on items which haven adjacent copy button + + CAVEAT: This won't currently work with right-to-left layout modes! + + This is because of the following assume left-to-right: + + 1. The + here assumes the button element's next in the DOM order + 2. The right border of the box gets switched off + */ +.resource-table * > .literal:has(+ button.arcade-ezcopy) { + border-radius: 0.4em 0 0 0.4em !important; +} +.resource-table .literal + button.arcade-ezcopy { + border-radius: 0 0.4em 0.4em 0 !important; +} + + + +.arcade-ezcopy > img { + margin: 0; + width: 100%; + height: 100%; +} +.arcade-ezcopy:hover { + background-color: #54c079; +} table.colorTable { border-width: 1px; diff --git a/doc/_static/filetiles/state-error.png b/doc/_static/filetiles/state-error.png new file mode 100644 index 000000000..4084bdd1a Binary files /dev/null and b/doc/_static/filetiles/state-error.png differ diff --git a/doc/_static/filetiles/type-glsl.png b/doc/_static/filetiles/type-glsl.png new file mode 100644 index 000000000..81589c995 Binary files /dev/null and b/doc/_static/filetiles/type-glsl.png differ diff --git a/doc/_static/filetiles/type-json.png b/doc/_static/filetiles/type-json.png new file mode 100644 index 000000000..cea40baf4 Binary files /dev/null and b/doc/_static/filetiles/type-json.png differ diff --git a/doc/_static/filetiles/type-unknown.png b/doc/_static/filetiles/type-unknown.png new file mode 100644 index 000000000..259d50f4f Binary files /dev/null and b/doc/_static/filetiles/type-unknown.png differ diff --git a/doc/_static/js/custom.js b/doc/_static/js/custom.js index 5f2b3672d..30e450fb2 100644 --- a/doc/_static/js/custom.js +++ b/doc/_static/js/custom.js @@ -55,13 +55,14 @@ function handleSidebarHeaderToggle() { registerOnScrollEvent(mediaQuery); } } - - /** * Load all custom code only once the DOM document has fully loaded. * * Notice that jQuery is already available in this file. */ $(document).ready(() => { - handleSidebarHeaderToggle() + handleSidebarHeaderToggle(); + // Re-use the base ClipboardJS provided by sphinx-copybutton + // .doc-ui-example-dummy marks a button as a training dummy on the resources page and elsewhere + document.ezcopy = new ClipboardJS('.arcade-ezcopy:not(.doc-ui-example-dummy'); }); diff --git a/doc/api_docs/images/tiled_icon_digi_pls_replace.png b/doc/api_docs/images/tiled_icon_digi_pls_replace.png new file mode 100644 index 000000000..18ac05b9d Binary files /dev/null and b/doc/api_docs/images/tiled_icon_digi_pls_replace.png differ diff --git a/doc/conf.py b/doc/conf.py index e576ade07..11df9852e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -14,6 +14,10 @@ import sphinx.transforms import sys +from docutils import nodes +from docutils.nodes import literal +from sphinx.util.docutils import SphinxRole + # As of pyglet==2.1.dev7, this is no longer set in pyglet/__init__.py # because Jupyter / IPython always load Sphinx into sys.modules. See # the following for more info: @@ -166,7 +170,7 @@ def run_util(filename, run_name="__main__", init_globals=None): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [ - "links.rst", + "_includes/*", "substitutions.rst", "_archive/*", ] @@ -247,15 +251,22 @@ def run_util(filename, run_name="__main__", init_globals=None): 'pymunk': ('https://www.pymunk.org/en/latest/', None), } - # These will be joined as one block and prepended to every source file. # Substitutions for |version| and |release| are predefined by Sphinx. PROLOG_PARTS = [ #".. include:: /links.rst", ".. |pyglet Player| replace:: pyglet :py:class:`~pyglet.media.player.Player`", - ".. _Arcade's License File on GitHub: {FMT_URL_REF_BASE}/license.rst" + ".. _Arcade's License File on GitHub: {FMT_URL_REF_BASE}/license.rst", + + ( # Allows explaining how to copy anywhere in the doc. + '.. |Example Copy Button| raw:: html\n\n' + '
\n' + ' \n\n' + '
\n\n' + ) + ] -with open("links.rst") as f: +with open("_includes/links.rst") as f: PROLOG_PARTS.extend(f.readlines()) rst_prolog = "\n".join(PROLOG_PARTS) @@ -409,6 +420,29 @@ class A(NamedTuple): A('doctreedir'), ) + +class ResourceRole(SphinxRole): # pending: 3.1 + """Get resource file and category cross-references sorta working. + + This needs improvement. + """ + def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]: + raw = self.text.removeprefix(":resource:") + page_id = self.text\ + .replace(':', '')\ + .replace('/', '-')\ + .replace('_', '-')\ + .replace('.', '-') + + filename = f"'{raw.split('/')[-1]}'" + node = nodes.reference(text=filename, refuri=''.join([ + '/api_docs/resources.html#', page_id]), + ) + + print("HALP?", locals()) + return [node], [] + + def setup(app): print("Diagnostic info since readthedocs doesn't use our make.py:") for attr, comment in APP_CONFIG_DIRS: @@ -432,7 +466,7 @@ def setup(app): app.connect('autodoc-process-signature', strip_init_return_typehint, -1000) app.connect('autodoc-process-bases', on_autodoc_process_bases) # app.add_transform(Transform) - + app.add_role('resource', ResourceRole()) # ------------------------------------------------------ # Old hacks that breaks the api docs. !!! DO NOT USE !!! diff --git a/util/create_resources_listing.py b/util/create_resources_listing.py index 7dd716b75..0aa04010b 100644 --- a/util/create_resources_listing.py +++ b/util/create_resources_listing.py @@ -7,16 +7,26 @@ # ruff: noqa from __future__ import annotations -import math +import copy +import html import re import sys +import textwrap from collections import defaultdict +from collections.abc import Mapping from functools import lru_cache, cache +from io import StringIO +from itertools import chain, cycle, islice from pathlib import Path -from typing import List, Callable, Protocol +from typing import List, Callable, Protocol, Sequence, Iterable, TypeVar, NamedTuple import logging -log = logging.getLogger(__name__) +import PIL.Image +from typing_extensions import TypedDict, NotRequired, Self + +FILE = Path(__file__) + +log = logging.getLogger(FILE.name) # Ensure we get utility and Arcade imports first sys.path.insert(0, str(Path(__file__).parent.resolve())) @@ -54,7 +64,13 @@ def announce_templating(var_name): MODULE_DIR = Path(__file__).parent.resolve() ARCADE_ROOT = MODULE_DIR.parent RESOURCE_DIR = ARCADE_ROOT / "arcade" / "resources" -OUT_FILE = ARCADE_ROOT / "doc" / "api_docs" / "resources.rst" +DOC_ROOT = ARCADE_ROOT / "doc" +INCLUDES_ROOT = DOC_ROOT / "_includes" +OUT_FILE = DOC_ROOT / "api_docs" / "resources.rst" + + +class SupportsLT(Protocol): + def __lt__(self, other): ... # Metadata for the resource list: utils\create_resource_list.py @@ -76,15 +92,6 @@ def skipped_file(file_path: Path): return file_path.suffix in skip_extensions -MAX_COLS: dict[str, int] = defaultdict(lambda: 3) -MAX_COLS[":resources:sounds/"] = 2 - - -@lru_cache(maxsize=None) -def get_header_num_cols(resource_stub: str, n_files = math.inf) -> int: - return int(min(MAX_COLS[resource_stub], n_files)) - - @lru_cache(maxsize=None) def get_column_widths_for_n(n: int) -> str: width = str(100 // n) @@ -92,65 +99,127 @@ def get_column_widths_for_n(n: int) -> str: @lru_cache(maxsize=None) # Cache b/c re-using elsewhere -def create_resource_path( +def path_as_resource_handle( path: Path, prefix: str = "", suffix: str = "", - restrict_to_bases=('system', 'assets') + restrict_to_bases=('system', 'assets'), + relative_to: str | Path = RESOURCE_DIR ) -> str: """ Create a resource path. We will use the resources handle and will need to the "assets" and "system" directory from the path. """ - path = path.relative_to(RESOURCE_DIR) + path = path.relative_to(relative_to) base = path.parts[0] if not restrict_to_bases or base in restrict_to_bases: path = path.relative_to(base) else: - raise ValueError(f"Unexpected path: {path}. Expected one of: {', '.join(repr(b) for b in expect_bases)}") + raise ValueError(f"Unexpected path: {path}. Expected one of: {', '.join(repr(b) for b in restrict_to_bases)}") - return f"{prefix}:resources:{path.as_posix()}{suffix}" + parts = [prefix, ":resources:"] + as_posix = path.as_posix() + if not as_posix.startswith('/'): + parts.append('/') + parts.extend((as_posix, suffix)) -KENNEY_TTFS = "Kenney TTFs" -LIBERATION_TTFS = "Liberation TTFs" + return ''.join(parts) + #return f"{prefix}:resources:{path.as_posix()}{suffix}" -PREFIX_REF_TARGET = { - KENNEY_TTFS: "resources-fonts-kenney", - LIBERATION_TTFS: "resources-fonts-liberation" -} -# pending: post-3.0 cleanup # unstructured kludge -REPLACE_TITLE_WORDS = { - "Kenney": KENNEY_TTFS, - "Liberation": LIBERATION_TTFS, - "gui": "GUI", - "window": "Window & Panel", - ".": "Top-level Resources" +class TableConfigDict(TypedDict): + widths: NotRequired[str | Sequence[str | int ]] + header_row: NotRequired[Sequence[str]] + + +class HeadingConfigDict(TypedDict): + ref_target: NotRequired[str] + skip: NotRequired[bool] + value: NotRequired[str] + level: NotRequired[int] + + +class HandleLevelConfigDict(TypedDict): + heading: NotRequired[HeadingConfigDict] + include: NotRequired[str] + list_table: NotRequired[TableConfigDict] + + +FONT_TABLE_DEFAULTS: TableConfigDict= { + 'widths' : (30, 15, 55), + 'header_row': ( + ':py:class:`font_name `', + "Style(s)", + ":ref:`Resource Handle `", + ), } -# NASTY! # pending: post-3.0 cleanup -OVERRIDE_LEVELS = { - KENNEY_TTFS: 2, - LIBERATION_TTFS: 2 + + +RESOURCE_HANDLE_CONFIGS: dict[str,HandleLevelConfigDict] = { + ":resources:/": { + "heading": { + "value": "Top-Level Resources", + "level": 1 + }, + "include": "resources_Top-Level_Resources.rst" + }, + ":resources:/fonts/ttf/": { + "heading": {"skip": True} + }, + ":resources:/fonts/ttf/Kenney/": { + "heading": { + "ref_target": "resources-fonts-kenney", + "value": "Kenney TTFs", + "level": 2, + }, + "include": "resources_Kenney.rst", + "list_table": {**FONT_TABLE_DEFAULTS} + }, + ":resources:/fonts/ttf/Liberation/": { + "heading": { + "ref_target": "resources-fonts-liberation", + "value": "Liberation TTFs", + "level": 2, + }, + "include": "resources_Liberation.rst", + "list_table": {**FONT_TABLE_DEFAULTS} + }, + ":resources:/images/": { + "heading": { + "value": "Image Theme Sets", + }, + "include": "resources_Image_Theme_Sets.rst" + }, + ":resources:/gui_basic_assets/": { + "heading": {"value": "GUI Basic Assets"}, + }, + ":resources:/gui_basic_assets/window/": { + "heading": {"value": "Window & Panel"} + } } +T = TypeVar('T') +R = TypeVar('R') # Result type + + # pending: post-3.0 cleanup # more unstructured filth -SKIP_TITLES = { - "Ttf" - # "Kenney TTFs" -} +SKIP_HANDLES = set([ + handle for handle, d in RESOURCE_HANDLE_CONFIGS.items() + if ( + 'heading' in d and d['heading'].get('skip', None) + ) +]) +# print("ALL_HANDLES", SKIP_HANDLES) + + +visited_headings = set() @cache def format_title_part(raw: str): - out = [] - for word in raw.split('_'): - if word in REPLACE_TITLE_WORDS: - out.append(REPLACE_TITLE_WORDS[word]) - else: - out.append(word.capitalize()) - + out = [word.capitalize() for word in raw.split('_')] return ' '.join(out) @@ -164,10 +233,12 @@ def format_title_part(raw: str): ) -visited_headings = set() - - -def do_heading(out, relative_heading_level: int, heading_text: str) -> None: +def do_heading( + out, + relative_heading_level: int, + heading_text: str, + ref_target: str | None = None +) -> None: """Writes a heading to the output file. If the page heading is beyond what we have symbols for, the Sphinx @@ -177,12 +248,15 @@ def do_heading(out, relative_heading_level: int, heading_text: str) -> None: out: A file-like object which acts like its opened with ``"w"`` relative_heading_level: Heading level relative to the page root. heading_text: The heading text to display. - + ref_target: ``True`` to auto-generate it or a str to use a specific one. """ out.write("\n") print(f"doing heading: {heading_text!r} {relative_heading_level}") num_headings = len(headings_lookup) + if ref_target: + out.write(f".. _{ref_target}:\n\n") + if relative_heading_level >= num_headings: # pending: post-3.0 cleanup log.warning( @@ -196,21 +270,19 @@ def do_heading(out, relative_heading_level: int, heading_text: str) -> None: out.write("\n") +# Yes, this *is* used: we have a PyInstaller folder! PRIVATE_NAME = re.compile(r'^__') -def is_nonprotected_dir(p: Path): +def is_nonprotected_dir(p: Path) -> bool: + """True if ``p`` is a folder which isn't marked with ``__`` or other privacy checks.""" return p.is_dir() and not PRIVATE_NAME.match(p.stem) -def is_unskipped_file(p: Path): +def is_unskipped_file(p: Path) -> bool: return not (p.is_dir() or p.suffix in skip_extensions) -class SupportsLT(Protocol): - def __lt__(self, other): ... - - def filter_dir( dir: Path, keep: Callable[[Path], bool] = lambda path: True, @@ -235,185 +307,431 @@ def filter_dir( return kept +def coerce_iterable_to_str( + i: str | Iterable[T], + converter: Callable[[T], R] = str +) -> str: + if isinstance(i, str): + return i + else: + return ' '.join(map(converter, i)) + + +_sphinx_option_handlers: dict[str, Callable] = defaultdict(lambda: str) +_sphinx_option_handlers.update({ + 'class': coerce_iterable_to_str, + 'widths': coerce_iterable_to_str, + 'header-row': coerce_iterable_to_str +}) + + +def sphinx_directive( + name: str, + *arguments: str, + options: Mapping[str, str | int | Iterable] | None = None, + body: str | Iterable | None = None +) -> str: + lines = [f".. {name}:: {' '.join(arguments)}\n"] + + if options: + for name, value in options.items(): + converter = _sphinx_option_handlers[name] + lines.append( + f" :{name}: {converter(value)}\n") + lines.append("\n") + if body: + if isinstance(body, str): + body = (body,) + # We could use extend but this is nice for debugging + for i, value in enumerate(body): + lines.append(indent(" ", value)) + lines.append("\n\n") + + return ''.join(lines) + + +known_dirs = {} + + def process_resource_directory(out, dir: Path): """ Go through resources in a directory. """ - for path in filter_dir(dir, keep=is_nonprotected_dir): + child_directories = filter_dir(dir, keep=is_nonprotected_dir) + if dir == RESOURCE_DIR: + child_directories.sort(reverse=True) + + for path in child_directories: # out.write(f"\n{cur_node.name}\n") # out.write("-" * len(cur_node.name) + "\n\n") + temp_rel = path.relative_to(RESOURCE_DIR.parent) + log.info(f" Checking subdir {temp_rel}...") file_list = filter_dir(path, keep=is_unskipped_file) num_files = len(file_list) - def _debug_print_files() -> None: # pending: post-3.0 cleanup - """Nasty little temp helper""" - for file in file_list: - print(file.name) + if num_files <= 0: + print(f" SKIP: No files... {num_files}") + else: + print(" HAS FILES!") + handle_raw = path_as_resource_handle(path, suffix="/") + config: HandleLevelConfigDict = RESOURCE_HANDLE_CONFIGS.get(handle_raw, {}) + resource_handle = handle_raw.removesuffix('./') + + # print("CONFIG:\n", + # "raw :", raw_resource_handle, "\n", + # "handle :", resource_handle, "\n", + # "config :", config) + + # Generate a list of full-length resource handles + handle_steps_parts = resource_handle.strip("/").split("/") + handle_steps_wholes = [f"{handle_steps_parts[0]}/"] + for handle_step_whole in islice(handle_steps_parts, 1, len(handle_steps_parts)): + handle_steps_wholes.append( + f"{handle_steps_wholes[-1]}{handle_step_whole}/") + + print(" Subdir Config:") + _l = locals() + for k in filter(lambda _k: 'handle' in _k and('steps' in _k or _k.count('_') <2), _l.keys()): + print(f" {k} : {_l.get(k, None)!r}" if k else '') + + # Process headings and render any new ones we haven't seen + for heading_level, handle_step_whole in enumerate(handle_steps_wholes, start=0): + print(" heading check", (heading_level, handle_step_whole)) + if handle_step_whole in SKIP_HANDLES: + print(" skipping excluded") + continue + if handle_step_whole in visited_headings: + print(" skipping visited") + continue + visited_headings.add(handle_step_whole) + + local_config = RESOURCE_HANDLE_CONFIGS.get(handle_step_whole, {}) + local_heading_config = local_config.get('heading', {}) + + # print("proceeding...", + # "\n config ", local_config, + # "\n heading_config ", local_heading_config, sep = "") + + # Heading config fetch and write + use_level = local_heading_config.get('level', heading_level) + use_target = local_heading_config.get('ref_target', None) + use_value = local_heading_config.get('value', None) + if use_value is None: + use_value = format_title_part(handle_steps_parts[heading_level]) + + do_heading(out, use_level, use_value, ref_target=use_target) + out.write(f"\n.. comment `{handle_step_whole!r}``\n\n") + + # Include any include .rst # pending: inline via pluginification + if include := local_config.get("include", None): + if isinstance(include, str): + include = INCLUDES_ROOT / include + log.info(f" INCLUDE: Include resolving to {include})") + out.include_file(include) + + # Write table, header, and stuff after it + # Calculate configuration + opts = copy.deepcopy(config.get('list_table', {})) + parent_name = path.parent.name + columns = 3 if parent_name == "ttf" else min(len(file_list), 2) + + log.info(f" Rendering table for {path=!r} with {columns=!r}, {parent_name!r}") + write_list_table_header(out, resource_handle, opts) + process_resource_files(out, file_list, columns) + + # Recurse dirs + process_resource_directory(out, path) - if num_files > 0: - # header_title = f":resources:{path.relative_to(RESOURCE_DIR).as_posix()}/" - raw_resource_handle = create_resource_path(path, suffix="/") - resource_handle = raw_resource_handle[:-2] if raw_resource_handle.endswith("./") else raw_resource_handle +def indent( # pending: post-3.0 refactor # why would indent come after the text?! + spacing: str, + to_indent: str, + as_row: bool = False +) -> str: + """More readable ergonomics for text wrapping.""" - # pending: post-3.0 time to refactor all of this - parts = raw_resource_handle.replace(":resources:", "").rstrip("/").split("/") - display_parts = [format_title_part(part) for part in parts] + if not as_row: + return textwrap.indent(to_indent, spacing) + raw = StringIO(to_indent) + new = StringIO() + it = chain((spacing,), cycle((' ' * len(spacing),))) + for prefix, line in zip(it, raw.readlines()): + new.write(textwrap.indent(line, prefix)) - for heading_level, part in enumerate(display_parts, start=1): - if part in SKIP_TITLES: - continue - as_tup = tuple(display_parts[:heading_level]) - if as_tup not in visited_headings: - # NASTY! # pending: post 3.0 cleanup - if part in OVERRIDE_LEVELS: - heading_level = OVERRIDE_LEVELS[part] - - # print("!!!", heading_level, part, as_tup) - - if ref_target := PREFIX_REF_TARGET.get(part, None): - out.write(f".. _{ref_target}:\n") - - do_heading(out, heading_level, part) - visited_headings.add(as_tup) - - if raw_resource_handle == ":resources:images/": - _debug_print_files() - - if raw_resource_handle.startswith(":resources:fonts/ttf/"): - _debug_print_files() - if raw_resource_handle.endswith("Kenney/"): - out.write("\n") - - out.write(".. figure:: images/fonts_blue.png\n") - # out.write(" :align: center\n") - out.write(" :alt: The bundled Kenney.nl fonts.\n") - out.write("\n") - # Put the text *after* the CSS, or add
via .. raw:: html blocks - # since the CSS may be broken. - out.write("Arcade includes the following fonts from `Kenney.nl's font pack `_\n") - out.write("are available using the path and filenames below.\n") - out.write("\n") - - elif raw_resource_handle.endswith("Liberation/"): - out.write( - "\n" - ".. figure:: images/fonts_liberation.png\n" - " :alt: The bundled Liberation font family trio.\n" - #" :align: center\n" - # Put the text *after* the CSS, or add
via .. raw:: html blocks - # since the CSS may be broken. - "\n" - "Arcade also includes the Liberation font family. This trio is designed and\n" - "licensed specifically to be a portable, drop-in set of substitutes for Times, Arial,\n" - "and Courier fonts. It uses the proven, commercial-friendly `SIL Open Font License`_.\n" - "\n" - "To use these fonts, you may use either approach:\n" - "\n" - "* load files for specific variants via :py:func:`arcade.load_font`\n" - "* load all variants at once with :py:func:`arcade.resources.load_liberation_fonts`.\n" - "\n" - ) - - n_cols = get_header_num_cols(raw_resource_handle, num_files) - widths = get_column_widths_for_n(n_cols) - - # out.write(f"\n{header_title}\n") - # out.write("-" * (len(header_title)) + "\n\n") - - out.write(f"\n") - out.write(f".. raw:: html\n\n") - out.write(f" {resource_handle}\n") - - # pending: post-3.0 cleanup? - #out.write(f".. list-table:: \"{header_title}\"\n") - out.write(f".. list-table::\n") - out.write(f" :widths: {widths}\n") - out.write(f" :header-rows: 0\n") - out.write(f" :class: resource-table\n\n") - - process_resource_files(out, file_list) - out.write("\n\n") - - process_resource_directory(out, path) + return new.getvalue() -SUFFIX_TO_AUDIO_TYPE = { - '.wav': 'x-wav', - '.ogg': 'ogg', - '.mp3': 'mpeg', -} -SUFFIX_TO_VIDEO_TYPE = { - '.mp4': 'mp4', - '.webm': 'webm', - '.avi': 'avi' +def html_copyable( + value: str, + resource_handle: str, + string_quote_char: str | None = "'" +) -> str: + if string_quote_char: + value = f"{string_quote_char}{value}{string_quote_char}" + escaped = html.escape(value) + + raw = ( + f"\n" + f" \n" + f" {escaped}\n" + f" \n" + f" \n" + f"\n" + f"
\n\n") + + return raw + + +def highlight_copyable(out, inner: str) -> None: + out.write(f".. code-block:: python\n\n") + out.write(f" {inner!r}\n\n", "") + + +# Regex because why not? We're detecting CapitalWordBounds. +BRITTLE_CAP_WORD_REGEX = re.compile(r"[A-Z][a-z0-9]*") +BRITTLE_FONT_NAME_REGEX = re.compile( + r"""^ + # The 'redundant' \_ escaping improves readability. + (?P + [A-Z][a-z0-9]* # first capitalized word + (?:\_[A-Z][a-z0-9]*)? # Optional second title _Word + ) + (?:\_ # Optional FaceStyleWords (Bold, Italic, etc) + (?P(?:[A-Z][a-z0-9]*)+) + )? + """, re.X) + + +class BrittleFontData(NamedTuple): + face_name: str + styles: Iterable[str] + + @classmethod + def from_path(cls, path: Path) -> Self: + face_name_parts = BRITTLE_FONT_NAME_REGEX.match(path.name).groupdict() + face_name_pieces = (face_name_parts.get("face_name") or '').split('_') + + raw_name = ' '.join(face_name_pieces) + print(face_name_parts) + + styles = tuple(BRITTLE_CAP_WORD_REGEX.findall( + face_name_parts.get('styles', None) or '')) + + return cls(raw_name, styles) + + +class MediaTypeConfig(TypedDict): + media_kind: str + mime_suffix: str + + +MEDIA_EMBED = { + '.wav': { + 'media_kind': 'audio', + 'mime_suffix': 'x-wav' + }, + '.ogg': { + 'media_kind': 'audio', + 'mime_suffix': 'x-wav' + }, + '.mp3': { + 'media_kind': 'audio', + 'mime_suffix': 'mpeg' + }, + '.mp4': { + 'media_kind': 'video', + 'mime_suffix': 'mp4' + }, + '.webm': { + 'media_kind': 'video', + 'mime_suffix': 'webm' + }, + '.avi': { + 'media_kind': 'video', + 'mime_suffix': 'avi' + } } -def process_resource_files(out, file_list: List[Path]): +def code_block( + inner: str, + language: str | None = None, + options: Mapping[str, str | int | Iterable] | None = None +) -> str: + return sphinx_directive( + "code-block", language if language else None, "\n", + options=options, + body=inner + ) + + +def write_list_table_header(out, handle: str, options: Mapping | None = None): + merged = { + 'class': 'resource-table', + **(options or {}) + } + if (header_row := merged.pop('header_row', None)) is not None: + merged['header-rows'] = 1 + + out.write(f"\n.. list-table:: ``{handle!r}``\n") + for k, v in merged.items(): + new_k = k.replace('_', '-') + new_v = _sphinx_option_handlers[new_k](v) + out.write(f" :{new_k}: {new_v}\n") + out.write("\n") + + # Write header row + if header_row is not None: + # this non-repeating style is best for broken header row detection? + # todo: add strict=True? + for prefix, col in zip(('*' + ' ' * (len(header_row) - 1)), header_row): + out.write(f" {prefix} - {col}\n") + out.write("\n") + + + +FILETILE_DIR = DOC_ROOT / "_static" / "filetiles" + + +def do_filetile(out, suffix: str | None = None, state: str = None): + name = None + if suffix is not None: + p = FILETILE_DIR / f"type-{suffix.strip('.')}.png" + log.info(f" FILETILE: {p}") + if p.exists(): + print(" KNOWN!") + name = p.name + else: + name = f"type-unknown.png" + print(" ... unknown :(") + else: + name = "state-error.png" + + out.write(indent(f" ", + f".. raw:: html\n\n" + f" \n\n")) + + +def process_resource_files( + out, + file_list: List[Path], + columns: int, +) -> None: + """ + Render the table without any recursion or real FS navigation. + + :param out: + :param file_list: + :return: + """ cell_count = 0 - prefix = create_resource_path(file_list[0].parent, suffix="/") + column_iter = cycle(chain('*', ' ' * (columns - 1))) - COLUMNS = get_header_num_cols(prefix, len(file_list)) + def start(): + nonlocal cell_count + cell_count += 1 + return next(column_iter) - log.info(f"Processing {prefix=!r} with {COLUMNS=!r}") for path in file_list: + + # Shared items resource_path = path.relative_to(ARCADE_ROOT).as_posix() + resource_handle_raw = path_as_resource_handle(path) + resource_copyable = html_copyable(path.name, resource_handle_raw) + resource_handle_no_prefix = resource_handle_raw\ + .replace(':', '')\ + .replace('/', '-')\ + .replace('_', '-')\ + .replace('.', '-') + + # Decide how we're going to render the file suffix = path.suffix - - if cell_count % COLUMNS == 0: - start_row = "*" - else: - start_row = " " - name = path.name - resource_copyable = f"{create_resource_path(path)}" if suffix in [".png", ".jpg", ".gif", ".svg"]: - out.write(f" {start_row} - .. image:: ../../{resource_path}\n") - # IMPORTANT: - # 1. 11 chars to match the start of "image" above - # 2. :class: checkered-bg to apply the checkers to transparent images - out.write(f" :class: checkered-bg\n") - # 3. :loading: lazy stops GitHub 429ing us ("chill pls") # pending: stop using GH raw as a CDN - out.write(f" :loading: lazy\n") - out.write("\n") - out.write(f" {name}\n") - - elif suffix in SUFFIX_TO_AUDIO_TYPE: - file_path = FMT_URL_REF_EMBED.format(resource_path) - src_type=SUFFIX_TO_AUDIO_TYPE[suffix] - out.write(f" {start_row} - .. raw:: html\n\n") - out.write(f" \n") - out.write(f"
"{resource_copyable}"\n") - # out.write(f"
{path.name} on GitHub\n") - elif suffix in SUFFIX_TO_VIDEO_TYPE: + + out.write(f" {start()} - .. raw:: html\n\n" + + indent(" ", resource_copyable + "\n")) + parts = [] + # out.write(indent(" ", resource_copyable)) + + tile_rst_code = sphinx_directive( + 'image', f'../../{resource_path}', + options={ + 'class':( + 'checkered-bg', # Show transparency via gray tile bg + 'resource-thumb', # Clamp max display size + ), + # lazy helps avoid GitHub and readthedocs from 429ing us ("chill pls") + 'loading': 'lazy', + 'name': resource_handle_no_prefix + } + ) + parts.append(tile_rst_code + "\n") + #out.write(indent(" ", tile_rst_code)) + + size_info = None + if suffix == ".svg": + size_info = "Scalable Vector Graphic" + else: + try: + im = PIL.Image.open(path) + im_width, im_height = im.size + size_info = f"{im_width}px x {im_height}px" + except Exception as e: + log.warning(f"FAILED to read size info for {path}:\n {e}") + + if size_info is None: + size_info = "Could not read size info" + parts.append(f"*({size_info})*\n") + out.write(indent(" ", '\n'.join(parts))) + out.write("\n\n") + + elif suffix in MEDIA_EMBED: + config = MEDIA_EMBED[suffix] + kind = config.get('media_kind') + mime_suffix = config.get('mime_suffix') file_path = FMT_URL_REF_EMBED.format(resource_path) - src_type = SUFFIX_TO_VIDEO_TYPE[suffix] - out.write(f" {start_row} - .. raw:: html\n\n") - out.write(f" \n") - out.write(f"
"{resource_copyable}"\n") - elif suffix == ".glsl": - file_path = FMT_URL_REF_PAGE.format(resource_path) - out.write(f" {start_row} - `{path} <{file_path}>`_\n") + + out.write(f" {start()} - .. raw:: html\n\n") + out.write(indent( + " ", resource_copyable)) + + out.write(f" .. raw:: html\n\n") + out.write(indent(" ", + f"<{kind} class=\"resource-thumb\" controls>\n" + f" \n" + f"\n\n")) + # Fonts elif suffix == ".ttf": + + data = BrittleFontData.from_path(path) + + style_string = ", ".join(data.styles or ("Regular",)) + + out.write(f" {start()} - .. code-block:: python\n\n") + out.write(f" {data.face_name!r}\n\n") + + out.write(f" {start()} - {style_string}\n\n") + # out.write(indent(f" ", code_block(resource_copyable, language='python'))) + out.write(f" {start()} - .. code-block:: python\n\n") + out.write(f" {resource_handle_raw!r}\n\n") + + # File tiles we don't have previews for + else:# suffix == ".json": file_path = FMT_URL_REF_PAGE.format(resource_path) - out.write(f" {start_row} - `{name} <{file_path}>`_\n") - # Tiled maps - elif suffix == ".json": - file_path = FMT_URL_REF_PAGE.format(resource_path) - out.write(f" {start_row} - `{name} <{file_path}>`_\n") - else: - out.write(f" {start_row} - {name}\n") - # The below doesn't work because of how raw HTML / Sphinx images interact: - # out.write(f"
{resource_copyable}\n") - cell_count += 1 + out.write(f" {start()} - .. raw:: html\n\n") + out.write(indent(" ", + resource_copyable)) + + do_filetile(out, suffix=suffix) # Finish any remaining columns with empty cells - while cell_count % COLUMNS > 0: - out.write(f" -\n") - cell_count += 1 + while cell_count % columns > 0: + out.write(f" {start()} -\n") def resources(): @@ -423,7 +741,8 @@ def resources(): do_heading(out, 0, "Built-In Resources") - out.write("\n") + out.write("\n\n:resource:`:resources:/gui_basic_assets/window/panel_green.png`\n\n") + # out.write("Linking test: :ref:`resources-gui-basic-assets-window-panel-green-png`.\n") out.write("Every file below is included when you :ref:`install Arcade `. This includes the images,\n" "sounds, fonts, and other files to help you get started quickly. You can still download them\n" "separately, but Arcade's resource handle system will usually be easier.\n") @@ -438,11 +757,23 @@ def resources(): do_heading(out, 1, "How do I use these?") out.write( - "Arcade projects can use any file on this page by passing a **resource handle** prefix.\n" - "These are strings which start with ``\":resources:\"``. To learn more, please see the following:\n\n" + # '.. |Example Copy Button| raw:: html\n\n' + # '
\n' + # ' \n\n' + # '
\n\n' + # + + "Arcade helps save time through **resource handle** strings. These strings start with\n" + "``':resources:'``. After you've installed Arcade, you'll need to:\n\n" + "#. Find the copy button (|Example Copy Button|) after a filename below\n" + "#. Click it to copy the string, such as ``':resources:/logo.png'``\n" + "#. Use the appropriate loading functions to load and display the data\n\n" + "Try it below with the Arcade logo, or see the following to learn more\n:" + "\n\n" "* :ref:`Sprite Examples ` for example code\n" "* :ref:`The Platformer Tutorial ` for step-by-step guidance\n" - "* The :ref:`resource_handles` page of the manual covers them in more depth\n") + "* The :ref:`resource_handles` page of the manual covers them in more depth\n" + "\n" + ) out.write("\n") process_resource_directory(out, RESOURCE_DIR) diff --git a/util/sphinx_static_file_temp_fix.py b/util/sphinx_static_file_temp_fix.py index 285239c3e..80d810750 100644 --- a/util/sphinx_static_file_temp_fix.py +++ b/util/sphinx_static_file_temp_fix.py @@ -57,15 +57,20 @@ log = logging.getLogger(str(FILE.relative_to(REPO_ROOT))) DOC_DIR = REPO_ROOT / "doc" -STATIC_SOURCE_DIR = DOC_DIR / "_static" ENABLE_DEVMACHINE_SPHINX_STATIC_FIX = REPO_ROOT / ".ENABLE_DEVMACHINE_SPHINX_STATIC_FIX" -BUILD_DIR = REPO_ROOT / "build" -BUILD_HTML_DIR = BUILD_DIR / "html" +BUILD_ROOT = REPO_ROOT / "build" +BUILD_HTML_DIR = BUILD_ROOT / "html" + +SOURCE_STATIC_DIR = DOC_DIR / "_static" BUILD_STATIC_DIR = BUILD_HTML_DIR / "_static" + BUILD_CSS_DIR = BUILD_STATIC_DIR / "css" -STATIC_CSS_DIR = STATIC_SOURCE_DIR / "css" +SOURCE_CSS_DIR = SOURCE_STATIC_DIR / "css" + +BUILD_JS_DIR = BUILD_STATIC_DIR / "js" +SOURCE_JS_DIR = SOURCE_STATIC_DIR / "js" force_copy_on_change: dict[Path, Path] = { # pending: sphinx >= 8.1.4 # You can add per-dir config the lazy way: @@ -73,7 +78,11 @@ # 2. modifying it with filtering **{ source_file: BUILD_CSS_DIR / source_file.name - for source_file in STATIC_CSS_DIR.glob("*.*") + for source_file in SOURCE_CSS_DIR.glob("*.*") + }, + **{ + source_file: BUILD_JS_DIR / source_file.name + for source_file in SOURCE_JS_DIR.glob("*.*") }, }