dsh-oc-tui 0.2.0-pre.13 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,6 +43,14 @@ export function loadMermaidEngine() {
43
43
  return enginePromise
44
44
  }
45
45
 
46
+ // One entry per (engine, source). The layout depends on the source alone, but
47
+ // the transcript's per-block line cache is keyed by revision and width — so a
48
+ // resize, or any edit above the fence, re-paints every diagram in the
49
+ // transcript. Without this cache each of those repaints re-runs the layout for
50
+ // every fence on screen.
51
+ const ART_CACHE = new WeakMap()
52
+ const ART_CACHE_MAX = 64
53
+
46
54
  /**
47
55
  * Lay one mermaid source out as box-drawing art.
48
56
  * @param {{render: Function}|null} engine - from {@link loadMermaidEngine}.
@@ -52,7 +60,18 @@ export function loadMermaidEngine() {
52
60
  */
53
61
  export function renderMermaidArt(engine, src) {
54
62
  if (!engine || typeof src !== 'string' || src.trim() === '') return null
55
- return engine.render(emojiWideSource(engine, src))
63
+ let bySource = ART_CACHE.get(engine)
64
+ if (!bySource) {
65
+ bySource = new Map()
66
+ ART_CACHE.set(engine, bySource)
67
+ }
68
+ if (bySource.has(src)) return bySource.get(src)
69
+ // A null answer is cached too: a grammar the engine cannot draw would
70
+ // otherwise be re-parsed on every repaint, only to fail the same way.
71
+ const art = engine.render(emojiWideSource(engine, src))
72
+ if (bySource.size >= ART_CACHE_MAX) bySource.delete(bySource.keys().next().value)
73
+ bySource.set(src, art)
74
+ return art
56
75
  }
57
76
 
58
77
  // The engine sizes a label from the `unicode-width` crate, which follows
package/lib/mermaid.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // 'local' never touches the network, 'off' renders nothing.
13
13
  import { spawn } from 'node:child_process'
14
14
  import { createHash } from 'node:crypto'
15
- import { readFile, mkdtemp, rm, writeFile } from 'node:fs/promises'
15
+ import { mkdir, readFile, mkdtemp, rm, writeFile } from 'node:fs/promises'
16
16
  import { tmpdir } from 'node:os'
17
17
  import { join } from 'node:path'
18
18
 
@@ -24,8 +24,14 @@ const sha1 = (s) => createHash('sha1').update(s).digest('hex')
24
24
 
25
25
  // Deterministic cache file name for a diagram (encoded diagrams are long and
26
26
  // URL-unsafe; a content hash keeps the cache dir tidy and stable).
27
- export function mermaidCachePath(code) {
28
- return sha1('mermaid:' + code).slice(0, 24) + '.png'
27
+ //
28
+ // The requested pixel width is part of the key: the same diagram rendered for a
29
+ // narrow terminal is a different raster from the one a wide terminal asks for,
30
+ // and serving the narrow file to the wide terminal would draw a diagram at half
31
+ // the width of the space reserved for it.
32
+ export function mermaidCachePath(code, pxWidth) {
33
+ const width = pxWidth ? ':' + Math.round(pxWidth) : ''
34
+ return sha1('mermaid:' + code + width).slice(0, 24) + '.png'
29
35
  }
30
36
 
31
37
  function spawnWithTimeout(cmd, args, timeoutMs) {
@@ -89,10 +95,13 @@ export function createMermaidRenderer({ fetch: fetchImpl = globalThis.fetch, mod
89
95
  }
90
96
  }
91
97
 
92
- async function renderViaInk(code) {
98
+ async function renderViaInk(code, { pxWidth }) {
93
99
  if (typeof fetchImpl !== 'function') return null
94
100
  const encoded = Buffer.from(code, 'utf8').toString('base64url')
95
- const url = 'https://mermaid.ink/img/' + encoded + '?type=png'
101
+ // `width` is the ink service's own pixel hint; without it the diagram comes
102
+ // back at the service's default and is downscaled here for no reason.
103
+ const width = Math.min(2000, Math.max(64, Math.round(pxWidth ?? 800)))
104
+ const url = 'https://mermaid.ink/img/' + encoded + '?type=png&width=' + width
96
105
  const res = await fetchImpl(url, { signal: AbortSignal.timeout(INK_TIMEOUT_MS) })
97
106
  if (!res.ok) return null
98
107
  const buf = Buffer.from(await res.arrayBuffer())
@@ -105,31 +114,41 @@ export function createMermaidRenderer({ fetch: fetchImpl = globalThis.fetch, mod
105
114
  if (effectiveMode === 'off') return null
106
115
  code = String(code ?? '')
107
116
  if (code.trim() === '') return null
108
- const cached = memory.get(code)
117
+ // Width is part of the memory key for the same reason it is part of the
118
+ // file name: a resize asks for a different raster.
119
+ const memoKey = code + '\u0000' + (pxWidth ?? 0)
120
+ const cached = memory.get(memoKey)
109
121
  if (cached) return cached
110
122
 
111
123
  let result = null
112
- if (effectiveMode === 'auto' || effectiveMode === 'local') {
124
+ const cacheFile = cacheDir ? join(cacheDir, mermaidCachePath(code, pxWidth)) : null
125
+ // Disk before any provider. A diagram this machine already rendered is a
126
+ // file read; going to the network first made every session pay for a fresh
127
+ // render of diagrams that were sitting on disk, and sent the diagram text
128
+ // to a public service for nothing.
129
+ if (cacheFile) {
130
+ try {
131
+ const bytes = await readFile(cacheFile)
132
+ if (bytes.length > 0) result = { bytes, mediaType: 'image/png' }
133
+ } catch { /* no cached file */ }
134
+ }
135
+ if (!result && (effectiveMode === 'auto' || effectiveMode === 'local')) {
113
136
  result = await renderViaMmdc(code, { pxWidth })
114
137
  }
115
138
  if (!result && (effectiveMode === 'auto' || effectiveMode === 'network')) {
116
139
  try {
117
- result = await renderViaInk(code)
140
+ result = await renderViaInk(code, { pxWidth })
118
141
  } catch { /* offline or blocked; fall through */ }
119
142
  }
120
- if (!result && cacheDir) {
121
- // A previous session may have rendered this exact diagram to disk.
122
- try {
123
- const path = join(cacheDir, mermaidCachePath(code))
124
- const bytes = await readFile(path)
125
- if (bytes.length > 0) result = { bytes, mediaType: 'image/png' }
126
- } catch { /* no cached file */ }
127
- }
128
143
  if (result) {
129
- memory.set(code, result)
130
- if (cacheDir) {
131
- const path = join(cacheDir, mermaidCachePath(code))
132
- void writeFile(path, result.bytes).catch(() => { /* best-effort disk cache */ })
144
+ memory.set(memoKey, result)
145
+ if (cacheFile) {
146
+ // The cache directory is created on first write: it does not exist on a
147
+ // fresh install, and a write into a missing directory fails silently —
148
+ // which is how the disk cache went unused on every run.
149
+ void mkdir(cacheDir, { recursive: true })
150
+ .then(() => writeFile(cacheFile, result.bytes))
151
+ .catch(() => { /* best-effort disk cache */ })
133
152
  }
134
153
  }
135
154
  return result