Skip to content

Commit c171fed

Browse files
authored
UI: Add railroad syntax diagrams to command pages (#2364)
* UI: (experimental) Add railroad syntax diagrams to command pages * Add rr diagrams to new 8.4 commands * Updates from comprehensive testing
1 parent f6d59f7 commit c171fed

File tree

1,058 files changed

+31244
-217
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,058 files changed

+31244
-217
lines changed

build/components/syntax.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22
import logging
33
from textwrap import fill
44
from typing import List
5+
import os
6+
import sys
57

68
# Non-breaking space
79
NBSP = '\xa0'
810

911
# HTML Word Break Opportunity
1012
WBR = '<wbr>'
1113

14+
# Import railroad diagrams library
15+
try:
16+
import railroad
17+
except ImportError:
18+
railroad = None
19+
logging.warning("railroad-diagrams library not available. Railroad diagram generation will be skipped.")
20+
1221
class ArgumentType(Enum):
1322
INTEGER = 'integer'
1423
DOUBLE = 'double'
@@ -93,6 +102,131 @@ def syntax(self, **kwargs) -> str:
93102
logging.debug("EXITING: ")
94103
return f'{syntax}'
95104

105+
def to_railroad(self) -> 'railroad.Node':
106+
"""Convert this argument to a railroad diagram component."""
107+
if railroad is None:
108+
raise ImportError("railroad-diagrams library not available")
109+
110+
logging.debug(f"Converting argument '{self._name}' of type {self._type} to railroad")
111+
112+
# Handle different argument types
113+
if self._type == ArgumentType.PURE_TOKEN:
114+
# Pure tokens are just terminal text
115+
component = railroad.Terminal(self._token or self._name)
116+
elif self._type == ArgumentType.BLOCK:
117+
# Blocks are sequences of their arguments
118+
components = []
119+
120+
# If the block has a token, add it first
121+
if self._token:
122+
components.append(railroad.Terminal(self._token))
123+
124+
# Add the block's arguments
125+
if self._arguments:
126+
components.extend([arg.to_railroad() for arg in self._arguments])
127+
128+
if components:
129+
component = railroad.Sequence(*components)
130+
else:
131+
component = railroad.Terminal(self._display)
132+
elif self._type == ArgumentType.ONEOF:
133+
# OneOf is a choice between arguments
134+
if self._arguments:
135+
components = [arg.to_railroad() for arg in self._arguments]
136+
# Use the first option as the default (index 0)
137+
choice_component = railroad.Choice(0, *components)
138+
139+
# If there's a token, create a sequence of token + choice
140+
if self._token:
141+
token_part = railroad.Terminal(self._token)
142+
component = railroad.Sequence(token_part, choice_component)
143+
else:
144+
component = choice_component
145+
else:
146+
component = railroad.Terminal(self._display)
147+
else:
148+
# Regular arguments (string, integer, etc.)
149+
if self._token:
150+
# If there's a token, create a sequence of token + argument
151+
token_part = railroad.Terminal(self._token)
152+
arg_part = railroad.NonTerminal(self._display)
153+
component = railroad.Sequence(token_part, arg_part)
154+
else:
155+
# Just the argument
156+
component = railroad.NonTerminal(self._display)
157+
158+
# Handle multiple (repeating) arguments
159+
if self._multiple:
160+
if self._type == ArgumentType.ONEOF:
161+
# For ONEOF with multiple=true, we want to allow selecting multiple options
162+
# This means: first_option [additional_options ...]
163+
# where additional_options is a choice of any of the original options
164+
if self._arguments:
165+
# Create a choice of all options for the additional selections
166+
additional_choice = railroad.Choice(0, *[arg.to_railroad() for arg in self._arguments])
167+
168+
# If there's a token, we need to include it in the repetition
169+
if self._token:
170+
token_part = railroad.Terminal(self._token)
171+
repeat_part = railroad.Sequence(token_part, additional_choice)
172+
component = railroad.Sequence(component, railroad.ZeroOrMore(repeat_part))
173+
else:
174+
component = railroad.Sequence(component, railroad.ZeroOrMore(additional_choice))
175+
else:
176+
component = railroad.OneOrMore(component)
177+
elif self._multiple_token and self._token:
178+
# For types with multiple_token=true, the token should be repeated with each occurrence
179+
if self._type == ArgumentType.BLOCK and self._arguments:
180+
# For BLOCK types: extract the arguments part for repetition to avoid duplicate tokens
181+
args_component = railroad.Sequence(*[arg.to_railroad() for arg in self._arguments])
182+
repeat_part = railroad.Sequence(railroad.Terminal(self._token), args_component)
183+
component = railroad.Sequence(component, railroad.ZeroOrMore(repeat_part))
184+
else:
185+
# For non-BLOCK types: create the complete pattern from scratch
186+
# Pattern: TOKEN arg [TOKEN arg ...]
187+
if self._type == ArgumentType.INTEGER:
188+
arg_part = railroad.NonTerminal(self._display)
189+
elif self._type == ArgumentType.STRING:
190+
arg_part = railroad.NonTerminal(self._display)
191+
elif self._type == ArgumentType.KEY:
192+
arg_part = railroad.NonTerminal(self._display)
193+
else:
194+
arg_part = railroad.NonTerminal(self._display)
195+
196+
# Create the first occurrence: TOKEN arg
197+
first_occurrence = railroad.Sequence(railroad.Terminal(self._token), arg_part)
198+
# Create the repeat pattern: [TOKEN arg ...]
199+
repeat_part = railroad.Sequence(railroad.Terminal(self._token), arg_part)
200+
# Combine: TOKEN arg [TOKEN arg ...]
201+
component = railroad.Sequence(first_occurrence, railroad.ZeroOrMore(repeat_part))
202+
elif self._token and self._multiple:
203+
# For non-BLOCK types with multiple=true and a token:
204+
# The token appears once, followed by one or more arguments
205+
# Pattern: TOKEN arg [arg ...]
206+
if self._type != ArgumentType.BLOCK:
207+
# Create the argument part without the token for repetition
208+
if self._type == ArgumentType.INTEGER:
209+
arg_part = railroad.NonTerminal(self._display)
210+
elif self._type == ArgumentType.STRING:
211+
arg_part = railroad.NonTerminal(self._display)
212+
elif self._type == ArgumentType.KEY:
213+
arg_part = railroad.NonTerminal(self._display)
214+
else:
215+
arg_part = railroad.NonTerminal(self._display)
216+
217+
# Token + first arg + [additional args ...]
218+
token_part = railroad.Terminal(self._token)
219+
component = railroad.Sequence(token_part, railroad.OneOrMore(arg_part))
220+
else:
221+
# Multiple without token: arg [arg ...]
222+
component = railroad.OneOrMore(component)
223+
224+
# Handle optional arguments
225+
if self._optional:
226+
component = railroad.Optional(component)
227+
228+
return component
229+
96230

97231
class Command(Argument):
98232
def __init__(self, cname: str, data: dict, max_width: int = 640) -> None:
@@ -125,3 +259,138 @@ def syntax(self, **kwargs):
125259
result = fill(' '.join(args), **opts)
126260
logging.debug("EXITING: ")
127261
return result
262+
263+
def to_railroad_diagram(self, output_path: str = None) -> str:
264+
"""Generate a railroad diagram for this command and return the SVG content."""
265+
if railroad is None:
266+
raise ImportError("railroad-diagrams library not available")
267+
268+
logging.debug(f"Generating railroad diagram for command: {self._cname}")
269+
270+
# Create the main command terminal
271+
command_terminal = railroad.Terminal(self._cname)
272+
273+
# Convert all arguments to railroad components
274+
arg_components = []
275+
for arg in self._arguments:
276+
try:
277+
arg_components.append(arg.to_railroad())
278+
except Exception as e:
279+
logging.warning(f"Failed to convert argument {arg._name} to railroad: {e}")
280+
# Fallback to a simple terminal
281+
arg_components.append(railroad.NonTerminal(arg._name))
282+
283+
# Create the complete diagram
284+
if arg_components:
285+
diagram_content = railroad.Sequence(command_terminal, *arg_components)
286+
else:
287+
diagram_content = command_terminal
288+
289+
# Create the diagram
290+
diagram = railroad.Diagram(diagram_content)
291+
292+
# Generate SVG
293+
svg_content = []
294+
diagram.writeSvg(svg_content.append)
295+
svg_string = ''.join(svg_content)
296+
297+
# Apply Redis red styling and transparent background
298+
svg_string = self._apply_redis_styling(svg_string)
299+
300+
# Save to file if output path is provided
301+
if output_path:
302+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
303+
with open(output_path, 'w', encoding='utf-8') as f:
304+
f.write(svg_string)
305+
logging.info(f"Railroad diagram saved to: {output_path}")
306+
307+
return svg_string
308+
309+
def _apply_redis_styling(self, svg_content: str) -> str:
310+
"""
311+
Apply Redis red color scheme and transparent background to SVG.
312+
313+
Args:
314+
svg_content: Original SVG content
315+
316+
Returns:
317+
Modified SVG content with Redis styling
318+
"""
319+
# Redis red color: #DC382D
320+
redis_red = "#DC382D"
321+
322+
# Make background transparent by removing fill from the main SVG
323+
svg_content = svg_content.replace('fill="white"', 'fill="none"')
324+
svg_content = svg_content.replace('fill="#fff"', 'fill="none"')
325+
326+
# Add custom CSS styling for Redis theme
327+
style_css = f'''<defs>
328+
<style type="text/css"><![CDATA[
329+
svg.railroad-diagram {{ background-color: transparent !important; }}
330+
.terminal rect {{ fill: {redis_red} !important; stroke: {redis_red} !important; }}
331+
.terminal text {{ fill: white !important; font-weight: bold; }}
332+
.nonterminal rect {{ fill: none !important; stroke: {redis_red} !important; stroke-width: 2; }}
333+
.nonterminal text {{ fill: {redis_red} !important; font-weight: bold; }}
334+
path {{ stroke: {redis_red} !important; stroke-width: 2; fill: none; }}
335+
circle {{ fill: {redis_red} !important; stroke: {redis_red} !important; }}
336+
]]></style>
337+
</defs>'''
338+
339+
# Insert the style after the opening SVG tag
340+
import re
341+
if '<defs>' in svg_content:
342+
# Replace existing defs
343+
svg_content = re.sub(r'<defs>.*?</defs>', style_css, svg_content, flags=re.DOTALL)
344+
else:
345+
# Insert new defs after svg opening tag
346+
svg_content = re.sub(r'<svg([^>]*)>', f'<svg\\1>\n{style_css}', svg_content, count=1)
347+
348+
# Override any existing background color and stroke styles
349+
import re
350+
351+
# Replace the entire default style section with our Redis-themed styles
352+
default_style_pattern = r'<style>/\* <!\[CDATA\[ \*/.*?/\* \]\]> \*/</style>'
353+
redis_style_replacement = f'''<style>/* <![CDATA[ */
354+
svg.railroad-diagram {{
355+
background-color: transparent;
356+
}}
357+
svg.railroad-diagram path {{
358+
stroke-width: 2;
359+
stroke: {redis_red};
360+
fill: rgba(0,0,0,0);
361+
}}
362+
svg.railroad-diagram text {{
363+
font: bold 14px monospace;
364+
text-anchor: middle;
365+
fill: {redis_red};
366+
}}
367+
svg.railroad-diagram text.label {{
368+
text-anchor: start;
369+
}}
370+
svg.railroad-diagram text.comment {{
371+
font: italic 12px monospace;
372+
}}
373+
svg.railroad-diagram rect {{
374+
stroke-width: 2;
375+
stroke: {redis_red};
376+
fill: none;
377+
}}
378+
svg.railroad-diagram rect.group-box {{
379+
stroke: {redis_red};
380+
stroke-dasharray: 10 5;
381+
fill: none;
382+
}}
383+
/* ]]> */</style>'''
384+
385+
svg_content = re.sub(default_style_pattern, redis_style_replacement, svg_content, flags=re.DOTALL)
386+
387+
# Additional specific overrides for any remaining default colors
388+
svg_content = re.sub(r'fill:hsl\(120,100%,90%\)', 'fill: none', svg_content)
389+
svg_content = re.sub(r'stroke: gray', f'stroke: {redis_red}', svg_content)
390+
391+
# Additional fallback overrides
392+
svg_content = re.sub(r'background-color:\s*[^;]+;', 'background-color: transparent;', svg_content)
393+
svg_content = re.sub(r'stroke:\s*black;', f'stroke: {redis_red};', svg_content)
394+
svg_content = re.sub(r'stroke:\s*#000;', f'stroke: {redis_red};', svg_content)
395+
396+
return svg_content

0 commit comments

Comments
 (0)