Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,31 @@
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):
super().__init__(*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):
Expand Down
11 changes: 11 additions & 0 deletions source/_icons/README.md
Original file line number Diff line number Diff line change
@@ -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/<name>.svg https://unpkg.com/lucide-static@latest/icons/<name>.svg
```
15 changes: 15 additions & 0 deletions source/_icons/wrench.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions source/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ html.dark {
margin-right: auto;
margin-left: auto;
}

.admonition.admonition-tool {
--icon-url: var(--icon-wrench-url);
}
37 changes: 37 additions & 0 deletions source/_static/fabric-permissions.js
Original file line number Diff line number Diff line change
@@ -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;
}
34 changes: 34 additions & 0 deletions source/_static/perm-converter.css
Original file line number Diff line number Diff line change
@@ -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;
}
75 changes: 75 additions & 0 deletions source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
122 changes: 122 additions & 0 deletions source/permissions.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
:page-js: fabric-permissions.js
:page-css: perm-converter.css

===========
Permissions
===========
Expand Down Expand Up @@ -228,3 +231,122 @@ Other Permissions
``worldedit.setnbt``,"Allows setting `extra data <https://minecraft.wiki/w/Block_entity>`_ on blocks (such as signs, chests, etc)."
``worldedit.report.pastebin``,"Allows uploading report files to pastebin automatically for the ``/worldedit report`` :doc:`command <commands>`."
``worldedit.scripting.execute.<filename>``,"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.<filename>``.
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 <noscript> should be moved to a globally applied <head> if we ever need this elsewhere.
.. only:: html and not epub and not singlehtml

.. raw:: html

<noscript>
<style>
.js-only {
display: none;
}
</style>
</noscript>

.. admonition:: Converter Tool
:class: admonition-tool

.. raw:: html

<form class="js-only perm-converter" onsubmit="return false" oninput="
showPermissionConversion(
'perm-converter-input',
'perm-converter-identifier',
'perm-converter-traditional'
)">
<div class="perm-converter-field">
<label for="perm-converter-input">WorldEdit permission node</label>
<input id="perm-converter-input" type="text" placeholder="Enter a permission node here..."
spellcheck="false" autocapitalize="off" autocorrect="off"
pattern="\s*[^.\s][^.]*\..*\S\s*"
aria-describedby="perm-converter-hint" />
<p class="perm-converter-hint" id="perm-converter-hint">Needs a namespace and a path
separated by a dot, such as <code>worldedit.region.set</code>. A node without a dot
is only matched by the fallback.</p>
</div>
<div class="perm-converter-field">
<label for="perm-converter-identifier">Identifier form</label>
<output id="perm-converter-identifier" for="perm-converter-input"></output>
</div>
<div class="perm-converter-field">
<label for="perm-converter-traditional">Traditional form</label>
<output id="perm-converter-traditional" for="perm-converter-input"></output>
</div>
</form>
<noscript>
<p>The interactive converter needs JavaScript. You can apply the rules below by hand instead.</p>
</noscript>

.. only:: epub or singlehtml or not html

.. note::

The interactive converter is only available in the online documentation.
You can apply the rules below by hand instead.

How the conversion works
-------------------------

#. Split the node at the **first** ``.``, into a *namespace* and *path*. If there is no ``.``, or it is the first or last character, the node is not usable with the API and only the fallback works.
#. Convert both parts to lowercase using Java's ``toLowerCase(Locale.ROOT)``, then encode it as UTF-8.
#. Keep each byte that is ``a`` to ``z``, ``0`` to ``9``, ``.`` or ``-``. The *path* must also keep ``/``. Replace every other byte with ``_`` followed by its two hex digits. Notably, this includes ``_`` itself, which is output as ``_5f``.
#. Join the two parts with ``:`` for the identifier form, or with ``.`` for the traditional form.

Examples
---------

.. csv-table::
:header: Node, Identifier form, Traditional form
:widths: 20, 20, 20

``worldedit.region.set``,"``worldedit:region.set``","``worldedit.region.set``"
``WorldEdit.scripting.execute.Build.js``,"``worldedit:scripting.execute.build.js``","``worldedit.scripting.execute.build.js``"
``worldedit.scripting.execute.my script_v1!``,"``worldedit:scripting.execute.my_20script_5fv1_21``","``worldedit.scripting.execute.my_20script_5fv1_21``"
``worldedit.scripting.execute.café``,"``worldedit:scripting.execute.caf_c3_a9``","``worldedit.scripting.execute.caf_c3_a9``"
``worldedit.scripting.execute/tools/build.js``,"``worldedit:scripting.execute/tools/build.js``","``worldedit.scripting.execute/tools/build.js``"
``worldedit``,"Not converted, only uses fallback","Not converted, only uses fallback"