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"{kind}>\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("*.*")
},
}