diff --git a/dev.py b/dev.py index 448c52e..fa21803 100755 --- a/dev.py +++ b/dev.py @@ -7,9 +7,22 @@ from queue import SimpleQueue import sphinx.cmd.build -from watchdog.events import FileSystemEventHandler +from watchdog.events import ( + EVENT_TYPE_CREATED, + EVENT_TYPE_DELETED, + EVENT_TYPE_MODIFIED, + EVENT_TYPE_MOVED, + FileSystemEventHandler, +) from watchdog.observers import Observer +MODIFICATION_EVENTS = frozenset({ + EVENT_TYPE_CREATED, + EVENT_TYPE_DELETED, + EVENT_TYPE_MODIFIED, + EVENT_TYPE_MOVED, +}) + class QueuingEventHandler(FileSystemEventHandler): def __init__(self, q: SimpleQueue, *args, **kwargs): @@ -17,7 +30,8 @@ def __init__(self, q: SimpleQueue, *args, **kwargs): self._queue = q def on_any_event(self, event) -> None: - self._queue.put_nowait(event) + if event.event_type in MODIFICATION_EVENTS: + self._queue.put_nowait(event) class CustomDirHandler(http.server.SimpleHTTPRequestHandler): diff --git a/source/_icons/README.md b/source/_icons/README.md new file mode 100644 index 0000000..ed798f7 --- /dev/null +++ b/source/_icons/README.md @@ -0,0 +1,11 @@ +# Icons + +SVGs in this directory are mapped to CSS variables by `conf.py`. + +For example, `wrench.svg` becomes `--icon-wrench-url`. + +To add [a lucide icon](https://lucide.dev/icons/), matching the ones that Shibuya itself ships: + +``` +curl -L -o source/_icons/.svg https://unpkg.com/lucide-static@latest/icons/.svg +``` diff --git a/source/_icons/wrench.svg b/source/_icons/wrench.svg new file mode 100644 index 0000000..3a68b9f --- /dev/null +++ b/source/_icons/wrench.svg @@ -0,0 +1,15 @@ + + + + diff --git a/source/_static/custom.css b/source/_static/custom.css index 367b95d..8f5d215 100644 --- a/source/_static/custom.css +++ b/source/_static/custom.css @@ -16,3 +16,7 @@ html.dark { margin-right: auto; margin-left: auto; } + +.admonition.admonition-tool { + --icon-url: var(--icon-wrench-url); +} diff --git a/source/_static/fabric-permissions.js b/source/_static/fabric-permissions.js new file mode 100644 index 0000000..942e84f --- /dev/null +++ b/source/_static/fabric-permissions.js @@ -0,0 +1,37 @@ +const NAMESPACE_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789.-'; +const PATH_CHARACTERS = NAMESPACE_CHARACTERS + '/'; + +function encodePermissionPart(value, allowSlash) { + const allowed = allowSlash ? PATH_CHARACTERS : NAMESPACE_CHARACTERS; + const bytes = new TextEncoder().encode(value.toLowerCase()); + let encoded = ''; + for (const byte of bytes) { + const character = String.fromCharCode(byte); + encoded += allowed.includes(character) + ? character + : '_' + byte.toString(16).padStart(2, '0'); + } + return encoded; +} + +function convertPermission(permission) { + const separator = permission.indexOf('.'); + if (separator < 1 || separator === permission.length - 1) { + return null; + } + const namespace = encodePermissionPart(permission.slice(0, separator), false); + const path = encodePermissionPart(permission.slice(separator + 1), true); + return { + identifier: namespace + ':' + path, + traditional: namespace + '.' + path, + }; +} + +function showPermissionConversion(inputId, identifierId, traditionalId) { + const identifier = document.getElementById(identifierId); + const traditional = document.getElementById(traditionalId); + const result = convertPermission(document.getElementById(inputId).value.trim()); + + identifier.value = result === null ? '' : result.identifier; + traditional.value = result === null ? '' : result.traditional; +} diff --git a/source/_static/perm-converter.css b/source/_static/perm-converter.css new file mode 100644 index 0000000..601439b --- /dev/null +++ b/source/_static/perm-converter.css @@ -0,0 +1,34 @@ +.perm-converter label { + display: block; +} + +.perm-converter input { + appearance: none; + width: 100%; + box-sizing: border-box; + font-family: var(--sy-f-mono); + padding: 4px 8px; + border: 1px solid var(--sy-c-border); + border-radius: 6px; + background-color: var(--sy-c-surface); +} + +.perm-converter input:user-invalid { + border-color: var(--danger-3); +} + +.perm-converter-hint { + font-size: 0.875em; + color: var(--sy-c-light); +} + +.perm-converter output { + display: block; + font-family: var(--sy-f-mono); + padding: 4px 8px; + border: 1px solid var(--sy-c-border); + border-radius: 6px; + background-color: var(--sy-c-surface); + overflow-wrap: anywhere; + min-height: 1lh; +} diff --git a/source/conf.py b/source/conf.py index f210b4f..bfb0c7b 100644 --- a/source/conf.py +++ b/source/conf.py @@ -12,9 +12,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. +import re import sys import os import datetime +import urllib.parse + +from sphinx.errors import ExtensionError # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -405,3 +409,74 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {} + + +SVG_SAFE_CHARACTERS = " /'=:,.-" + +# Non-greedy so adjacent comments don't merge, and DOTALL as they may span lines. +SVG_COMMENT = re.compile(r'', flags=re.DOTALL) + + +def render_icon_css(icon_directory, icons): + notices = [] + declarations = [] + for name in icons: + with open(os.path.join(icon_directory, name), encoding='utf-8') as icon: + source = icon.read() + # Add licenses to common comment + notices.extend( + comment.strip() for comment in SVG_COMMENT.findall(source) if '@license' in comment + ) + # Remove comments and newlines + svg = ' '.join(SVG_COMMENT.sub('', source).split()) + declarations.append( + ' --icon-%s-url: url("data:image/svg+xml;utf8,%s");' + % (name[:-4], urllib.parse.quote(svg, safe=SVG_SAFE_CHARACTERS)) + ) + # Icons from the same library repeat a notice, so emit only the first of each + header = ''.join('/*\n * %s\n */\n' % notice for notice in dict.fromkeys(notices)) + return '%s:root {\n%s\n}\n' % (header, '\n'.join(declarations)) + + +def build_icon_css(app): + """Builds the icon SVGs into CSS variables in a generated static directory""" + icon_directory = os.path.join(app.srcdir, '_icons') + icons = sorted(name for name in os.listdir(icon_directory) if name.endswith('.svg')) + output_directory = os.path.join(app.doctreedir, 'generated-static') + + os.makedirs(output_directory, exist_ok=True) + with open(os.path.join(output_directory, 'icons.css'), 'w', encoding='utf-8') as output: + output.write(render_icon_css(icon_directory, icons)) + app.add_static_dir(output_directory) + + +def check_page_asset(app, pagename, filename): + if '://' in filename: + raise ExtensionError( + '%s requested the remote page asset %r, which is not allowed' + % (pagename, filename) + ) + if not any( + os.path.exists(os.path.join(app.confdir, static_path, filename)) + for static_path in app.config.html_static_path + ): + raise ExtensionError( + '%s requested the page asset %r, which is not in html_static_path' + % (pagename, filename) + ) + + +def add_page_assets(app, pagename, templatename, context, doctree): + metadata = app.env.metadata.get(pagename, {}) + for js_file in metadata.get('page-js', '').split(): + check_page_asset(app, pagename, js_file) + app.add_js_file(js_file, loading_method='defer') + for css_file in metadata.get('page-css', '').split(): + check_page_asset(app, pagename, css_file) + app.add_css_file(css_file) + + +def setup(app): + app.add_css_file('icons.css') + app.connect('builder-inited', build_icon_css) + app.connect('html-page-context', add_page_assets) diff --git a/source/permissions.rst b/source/permissions.rst index 3d20279..43a7526 100644 --- a/source/permissions.rst +++ b/source/permissions.rst @@ -1,3 +1,6 @@ +:page-js: fabric-permissions.js +:page-css: perm-converter.css + =========== Permissions =========== @@ -228,3 +231,122 @@ Other Permissions ``worldedit.setnbt``,"Allows setting `extra data `_ on blocks (such as signs, chests, etc)." ``worldedit.report.pastebin``,"Allows uploading report files to pastebin automatically for the ``/worldedit report`` :doc:`command `." ``worldedit.scripting.execute.``,"Allows using the CraftScript with the given filename." + +Fabric Permissions Integration +============================== + +.. versionadded:: 7.4.5 + +WorldEdit integrates into the Fabric Permissions API v1. +The API identifies permissions with a namespaced Minecraft ``Identifier`` rather than a dotted string, +so the nodes listed above are converted before they are checked. Depending on which permissions mod you use, you may need to write them differently. + +.. warning:: + + If a node cannot be converted, WorldEdit falls back to the Fabric Permissions API v0, and then to vanilla operator levels. + This should not happen in practice as we generally don't have ``worldedit`` as its own permission node, + but if other mods use our checks this may be an issue. + +Which form do I use? +--------------------- + +Due to how permissions have historically worked, you might need to use different forms of the converted permission node. +There are two forms of every node, and both come from the same conversion: + +``worldedit:region.set`` + The **identifier form**, for mods that expose the permission API's identifiers directly. + +``worldedit.region.set`` + The **traditional form**. Some mods, like LuckPerms, rejoin the identifier's namespace and path with a ``.`` instead of a ``:`` + to match how permission nodes were traditionally formatted. + +.. warning:: + + The traditional form is *not* always the same as the original node listed above. + It is the converted node rejoined with a dot, so any lowercasing or escaping still applies. + For example, ``worldedit.scripting.execute.my script_v1!`` becomes ``worldedit.scripting.execute.my_20script_5fv1_21``. + + This only matters in practice for dynamic nodes, such as ``worldedit.scripting.execute.``. + Every static node WorldEdit uses is already lowercase and made of safe characters, so the result is identical. + +Converter +---------- + +To assist with setting permissions, we've provided a small tool that converts from a permission node to the forms described above: + +.. + This