feat(editor): make code snippets readable and syntax highlighted

Closes #90.

Code blocks and inline code inherited the typography plugin's defaults,
which shrank them to ~86% of an already-monospace body and painted them
--bg-surface — the same colour as the default note card — so a snippet
was only recognisable by being smaller. Long lines also grew a horizontal
scrollbar inside card previews.

- New --code-bg surface (one step below the page in light mode, page-level
  ink in dark) with a subtle border, and code keeps the surrounding text
  size. Decorative backtick pseudo-elements dropped now that the chip
  carries the signal.
- Card previews clip wide code instead of scrolling; editor and share page
  keep horizontal scroll.
- Syntax highlighting via one shared lowlight instance feeding both the
  TipTap editor (extension-code-block-lowlight) and the markdown-it
  renderer, so a fence looks identical in the editor, on cards and on the
  share page. Only highlight.js "common" grammars are bundled.
- No auto-detection: highlight.js misreads note-sized snippets too often
  (JavaScript as CSS, SQL as VB.NET). Unlabelled fences stay plain; the
  language comes from the fence info string or a toolbar picker shown
  while the cursor is in a code block.
- Six --hl-* token colours per theme; a unit test parses app.css and
  asserts AA contrast against --code-bg and mutual distinguishability.
- Sanitizer now allows only language-* / hljs-* classes on code and span.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014u5WimmZvshm9RJSd1c6wx
This commit is contained in:
Claude
2026-09-17 21:42:35 +02:00
committed by Fred Cabassut
parent 7bf06edd97
commit 58e5b5dd66
14 changed files with 698 additions and 6 deletions
+63
View File
@@ -27,6 +27,9 @@ importers:
'@tiptap/core':
specifier: ^3.31.0
version: 3.31.0(@tiptap/pm@3.31.0)
'@tiptap/extension-code-block-lowlight':
specifier: ^3.31.0
version: 3.31.0(@tiptap/core@3.31.0(@tiptap/pm@3.31.0))(@tiptap/extension-code-block@3.31.0(@tiptap/core@3.31.0(@tiptap/pm@3.31.0))(@tiptap/pm@3.31.0))(@tiptap/pm@3.31.0)(highlight.js@11.11.2)(lowlight@3.3.0)
'@tiptap/extension-link':
specifier: ^3.31.0
version: 3.31.0(@tiptap/core@3.31.0(@tiptap/pm@3.31.0))(@tiptap/pm@3.31.0)
@@ -78,6 +81,9 @@ importers:
idb:
specifier: ^8.0.3
version: 8.0.3
lowlight:
specifier: ^3.3.0
version: 3.3.0
lucide-svelte:
specifier: ^0.577.0
version: 0.577.0(svelte@5.57.0)
@@ -1563,6 +1569,15 @@ packages:
peerDependencies:
'@tiptap/extension-list': 3.31.0
'@tiptap/extension-code-block-lowlight@3.31.0':
resolution: {integrity: sha512-fFwXqwh0CwJkQzdqgv4ObcS70m0Muq1RbAZUTQNZFqCDcu1krDJW0PWNmua7+nZdmXwOyA2PDUTAtWYFQ+srpw==}
peerDependencies:
'@tiptap/core': 3.31.0
'@tiptap/extension-code-block': 3.31.0
'@tiptap/pm': 3.31.0
highlight.js: ^11
lowlight: ^2 || ^3
'@tiptap/extension-code-block@3.31.0':
resolution: {integrity: sha512-y/A6N96gBPjEHqVYZ3u/yO/Anyo92iDWgYqShMXAvHprn2Ipmn32wIkX05Kv983fa6j6ySXF7A1nJdHcGCl5aQ==}
peerDependencies:
@@ -1731,6 +1746,9 @@ packages:
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/hast@3.0.5':
resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==}
'@types/linkify-it@3.0.5':
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
@@ -1764,6 +1782,9 @@ packages:
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/zxcvbn@4.4.5':
resolution: {integrity: sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==}
@@ -2183,6 +2204,10 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -2190,6 +2215,9 @@ packages:
devalue@5.9.2:
resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@@ -2664,6 +2692,10 @@ packages:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
highlight.js@11.11.2:
resolution: {integrity: sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ==}
engines: {node: '>=12.0.0'}
hono@4.13.5:
resolution: {integrity: sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==}
engines: {node: '>=16.9.0'}
@@ -3073,6 +3105,9 @@ packages:
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
lowlight@3.3.0:
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -5560,6 +5595,14 @@ snapshots:
dependencies:
'@tiptap/extension-list': 3.31.0(@tiptap/core@3.31.0(@tiptap/pm@3.31.0))(@tiptap/pm@3.31.0)
'@tiptap/extension-code-block-lowlight@3.31.0(@tiptap/core@3.31.0(@tiptap/pm@3.31.0))(@tiptap/extension-code-block@3.31.0(@tiptap/core@3.31.0(@tiptap/pm@3.31.0))(@tiptap/pm@3.31.0))(@tiptap/pm@3.31.0)(highlight.js@11.11.2)(lowlight@3.3.0)':
dependencies:
'@tiptap/core': 3.31.0(@tiptap/pm@3.31.0)
'@tiptap/extension-code-block': 3.31.0(@tiptap/core@3.31.0(@tiptap/pm@3.31.0))(@tiptap/pm@3.31.0)
'@tiptap/pm': 3.31.0
highlight.js: 11.11.2
lowlight: 3.3.0
'@tiptap/extension-code-block@3.31.0(@tiptap/core@3.31.0(@tiptap/pm@3.31.0))(@tiptap/pm@3.31.0)':
dependencies:
'@tiptap/core': 3.31.0(@tiptap/pm@3.31.0)
@@ -5739,6 +5782,10 @@ snapshots:
'@types/estree@1.0.9': {}
'@types/hast@3.0.5':
dependencies:
'@types/unist': 3.0.3
'@types/linkify-it@3.0.5': {}
'@types/linkify-it@5.0.0': {}
@@ -5773,6 +5820,8 @@ snapshots:
'@types/trusted-types@2.0.7': {}
'@types/unist@3.0.3': {}
'@types/zxcvbn@4.4.5': {}
'@vite-pwa/sveltekit@1.1.0(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@26.4.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0)))(svelte@5.57.0)(typescript@5.9.3)(vite@7.3.6(@types/node@26.4.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0)))(vite@7.3.6(@types/node@26.4.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0))(workbox-build@7.4.0)(workbox-window@7.4.1)':
@@ -6213,10 +6262,16 @@ snapshots:
depd@2.0.0: {}
dequal@2.0.3: {}
detect-libc@2.1.2: {}
devalue@5.9.2: {}
devlop@1.1.0:
dependencies:
dequal: 2.0.3
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
@@ -6726,6 +6781,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
highlight.js@11.11.2: {}
hono@4.13.5: {}
html-encoding-sniffer@6.0.0:
@@ -7129,6 +7186,12 @@ snapshots:
loupe@3.2.1: {}
lowlight@3.3.0:
dependencies:
'@types/hast': 3.0.5
devlop: 1.1.0
highlight.js: 11.11.2
lru-cache@10.4.3: {}
lru-cache@11.5.2: {}