Skip to content

Commit 54aeaff

Browse files
nex3LegendaryLinuxbeauxq
authored andcommitted
WebHost/Core/Lingo: Render option documentation as reStructuredText in the WebView (ArchipelagoMW#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]>
1 parent 982851a commit 54aeaff

File tree

11 files changed

+265
-80
lines changed

11 files changed

+265
-80
lines changed

Options.py

+44-10
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,23 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
126126
# can be weighted between selections
127127
supports_weighting = True
128128

129+
rich_text_doc: typing.Optional[bool] = None
130+
"""Whether the WebHost should render the Option's docstring as rich text.
131+
132+
If this is True, the Option's docstring is interpreted as reStructuredText_,
133+
the standard Python markup format. In the WebHost, it's rendered to HTML so
134+
that lists, emphasis, and other rich text features are displayed properly.
135+
136+
If this is False, the docstring is instead interpreted as plain text, and
137+
displayed as-is on the WebHost with whitespace preserved.
138+
139+
If this is None, it inherits the value of `World.rich_text_options_doc`. For
140+
backwards compatibility, this defaults to False, but worlds are encouraged to
141+
set it to True and use reStructuredText for their Option documentation.
142+
143+
.. _reStructuredText: https://docutils.sourceforge.io/rst.html
144+
"""
145+
129146
# filled by AssembleOptions:
130147
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
131148
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore
@@ -1127,10 +1144,13 @@ def __len__(self) -> int:
11271144

11281145
class Accessibility(Choice):
11291146
"""Set rules for reachability of your items/locations.
1130-
Locations: ensure everything can be reached and acquired.
1131-
Items: ensure all logically relevant items can be acquired.
1132-
Minimal: ensure what is needed to reach your goal can be acquired."""
1147+
1148+
- **Locations:** ensure everything can be reached and acquired.
1149+
- **Items:** ensure all logically relevant items can be acquired.
1150+
- **Minimal:** ensure what is needed to reach your goal can be acquired.
1151+
"""
11331152
display_name = "Accessibility"
1153+
rich_text_doc = True
11341154
option_locations = 0
11351155
option_items = 1
11361156
option_minimal = 2
@@ -1139,14 +1159,15 @@ class Accessibility(Choice):
11391159

11401160

11411161
class ProgressionBalancing(NamedRange):
1142-
"""
1143-
A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
1162+
"""A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
1163+
11441164
A lower setting means more getting stuck. A higher setting means less getting stuck.
11451165
"""
11461166
default = 50
11471167
range_start = 0
11481168
range_end = 99
11491169
display_name = "Progression Balancing"
1170+
rich_text_doc = True
11501171
special_range_names = {
11511172
"disabled": 0,
11521173
"normal": 50,
@@ -1211,29 +1232,36 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str,
12111232
class LocalItems(ItemSet):
12121233
"""Forces these items to be in their native world."""
12131234
display_name = "Local Items"
1235+
rich_text_doc = True
12141236

12151237

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

12201243

12211244
class StartInventory(ItemDict):
12221245
"""Start with these items."""
12231246
verify_item_name = True
12241247
display_name = "Start Inventory"
1248+
rich_text_doc = True
12251249

12261250

12271251
class StartInventoryPool(StartInventory):
12281252
"""Start with these items and don't place them in the world.
1229-
The game decides what the replacement items will be."""
1253+
1254+
The game decides what the replacement items will be.
1255+
"""
12301256
verify_item_name = True
12311257
display_name = "Start Inventory from Pool"
1258+
rich_text_doc = True
12321259

12331260

12341261
class StartHints(ItemSet):
1235-
"""Start with these item's locations prefilled into the !hint command."""
1262+
"""Start with these item's locations prefilled into the ``!hint`` command."""
12361263
display_name = "Start Hints"
1264+
rich_text_doc = True
12371265

12381266

12391267
class LocationSet(OptionSet):
@@ -1242,28 +1270,33 @@ class LocationSet(OptionSet):
12421270

12431271

12441272
class StartLocationHints(LocationSet):
1245-
"""Start with these locations and their item prefilled into the !hint command"""
1273+
"""Start with these locations and their item prefilled into the ``!hint`` command."""
12461274
display_name = "Start Location Hints"
1275+
rich_text_doc = True
12471276

12481277

12491278
class ExcludeLocations(LocationSet):
1250-
"""Prevent these locations from having an important item"""
1279+
"""Prevent these locations from having an important item."""
12511280
display_name = "Excluded Locations"
1281+
rich_text_doc = True
12521282

12531283

12541284
class PriorityLocations(LocationSet):
1255-
"""Prevent these locations from having an unimportant item"""
1285+
"""Prevent these locations from having an unimportant item."""
12561286
display_name = "Priority Locations"
1287+
rich_text_doc = True
12571288

12581289

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

12631295

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

13311364
class Removed(FreeText):
13321365
"""This Option has been Removed."""
1366+
rich_text_doc = True
13331367
default = ""
13341368
visibility = Visibility.none
13351369

WebHostLib/options.py

+17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
from textwrap import dedent
55
from typing import Dict, Union
6+
from docutils.core import publish_parts
67

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

6869

70+
@app.template_filter("rst_to_html")
71+
def filter_rst_to_html(text: str) -> str:
72+
"""Converts reStructuredText (such as a Python docstring) to HTML."""
73+
if text.startswith(" ") or text.startswith("\t"):
74+
text = dedent(text)
75+
elif "\n" in text:
76+
lines = text.splitlines()
77+
text = lines[0] + "\n" + dedent("\n".join(lines[1:]))
78+
79+
return publish_parts(text, writer_name='html', settings=None, settings_overrides={
80+
'raw_enable': False,
81+
'file_insertion_enabled': False,
82+
'output_encoding': 'unicode'
83+
})['body']
84+
85+
6986
@app.template_test("ordered")
7087
def test_ordered(obj):
7188
return isinstance(obj, collections.abc.Sequence)

WebHostLib/static/styles/tooltip.css

+34-20
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
1212
*/
1313

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

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

42-
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
42+
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
43+
.tooltip-container:hover .tooltip {
4344
visibility: visible;
4445
opacity: 1;
4546
word-break: break-word;
4647
}
4748

4849
/** Directional arrow styles */
49-
.tooltip:before, [data-tooltip]:before {
50+
[data-tooltip]:before, .tooltip-container:before {
5051
z-index: 10000;
5152
border: 6px solid transparent;
5253
background: transparent;
5354
content: "";
5455
}
5556

5657
/** Content styles */
57-
.tooltip:after, [data-tooltip]:after {
58+
[data-tooltip]:after, .tooltip {
5859
width: 260px;
5960
z-index: 10000;
6061
padding: 8px;
@@ -63,44 +64,46 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
6364
background-color: hsla(0, 0%, 20%, 0.9);
6465
color: #fff;
6566
content: attr(data-tooltip);
66-
white-space: pre-wrap;
6767
font-size: 14px;
6868
line-height: 1.2;
6969
}
7070

71-
[data-tooltip]:before, [data-tooltip]:after{
71+
[data-tooltip]:after {
72+
white-space: pre-wrap;
73+
}
74+
75+
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
7276
visibility: hidden;
7377
opacity: 0;
7478
pointer-events: none;
7579
}
7680

77-
[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after,
78-
.tooltip-top:before, .tooltip-top:after {
81+
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
7982
bottom: 100%;
8083
left: 50%;
8184
}
8285

83-
[data-tooltip]:before, .tooltip:before, .tooltip-top:before {
86+
[data-tooltip]:before, .tooltip-container:before {
8487
margin-left: -6px;
8588
margin-bottom: -12px;
8689
border-top-color: #000;
8790
border-top-color: hsla(0, 0%, 20%, 0.9);
8891
}
8992

9093
/** Horizontally align tooltips on the top and bottom */
91-
[data-tooltip]:after, .tooltip:after, .tooltip-top:after {
94+
[data-tooltip]:after, .tooltip {
9295
margin-left: -80px;
9396
}
9497

95-
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after,
96-
.tooltip-top:hover:before, .tooltip-top:hover:after {
98+
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
99+
.tooltip-container:hover .tooltip {
97100
-webkit-transform: translateY(-12px);
98101
-moz-transform: translateY(-12px);
99102
transform: translateY(-12px);
100103
}
101104

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

118-
.tooltip-left:hover:before, .tooltip-left:hover:after {
121+
.tooltip-left:hover:before, [data-tooltip].tooltip-left:hover:after, .tooltip-left:hover .tooltip {
119122
-webkit-transform: translateX(-12px);
120123
-moz-transform: translateX(-12px);
121124
transform: translateX(-12px);
122125
}
123126

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

139-
.tooltip-bottom:hover:before, .tooltip-bottom:hover:after {
142+
.tooltip-bottom:hover:before, [data-tooltip].tooltip-bottom:hover:after,
143+
.tooltip-bottom:hover .tooltip {
140144
-webkit-transform: translateY(12px);
141145
-moz-transform: translateY(12px);
142146
transform: translateY(12px);
143147
}
144148

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

159-
.tooltip-right:hover:before, .tooltip-right:hover:after {
163+
.tooltip-right:hover:before, [data-tooltip].tooltip-right:hover:after,
164+
.tooltip-right:hover .tooltip {
160165
-webkit-transform: translateX(12px);
161166
-moz-transform: translateX(12px);
162167
transform: translateX(12px);
@@ -168,7 +173,16 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
168173
}
169174

170175
/** Center content vertically for tooltips ont he left and right */
171-
.tooltip-left:after, .tooltip-right:after {
176+
[data-tooltip].tooltip-left:after, [data-tooltip].tooltip-right:after,
177+
.tooltip-left .tooltip, .tooltip-right .tooltip {
172178
margin-left: 0;
173179
margin-bottom: -16px;
174180
}
181+
182+
.tooltip ul, .tooltip ol {
183+
padding-left: 1rem;
184+
}
185+
186+
.tooltip :last-child {
187+
margin-bottom: 0;
188+
}

WebHostLib/templates/playerOptions/macros.html

+15-4
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
</div>
112112
{% endmacro %}
113113

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

138-
{% macro LocationSet(option_name, option, world) %}
138+
{% macro LocationSet(option_name, option) %}
139139
{{ OptionTitle(option_name, option) }}
140140
<div class="option-container">
141141
{% for group_name in world.location_name_groups.keys()|sort %}
@@ -158,7 +158,7 @@
158158
</div>
159159
{% endmacro %}
160160

161-
{% macro ItemSet(option_name, option, world) %}
161+
{% macro ItemSet(option_name, option) %}
162162
{{ OptionTitle(option_name, option) }}
163163
<div class="option-container">
164164
{% for group_name in world.item_name_groups.keys()|sort %}
@@ -196,7 +196,18 @@
196196
{% macro OptionTitle(option_name, option) %}
197197
<label for="{{ option_name }}">
198198
{{ option.display_name|default(option_name) }}:
199-
<span class="interactive" data-tooltip="{{(option.__doc__ | default("Please document me!"))|replace('\n ', '\n')|escape|trim}}">(?)</span>
199+
<span
200+
class="interactive tooltip-container"
201+
{% if not (option.rich_text_doc | default(world.web.rich_text_options_doc, true)) %}
202+
data-tooltip="{{(option.__doc__ | default("Please document me!"))|replace('\n ', '\n')|escape|trim}}"
203+
{% endif %}>
204+
(?)
205+
{% if option.rich_text_doc | default(world.web.rich_text_options_doc, true) %}
206+
<div class="tooltip">
207+
{{ option.__doc__ | default("**Please document me!**") | rst_to_html | safe }}
208+
</div>
209+
{% endif %}
210+
</span>
200211
</label>
201212
{% endmacro %}
202213

0 commit comments

Comments
 (0)