Skip to content
Open
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
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@
"jsdoc/no-defaults": "warn",
"jsdoc/no-types": "warn",
"jsdoc/require-hyphen-before-param-description": "warn",
"jsdoc/require-jsdoc": "warn",
"jsdoc/require-jsdoc": ["warn", {
"publicOnly": true
}],
"jsdoc/require-param-description": "warn",
"jsdoc/require-param-name": "warn",
"jsdoc/require-param": [
Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/date-author.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ You can use this function to retrieve the date and author information for all si

An example of calling the function is shown below:

``` py
``` py { data-download }
from pathlib import Path
from datetime import datetime

Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ External plugins are developed by third-party developers and require manual inst

**Usage**: use `markmap` fence blocks to enclose markdown content

`````
````` md { data-download }
````markmap

## Mind Map
Expand Down
4 changes: 2 additions & 2 deletions docs/publishing-your-site.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ documentation. At the root of your repository, create a new GitHub Actions
workflow, e.g. `.github/workflows/ci.yml`, and copy and paste the following
contents:

``` yaml
``` yaml { title=".github/workflows/ci.yaml" data-download }
name: ci # (1)!
on:
push:
Expand Down Expand Up @@ -113,7 +113,7 @@ by using the [GitLab CI] task runner. At the root of your repository, create a
task definition named `.gitlab-ci.yml` and copy and paste the following
contents:

``` yaml
``` yaml { title=".gitlab-ci.yml" data-download }
pages:
stage: deploy
image: python:latest
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/admonitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ If you want to restore this appearance, add the following CSS to an

=== ":octicons-file-code-16: `docs/stylesheets/extra.css`"

``` css
``` css { data-download }
.md-typeset .admonition,
.md-typeset details {
--md-admonition-title-bg-color: color-mix(in srgb, var(--md-admonition-color), transparent 90%);
Expand Down Expand Up @@ -566,7 +566,7 @@ If you want to restore this appearance, add the following CSS to an

=== ":octicons-file-code-16: `docs/stylesheets/extra.css`"

``` css
``` css { data-download }
.md-typeset .admonition,
.md-typeset details {
--md-admonition-title-bg-color: color-mix(in srgb, var(--md-admonition-color), transparent 90%);
Expand All @@ -591,7 +591,7 @@ If you require full custom styling for admonitions, the Additional CSS approach

=== ":octicons-file-code-16: `docs/stylesheets/extra.css`"

``` css
``` css { data-download }
.md-typeset .admonition.pied-piper,
.md-typeset details.pied-piper {
--md-admonition-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M244 246c-3.2-2-6.3-2.9-10.1-2.9-6.6 0-12.6 3.2-19.3 3.7l1.7 4.9zm135.9 197.9c-19 0-64.1 9.5-79.9 19.8l6.9 45.1c35.7 6.1 70.1 3.6 106-9.8-4.8-10-23.5-55.1-33-55.1zM340.8 177c6.6 2.8 11.5 9.2 22.7 22.1 2-1.4 7.5-5.2 7.5-8.6 0-4.9-11.8-13.2-13.2-23 11.2-5.7 25.2-6 37.6-8.9 68.1-16.4 116.3-52.9 146.8-116.7C548.3 29.3 554 16.1 554.6 2l-2 2.6c-28.4 50-33 63.2-81.3 100-31.9 24.4-69.2 40.2-106.6 54.6l-6.3-.3v-21.8c-19.6 1.6-19.7-14.6-31.6-23-18.7 20.6-31.6 40.8-58.9 51.1-12.7 4.8-19.6 10-25.9 21.8 34.9-16.4 91.2-13.5 98.8-10zM555.5 0l-.6 1.1-.3.9.6-.6zm-59.2 382.1c-33.9-56.9-75.3-118.4-150-115.5l-.3-6c-1.1-13.5 32.8 3.2 35.1-31l-14.4 7.2c-19.8-45.7-8.6-54.3-65.5-54.3-14.7 0-26.7 1.7-41.4 4.6 2.9 18.6 2.2 36.7-10.9 50.3l19.5 5.5c-1.7 3.2-2.9 6.3-2.9 9.8 0 21 42.8 2.9 42.8 33.6 0 18.4-36.8 60.1-54.9 60.1-8 0-53.7-50-53.4-60.1l.3-4.6 52.3-11.5c13-2.6 12.3-22.7-2.9-22.7-3.7 0-43.1 9.2-49.4 10.6-2-5.2-7.5-14.1-13.8-14.1-3.2 0-6.3 3.2-9.5 4-9.2 2.6-31 2.9-21.5 20.1L15.9 298.5c-5.5 1.1-8.9 6.3-8.9 11.8 0 6 5.5 10.9 11.5 10.9 8 0 131.3-28.4 147.4-32.2 2.6 3.2 4.6 6.3 7.8 8.6 20.1 14.4 59.8 85.9 76.4 85.9 24.1 0 58-22.4 71.3-41.9 3.2-4.3 6.9-7.5 12.4-6.9.6 13.8-31.6 34.2-33 43.7-1.4 10.2-1 35.2-.3 41.1 26.7 8.1 52-3.6 77.9-2.9 4.3-21 10.6-41.9 9.8-63.5l-.3-9.5c-1.4-34.2-10.9-38.5-34.8-58.6-1.1-1.1-2.6-2.6-3.7-4 2.2-1.4 1.1-1 4.6-1.7 88.5 0 56.3 183.6 111.5 229.9 33.1-15 72.5-27.9 103.5-47.2-29-25.6-52.6-45.7-72.7-79.9zm-196.2 46.1v27.2l11.8-3.4-2.9-23.8zm-68.7-150.4l24.1 61.2 21-13.8-31.3-50.9zm84.4 154.9l2 12.4c9-1.5 58.4-6.6 58.4-14.1 0-1.4-.6-3.2-.9-4.6-26.8 0-36.9 3.8-59.5 6.3z"/></svg>');
Expand Down
105 changes: 105 additions & 0 deletions docs/reference/code-blocks.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
icon: material/code-json
status: new
---

# Code blocks
Expand Down Expand Up @@ -123,6 +124,110 @@ theme:

[line highlighting]: #highlighting-specific-lines

### Code download button

<!-- md:version 10.1.5 -->
<!-- md:feature -->

Code blocks can include a download button that supports blobs, URLs, and local file downloads.

#### Enable Button

Add the `data-download` attribute to the fence block header to enable download:

=== "Blob"

```` yaml { data-download="blob" }
``` yaml { data-download="blob" }

markdown_extensions:
- material.extensions.code_download
```
````

=== "Local File"

```` yaml { data-download="../assets/mkdocs.yml" }
``` yaml { data-download="assets/xxx.yaml" }

markdown_extensions:
- material.extensions.code_download
```
````

=== "URL"

```` yaml { data-download="https://jaywhj.github.io/mkdocs-materialx/assets/mkdocs.yml" }
``` yaml { data-download="https://example.com/xxx.yaml" } # (1)!

markdown_extensions:
- material.extensions.code_download
```
````

1. Note: Cross-origin downloads may fall back to page redirection due to browser permission restrictions.

The filename is determined by the following priority:

1. Original filename
2. `<title>.<lang>`
3. `download.<lang>` or `<title>.text`
4. `download.text`

!!! warning "Note"

If you need to add more attributes such as `title`, `hl_lines`, and `linenums` in the fence block header, follow the standard [attr_list]{target="_blank"} syntax - **wrap all attributes inside `{ }`**:

```` yaml
``` yaml { title="config.yaml" data-download="blob" }
# Code block content
```
````

Or enable the Enhanced Extension below.

#### Enhanced Extension

This project upstream dependencies - [attr_list]{target="_blank"} from Python-Markdown and [superfences]{target="_blank"} from PyMdown Extensions,
the standard way to add attributes is to **wrap all attributes inside { }**.

Previously, attributes such as `title`, `hl_lines`, and `linenums` could be used without curly braces because PyMdown added special compatibility handling exclusively for these three attributes.

If you want to use this simplified syntax for **all custom attributes**, you can enable the following extension:

``` yaml
markdown_extensions:
- material.extensions.code_download
```

This extension supports:

- Attributes defined via attr_list syntax: `{ .class key=value }`
- Attributes declared directly in fence info strings: `linenos=true`
- Mixed combination of the two formats: `linenos=true { .class key=value }`

For example, the following formats are all supported:

```` yaml
``` data-download # (1)!
``` data-download="blob"
``` py data-download
``` py title="title" data-download="blob"

``` py { data-download }
``` py { data-download="blob" }
``` py { title="title0 linenums="10" hl_lines="2" data-download }

``` py title="title1" { data-download="blob" }
``` py title="title2" { data-download hl_lines="2" } linenums="20"
``` py title="title3" { data-download } { hl_lines="2" } linenums="30"
````

1. Equivalent to blob

[attr_list]: https://python-markdown.github.io/extensions/attr_list/
[superfences]: https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#injecting-classes-ids-and-attributes

### Code annotations

<!-- md:version 8.0.0 -->
Expand Down
2 changes: 1 addition & 1 deletion docs/setup/adding-a-git-repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ automatically requested and rendered.
#### Repository name

<!-- md:version 0.1.0 -->
<!-- md:default _automatically set to_ `GitHub`, `GitLab` _or_ `Bitbucket` -->
<!-- md:default `GitHub`, `GitLab` _or_ `Bitbucket` -->

MkDocs will infer the source provider by examining the URL and try to set the
_repository name_ automatically. If you wish to customize the name, set
Expand Down
184 changes: 184 additions & 0 deletions material/extensions/code_download.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import re
import shlex
from typing import Iterable
from markdown import Markdown
from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor


_FENCE_OPEN_RE = re.compile(
r'^([ \t]*)(`{3,}|~{3,})([^\n`]*)$'
)
_FENCE_CLOSE_RE = re.compile(
r'^([ \t]*)(`{3,}|~{3,})[ \t]*$'
)
_ATTR_RE = re.compile(r'\{([^}]*)\}')


# -----------------------------------------------------------------------------
# Preprocessor
# -----------------------------------------------------------------------------

class CodeDownloadPreprocessor(Preprocessor):

def run(self, lines: Iterable[str]):
result: list[str] = []

in_fence = False
fence_char = ""
fence_len = 0

for line in lines:
if not in_fence:
stripped = line.lstrip(" \t")
if not stripped or stripped[0] not in ("`", "~"):
result.append(line)
continue

match = _FENCE_OPEN_RE.match(line)
if not match:
result.append(line)
continue

indent, fence, trailing = match.groups()
info = trailing.strip()

result.append(_normalize_fence_opening(indent, fence, info))

in_fence = True
fence_char = fence[0]
fence_len = len(fence)
continue

# inside fence
result.append(line)

stripped = line.lstrip(" \t")
if stripped and stripped[0] == fence_char:
match = _FENCE_CLOSE_RE.match(line)
if match:
_, fence = match.groups()
if fence[0] == fence_char and len(fence) >= fence_len:
in_fence = False

return result

# -----------------------------------------------------------------------------
# Extension
# -----------------------------------------------------------------------------

class CodeDownloadExtension(Extension):

def extendMarkdown(self, md: Markdown):
md.registerExtension(self)
md.preprocessors.register(
CodeDownloadPreprocessor(md),
"code_download",
35
)

# -----------------------------------------------------------------------------
# Core
# -----------------------------------------------------------------------------

def _normalize_fence_opening(indent: str, fence: str, info: str) -> str:
if not info:
return f"{indent}{fence}"

if "data-download" not in info:
return f"{indent}{fence} {info}"

# 按顺序解析 tokens
tokens: list[str] = []
pos = 0
try:
for m in _ATTR_RE.finditer(info):
# 非 attr_list 部分
before = info[pos:m.start()].strip()
if before:
tokens.extend(shlex.split(before, posix=True))

# attr_list 内部
inner = m.group(1)
if inner:
tokens.extend(shlex.split(inner, posix=True))

pos = m.end()

# 尾部
tail = info[pos:].strip()
if tail:
tokens.extend(shlex.split(tail, posix=True))
except ValueError:
return f"{indent}{fence} {info}"

if not tokens:
return f"{indent}{fence} {info}"

# language 识别
language = None
if tokens and _looks_like_language(tokens[0]):
language = tokens[0]
tokens = tokens[1:]

# 构建 attrs
attrs: list[str] = []
has_download = False

for token in tokens:
key, value = _split_token(token)

if key == "data-download":
has_download = True
value = _normalize_data_download(value)

if value is None:
if key and key[0] in ".#":
attrs.append(key)
continue
value = key

attrs.append(f"{key}=\"{_escape_attr(value)}\"")

if not has_download:
return f"{indent}{fence} {info}"

# 统一输出 attr_list
parts: list[str] = []
if language:
parts.append(f".{language}")
parts.extend(attrs)

return f"{indent}{fence} {{ {' '.join(parts)} }}"

# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------

def _looks_like_language(token: str) -> bool:
return (
"=" not in token and
not token.startswith((".", "#")) and
not token.startswith("data-") and
"{" not in token and
"}" not in token
)

def _split_token(token: str) -> tuple[str, str | None]:
if "=" not in token:
return token, None
key, value = token.split("=", 1)
return key, value.strip('"\'')

def _normalize_data_download(value: str | None) -> str:
if value not in (None, "", "data-download"):
return value
return "blob"

def _escape_attr(value: str) -> str:
return value.replace("\\", "\\\\").replace("\"", "\\\"")

# -----------------------------------------------------------------------------

def makeExtension(**kwargs):
return CodeDownloadExtension(**kwargs)
Loading