mkdocs-sumi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. mkdocs_sumi/__init__.py +3 -0
  2. mkdocs_sumi/i18n.py +192 -0
  3. mkdocs_sumi/icons.py +52 -0
  4. mkdocs_sumi/landing.py +84 -0
  5. mkdocs_sumi/markdown/__init__.py +0 -0
  6. mkdocs_sumi/markdown/attrs.py +47 -0
  7. mkdocs_sumi/markdown/extension.py +577 -0
  8. mkdocs_sumi/markdown/filetree.py +133 -0
  9. mkdocs_sumi/plugin.py +622 -0
  10. mkdocs_sumi/theme/404.html +15 -0
  11. mkdocs_sumi/theme/__init__.py +0 -0
  12. mkdocs_sumi/theme/assets/css/sumi.css +1001 -0
  13. mkdocs_sumi/theme/assets/fonts/LICENSE-Inter.txt +93 -0
  14. mkdocs_sumi/theme/assets/fonts/LICENSE-JetBrainsMono.txt +93 -0
  15. mkdocs_sumi/theme/assets/fonts/inter-latin-ext-wght-italic.woff2 +0 -0
  16. mkdocs_sumi/theme/assets/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  17. mkdocs_sumi/theme/assets/fonts/inter-latin-wght-italic.woff2 +0 -0
  18. mkdocs_sumi/theme/assets/fonts/inter-latin-wght-normal.woff2 +0 -0
  19. mkdocs_sumi/theme/assets/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
  20. mkdocs_sumi/theme/assets/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
  21. mkdocs_sumi/theme/assets/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  22. mkdocs_sumi/theme/assets/images/favicon.svg +1 -0
  23. mkdocs_sumi/theme/assets/js/sumi.js +607 -0
  24. mkdocs_sumi/theme/base.html +150 -0
  25. mkdocs_sumi/theme/blog-post.html +36 -0
  26. mkdocs_sumi/theme/blog.html +63 -0
  27. mkdocs_sumi/theme/landing.html +143 -0
  28. mkdocs_sumi/theme/main.html +2 -0
  29. mkdocs_sumi/theme/mkdocs_theme.yml +30 -0
  30. mkdocs_sumi/theme/partials/analytics.html +8 -0
  31. mkdocs_sumi/theme/partials/article-footer.html +18 -0
  32. mkdocs_sumi/theme/partials/breadcrumbs.html +11 -0
  33. mkdocs_sumi/theme/partials/footer.html +20 -0
  34. mkdocs_sumi/theme/partials/header.html +49 -0
  35. mkdocs_sumi/theme/partials/logo.html +1 -0
  36. mkdocs_sumi/theme/partials/macros.html +77 -0
  37. mkdocs_sumi/theme/partials/page-actions.html +23 -0
  38. mkdocs_sumi/theme/partials/palette.html +29 -0
  39. mkdocs_sumi/theme/partials/toc.html +13 -0
  40. mkdocs_sumi/theme/print.html +27 -0
  41. mkdocs_sumi/theme/releases.html +45 -0
  42. mkdocs_sumi/vendor/icons/LICENSE-lucide +43 -0
  43. mkdocs_sumi/vendor/icons/LICENSE-simple-icons.md +30 -0
  44. mkdocs_sumi/vendor/icons/brands.json +1 -0
  45. mkdocs_sumi/vendor/icons/lucide.json +1 -0
  46. mkdocs_sumi/vendor/katex/LICENSE +21 -0
  47. mkdocs_sumi/vendor/katex/auto-render.min.js +1 -0
  48. mkdocs_sumi/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  49. mkdocs_sumi/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  50. mkdocs_sumi/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  51. mkdocs_sumi/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  52. mkdocs_sumi/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  53. mkdocs_sumi/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  54. mkdocs_sumi/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  55. mkdocs_sumi/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  56. mkdocs_sumi/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  57. mkdocs_sumi/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  58. mkdocs_sumi/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  59. mkdocs_sumi/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  60. mkdocs_sumi/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  61. mkdocs_sumi/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  62. mkdocs_sumi/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  63. mkdocs_sumi/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  64. mkdocs_sumi/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  65. mkdocs_sumi/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  66. mkdocs_sumi/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  67. mkdocs_sumi/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  68. mkdocs_sumi/vendor/katex/katex.min.css +1 -0
  69. mkdocs_sumi/vendor/katex/katex.min.js +1 -0
  70. mkdocs_sumi/vendor/mermaid/LICENSE +21 -0
  71. mkdocs_sumi/vendor/mermaid/mermaid.min.js +4330 -0
  72. mkdocs_sumi-0.1.0.dist-info/METADATA +76 -0
  73. mkdocs_sumi-0.1.0.dist-info/RECORD +77 -0
  74. mkdocs_sumi-0.1.0.dist-info/WHEEL +4 -0
  75. mkdocs_sumi-0.1.0.dist-info/entry_points.txt +8 -0
  76. mkdocs_sumi-0.1.0.dist-info/licenses/LICENSE +202 -0
  77. mkdocs_sumi-0.1.0.dist-info/licenses/NOTICE +23 -0
@@ -0,0 +1,3 @@
1
+ """Sumi: a modern, local-first documentation theme for MkDocs."""
2
+
3
+ __version__ = "0.1.0"
mkdocs_sumi/i18n.py ADDED
@@ -0,0 +1,192 @@
1
+ """Interface strings for the theme. English is the fallback for every key."""
2
+
3
+ from __future__ import annotations
4
+
5
+ STRINGS: dict[str, dict[str, str]] = {
6
+ 'en': {
7
+ 'search': 'Search',
8
+ 'search_placeholder': 'Search docs or run a command…',
9
+ 'search_no_results': 'No results for',
10
+ 'search_loading': 'Loading index…',
11
+ 'search_hint_open': 'to open',
12
+ 'search_hint_move': 'to move',
13
+ 'search_hint_close': 'to close',
14
+ 'pages': 'Pages',
15
+ 'commands': 'Commands',
16
+ 'on_this_page': 'On this page',
17
+ 'back_to_top': 'Back to top',
18
+ 'previous': 'Previous',
19
+ 'next': 'Next',
20
+ 'edit_page': 'Edit this page',
21
+ 'view_source': 'View source',
22
+ 'copy_markdown': 'Copy as Markdown',
23
+ 'copied': 'Copied',
24
+ 'view_markdown': 'View as Markdown',
25
+ 'open_in': 'Open in {name}',
26
+ 'print': 'Print',
27
+ 'page_actions': 'Page actions',
28
+ 'copy': 'Copy',
29
+ 'copy_code': 'Copy code',
30
+ 'toggle_theme': 'Toggle dark mode',
31
+ 'theme_light': 'Light',
32
+ 'theme_dark': 'Dark',
33
+ 'theme_auto': 'System',
34
+ 'menu': 'Menu',
35
+ 'close': 'Close',
36
+ 'navigation': 'Navigation',
37
+ 'skip_to_content': 'Skip to content',
38
+ 'last_updated': 'Last updated',
39
+ 'reading_time': '{n} min read',
40
+ 'language': 'Language',
41
+ 'version': 'Version',
42
+ 'repository': 'Repository',
43
+ 'keyboard_shortcuts': 'Keyboard shortcuts',
44
+ 'shortcut_search': 'Open search and commands',
45
+ 'shortcut_theme': 'Toggle dark mode',
46
+ 'shortcut_prev_next': 'Previous / next page',
47
+ 'shortcut_toc': 'Previous / next heading',
48
+ 'shortcut_help': 'Show this help',
49
+ 'built_with': 'Built with {mkdocs} and {sumi}',
50
+ 'not_found_title': 'Page not found',
51
+ 'not_found_text': 'The page you are looking for does not exist or has moved.',
52
+ 'go_home': 'Go to the home page',
53
+ 'blog': 'Blog',
54
+ 'all_posts': 'All posts',
55
+ 'tags': 'Tags',
56
+ 'posted_on': 'Posted on',
57
+ 'older_post': 'Older post',
58
+ 'newer_post': 'Newer post',
59
+ 'subscribe_rss': 'Subscribe via RSS',
60
+ 'releases': 'Releases',
61
+ 'latest': 'Latest',
62
+ 'prerelease': 'Pre-release',
63
+ 'archived': 'Archived',
64
+ 'download': 'Download',
65
+ 'asset': 'Asset',
66
+ 'size': 'Size',
67
+ 'checksum': 'SHA-256',
68
+ 'release_notes': 'Release notes',
69
+ 'old_version': 'You are reading the documentation for an older version.',
70
+ 'go_latest': 'Go to the latest version',
71
+ 'expand_all': 'Expand all',
72
+ 'zoom_image': 'Zoom image',
73
+ 'cmd_toggle_theme': 'Toggle light / dark',
74
+ 'cmd_copy_markdown': 'Copy this page as Markdown',
75
+ 'cmd_view_markdown': 'Open this page as Markdown',
76
+ 'cmd_edit': 'Edit this page',
77
+ 'cmd_print': 'Print this page',
78
+ 'cmd_top': 'Scroll to top',
79
+ 'cmd_home': 'Go to the home page',
80
+ 'cmd_language': 'Switch language: {name}',
81
+ 'cmd_version': 'Switch version: {name}',
82
+ 'cmd_llms': 'Open llms.txt',
83
+ 'whole_site': 'Whole site',
84
+ 'print_title': 'Printable edition',
85
+ 'contents': 'Contents',
86
+ },
87
+ 'zh': {
88
+ 'search': '搜索',
89
+ 'search_placeholder': '搜索文档或执行命令…',
90
+ 'search_no_results': '没有找到',
91
+ 'search_loading': '正在加载索引…',
92
+ 'search_hint_open': '打开',
93
+ 'search_hint_move': '移动',
94
+ 'search_hint_close': '关闭',
95
+ 'pages': '页面',
96
+ 'commands': '命令',
97
+ 'on_this_page': '本页目录',
98
+ 'back_to_top': '回到顶部',
99
+ 'previous': '上一篇',
100
+ 'next': '下一篇',
101
+ 'edit_page': '编辑此页',
102
+ 'view_source': '查看源码',
103
+ 'copy_markdown': '复制为 Markdown',
104
+ 'copied': '已复制',
105
+ 'view_markdown': '查看 Markdown',
106
+ 'open_in': '在 {name} 中打开',
107
+ 'print': '打印',
108
+ 'page_actions': '页面操作',
109
+ 'copy': '复制',
110
+ 'copy_code': '复制代码',
111
+ 'toggle_theme': '切换深色模式',
112
+ 'theme_light': '浅色',
113
+ 'theme_dark': '深色',
114
+ 'theme_auto': '跟随系统',
115
+ 'menu': '菜单',
116
+ 'close': '关闭',
117
+ 'navigation': '导航',
118
+ 'skip_to_content': '跳到正文',
119
+ 'last_updated': '最后更新',
120
+ 'reading_time': '约 {n} 分钟',
121
+ 'language': '语言',
122
+ 'version': '版本',
123
+ 'repository': '代码仓库',
124
+ 'keyboard_shortcuts': '键盘快捷键',
125
+ 'shortcut_search': '打开搜索与命令面板',
126
+ 'shortcut_theme': '切换深色模式',
127
+ 'shortcut_prev_next': '上一页 / 下一页',
128
+ 'shortcut_toc': '上一个 / 下一个标题',
129
+ 'shortcut_help': '显示本帮助',
130
+ 'built_with': '由 {mkdocs} 与 {sumi} 构建',
131
+ 'not_found_title': '页面不存在',
132
+ 'not_found_text': '你要找的页面不存在或已经移动。',
133
+ 'go_home': '返回首页',
134
+ 'blog': '博客',
135
+ 'all_posts': '全部文章',
136
+ 'tags': '标签',
137
+ 'posted_on': '发布于',
138
+ 'older_post': '更早的文章',
139
+ 'newer_post': '更新的文章',
140
+ 'subscribe_rss': '通过 RSS 订阅',
141
+ 'releases': '版本发布',
142
+ 'latest': '最新',
143
+ 'prerelease': '预发布',
144
+ 'archived': '已归档',
145
+ 'download': '下载',
146
+ 'asset': '文件',
147
+ 'size': '大小',
148
+ 'checksum': 'SHA-256',
149
+ 'release_notes': '发布说明',
150
+ 'old_version': '你正在阅读旧版本的文档。',
151
+ 'go_latest': '前往最新版本',
152
+ 'expand_all': '全部展开',
153
+ 'zoom_image': '放大图片',
154
+ 'cmd_toggle_theme': '切换浅色 / 深色',
155
+ 'cmd_copy_markdown': '复制本页 Markdown',
156
+ 'cmd_view_markdown': '以 Markdown 打开本页',
157
+ 'cmd_edit': '编辑本页',
158
+ 'cmd_print': '打印本页',
159
+ 'cmd_top': '回到顶部',
160
+ 'cmd_home': '回到首页',
161
+ 'cmd_language': '切换语言:{name}',
162
+ 'cmd_version': '切换版本:{name}',
163
+ 'cmd_llms': '打开 llms.txt',
164
+ 'whole_site': '全站',
165
+ 'print_title': '打印版',
166
+ 'contents': '目录',
167
+ },
168
+ }
169
+ STRINGS['zh_TW'] = {
170
+ **STRINGS['zh'],
171
+ 'search': '搜尋', 'search_placeholder': '搜尋文件或執行命令…', 'on_this_page': '本頁目錄',
172
+ 'previous': '上一篇', 'next': '下一篇', 'edit_page': '編輯此頁', 'copy_markdown': '複製為 Markdown',
173
+ 'copied': '已複製', 'view_markdown': '檢視 Markdown', 'print': '列印', 'copy': '複製',
174
+ 'copy_code': '複製程式碼', 'toggle_theme': '切換深色模式', 'navigation': '導覽', 'language': '語言',
175
+ 'version': '版本', 'last_updated': '最後更新', 'keyboard_shortcuts': '鍵盤快捷鍵',
176
+ 'not_found_title': '找不到頁面', 'blog': '部落格', 'tags': '標籤',
177
+ }
178
+
179
+
180
+ def translator(locale: str):
181
+ """Return `t(key, **kwargs)` for a locale such as `en`, `zh`, `zh_CN` or `zh-TW`."""
182
+ norm = (locale or 'en').replace('-', '_')
183
+ table = STRINGS.get(norm) or STRINGS.get(norm.split('_')[0]) or STRINGS['en']
184
+ if norm.lower() in ('zh_tw', 'zh_hk', 'zh_hant'):
185
+ table = STRINGS['zh_TW']
186
+ fallback = STRINGS['en']
187
+
188
+ def t(key: str, **kwargs: object) -> str:
189
+ text = table.get(key) or fallback.get(key) or key
190
+ return text.format(**kwargs) if kwargs else text
191
+
192
+ return t
mkdocs_sumi/icons.py ADDED
@@ -0,0 +1,52 @@
1
+ """Inline SVG icons: Lucide (ISC) for interface icons, Simple Icons (CC0) for brands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import json
7
+ from importlib import resources
8
+
9
+ from markupsafe import Markup, escape
10
+
11
+
12
+ @functools.cache
13
+ def _lucide() -> dict[str, str]:
14
+ return json.loads(resources.files(__package__).joinpath('vendor/icons/lucide.json').read_text('utf-8'))
15
+
16
+
17
+ @functools.cache
18
+ def _brands() -> dict[str, str]:
19
+ return json.loads(resources.files(__package__).joinpath('vendor/icons/brands.json').read_text('utf-8'))
20
+
21
+
22
+ def has_icon(name: str) -> bool:
23
+ name = name.removeprefix('lucide-')
24
+ if name.startswith('brand-'):
25
+ return name[6:] in _brands()
26
+ return name in _lucide()
27
+
28
+
29
+ def icon(name: str, cls: str = '', label: str | None = None) -> Markup:
30
+ """Return the icon as inline SVG, or an empty string for an unknown name.
31
+
32
+ `name` is a Lucide icon name (`rocket`, `book-open`) or `brand-<slug>` for
33
+ a Simple Icons brand (`brand-github`).
34
+ """
35
+ name = (name or '').removeprefix('lucide-')
36
+ aria = f' role="img" aria-label="{escape(label)}"' if label else ' aria-hidden="true"'
37
+ classes = f'sumi-icon {cls}'.strip()
38
+ if name.startswith('brand-'):
39
+ path = _brands().get(name[6:])
40
+ if path is None:
41
+ return Markup('')
42
+ return Markup(
43
+ f'<svg class="{classes}" viewBox="0 0 24 24" fill="currentColor"{aria}>'
44
+ f'<path d="{path}"/></svg>'
45
+ )
46
+ body = _lucide().get(name)
47
+ if body is None:
48
+ return Markup('')
49
+ return Markup(
50
+ f'<svg class="{classes}" viewBox="0 0 24 24" fill="none" stroke="currentColor" '
51
+ f'stroke-width="2" stroke-linecap="round" stroke-linejoin="round"{aria}>{body}</svg>'
52
+ )
mkdocs_sumi/landing.py ADDED
@@ -0,0 +1,84 @@
1
+ """A Markdown version of a landing page, for its `.md` copy and `llms.txt` readers.
2
+
3
+ A landing page keeps its content in front matter, so its Markdown body is
4
+ often empty. This turns the hero and sections into headings and lists.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Any
11
+
12
+ _TAG_RE = re.compile(r'<br\s*/?>|<[^>]+>')
13
+
14
+
15
+ def _text(value: Any) -> str:
16
+ text = _TAG_RE.sub(lambda m: ' ' if m.group(0).lower().startswith('<br') else '', str(value or ''))
17
+ return ' '.join(text.split())
18
+
19
+
20
+ def _link(text: str, link: Any) -> str:
21
+ return f'[{text}]({link})' if link else text
22
+
23
+
24
+ def _actions(items: Any) -> list[str]:
25
+ return [f'- {_link(_text(a.get("text")), a.get("link"))}' for a in items or [] if isinstance(a, dict) and a.get('text')]
26
+
27
+
28
+ def _code(code: Any) -> list[str]:
29
+ if not isinstance(code, dict) or not code.get('content'):
30
+ return []
31
+ body = str(code['content']).rstrip('\n')
32
+ fence = '`' * max(3, max((len(m) for m in re.findall(r'`+', body)), default=0) + 1)
33
+ title = f' title="{code["title"]}"' if code.get('title') else ''
34
+ return [f'{fence}{code.get("lang") or "text"}{title}', body, fence, '']
35
+
36
+
37
+ def landing_markdown(meta: dict[str, Any], body: str, h1: str | None) -> str:
38
+ """`h1` is the page's own heading; without one, MkDocs adds the page title."""
39
+ hero = meta.get('hero') or {}
40
+ out: list[str] = [f'# {h1}', ''] if h1 else []
41
+ if hero.get('title') and _text(hero['title']) != h1:
42
+ out += [f'**{_text(hero["title"])}**', '']
43
+ if hero.get('text'):
44
+ out += [f'> {_text(hero["text"])}', '']
45
+ if hero.get('install'):
46
+ out += ['```sh', str(hero['install']), '```', '']
47
+ out += _code(hero.get('code'))
48
+ if hero.get('actions'):
49
+ out += [*_actions(hero['actions']), '']
50
+ for s in meta.get('sections') or []:
51
+ if not isinstance(s, dict):
52
+ continue
53
+ kind = s.get('type') or 'content'
54
+ if kind == 'content':
55
+ if body.strip():
56
+ out += [body.strip(), '']
57
+ continue
58
+ if s.get('title'):
59
+ out += [f'## {_text(s["title"])}', '']
60
+ if s.get('text'):
61
+ out += [_text(s['text']), '']
62
+ items = s.get('items') or []
63
+ if kind == 'features':
64
+ for f in items:
65
+ name = _link(f'**{_text(f.get("title"))}**', f.get('link'))
66
+ out.append(f'- {name}: {_text(f.get("text"))}')
67
+ out.append('')
68
+ elif kind == 'stats':
69
+ out += [f'- **{_text(st.get("value"))}** {_text(st.get("label"))}' for st in items] + ['']
70
+ elif kind == 'steps':
71
+ out += [f'{n}. **{_text(st.get("title"))}**: {_text(st.get("text"))}' for n, st in enumerate(items, 1)] + ['']
72
+ elif kind == 'faq':
73
+ for f in items:
74
+ out += [f'### {_text(f.get("q"))}', '', str(f.get('a') or '').strip(), '']
75
+ elif kind == 'split':
76
+ out += [f'- {_text(b)}' for b in s.get('bullets') or []] + ([''] if s.get('bullets') else [])
77
+ out += _code(s.get('code'))
78
+ if s.get('markdown'):
79
+ out += [str(s['markdown']).strip(), '']
80
+ if s.get('actions'):
81
+ out += [*_actions(s['actions']), '']
82
+ if not any((s or {}).get('type', 'content') == 'content' for s in meta.get('sections') or []) and body.strip():
83
+ out += [body.strip(), '']
84
+ return '\n'.join(out).strip() + '\n'
File without changes
@@ -0,0 +1,47 @@
1
+ """Parse `{...}` attribute lines such as `{.steps}` or `{.fields meta="type default"}`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+
8
+ # A line that holds nothing but an attribute list.
9
+ ATTR_LINE_RE = re.compile(r'^\{:?\s*((?:[.#][\w-]+|[\w-]+=(?:"[^"]*"|\'[^\']*\'|[^\s}]+))(?:\s+(?:[.#][\w-]+|[\w-]+=(?:"[^"]*"|\'[^\']*\'|[^\s}]+)))*)\s*\}\s*$')
10
+
11
+ _TOKEN_RE = re.compile(r'([.#][\w-]+)|([\w-]+)=("[^"]*"|\'[^\']*\'|[^\s}]+)')
12
+
13
+
14
+ @dataclass
15
+ class Attrs:
16
+ id: str | None = None
17
+ classes: list[str] = field(default_factory=list)
18
+ values: dict[str, str] = field(default_factory=dict)
19
+
20
+ def has(self, cls: str) -> bool:
21
+ return cls in self.classes
22
+
23
+
24
+ def parse_attrs(text: str) -> Attrs:
25
+ """Parse the inside of an attribute list (without the braces)."""
26
+ attrs = Attrs()
27
+ for m in _TOKEN_RE.finditer(text):
28
+ if m.group(1):
29
+ token = m.group(1)
30
+ if token[0] == '.':
31
+ attrs.classes.append(token[1:])
32
+ else:
33
+ attrs.id = token[1:]
34
+ else:
35
+ value = m.group(3)
36
+ if value[:1] in ('"', "'"):
37
+ value = value[1:-1]
38
+ attrs.values[m.group(2)] = value
39
+ return attrs
40
+
41
+
42
+ def match_attr_line(text: str) -> Attrs | None:
43
+ """Return the parsed attributes if `text` is a lone attribute list."""
44
+ m = ATTR_LINE_RE.match(text.strip())
45
+ if not m:
46
+ return None
47
+ return parse_attrs(m.group(1))