Skip to content

Commit a1cb0b2

Browse files
committed
Add heading links to markdown parser
1 parent 0b947bb commit a1cb0b2

File tree

4 files changed

+72
-3
lines changed

4 files changed

+72
-3
lines changed

docs/components_page/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import dash
55
import dash_bootstrap_components as dbc
6-
from dash import html
6+
from dash import ClientsideFunction, Input, Output, html
77
from jinja2 import Environment, FileSystemLoader
88

99
from .components.table.simple import table_body, table_header # noqa
@@ -149,6 +149,7 @@ def register_apps():
149149
)
150150
app = dash.Dash(
151151
external_stylesheets=["/static/loading.css"],
152+
external_scripts=["/static/js/clientside.js"],
152153
requests_pathname_prefix=requests_pathname_prefix,
153154
suppress_callback_exceptions=True,
154155
serve_locally=SERVE_LOCALLY,
@@ -171,6 +172,17 @@ def register_apps():
171172
)
172173
else:
173174
app.layout = parse(app, **kwargs)
175+
176+
app.clientside_callback(
177+
ClientsideFunction(
178+
namespace="clientside", function_name="scrollAfterLoad"
179+
),
180+
# id won't actually be updated, we just want the callback to run
181+
# once Dash has initialised and hydrated the page
182+
Output("url", "id"),
183+
Input("url", "hash"),
184+
)
185+
174186
if slug == "index":
175187
routes["/docs/components"] = app
176188
else:

docs/components_page/markdown_parser.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
PROP_NAME_PATTERN = re.compile(r"""(\n- )(\w+?) \(""")
2828
NESTED_PROP_NAME_PATTERN = re.compile(r"""(\n\s+- )(\w+?) \(""")
2929

30+
HEADING_TEMPLATE = """<h{level} id="{id_}" style="scroll-margin-top:60px">
31+
{heading}<a href="#{id_}" class="anchor-link">#</a>
32+
</h{level}>"""
33+
3034

3135
def parse(app, markdown_path, extra_env_vars=None):
3236
extra_env_vars = extra_env_vars or {}
@@ -46,7 +50,8 @@ def parse(app, markdown_path, extra_env_vars=None):
4650

4751
markdown_blocks = SPLIT_PATTERN.split(raw)
4852
markdown_blocks = [
49-
dcc.Markdown(block.strip()) for block in markdown_blocks
53+
dcc.Markdown(_preprocess_markdown(block), dangerously_allow_html=True)
54+
for block in markdown_blocks
5055
]
5156

5257
examples_docs = EXAMPLE_DOC_PATTERN.findall(raw)
@@ -55,7 +60,24 @@ def parse(app, markdown_path, extra_env_vars=None):
5560
]
5661

5762
content.extend(_interleave(markdown_blocks, examples_docs))
58-
return html.Div(content, key=str(markdown_path))
63+
return html.Div([dcc.Location(id="url")] + content, key=str(markdown_path))
64+
65+
66+
def _preprocess_markdown(text):
67+
text = text.strip()
68+
lines = text.split("\n")
69+
new_lines = []
70+
for line in lines:
71+
if line.startswith("#"):
72+
level, heading = line.split(" ", 1)
73+
level = level.count("#")
74+
id_ = heading.replace(" ", "-").lower()
75+
new_lines.append(
76+
HEADING_TEMPLATE.format(level=level, id_=id_, heading=heading)
77+
)
78+
else:
79+
new_lines.append(line)
80+
return "\n".join(new_lines)
5981

6082

6183
def _parse_block(block, app, extra_env_vars):

docs/static/docs.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,16 @@ span.hljs-meta {
479479
.superhero-demo .dropdown-divider {
480480
border-top: 1px solid rgba(0, 0, 0, 0.15);
481481
}
482+
483+
.anchor-link {
484+
color: var(--bs-secondary);
485+
opacity: 0.5;
486+
margin-left: 0.5rem;
487+
text-decoration: none;
488+
transition: color 0.15s ease-in-out, opacity 0.15s ease-in-out;
489+
}
490+
491+
.anchor-link:hover, :hover>.anchor-link {
492+
color: var(--bs-primary);
493+
opacity: 1;
494+
}

docs/static/js/clientside.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const delays = [100, 100, 300, 500, 1000, 2000];
2+
3+
function scrollWithRetry(id, idx) {
4+
const targetElement = document.getElementById(id);
5+
if (targetElement) {
6+
targetElement.scrollIntoView();
7+
} else if (idx < delays.length) {
8+
setTimeout(() => scrollWithRetry(id, idx + 1), delays[idx]);
9+
}
10+
}
11+
12+
if (!window.dash_clientside) {
13+
window.dash_clientside = {};
14+
}
15+
window.dash_clientside.clientside = {
16+
scrollAfterLoad: function(hash) {
17+
if (hash) {
18+
scrollWithRetry(hash.slice(1), 0);
19+
}
20+
return window.dash_clientside.no_update;
21+
}
22+
};

0 commit comments

Comments
 (0)