Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate railroad diagrams #89

Merged
merged 6 commits into from
Dec 2, 2024
Merged
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ Raise a [GH issue](https://github.com/questdb/documentation/issues/new/choose) o

## Syntax

### Railroad diagrams

Our SQL syntax diagrams are created using Railroad.

To create a diagram, use the [Railroad online editor](https://www.bottlecaps.de/rr/ui) to see it rendered.

Once you're happy with it, copy the Railroad syntax and add it to the `static/images/docs/diagrams/.railroad` file.

Next, run the `scripts/railroad.py` script to generate the SVG image.

During its final output, a markdown image with the appropriate syntax is printed.

Copy this syntax and paste it into the markdown file where you want the diagram to appear.

The script requires:

* Java (to run the `rr.war` file)
* Python (to execute the `railroad.py` script)

### Math Expressions

Use LaTeX-style math between `$` for inline or `$$` for block equations:
Expand Down
Binary file added rr.war
Binary file not shown.
217 changes: 217 additions & 0 deletions scripts/railroad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import os
import subprocess
import re
from pathlib import Path

PROJECT_ROOT = Path(os.getcwd())
RR_WAR_PATH = PROJECT_ROOT / "rr.war"
INPUT_FILE = PROJECT_ROOT / "static/images/docs/diagrams/.railroad"
OUTPUT_DIR = PROJECT_ROOT / "static/images/docs/diagrams"

print(f"Current working directory: {PROJECT_ROOT}")
print(f"RR.war path: {RR_WAR_PATH}")
print(f"Checking if input file exists: {INPUT_FILE.exists()}")
print(f"Checking if output dir exists: {OUTPUT_DIR.exists()}")
print(f"Checking if rr.war exists: {RR_WAR_PATH.exists()}")

# Custom CSS style to inject
CUSTOM_STYLE = '''
<style type="text/css">
@namespace "http://www.w3.org/2000/svg";
text.nonterminal, text.terminal {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, Helvetica, sans-serif;
font-size: 12px;
}
text.terminal {
fill: #ffffff;
font-weight: bold;
}
text.nonterminal {
fill: #e289a4;
font-weight: normal;
}
polygon, rect {
fill: none;
stroke: none;
}
rect.terminal {
fill: none;
stroke: #be2f5b;
}
rect.nonterminal {
fill: rgba(255,255,255,0.1);
stroke: none;
}
</style>
'''

def extract_diagrams(file_path):
"""Extract diagram definitions from the input file."""
diagrams = {}
current_name = None
current_definition = []

print(f"Reading from file: {file_path}")
with open(file_path, 'r') as f:
previous_line = ""
for line in f:
line = line.rstrip()

if not line or line.startswith('#'):
previous_line = ""
continue

if '::=' in line:
if previous_line and not previous_line.startswith('-'):
if current_name and current_definition:
diagrams[current_name] = '\n'.join(current_definition)

current_name = previous_line.strip()
current_definition = [f"{current_name} {line.strip()}"]
print(f"Found diagram: {current_name}")

elif current_name and line:
current_definition.append(line)

previous_line = line

if current_name and current_definition:
diagrams[current_name] = '\n'.join(current_definition)

print(f"\nFound {len(diagrams)} diagrams: {sorted(diagrams.keys())}")

if diagrams:
first_key = sorted(diagrams.keys())[0]
print(f"\nFirst diagram '{first_key}' content:")
print(diagrams[first_key])

return diagrams

def generate_svg(name, definition, temp_dir):
"""Generate SVG for a single diagram definition."""
temp_grammar = temp_dir / f"{name}.grammar"
temp_grammar.write_text(definition)
print(f"Created temporary grammar file: {temp_grammar}")

output_path = OUTPUT_DIR / f"{name}.svg"
command = [
"java", "-jar", str(RR_WAR_PATH),
"-suppressebnf",
f"-out:{output_path}",
str(temp_grammar)
]
print(f"Executing command: {' '.join(command)}")

result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error output: {result.stderr}")
raise Exception(f"Failed to generate SVG: {result.stderr}")

print(f"Generated SVG at: {output_path}")
return output_path

def inject_custom_style(svg_path):
"""Extract SVG content, normalize it, and inject custom CSS style."""
with open(svg_path, 'r') as f:
content = f.read()

svg_match = re.search(r'<svg[^>]*width="[^"]*"[^>]*height="[^"]*"[^>]*>(.*?)</svg>', content, re.DOTALL)
if not svg_match:
print(f"Warning: No diagram SVG found in {svg_path}")
return

width_match = re.search(r'width="([^"]*)"', svg_match.group(0))
height_match = re.search(r'height="([^"]*)"', svg_match.group(0))

if not width_match or not height_match:
print(f"Warning: Missing width or height in {svg_path}")
return

# Create the new SVG with proper opening tag and our style
new_svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{width_match.group(1)}" height="{height_match.group(1)}">
<defs>
<style type="text/css">
@namespace "http://www.w3.org/2000/svg";
.line {{fill: none; stroke: #636273;}}
.bold-line {{stroke: #636273; shape-rendering: crispEdges; stroke-width: 2; }}
.thin-line {{stroke: #636273; shape-rendering: crispEdges}}
.filled {{fill: #636273; stroke: none;}}
text.terminal {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, Helvetica, sans-serif;
font-size: 12px;
fill: #ffffff;
font-weight: bold;
}}
text.nonterminal {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, Helvetica, sans-serif;
font-size: 12px;
fill: #e289a4;
font-weight: normal;
}}
text.regexp {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, Helvetica, sans-serif;
font-size: 12px;
fill: #00141F;
font-weight: normal;
}}
rect, circle, polygon {{fill: none; stroke: none;}}
rect.terminal {{fill: none; stroke: #be2f5b;}}
rect.nonterminal {{fill: rgba(255,255,255,0.1); stroke: none;}}
rect.text {{fill: none; stroke: none;}}
polygon.regexp {{fill: #C7ECFF; stroke: #038cbc;}}
</style>
</defs>'''

inner_content = svg_match.group(1)

inner_content = re.sub(r'\s+xmlns="[^"]*"', '', inner_content)
inner_content = re.sub(r'\s+style="[^"]*"', '', inner_content)
inner_content = inner_content.strip()

final_svg = f"{new_svg}\n {inner_content}\n</svg>"

with open(svg_path, 'w') as f:
f.write(final_svg)

def main():
temp_dir = PROJECT_ROOT / "temp_grammar"
temp_dir.mkdir(exist_ok=True)
print(f"Created temp directory: {temp_dir}")

markdown_syntax_list = []

try:
diagrams = extract_diagrams(INPUT_FILE)

for name, definition in diagrams.items():
print(f"\nProcessing diagram: {name}")

output_path = OUTPUT_DIR / f"{name}.svg"
if output_path.exists():
print(f"Skipping existing diagram: {name}")
continue

try:
svg_path = generate_svg(name, definition, temp_dir)

inject_custom_style(svg_path)

print(f"Successfully generated: {name}.svg")

markdown_syntax_list.append(f"![Diagram for {name}](/images/docs/diagrams/{name}.svg)")

except Exception as e:
print(f"Error processing {name}: {str(e)}")

finally:
print("\nCleaning up...")
for file in temp_dir.glob("*.grammar"):
print(f"Removing temporary file: {file}")
file.unlink()
temp_dir.rmdir()
print("Cleanup complete")

if markdown_syntax_list:
print("\nCopy the image syntax below and paste it into your markdown file:")
for syntax in markdown_syntax_list:
print(syntax)

if __name__ == "__main__":
main()
10 changes: 5 additions & 5 deletions src/internals/ssr.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ module.exports = ({ customFields, favicon, organizationName, url }) => `
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@questdb" />
<meta name="generator" content="Docusaurus v<%= it.version %>" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/images/icons/apple-180x180.webp" sizes="180x180" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
<link rel="icon" href="/docs/favicon.ico" />
<link rel="icon" href="/docs/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/docs/images/icons/apple-180x180.webp" sizes="180x180" />
<meta name="msapplication-config" content="/docs/browserconfig.xml" />
<link rel="sitemap" type="application/xml" href="/docs/sitemap.xml" />
<%~ it.headTags %>
<% it.metaAttributes.forEach((metaAttribute) => { %>
<%~ metaAttribute %>
Expand Down
11 changes: 6 additions & 5 deletions static/images/docs/diagrams/.railroad
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# this file contains the grammar used to generate railroad diagrams.
# they are used to visualize QuestDB syntax in documentation.
# to learn how to create or edit these diagrams, please see this (private!) document:
# https://questdb.slab.com/posts/how-to-do-railroad-diagrams-he6wsuc0
# Add new syntax to this file to generate railroad diagrams
# Run `scripts/railroad.py`.
# Link using `![Diagram](/images/docs/diagrams/.railroad/diagramName.svg)`
# Will take file name from first line
# Then syntax from following lines

alterUser
::= 'ALTER' 'USER' userName ( 'ENABLE' | 'DISABLE' | 'WITH' ('NO' 'PASSWORD' | 'PASSWORD' password )
Expand Down Expand Up @@ -255,7 +256,7 @@ addIndex
dropIndex
::= 'ALTER' 'TABLE' tableName 'ALTER' 'COLUMN' columnName 'DROP' 'INDEX'

(no)cachColumn
noCacheColumn
::= 'ALTER' 'TABLE' tableName 'ALTER' 'COLUMN' columnName ( 'NOCACHE' | 'CACHE' )

dropColumn
Expand Down
Loading
Loading