shortcodes.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. # Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
  2. # Permission is hereby granted, free of charge, to any person obtaining a copy
  3. # of this software and associated documentation files (the "Software"), to
  4. # deal in the Software without restriction, including without limitation the
  5. # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  6. # sell copies of the Software, and to permit persons to whom the Software is
  7. # furnished to do so, subject to the following conditions:
  8. # The above copyright notice and this permission notice shall be included in
  9. # all copies or substantial portions of the Software.
  10. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  11. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  12. # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
  13. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  14. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  15. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  16. # IN THE SOFTWARE.
  17. from __future__ import annotations
  18. import posixpath
  19. import re
  20. from mkdocs.config.defaults import MkDocsConfig
  21. from mkdocs.structure.files import File, Files
  22. from mkdocs.structure.pages import Page
  23. from re import Match
  24. # -----------------------------------------------------------------------------
  25. # Hooks
  26. # -----------------------------------------------------------------------------
  27. # @todo
  28. def on_page_markdown(
  29. markdown: str, *, page: Page, config: MkDocsConfig, files: Files
  30. ):
  31. # Replace callback
  32. def replace(match: Match):
  33. type, args = match.groups()
  34. args = args.strip()
  35. if type == "version":
  36. if args.startswith("insiders-"):
  37. return _badge_for_version_insiders(args, page, files)
  38. else:
  39. return _badge_for_version(args, page, files)
  40. elif type == "sponsors": return _badge_for_sponsors(page, files)
  41. elif type == "flag": return flag(args, page, files)
  42. elif type == "option": return option(args)
  43. elif type == "setting": return setting(args)
  44. elif type == "feature": return _badge_for_feature(args, page, files)
  45. elif type == "plugin": return _badge_for_plugin(args, page, files)
  46. elif type == "extension": return _badge_for_extension(args, page, files)
  47. elif type == "utility": return _badge_for_utility(args, page, files)
  48. elif type == "example": return _badge_for_example(args, page, files)
  49. elif type == "default":
  50. if args == "none": return _badge_for_default_none(page, files)
  51. elif args == "computed": return _badge_for_default_computed(page, files)
  52. else: return _badge_for_default(args, page, files)
  53. # Otherwise, raise an error
  54. raise RuntimeError(f"Unknown shortcode: {type}")
  55. # Find and replace all external asset URLs in current page
  56. return re.sub(
  57. r"<!-- md:(\w+)(.*?) -->",
  58. replace, markdown, flags = re.I | re.M
  59. )
  60. # -----------------------------------------------------------------------------
  61. # Helper functions
  62. # -----------------------------------------------------------------------------
  63. # Create a flag of a specific type
  64. def flag(args: str, page: Page, files: Files):
  65. type, *_ = args.split(" ", 1)
  66. if type == "experimental": return _badge_for_experimental(page, files)
  67. elif type == "required": return _badge_for_required(page, files)
  68. elif type == "customization": return _badge_for_customization(page, files)
  69. elif type == "metadata": return _badge_for_metadata(page, files)
  70. elif type == "multiple": return _badge_for_multiple(page, files)
  71. raise RuntimeError(f"Unknown type: {type}")
  72. # Create a linkable option
  73. def option(type: str):
  74. _, *_, name = re.split(r"[.:]", type)
  75. return f"[`{name}`](#+{type}){{ #+{type} }}\n\n"
  76. # Create a linkable setting - @todo append them to the bottom of the page
  77. def setting(type: str):
  78. _, *_, name = re.split(r"[.*]", type)
  79. return f"`{name}` {{ #{type} }}\n\n[{type}]: #{type}\n\n"
  80. # -----------------------------------------------------------------------------
  81. # Resolve path of file relative to given page - the posixpath always includes
  82. # one additional level of `..` which we need to remove
  83. def _resolve_path(path: str, page: Page, files: Files):
  84. path, anchor, *_ = f"{path}#".split("#")
  85. path = _resolve(files.get_file_from_path(path), page)
  86. return "#".join([path, anchor]) if anchor else path
  87. # Resolve path of file relative to given page - the posixpath always includes
  88. # one additional level of `..` which we need to remove
  89. def _resolve(file: File, page: Page):
  90. path = posixpath.relpath(file.src_uri, page.file.src_uri)
  91. return posixpath.sep.join(path.split(posixpath.sep)[1:])
  92. # -----------------------------------------------------------------------------
  93. # Create badge
  94. def _badge(icon: str, text: str = "", type: str = ""):
  95. classes = f"mdx-badge mdx-badge--{type}" if type else "mdx-badge"
  96. return "".join([
  97. f"<span class=\"{classes}\">",
  98. *([f"<span class=\"mdx-badge__icon\">{icon}</span>"] if icon else []),
  99. *([f"<span class=\"mdx-badge__text\">{text}</span>"] if text else []),
  100. f"</span>",
  101. ])
  102. # Create sponsors badge
  103. def _badge_for_sponsors(page: Page, files: Files):
  104. icon = "material-heart"
  105. href = _resolve_path("insiders/index.md", page, files)
  106. return _badge(
  107. icon = f"[:{icon}:]({href} 'Sponsors only')",
  108. type = "heart"
  109. )
  110. # Create badge for version
  111. def _badge_for_version(text: str, page: Page, files: Files):
  112. spec = text
  113. path = f"changelog/index.md#{spec}"
  114. # Return badge
  115. icon = "material-tag-outline"
  116. href = _resolve_path("conventions.md#version", page, files)
  117. return _badge(
  118. icon = f"[:{icon}:]({href} 'Minimum version')",
  119. text = f"[{text}]({_resolve_path(path, page, files)})" if spec else ""
  120. )
  121. # Create badge for version of Insiders
  122. def _badge_for_version_insiders(text: str, page: Page, files: Files):
  123. spec = text.replace("insiders-", "")
  124. path = f"insiders/changelog/index.md#{spec}"
  125. # Return badge
  126. icon = "material-tag-heart-outline"
  127. href = _resolve_path("conventions.md#version-insiders", page, files)
  128. return _badge(
  129. icon = f"[:{icon}:]({href} 'Minimum version')",
  130. text = f"[{text}]({_resolve_path(path, page, files)})" if spec else ""
  131. )
  132. # Create badge for feature
  133. def _badge_for_feature(text: str, page: Page, files: Files):
  134. icon = "material-toggle-switch"
  135. href = _resolve_path("conventions.md#feature", page, files)
  136. return _badge(
  137. icon = f"[:{icon}:]({href} 'Optional feature')",
  138. text = text
  139. )
  140. # Create badge for plugin
  141. def _badge_for_plugin(text: str, page: Page, files: Files):
  142. icon = "material-floppy"
  143. href = _resolve_path("conventions.md#plugin", page, files)
  144. return _badge(
  145. icon = f"[:{icon}:]({href} 'Plugin')",
  146. text = text
  147. )
  148. # Create badge for extension
  149. def _badge_for_extension(text: str, page: Page, files: Files):
  150. icon = "material-language-markdown"
  151. href = _resolve_path("conventions.md#extension", page, files)
  152. return _badge(
  153. icon = f"[:{icon}:]({href} 'Markdown extension')",
  154. text = text
  155. )
  156. # Create badge for utility
  157. def _badge_for_utility(text: str, page: Page, files: Files):
  158. icon = "material-package-variant"
  159. href = _resolve_path("conventions.md#utility", page, files)
  160. return _badge(
  161. icon = f"[:{icon}:]({href} 'Third-party utility')",
  162. text = text
  163. )
  164. # Create badge for example
  165. def _badge_for_example(text: str, page: Page, files: Files):
  166. return "\n".join([
  167. _badge_for_example_download(text, page, files),
  168. _badge_for_example_view(text, page, files)
  169. ])
  170. # Create badge for example view
  171. def _badge_for_example_view(text: str, page: Page, files: Files):
  172. icon = "material-folder-eye"
  173. href = f"https://mkdocs-material.github.io/examples/{text}/"
  174. return _badge(
  175. icon = f"[:{icon}:]({href} 'View example')",
  176. type = "right"
  177. )
  178. # Create badge for example download
  179. def _badge_for_example_download(text: str, page: Page, files: Files):
  180. icon = "material-folder-download"
  181. href = f"https://mkdocs-material.github.io/examples/{text}.zip"
  182. return _badge(
  183. icon = f"[:{icon}:]({href} 'Download example')",
  184. text = f"[`.zip`]({href})",
  185. type = "right"
  186. )
  187. # Create badge for default value
  188. def _badge_for_default(text: str, page: Page, files: Files):
  189. icon = "material-water"
  190. href = _resolve_path("conventions.md#default", page, files)
  191. return _badge(
  192. icon = f"[:{icon}:]({href} 'Default value')",
  193. text = text
  194. )
  195. # Create badge for empty default value
  196. def _badge_for_default_none(page: Page, files: Files):
  197. icon = "material-water-outline"
  198. href = _resolve_path("conventions.md#default", page, files)
  199. return _badge(
  200. icon = f"[:{icon}:]({href} 'Default value is empty')"
  201. )
  202. # Create badge for computed default value
  203. def _badge_for_default_computed(page: Page, files: Files):
  204. icon = "material-water-check"
  205. href = _resolve_path("conventions.md#default", page, files)
  206. return _badge(
  207. icon = f"[:{icon}:]({href} 'Default value is computed')"
  208. )
  209. # Create badge for metadata property flag
  210. def _badge_for_metadata(page: Page, files: Files):
  211. icon = "material-list-box-outline"
  212. href = _resolve_path("conventions.md#metadata", page, files)
  213. return _badge(
  214. icon = f"[:{icon}:]({href} 'Metadata property')"
  215. )
  216. # Create badge for required value flag
  217. def _badge_for_required(page: Page, files: Files):
  218. icon = "material-alert"
  219. href = _resolve_path("conventions.md#required", page, files)
  220. return _badge(
  221. icon = f"[:{icon}:]({href} 'Required value')"
  222. )
  223. # Create badge for customization flag
  224. def _badge_for_customization(page: Page, files: Files):
  225. icon = "material-brush-variant"
  226. href = _resolve_path("conventions.md#customization", page, files)
  227. return _badge(
  228. icon = f"[:{icon}:]({href} 'Customization')"
  229. )
  230. # Create badge for multiple instance flag
  231. def _badge_for_multiple(page: Page, files: Files):
  232. icon = "material-inbox-multiple"
  233. href = _resolve_path("conventions.md#multiple-instances", page, files)
  234. return _badge(
  235. icon = f"[:{icon}:]({href} 'Multiple instances')"
  236. )
  237. # Create badge for experimental flag
  238. def _badge_for_experimental(page: Page, files: Files):
  239. icon = "material-flask-outline"
  240. href = _resolve_path("conventions.md#experimental", page, files)
  241. return _badge(
  242. icon = f"[:{icon}:]({href} 'Experimental')"
  243. )