Coverage for /builds/svesch/cikit/src/cikit/sphinx_ext/hash.py: 100%

28 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-09 14:17 +0000

1# SPDX-License-Identifier: GPL-3.0-or-later 

2"""Sphinx role for compact and copyable SHA-256 hashes. 

3 

4The ``cikit-hash`` role renders a shortened SHA-256 value in HTML while keeping 

5the complete digest available as a tooltip. Clicking the rendered value copies 

6the full digest to the clipboard; non-HTML builders receive the complete hash. 

7 

8Author: 

9 Sven Schlender 

10Date: 

11 2026-09-25 

12""" 

13 

14from html import escape 

15import re 

16 

17from docutils import nodes 

18from sphinx import addnodes 

19from sphinx.errors import SphinxError 

20from sphinx.util.docutils import SphinxRole 

21 

22 

23HASH_PREVIEW_LENGTH = 12 

24SHA256_PATTERN = re.compile(r"^[0-9a-fA-F]{64}$") 

25 

26COPY_SCRIPT = """ 

27(() => { 

28 const selector = ".cikit-hash[data-hash]"; 

29 

30 const copyHash = (element) => { 

31 const value = element.dataset.hash; 

32 if (!value || !navigator.clipboard || !navigator.clipboard.writeText) { 

33 return; 

34 } 

35 navigator.clipboard.writeText(value); 

36 }; 

37 

38 document.addEventListener("click", (event) => { 

39 const target = event.target.closest?.(selector); 

40 if (target) { 

41 copyHash(target); 

42 } 

43 }); 

44 

45 document.addEventListener("keydown", (event) => { 

46 const target = event.target.closest?.(selector); 

47 if (target && (event.key === "Enter" || event.key === " ")) { 

48 event.preventDefault(); 

49 copyHash(target); 

50 } 

51 }); 

52})(); 

53""".strip() 

54 

55 

56class CikitHashRole(SphinxRole): 

57 """Render a SHA-256 digest as a compact interactive inline value. 

58 

59 HTML output contains a shortened digest with the complete value stored in 

60 the tooltip and a data attribute used by the clipboard helper. Non-HTML 

61 output receives the complete digest so no information is lost. 

62 """ 

63 

64 def run(self): 

65 """Validate and render the SHA-256 value supplied to the role. 

66 

67 The digest is normalized to lowercase before rendering. Invalid values 

68 fail the Sphinx build instead of silently producing a misleading hash 

69 representation in generated reports. 

70 

71 Returns: 

72 Tuple containing builder-specific nodes and system messages. 

73 

74 Raises: 

75 SphinxError: If the role content is not a complete SHA-256 digest. 

76 """ 

77 value = self.text.strip() 

78 if not SHA256_PATTERN.fullmatch(value): 

79 raise SphinxError("Invalid cikit hash, expected a 64-character SHA-256 digest.") 

80 

81 value = value.lower() 

82 preview = f"{value[:HASH_PREVIEW_LENGTH]}…" 

83 safe_value = escape(value, quote=True) 

84 safe_preview = escape(preview) 

85 

86 html = (f'<code class="cikit-hash" data-hash="{safe_value}" ' 

87 f'title="SHA-256: {safe_value}" role="button" tabindex="0" ' 

88 f'style="cursor: copy;">{safe_preview}</code>') 

89 

90 html_node = addnodes.only(expr="html") 

91 html_node += nodes.raw("", html, format="html") 

92 

93 fallback_node = addnodes.only(expr="not html") 

94 fallback_node += nodes.literal(self.rawtext, value) 

95 

96 return [html_node, fallback_node], [] 

97 

98 

99def setup(app): 

100 """Register the cikit SHA-256 role and clipboard helper with Sphinx. 

101 

102 The role is available as ``cikit-hash``. Clipboard support is injected as 

103 inline JavaScript through Sphinx, avoiding additional static package files 

104 and keeping the extension self-contained. 

105 

106 Args: 

107 app: Active Sphinx application. 

108 

109 Returns: 

110 Sphinx extension metadata. 

111 """ 

112 app.add_role("cikit-hash", CikitHashRole()) 

113 app.add_js_file(None, body=COPY_SCRIPT) 

114 

115 return {"version": "1.0", "parallel_read_safe": True, "parallel_write_safe": True}