feat(markdown): render TikZ fenced blocks to SVG

Adds a markdown-it extractor for ```tikz blocks that emits a sanitized
placeholder, plus a runTikz() that lazy-loads the obsidian-tikzjax 0.5.2
bundle from jsdelivr on first encounter, sanitizes the produced SVG with
DOMPurify, and caches it in PouchDB keyed by sha256(source) so repeat
views are instant and offline-capable.
This commit is contained in:
Julien Calixte
2026-05-15 15:38:48 +02:00
parent bfd981de13
commit bf19e982fe
10 changed files with 265 additions and 178 deletions

186
pnpm-lock.yaml generated
View File

@@ -68,6 +68,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
dompurify:
specifier: ^3.4.3
version: 3.4.3
events:
specifier: ^3.3.0
version: 3.3.0
@@ -200,7 +203,7 @@ importers:
version: 12.1.1(eslint@8.57.1)
eslint-plugin-unused-imports:
specifier: ^4.4.1
version: 4.4.1(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)
version: 4.4.1(eslint@8.57.1)
esno:
specifier: ^4.8.0
version: 4.8.0
@@ -2337,97 +2340,39 @@ packages:
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@typescript-eslint/eslint-plugin@8.46.2':
resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.46.2
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.46.2':
resolution: {integrity: sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.38.0':
resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
'@typescript-eslint/project-service@8.46.2':
resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.38.0':
resolution: {integrity: sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.46.2':
resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.38.0':
resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
'@typescript-eslint/tsconfig-utils@8.46.2':
resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.46.2':
resolution: {integrity: sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/types@8.38.0':
resolution: {integrity: sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.46.2':
resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.38.0':
resolution: {integrity: sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
'@typescript-eslint/typescript-estree@8.46.2':
resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.46.2':
resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.38.0':
resolution: {integrity: sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.46.2':
resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@vite-pwa/assets-generator@1.0.2':
resolution: {integrity: sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==}
@@ -3447,6 +3392,9 @@ packages:
dompurify@3.2.6:
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
dompurify@3.4.3:
resolution: {integrity: sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==}
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
@@ -4067,10 +4015,6 @@ packages:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
ignore@7.0.4:
resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==}
engines: {node: '>= 4'}
immutable@5.1.5:
resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
@@ -8688,37 +8632,6 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
'@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.46.2
'@typescript-eslint/type-utils': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.46.2
eslint: 8.57.1
graphemer: 1.4.0
ignore: 7.0.4
natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
optional: true
'@typescript-eslint/parser@8.46.2(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.46.2
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.46.2
debug: 4.4.3
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
optional: true
'@typescript-eslint/project-service@8.38.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.9.3)
@@ -8728,54 +8641,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.46.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
'@typescript-eslint/types': 8.46.2
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
optional: true
'@typescript-eslint/scope-manager@8.38.0':
dependencies:
'@typescript-eslint/types': 8.38.0
'@typescript-eslint/visitor-keys': 8.38.0
'@typescript-eslint/scope-manager@8.46.2':
dependencies:
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/visitor-keys': 8.46.2
optional: true
'@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
optional: true
'@typescript-eslint/type-utils@8.46.2(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
'@typescript-eslint/utils': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.3
eslint: 8.57.1
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
optional: true
'@typescript-eslint/types@8.38.0': {}
'@typescript-eslint/types@8.46.2':
optional: true
'@typescript-eslint/typescript-estree@8.38.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.38.0(typescript@5.9.3)
@@ -8792,46 +8668,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.46.2(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/visitor-keys': 8.46.2
debug: 4.4.3
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.6.3
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
optional: true
'@typescript-eslint/utils@8.46.2(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.46.2
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
optional: true
'@typescript-eslint/visitor-keys@8.38.0':
dependencies:
'@typescript-eslint/types': 8.38.0
eslint-visitor-keys: 4.2.1
'@typescript-eslint/visitor-keys@8.46.2':
dependencies:
'@typescript-eslint/types': 8.46.2
eslint-visitor-keys: 4.2.1
optional: true
'@ungap/structured-clone@1.2.0': {}
'@vite-pwa/assets-generator@1.0.2':
@@ -10005,6 +9846,10 @@ snapshots:
optionalDependencies:
'@types/trusted-types': 2.0.7
dompurify@3.4.3:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@3.1.0:
dependencies:
dom-serializer: 2.0.0
@@ -10192,11 +10037,9 @@ snapshots:
dependencies:
eslint: 8.57.1
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1):
eslint-plugin-unused-imports@4.4.1(eslint@8.57.1):
dependencies:
eslint: 8.57.1
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
eslint-scope@7.2.2:
dependencies:
@@ -10768,9 +10611,6 @@ snapshots:
ignore@5.2.4: {}
ignore@7.0.4:
optional: true
immutable@5.1.5: {}
import-fresh@3.3.0: