keys.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. """
  2. Keys.
  3. pymdownx.keys
  4. Markdown extension for keystroke (user keyboard input) formatting.
  5. It wraps the syntax `++key+key+key++` (for individual keystrokes with modifiers)
  6. or `++"string"++` (for continuous keyboard input) into HTML `<kbd>` elements.
  7. If a key is found in the extension's database, its `<kbd>` element gets a matching class.
  8. Common synonyms are included, e.g. `++pg-up++` will match as `++page-up++`.
  9. ## Config
  10. If `strict` is `True`, the entire series of keystrokes is wrapped into an outer`<kbd>` element, and then,
  11. each keystroke is wrapped into a separate inner `<kbd>` element, which matches the HTML5 spec.
  12. If `strict` is `False`, an outer `<span>` is used, which matches the practice on Github or StackOverflow.
  13. The resulting `<kbd>` elements are separated by `separator` (`+` by default, can be `''` or something else).
  14. If `camel_case` is `True`, `++PageUp++` will match the same as `++page-up++`.
  15. The database can be extended or modified with the `key_map` dict.
  16. ## Examples
  17. ### Input
  18. ```
  19. Press ++Shift+Alt+PgUp++, type in ++"Hello"++ and press ++Enter++.
  20. ```
  21. ### Config 1
  22. ```
  23. pymdownx.keys:
  24. camel_case: true
  25. strict: false
  26. separator: '+'
  27. ```
  28. ### Output 1
  29. ```
  30. <p>Press <span class="keys"><kbd class="key-shift">Shift</kbd><span>+</span><kbd
  31. class="key-alt">Alt</kbd><span>+</span><kbd class="key-page-up">Page Up</kbd></span>, type in <span
  32. class="keys"><kbd>Hello</kbd></span> and press <span class="keys"><kbd class="key-enter">Enter</kbd></span>.</p>
  33. ```
  34. ### Config 2
  35. ```
  36. pymdownx.keys:
  37. camel_case: true
  38. strict: true
  39. separator: ''
  40. ```
  41. ### Output 2
  42. ```
  43. <p>Press <kbd class="keys"><kbd class="key-shift">Shift</kbd><kbd class="key-alt">Alt</kbd><kbd
  44. class="key-page-up">Page Up</kbd></kbd>, type in <kbd class="keys"><kbd>Hello</kbd></kbd> and press <kbd
  45. class="keys"><kbd class="key-enter">Enter</kbd></kbd>.</p>
  46. ```
  47. Idea by Adam Twardoch and coded by Isaac Muse.
  48. Copyright (c) 2017 Isaac Muse <isaacmuse@gmail.com>
  49. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  50. documentation files (the "Software"), to deal in the Software without restriction, including without limitation
  51. the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
  52. and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  53. The above copyright notice and this permission notice shall be included in all copies or substantial portions
  54. of the Software.
  55. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
  56. TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  57. THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  58. CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  59. DEALINGS IN THE SOFTWARE.
  60. """
  61. import html
  62. from markdown import Extension
  63. from markdown.inlinepatterns import InlineProcessor
  64. from markdown import util as md_util
  65. import xml.etree.ElementTree as etree
  66. from . import util
  67. from . import keymap_db as keymap
  68. import re
  69. RE_KBD = r'''(?x)
  70. (?:
  71. # Escape
  72. (?<!\\)(?P<escapes>(?:\\{2})+)(?=\+)|
  73. # Key
  74. (?<!\\)\+{2}
  75. (
  76. (?:(?:[\w\-]+|"(?:\\.|[^"])+"|\'(?:\\.|[^\'])+\')\+)*?
  77. (?:[\w\-]+|"(?:\\.|[^"])+"|\'(?:\\.|[^\'])+\')
  78. )
  79. \+{2}
  80. )
  81. '''
  82. ESCAPE_RE = re.compile(r'''(?<!\\)(?:\\\\)*\\(.)''')
  83. UNESCAPED_PLUS = re.compile(r'''(?<!\\)(?:\\\\)*(\+)''')
  84. ESCAPED_BSLASH = '%s%s%s' % (md_util.STX, ord('\\'), md_util.ETX)
  85. DOUBLE_BSLASH = '\\\\'
  86. class KeysPattern(InlineProcessor):
  87. """Return kbd tag."""
  88. def __init__(self, pattern, config, md):
  89. """Initialize."""
  90. self.ksep = config['separator']
  91. self.strict = config['strict']
  92. self.classes = config['class'].split(' ')
  93. self.map = self.merge(keymap.keymap, config['key_map'])
  94. self.aliases = keymap.aliases
  95. self.camel = config['camel_case']
  96. super(KeysPattern, self).__init__(pattern, md)
  97. def merge(self, x, y):
  98. """Given two dicts, merge them into a new dict."""
  99. z = x.copy()
  100. z.update(y)
  101. return z
  102. def normalize(self, key):
  103. """Normalize the value."""
  104. if not self.camel:
  105. return key
  106. norm_key = []
  107. last = ''
  108. for c in key:
  109. if c.isupper():
  110. if not last or last == '-':
  111. norm_key.append(c.lower())
  112. else:
  113. norm_key.extend(['-', c.lower()])
  114. else:
  115. norm_key.append(c)
  116. last = c
  117. return ''.join(norm_key)
  118. def process_key(self, key):
  119. """Process key."""
  120. if key.startswith(('"', "'")):
  121. value = (None, html.unescape(ESCAPE_RE.sub(r'\1', key[1:-1])).strip())
  122. else:
  123. norm_key = self.normalize(key)
  124. canonical_key = self.aliases.get(norm_key, norm_key)
  125. name = self.map.get(canonical_key, None)
  126. value = (canonical_key, name) if name else None
  127. return value
  128. def handleMatch(self, m, data):
  129. """Handle kbd pattern matches."""
  130. if m.group(1):
  131. return m.group('escapes').replace(DOUBLE_BSLASH, ESCAPED_BSLASH), m.start(0), m.end(0)
  132. content = [self.process_key(key) for key in UNESCAPED_PLUS.split(m.group(2)) if key != '+']
  133. if None in content:
  134. return None, None, None
  135. el = etree.Element(
  136. ('kbd' if self.strict else 'span'),
  137. ({'class': ' '.join(self.classes)} if self.classes else {})
  138. )
  139. last = None
  140. for item_class, item_name in content:
  141. classes = []
  142. if item_class:
  143. classes.append('key-' + item_class)
  144. if last is not None and self.ksep:
  145. span = etree.SubElement(el, 'span')
  146. span.text = md_util.AtomicString(self.ksep)
  147. attr = {}
  148. if classes:
  149. attr['class'] = ' '.join(classes)
  150. kbd = etree.SubElement(el, 'kbd', attr)
  151. kbd.text = md_util.AtomicString(item_name)
  152. last = kbd
  153. return el, m.start(0), m.end(0)
  154. class KeysExtension(Extension):
  155. """Add `keys`` extension to Markdown class."""
  156. def __init__(self, *args, **kwargs):
  157. """Initialize."""
  158. self.config = {
  159. 'separator': ['+', "Provide a keyboard separator - Default: \"+\""],
  160. 'strict': [False, "Format keys and menus according to HTML5 spec - Default: False"],
  161. 'class': ['keys', "Provide class(es) for the kbd elements - Default: \"keys\""],
  162. 'camel_case': [False, 'Allow camelCase conversion for key names PgDn -> pg-dn - Default: False'],
  163. 'key_map': [{}, 'Additional keys to include or keys to override - Default: {}']
  164. }
  165. super(KeysExtension, self).__init__(*args, **kwargs)
  166. def extendMarkdown(self, md):
  167. """Add support for keys."""
  168. util.escape_chars(md, ['+'])
  169. md.inlinePatterns.register(KeysPattern(RE_KBD, self.getConfigs(), md), "keys", 185)
  170. def makeExtension(*args, **kwargs):
  171. """Return extension."""
  172. return KeysExtension(*args, **kwargs)