Skip to content

Commit

Permalink
WebHost/Core/Lingo: Render option documentation as reStructuredText i…
Browse files Browse the repository at this point in the history
…n the WebView (#3511)

* Render option documentation as reStructuredText in the WebView

This means that options can use the standard Python documentation
format, while producing much nicer-looking documentation in the
WebView with things like emphasis, lists, and so on.

* Opt existing worlds out of rich option docs

This avoids breaking the rendering of existing option docs which were
written with the old plain text rendering in mind, while also allowing
new options to default to the rich text rendering instead.

* Use reStructuredText formatting for Lingo Options docstrings

* Disable raw and file insertion RST directives

* Update doc comments per code review

* Make rich text docs opt-in

* Put rich_text_options_doc on WebWorld

* Document rich text API

* Code review

* Update docs/options api.md

Co-authored-by: Doug Hoskisson <[email protected]>

* Update Options.py

Co-authored-by: Doug Hoskisson <[email protected]>

---------

Co-authored-by: Chris Wilson <[email protected]>
Co-authored-by: Doug Hoskisson <[email protected]>
  • Loading branch information
3 people authored Jun 14, 2024
1 parent 3972b1b commit c61505b
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 80 deletions.
54 changes: 44 additions & 10 deletions Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
# can be weighted between selections
supports_weighting = True

rich_text_doc: typing.Optional[bool] = None
"""Whether the WebHost should render the Option's docstring as rich text.
If this is True, the Option's docstring is interpreted as reStructuredText_,
the standard Python markup format. In the WebHost, it's rendered to HTML so
that lists, emphasis, and other rich text features are displayed properly.
If this is False, the docstring is instead interpreted as plain text, and
displayed as-is on the WebHost with whitespace preserved.
If this is None, it inherits the value of `World.rich_text_options_doc`. For
backwards compatibility, this defaults to False, but worlds are encouraged to
set it to True and use reStructuredText for their Option documentation.
.. _reStructuredText: https://docutils.sourceforge.io/rst.html
"""

# filled by AssembleOptions:
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore
Expand Down Expand Up @@ -1127,10 +1144,13 @@ def __len__(self) -> int:

class Accessibility(Choice):
"""Set rules for reachability of your items/locations.
Locations: ensure everything can be reached and acquired.
Items: ensure all logically relevant items can be acquired.
Minimal: ensure what is needed to reach your goal can be acquired."""
- **Locations:** ensure everything can be reached and acquired.
- **Items:** ensure all logically relevant items can be acquired.
- **Minimal:** ensure what is needed to reach your goal can be acquired.
"""
display_name = "Accessibility"
rich_text_doc = True
option_locations = 0
option_items = 1
option_minimal = 2
Expand All @@ -1139,14 +1159,15 @@ class Accessibility(Choice):


class ProgressionBalancing(NamedRange):
"""
A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
"""A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
A lower setting means more getting stuck. A higher setting means less getting stuck.
"""
default = 50
range_start = 0
range_end = 99
display_name = "Progression Balancing"
rich_text_doc = True
special_range_names = {
"disabled": 0,
"normal": 50,
Expand Down Expand Up @@ -1211,29 +1232,36 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str,
class LocalItems(ItemSet):
"""Forces these items to be in their native world."""
display_name = "Local Items"
rich_text_doc = True


class NonLocalItems(ItemSet):
"""Forces these items to be outside their native world."""
display_name = "Non-local Items"
rich_text_doc = True


class StartInventory(ItemDict):
"""Start with these items."""
verify_item_name = True
display_name = "Start Inventory"
rich_text_doc = True


class StartInventoryPool(StartInventory):
"""Start with these items and don't place them in the world.
The game decides what the replacement items will be."""
The game decides what the replacement items will be.
"""
verify_item_name = True
display_name = "Start Inventory from Pool"
rich_text_doc = True


class StartHints(ItemSet):
"""Start with these item's locations prefilled into the !hint command."""
"""Start with these item's locations prefilled into the ``!hint`` command."""
display_name = "Start Hints"
rich_text_doc = True


class LocationSet(OptionSet):
Expand All @@ -1242,28 +1270,33 @@ class LocationSet(OptionSet):


class StartLocationHints(LocationSet):
"""Start with these locations and their item prefilled into the !hint command"""
"""Start with these locations and their item prefilled into the ``!hint`` command."""
display_name = "Start Location Hints"
rich_text_doc = True


class ExcludeLocations(LocationSet):
"""Prevent these locations from having an important item"""
"""Prevent these locations from having an important item."""
display_name = "Excluded Locations"
rich_text_doc = True


class PriorityLocations(LocationSet):
"""Prevent these locations from having an unimportant item"""
"""Prevent these locations from having an unimportant item."""
display_name = "Priority Locations"
rich_text_doc = True


class DeathLink(Toggle):
"""When you die, everyone dies. Of course the reverse is true too."""
display_name = "Death Link"
rich_text_doc = True


class ItemLinks(OptionList):
"""Share part of your item pool with other players."""
display_name = "Item Links"
rich_text_doc = True
default = []
schema = Schema([
{
Expand Down Expand Up @@ -1330,6 +1363,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P

class Removed(FreeText):
"""This Option has been Removed."""
rich_text_doc = True
default = ""
visibility = Visibility.none

Expand Down
17 changes: 17 additions & 0 deletions WebHostLib/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
from textwrap import dedent
from typing import Dict, Union
from docutils.core import publish_parts

import yaml
from flask import redirect, render_template, request, Response
Expand Down Expand Up @@ -66,6 +67,22 @@ def filter_dedent(text: str) -> str:
return dedent(text).strip("\n ")


@app.template_filter("rst_to_html")
def filter_rst_to_html(text: str) -> str:
"""Converts reStructuredText (such as a Python docstring) to HTML."""
if text.startswith(" ") or text.startswith("\t"):
text = dedent(text)
elif "\n" in text:
lines = text.splitlines()
text = lines[0] + "\n" + dedent("\n".join(lines[1:]))

return publish_parts(text, writer_name='html', settings=None, settings_overrides={
'raw_enable': False,
'file_insertion_enabled': False,
'output_encoding': 'unicode'
})['body']


@app.template_test("ordered")
def test_ordered(obj):
return isinstance(obj, collections.abc.Sequence)
Expand Down
54 changes: 34 additions & 20 deletions WebHostLib/static/styles/tooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
*/

/* Base styles for the element that has a tooltip */
[data-tooltip], .tooltip {
[data-tooltip], .tooltip-container {
position: relative;
}

/* Base styles for the entire tooltip */
[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after {
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
position: absolute;
visibility: hidden;
opacity: 0;
Expand All @@ -39,22 +39,23 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
pointer-events: none;
}

[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
.tooltip-container:hover .tooltip {
visibility: visible;
opacity: 1;
word-break: break-word;
}

/** Directional arrow styles */
.tooltip:before, [data-tooltip]:before {
[data-tooltip]:before, .tooltip-container:before {
z-index: 10000;
border: 6px solid transparent;
background: transparent;
content: "";
}

/** Content styles */
.tooltip:after, [data-tooltip]:after {
[data-tooltip]:after, .tooltip {
width: 260px;
z-index: 10000;
padding: 8px;
Expand All @@ -63,44 +64,46 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
background-color: hsla(0, 0%, 20%, 0.9);
color: #fff;
content: attr(data-tooltip);
white-space: pre-wrap;
font-size: 14px;
line-height: 1.2;
}

[data-tooltip]:before, [data-tooltip]:after{
[data-tooltip]:after {
white-space: pre-wrap;
}

[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
visibility: hidden;
opacity: 0;
pointer-events: none;
}

[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after,
.tooltip-top:before, .tooltip-top:after {
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
bottom: 100%;
left: 50%;
}

[data-tooltip]:before, .tooltip:before, .tooltip-top:before {
[data-tooltip]:before, .tooltip-container:before {
margin-left: -6px;
margin-bottom: -12px;
border-top-color: #000;
border-top-color: hsla(0, 0%, 20%, 0.9);
}

/** Horizontally align tooltips on the top and bottom */
[data-tooltip]:after, .tooltip:after, .tooltip-top:after {
[data-tooltip]:after, .tooltip {
margin-left: -80px;
}

[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after,
.tooltip-top:hover:before, .tooltip-top:hover:after {
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
.tooltip-container:hover .tooltip {
-webkit-transform: translateY(-12px);
-moz-transform: translateY(-12px);
transform: translateY(-12px);
}

/** Tooltips on the left */
.tooltip-left:before, .tooltip-left:after {
.tooltip-left:before, [data-tooltip].tooltip-left:after, .tooltip-left .tooltip {
right: 100%;
bottom: 50%;
left: auto;
Expand All @@ -115,14 +118,14 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-left-color: hsla(0, 0%, 20%, 0.9);
}

.tooltip-left:hover:before, .tooltip-left:hover:after {
.tooltip-left:hover:before, [data-tooltip].tooltip-left:hover:after, .tooltip-left:hover .tooltip {
-webkit-transform: translateX(-12px);
-moz-transform: translateX(-12px);
transform: translateX(-12px);
}

/** Tooltips on the bottom */
.tooltip-bottom:before, .tooltip-bottom:after {
.tooltip-bottom:before, [data-tooltip].tooltip-bottom:after, .tooltip-bottom .tooltip {
top: 100%;
bottom: auto;
left: 50%;
Expand All @@ -136,14 +139,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-bottom-color: hsla(0, 0%, 20%, 0.9);
}

.tooltip-bottom:hover:before, .tooltip-bottom:hover:after {
.tooltip-bottom:hover:before, [data-tooltip].tooltip-bottom:hover:after,
.tooltip-bottom:hover .tooltip {
-webkit-transform: translateY(12px);
-moz-transform: translateY(12px);
transform: translateY(12px);
}

/** Tooltips on the right */
.tooltip-right:before, .tooltip-right:after {
.tooltip-right:before, [data-tooltip].tooltip-right:after, .tooltip-right .tooltip {
bottom: 50%;
left: 100%;
}
Expand All @@ -156,7 +160,8 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-right-color: hsla(0, 0%, 20%, 0.9);
}

.tooltip-right:hover:before, .tooltip-right:hover:after {
.tooltip-right:hover:before, [data-tooltip].tooltip-right:hover:after,
.tooltip-right:hover .tooltip {
-webkit-transform: translateX(12px);
-moz-transform: translateX(12px);
transform: translateX(12px);
Expand All @@ -168,7 +173,16 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
}

/** Center content vertically for tooltips ont he left and right */
.tooltip-left:after, .tooltip-right:after {
[data-tooltip].tooltip-left:after, [data-tooltip].tooltip-right:after,
.tooltip-left .tooltip, .tooltip-right .tooltip {
margin-left: 0;
margin-bottom: -16px;
}

.tooltip ul, .tooltip ol {
padding-left: 1rem;
}

.tooltip :last-child {
margin-bottom: 0;
}
19 changes: 15 additions & 4 deletions WebHostLib/templates/playerOptions/macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
</div>
{% endmacro %}

{% macro ItemDict(option_name, option, world) %}
{% macro ItemDict(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
Expand All @@ -135,7 +135,7 @@
</div>
{% endmacro %}

{% macro LocationSet(option_name, option, world) %}
{% macro LocationSet(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for group_name in world.location_name_groups.keys()|sort %}
Expand All @@ -158,7 +158,7 @@
</div>
{% endmacro %}

{% macro ItemSet(option_name, option, world) %}
{% macro ItemSet(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="option-container">
{% for group_name in world.item_name_groups.keys()|sort %}
Expand Down Expand Up @@ -196,7 +196,18 @@
{% macro OptionTitle(option_name, option) %}
<label for="{{ option_name }}">
{{ option.display_name|default(option_name) }}:
<span class="interactive" data-tooltip="{{(option.__doc__ | default("Please document me!"))|replace('\n ', '\n')|escape|trim}}">(?)</span>
<span
class="interactive tooltip-container"
{% if not (option.rich_text_doc | default(world.web.rich_text_options_doc, true)) %}
data-tooltip="{{(option.__doc__ | default("Please document me!"))|replace('\n ', '\n')|escape|trim}}"
{% endif %}>
(?)
{% if option.rich_text_doc | default(world.web.rich_text_options_doc, true) %}
<div class="tooltip">
{{ option.__doc__ | default("**Please document me!**") | rst_to_html | safe }}
</div>
{% endif %}
</span>
</label>
{% endmacro %}

Expand Down
Loading

0 comments on commit c61505b

Please sign in to comment.