@taliesinb/mash 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/mash.ts +253 -0
- package/dist/bin/mash.d.ts +39 -0
- package/dist/bin/mash.d.ts.map +1 -0
- package/dist/bin/mash.js +281 -0
- package/dist/bin/mash.js.map +1 -0
- package/dist/src/analysis/analyze.d.ts +97 -0
- package/dist/src/analysis/analyze.d.ts.map +1 -0
- package/dist/src/analysis/analyze.js +1406 -0
- package/dist/src/analysis/analyze.js.map +1 -0
- package/dist/src/analysis/fs.d.ts +39 -0
- package/dist/src/analysis/fs.d.ts.map +1 -0
- package/dist/src/analysis/fs.js +117 -0
- package/dist/src/analysis/fs.js.map +1 -0
- package/dist/src/analysis/outputs.d.ts +16 -0
- package/dist/src/analysis/outputs.d.ts.map +1 -0
- package/dist/src/analysis/outputs.js +277 -0
- package/dist/src/analysis/outputs.js.map +1 -0
- package/dist/src/analysis/value.d.ts +76 -0
- package/dist/src/analysis/value.d.ts.map +1 -0
- package/dist/src/analysis/value.js +248 -0
- package/dist/src/analysis/value.js.map +1 -0
- package/dist/src/behaviors/defs/core.d.ts +15 -0
- package/dist/src/behaviors/defs/core.d.ts.map +1 -0
- package/dist/src/behaviors/defs/core.js +1084 -0
- package/dist/src/behaviors/defs/core.js.map +1 -0
- package/dist/src/behaviors/defs/devtools.d.ts +9 -0
- package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/devtools.js +432 -0
- package/dist/src/behaviors/defs/devtools.js.map +1 -0
- package/dist/src/behaviors/defs/macos.d.ts +12 -0
- package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
- package/dist/src/behaviors/defs/macos.js +454 -0
- package/dist/src/behaviors/defs/macos.js.map +1 -0
- package/dist/src/behaviors/defs/tools.d.ts +9 -0
- package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/tools.js +639 -0
- package/dist/src/behaviors/defs/tools.js.map +1 -0
- package/dist/src/behaviors/dsl.d.ts +95 -0
- package/dist/src/behaviors/dsl.d.ts.map +1 -0
- package/dist/src/behaviors/dsl.js +267 -0
- package/dist/src/behaviors/dsl.js.map +1 -0
- package/dist/src/behaviors/index.d.ts +12 -0
- package/dist/src/behaviors/index.d.ts.map +1 -0
- package/dist/src/behaviors/index.js +18 -0
- package/dist/src/behaviors/index.js.map +1 -0
- package/dist/src/behaviors/registry.d.ts +45 -0
- package/dist/src/behaviors/registry.d.ts.map +1 -0
- package/dist/src/behaviors/registry.js +266 -0
- package/dist/src/behaviors/registry.js.map +1 -0
- package/dist/src/behaviors/roles.d.ts +29 -0
- package/dist/src/behaviors/roles.d.ts.map +1 -0
- package/dist/src/behaviors/roles.js +198 -0
- package/dist/src/behaviors/roles.js.map +1 -0
- package/dist/src/behaviors/types.d.ts +120 -0
- package/dist/src/behaviors/types.d.ts.map +1 -0
- package/dist/src/behaviors/types.js +12 -0
- package/dist/src/behaviors/types.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/deepsea.d.ts +121 -0
- package/dist/src/integrations/deepsea.d.ts.map +1 -0
- package/dist/src/integrations/deepsea.js +199 -0
- package/dist/src/integrations/deepsea.js.map +1 -0
- package/dist/src/machine.d.ts +139 -0
- package/dist/src/machine.d.ts.map +1 -0
- package/dist/src/machine.js +400 -0
- package/dist/src/machine.js.map +1 -0
- package/dist/src/parser/ast.d.ts +369 -0
- package/dist/src/parser/ast.d.ts.map +1 -0
- package/dist/src/parser/ast.js +17 -0
- package/dist/src/parser/ast.js.map +1 -0
- package/dist/src/parser/parser.d.ts +48 -0
- package/dist/src/parser/parser.d.ts.map +1 -0
- package/dist/src/parser/parser.js +1950 -0
- package/dist/src/parser/parser.js.map +1 -0
- package/dist/src/policy/effects.d.ts +86 -0
- package/dist/src/policy/effects.d.ts.map +1 -0
- package/dist/src/policy/effects.js +53 -0
- package/dist/src/policy/effects.js.map +1 -0
- package/dist/src/policy/policy.d.ts +130 -0
- package/dist/src/policy/policy.d.ts.map +1 -0
- package/dist/src/policy/policy.js +532 -0
- package/dist/src/policy/policy.js.map +1 -0
- package/dist/src/printer/doc.d.ts +151 -0
- package/dist/src/printer/doc.d.ts.map +1 -0
- package/dist/src/printer/doc.js +409 -0
- package/dist/src/printer/doc.js.map +1 -0
- package/dist/src/printer/highlight.d.ts +53 -0
- package/dist/src/printer/highlight.d.ts.map +1 -0
- package/dist/src/printer/highlight.js +339 -0
- package/dist/src/printer/highlight.js.map +1 -0
- package/dist/src/printer/html.d.ts +72 -0
- package/dist/src/printer/html.d.ts.map +1 -0
- package/dist/src/printer/html.js +423 -0
- package/dist/src/printer/html.js.map +1 -0
- package/dist/src/printer/print.d.ts +63 -0
- package/dist/src/printer/print.d.ts.map +1 -0
- package/dist/src/printer/print.js +484 -0
- package/dist/src/printer/print.js.map +1 -0
- package/dist/src/printer/summary.d.ts +50 -0
- package/dist/src/printer/summary.d.ts.map +1 -0
- package/dist/src/printer/summary.js +621 -0
- package/dist/src/printer/summary.js.map +1 -0
- package/dist/src/trace/events.d.ts +183 -0
- package/dist/src/trace/events.d.ts.map +1 -0
- package/dist/src/trace/events.js +9 -0
- package/dist/src/trace/events.js.map +1 -0
- package/dist/src/trace/progress.d.ts +58 -0
- package/dist/src/trace/progress.d.ts.map +1 -0
- package/dist/src/trace/progress.js +83 -0
- package/dist/src/trace/progress.js.map +1 -0
- package/dist/src/vm/arith.d.ts +29 -0
- package/dist/src/vm/arith.d.ts.map +1 -0
- package/dist/src/vm/arith.js +258 -0
- package/dist/src/vm/arith.js.map +1 -0
- package/dist/src/vm/builtins.d.ts +31 -0
- package/dist/src/vm/builtins.d.ts.map +1 -0
- package/dist/src/vm/builtins.js +1773 -0
- package/dist/src/vm/builtins.js.map +1 -0
- package/dist/src/vm/emulate.d.ts +75 -0
- package/dist/src/vm/emulate.d.ts.map +1 -0
- package/dist/src/vm/emulate.js +380 -0
- package/dist/src/vm/emulate.js.map +1 -0
- package/dist/src/vm/expand.d.ts +68 -0
- package/dist/src/vm/expand.d.ts.map +1 -0
- package/dist/src/vm/expand.js +744 -0
- package/dist/src/vm/expand.js.map +1 -0
- package/dist/src/vm/external.d.ts +34 -0
- package/dist/src/vm/external.d.ts.map +1 -0
- package/dist/src/vm/external.js +228 -0
- package/dist/src/vm/external.js.map +1 -0
- package/dist/src/vm/fifo.d.ts +66 -0
- package/dist/src/vm/fifo.d.ts.map +1 -0
- package/dist/src/vm/fifo.js +232 -0
- package/dist/src/vm/fifo.js.map +1 -0
- package/dist/src/vm/interp.d.ts +209 -0
- package/dist/src/vm/interp.d.ts.map +1 -0
- package/dist/src/vm/interp.js +1641 -0
- package/dist/src/vm/interp.js.map +1 -0
- package/dist/src/vm/io.d.ts +180 -0
- package/dist/src/vm/io.d.ts.map +1 -0
- package/dist/src/vm/io.js +502 -0
- package/dist/src/vm/io.js.map +1 -0
- package/dist/src/vm/pattern.d.ts +46 -0
- package/dist/src/vm/pattern.d.ts.map +1 -0
- package/dist/src/vm/pattern.js +498 -0
- package/dist/src/vm/pattern.js.map +1 -0
- package/dist/src/vm/proc.d.ts +94 -0
- package/dist/src/vm/proc.d.ts.map +1 -0
- package/dist/src/vm/proc.js +130 -0
- package/dist/src/vm/proc.js.map +1 -0
- package/dist/src/vm/state.d.ts +104 -0
- package/dist/src/vm/state.d.ts.map +1 -0
- package/dist/src/vm/state.js +233 -0
- package/dist/src/vm/state.js.map +1 -0
- package/dist/src/vm/test.d.ts +11 -0
- package/dist/src/vm/test.d.ts.map +1 -0
- package/dist/src/vm/test.js +277 -0
- package/dist/src/vm/test.js.map +1 -0
- package/package.json +63 -4
- package/src/analysis/analyze.ts +1395 -0
- package/src/analysis/fs.ts +121 -0
- package/src/analysis/outputs.ts +242 -0
- package/src/analysis/value.ts +264 -0
- package/src/behaviors/defs/core.ts +970 -0
- package/src/behaviors/defs/devtools.ts +400 -0
- package/src/behaviors/defs/macos.ts +426 -0
- package/src/behaviors/defs/tools.ts +596 -0
- package/src/behaviors/dsl.ts +293 -0
- package/src/behaviors/index.ts +19 -0
- package/src/behaviors/registry.ts +261 -0
- package/src/behaviors/roles.ts +187 -0
- package/src/behaviors/types.ts +127 -0
- package/src/index.ts +27 -0
- package/src/integrations/deepsea.ts +259 -0
- package/src/machine.ts +483 -0
- package/src/parser/ast.ts +472 -0
- package/src/parser/parser.ts +1826 -0
- package/src/policy/effects.ts +102 -0
- package/src/policy/policy.ts +578 -0
- package/src/printer/doc.ts +454 -0
- package/src/printer/highlight.ts +283 -0
- package/src/printer/html.ts +441 -0
- package/src/printer/print.ts +528 -0
- package/src/printer/summary.ts +601 -0
- package/src/trace/events.ts +102 -0
- package/src/trace/progress.ts +119 -0
- package/src/vm/arith.ts +236 -0
- package/src/vm/builtins.ts +1590 -0
- package/src/vm/emulate.ts +337 -0
- package/src/vm/expand.ts +721 -0
- package/src/vm/external.ts +265 -0
- package/src/vm/fifo.ts +226 -0
- package/src/vm/interp.ts +1623 -0
- package/src/vm/io.ts +570 -0
- package/src/vm/pattern.ts +477 -0
- package/src/vm/proc.ts +147 -0
- package/src/vm/state.ts +267 -0
- package/src/vm/test.ts +253 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML-native pretty-printing: the same layout as `format`, emitted as
|
|
3
|
+
* syntax-highlighted spans, with best-effort truncation of over-long words so
|
|
4
|
+
* lines fit the width. Truncated output is for display only (it is not valid
|
|
5
|
+
* bash); hidden text stays in the DOM and a click can reveal it.
|
|
6
|
+
*
|
|
7
|
+
* Truncation is a small search, not an optimum: render, find lines over the
|
|
8
|
+
* width, shorten the most suitable atom on each (quoted strings and
|
|
9
|
+
* substitutions first, never command names or flags), re-render, repeat until
|
|
10
|
+
* nothing changes. Multi-line atoms (quoted strings, here-doc bodies) get
|
|
11
|
+
* their continuation lines clipped to the width and are collapsed to their
|
|
12
|
+
* first lines when they are longer than `maxLines`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type * as A from '../parser/ast.ts'
|
|
16
|
+
import { literalOf, parse } from '../parser/parser.ts'
|
|
17
|
+
import { analyzeScript } from '../analysis/analyze.ts'
|
|
18
|
+
import { concrete, hole, known } from '../analysis/value.ts'
|
|
19
|
+
import { defaultBehaviors } from '../behaviors/index.ts'
|
|
20
|
+
import type { BehaviorRegistry } from '../behaviors/registry.ts'
|
|
21
|
+
import type { AtomDisplay, AtomInfo, Doc, RenderResult } from './doc.ts'
|
|
22
|
+
import { render } from './doc.ts'
|
|
23
|
+
import { Highlight, simpleCommandRoles } from './highlight.ts'
|
|
24
|
+
import { buildDoc, type FormatOptions } from './print.ts'
|
|
25
|
+
|
|
26
|
+
export interface HtmlOptions extends FormatOptions {
|
|
27
|
+
/** Shorten over-long atoms so lines fit `width` (default true). */
|
|
28
|
+
truncate?: boolean
|
|
29
|
+
/** Lines shown of a multi-line string or here-doc body before collapsing (default 8). */
|
|
30
|
+
maxLines?: number
|
|
31
|
+
/** Never shorten an atom's first line below this many columns (default 16). */
|
|
32
|
+
minKeep?: number
|
|
33
|
+
/** Re-layout rounds (default 12). */
|
|
34
|
+
maxIterations?: number
|
|
35
|
+
/**
|
|
36
|
+
* Prefer shortening over breaking (default true): a line that fits once its
|
|
37
|
+
* strings and paths are shortened is not wrapped; the full text stays in
|
|
38
|
+
* the DOM (and tooltip). False lays out like `format`, then shortens.
|
|
39
|
+
*/
|
|
40
|
+
preferTruncation?: boolean
|
|
41
|
+
/**
|
|
42
|
+
* Resolve file and directory arguments statically (default true): the
|
|
43
|
+
* working directory at each command comes from the static analysis (it
|
|
44
|
+
* follows `cd`), starting from `cwd` (unknown when not given). Known paths
|
|
45
|
+
* get a tooltip (`as written → /absolute/path` when they differ beyond `~`)
|
|
46
|
+
* and a `data-href="file://…"` that {@link attachHtmlHandlers} opens.
|
|
47
|
+
*/
|
|
48
|
+
paths?: boolean
|
|
49
|
+
/** Working directory the snippet starts in. */
|
|
50
|
+
cwd?: string
|
|
51
|
+
/** Home directory for `~` (default: $HOME). */
|
|
52
|
+
home?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A file or directory argument, as written and (when known statically) absolute. */
|
|
56
|
+
interface PathInfo {
|
|
57
|
+
written: string
|
|
58
|
+
abs?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `/a/./b/../c` → `/a/c` (absolute paths). */
|
|
62
|
+
function normalizePath(p: string): string {
|
|
63
|
+
const out: string[] = []
|
|
64
|
+
for (const part of p.split('/')) {
|
|
65
|
+
if (part === '' || part === '.') continue
|
|
66
|
+
if (part === '..') out.pop()
|
|
67
|
+
else out.push(part)
|
|
68
|
+
}
|
|
69
|
+
return '/' + out.join('/') + (p.endsWith('/') && out.length ? '/' : '')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let sharedBehaviors: BehaviorRegistry | null = null
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* File and directory arguments (and redirection targets) by word id, with
|
|
76
|
+
* their absolute path when the static analysis knows the working directory.
|
|
77
|
+
*/
|
|
78
|
+
function staticPaths(script: A.Script, source: string, cwd: string | undefined, home: string | undefined): Map<number, PathInfo> {
|
|
79
|
+
const out = new Map<number, PathInfo>()
|
|
80
|
+
const cwdAt = new Map<number, string | undefined>()
|
|
81
|
+
try {
|
|
82
|
+
const report = analyzeScript(script, source, {
|
|
83
|
+
cwd: cwd ? known(cwd) : hole('path', 'cwd'),
|
|
84
|
+
env: home ? { HOME: home } : {},
|
|
85
|
+
policy: { mode: 'danger-full-access', workspaceRoot: cwd ?? '/', writableTemp: true, network: 'allow', unknownCommands: 'allow' },
|
|
86
|
+
behaviors: (sharedBehaviors ??= defaultBehaviors()),
|
|
87
|
+
})
|
|
88
|
+
for (const c of report.commands) cwdAt.set(c.nodeId, concrete(c.cwd))
|
|
89
|
+
} catch {
|
|
90
|
+
// display only: paths stay unresolved
|
|
91
|
+
}
|
|
92
|
+
const absOf = (w: string, dir: string | undefined): string | undefined => {
|
|
93
|
+
if (/[*?[\]{}$`\\]/.test(w)) return undefined // globs and anything unexpanded
|
|
94
|
+
if (w.startsWith('/')) return normalizePath(w)
|
|
95
|
+
if (w === '~' || w.startsWith('~/')) return home ? normalizePath(home + w.slice(1)) : undefined
|
|
96
|
+
if (w.startsWith('~')) return undefined // ~user
|
|
97
|
+
return dir ? normalizePath(`${dir}/${w}`) : undefined
|
|
98
|
+
}
|
|
99
|
+
const add = (id: number, w: string | null, dir: string | undefined): void => {
|
|
100
|
+
if (w === null || w === '') return
|
|
101
|
+
out.set(id, { written: w, abs: absOf(w, dir) })
|
|
102
|
+
}
|
|
103
|
+
const visit = (n: unknown): void => {
|
|
104
|
+
if (Array.isArray(n)) {
|
|
105
|
+
for (const x of n) visit(x)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (n === null || typeof n !== 'object') return
|
|
109
|
+
const node = n as A.Node & Record<string, unknown>
|
|
110
|
+
if (node.type === 'SimpleCommand') {
|
|
111
|
+
const c = node as unknown as A.SimpleCommand
|
|
112
|
+
const dir = cwdAt.get(c.id)
|
|
113
|
+
let roles: ReturnType<typeof simpleCommandRoles> = []
|
|
114
|
+
try {
|
|
115
|
+
roles = simpleCommandRoles(c)
|
|
116
|
+
} catch {
|
|
117
|
+
// display only
|
|
118
|
+
}
|
|
119
|
+
c.words.forEach((w, i) => {
|
|
120
|
+
if (w.type === 'Word' && (roles[i] === 'file' || roles[i] === 'dir')) add(w.id, literalOf(w), dir)
|
|
121
|
+
})
|
|
122
|
+
for (const r of c.redirs) {
|
|
123
|
+
if (r.heredoc || /&\s*$/.test(source.slice(r.span.start, r.target.span.start))) continue
|
|
124
|
+
add(r.target.id, literalOf(r.target), dir)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const [k, v] of Object.entries(node)) if (k !== 'span' && v && typeof v === 'object') visit(v)
|
|
128
|
+
}
|
|
129
|
+
visit(script.body)
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The tooltip for a path: `as written → absolute` when they differ beyond `~`, else the path. */
|
|
134
|
+
function pathTitle(p: PathInfo, home: string | undefined): string {
|
|
135
|
+
if (!p.abs) return p.written
|
|
136
|
+
const tilde = home && (p.written === '~' || p.written.startsWith('~/')) ? home + p.written.slice(1) : p.written
|
|
137
|
+
return tilde === p.abs || tilde === p.abs.replace(/\/$/, '') ? p.abs : `${p.written} → ${p.abs}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fileHref = (abs: string): string => 'file://' + encodeURI(abs).replace(/#/g, '%23').replace(/\?/g, '%3F')
|
|
141
|
+
|
|
142
|
+
export interface HtmlResult {
|
|
143
|
+
/** `<pre class="mash">…</pre>` */
|
|
144
|
+
html: string
|
|
145
|
+
/** The displayed text (with `…` where truncated). */
|
|
146
|
+
text: string
|
|
147
|
+
/** Number of atoms shortened or collapsed. */
|
|
148
|
+
truncated: number
|
|
149
|
+
/** Lines still wider than `width` (unbreakable code, not truncatable). */
|
|
150
|
+
overflowing: number
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const ELLIPSIS = '…'
|
|
154
|
+
|
|
155
|
+
interface Atom extends AtomInfo {
|
|
156
|
+
start: number
|
|
157
|
+
end: number
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Atoms referenced by a document, with their source ranges. */
|
|
161
|
+
function collectAtoms(doc: Doc, out = new Map<number, Atom>()): Map<number, Atom> {
|
|
162
|
+
if (typeof doc === 'string') return out
|
|
163
|
+
if (Array.isArray(doc)) {
|
|
164
|
+
for (const d of doc) collectAtoms(d, out)
|
|
165
|
+
return out
|
|
166
|
+
}
|
|
167
|
+
switch (doc.t) {
|
|
168
|
+
case 'src':
|
|
169
|
+
case 'heredoc':
|
|
170
|
+
if (doc.atom) out.set(doc.atom.id, { ...doc.atom, start: doc.start, end: doc.end })
|
|
171
|
+
break
|
|
172
|
+
case 'group': case 'indent': case 'mark': collectAtoms(doc.doc, out); break
|
|
173
|
+
case 'fill': for (const p of doc.parts) collectAtoms(p, out); break
|
|
174
|
+
case 'ifBreak': collectAtoms(doc.broken, out); collectAtoms(doc.flat, out); break
|
|
175
|
+
case 'suffix': collectAtoms(doc.doc, out); break
|
|
176
|
+
default: break
|
|
177
|
+
}
|
|
178
|
+
return out
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* How an atom is shown under the current limits, or null when it is shown in
|
|
183
|
+
* full. `first` limits the width of its first line; continuation lines are
|
|
184
|
+
* clipped to `cols` (they start at column 0); more than `maxLines` lines collapse.
|
|
185
|
+
*/
|
|
186
|
+
/** The narrowest a path atom gets: `…/basename` (whole last component). */
|
|
187
|
+
function tailFloor(source: string, a: Atom): number {
|
|
188
|
+
const slash = source.lastIndexOf('/', a.end - 2)
|
|
189
|
+
return slash > a.start ? Math.min(a.end - a.start, 1 + a.end - slash) : a.end - a.start
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A path shortened from the left at a `/` where possible: `…/pages/395.html`. */
|
|
193
|
+
function tailDisplay(source: string, a: Atom, limit: number): AtomDisplay | null {
|
|
194
|
+
const full = a.end - a.start
|
|
195
|
+
if (limit >= full) return null
|
|
196
|
+
let from = a.end - Math.max(1, limit - 1)
|
|
197
|
+
const slash = source.indexOf('/', from)
|
|
198
|
+
if (slash >= 0 && slash < a.end - 1) from = slash
|
|
199
|
+
if (from <= a.start) return null
|
|
200
|
+
const parts: AtomDisplay['parts'] = [
|
|
201
|
+
{ kind: 'hide', start: a.start, end: from },
|
|
202
|
+
{ kind: 'mark', text: ELLIPSIS },
|
|
203
|
+
{ kind: 'show', start: from, end: a.end },
|
|
204
|
+
]
|
|
205
|
+
return { text: ELLIPSIS + source.slice(from, a.end), parts }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function displayFor(source: string, a: Atom, first: number, cols: number, maxLines: number): AtomDisplay | null {
|
|
209
|
+
if (a.tail) return tailDisplay(source, a, first)
|
|
210
|
+
const lines: [number, number][] = []
|
|
211
|
+
let pos = a.start
|
|
212
|
+
while (pos <= a.end) {
|
|
213
|
+
const nlAt = source.indexOf('\n', pos)
|
|
214
|
+
const e = nlAt === -1 || nlAt >= a.end ? a.end : nlAt
|
|
215
|
+
lines.push([pos, e])
|
|
216
|
+
if (e >= a.end) break
|
|
217
|
+
pos = e + 1
|
|
218
|
+
}
|
|
219
|
+
// a here-doc body ends with a newline: drop the empty line after it
|
|
220
|
+
if (lines.length > 1 && lines[lines.length - 1][0] === lines[lines.length - 1][1] && a.end > a.start && source[a.end - 1] === '\n') lines.pop()
|
|
221
|
+
const lastChar = source[a.end - 1]
|
|
222
|
+
const closeLen = a.kind === 'word' && a.end - a.start > 2 && `'")}\``.includes(lastChar) ? 1 : 0
|
|
223
|
+
const parts: AtomDisplay['parts'] = []
|
|
224
|
+
let hidden = false
|
|
225
|
+
const clip = (ls: number, le: number, limit: number, close: number): void => {
|
|
226
|
+
if (le - ls <= limit) {
|
|
227
|
+
parts.push({ kind: 'show', start: ls, end: le })
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
const keep = Math.max(1, limit - 1 - close)
|
|
231
|
+
parts.push({ kind: 'show', start: ls, end: ls + keep })
|
|
232
|
+
parts.push({ kind: 'hide', start: ls + keep, end: le - close })
|
|
233
|
+
parts.push({ kind: 'mark', text: ELLIPSIS })
|
|
234
|
+
if (close) parts.push({ kind: 'show', start: le - close, end: le })
|
|
235
|
+
hidden = true
|
|
236
|
+
}
|
|
237
|
+
const newlineAfter = (i: number): void => {
|
|
238
|
+
const e = lines[i][1]
|
|
239
|
+
if (e < a.end && source[e] === '\n') parts.push({ kind: 'show', start: e, end: e + 1 })
|
|
240
|
+
}
|
|
241
|
+
const n = lines.length
|
|
242
|
+
const limitOf = (i: number): number => (i === 0 && a.kind !== 'hd' ? first : cols)
|
|
243
|
+
const closeOf = (i: number): number => (i === n - 1 ? closeLen : 0)
|
|
244
|
+
let shown: number[]
|
|
245
|
+
let collapseFrom = -1
|
|
246
|
+
let collapseTo = -1
|
|
247
|
+
if (n > maxLines) {
|
|
248
|
+
hidden = true
|
|
249
|
+
if (a.kind === 'hd') {
|
|
250
|
+
shown = [...Array(maxLines).keys()]
|
|
251
|
+
collapseFrom = maxLines
|
|
252
|
+
collapseTo = n
|
|
253
|
+
} else {
|
|
254
|
+
shown = [...Array(Math.max(1, maxLines - 1)).keys(), n - 1]
|
|
255
|
+
collapseFrom = Math.max(1, maxLines - 1)
|
|
256
|
+
collapseTo = n - 1
|
|
257
|
+
}
|
|
258
|
+
} else shown = [...Array(n).keys()]
|
|
259
|
+
for (const i of shown) {
|
|
260
|
+
if (i === collapseTo && collapseFrom >= 0) {
|
|
261
|
+
const hs = lines[collapseFrom][0]
|
|
262
|
+
parts.push({ kind: 'hide', start: hs, end: lines[i][0] })
|
|
263
|
+
parts.push({ kind: 'mark', text: `⋯ ${collapseTo - collapseFrom} more lines\n` })
|
|
264
|
+
}
|
|
265
|
+
clip(lines[i][0], lines[i][1], limitOf(i), closeOf(i))
|
|
266
|
+
if (i !== n - 1 || a.kind === 'hd') newlineAfter(i)
|
|
267
|
+
}
|
|
268
|
+
if (collapseFrom >= 0 && collapseTo === n) {
|
|
269
|
+
const hs = lines[collapseFrom][0]
|
|
270
|
+
parts.push({ kind: 'hide', start: hs, end: a.end })
|
|
271
|
+
parts.push({ kind: 'mark', text: `⋯ ${collapseTo - collapseFrom} more lines\n` })
|
|
272
|
+
}
|
|
273
|
+
if (!hidden) return null
|
|
274
|
+
const text = parts.map((p) => (p.kind === 'mark' ? p.text : p.kind === 'show' ? source.slice(p.start, p.end) : '')).join('')
|
|
275
|
+
return { text, parts }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const esc = (s: string): string => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]!)
|
|
279
|
+
|
|
280
|
+
/** Pretty-print `source` as highlighted (and, by default, truncated) HTML. */
|
|
281
|
+
export function formatHtml(source: string, options: HtmlOptions = {}): HtmlResult {
|
|
282
|
+
return printScriptHtml(parse(source), source, options)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function printScriptHtml(script: A.Script, source: string, options: HtmlOptions = {}): HtmlResult {
|
|
286
|
+
const truncate = options.truncate ?? true
|
|
287
|
+
const maxLines = options.maxLines ?? 8
|
|
288
|
+
const minKeep = options.minKeep ?? 16
|
|
289
|
+
const maxIterations = options.maxIterations ?? 12
|
|
290
|
+
const { doc, opts } = buildDoc(script, source, options)
|
|
291
|
+
const width = opts.width
|
|
292
|
+
const atoms = collectAtoms(doc)
|
|
293
|
+
const limits = new Map<number, number>()
|
|
294
|
+
// how far each atom may shrink: paths to `…/basename`, other words to minKeep
|
|
295
|
+
const floor = new Map<number, number>()
|
|
296
|
+
for (const a of atoms.values()) {
|
|
297
|
+
if (a.kind !== 'word' || a.weight <= 0) continue
|
|
298
|
+
const firstLine = source.slice(a.start, a.end).split('\n')[0].length
|
|
299
|
+
if (source.slice(a.start, a.end).includes('\n')) continue
|
|
300
|
+
floor.set(a.id, a.tail ? tailFloor(source, a) : Math.min(firstLine, minKeep))
|
|
301
|
+
}
|
|
302
|
+
// prefer shortening atoms over breaking lines: layout measures atoms at their floor
|
|
303
|
+
const minWidth = truncate && (options.preferTruncation ?? true) ? floor : undefined
|
|
304
|
+
const hl = new Highlight(script, source)
|
|
305
|
+
|
|
306
|
+
let display = new Map<number, AtomDisplay>()
|
|
307
|
+
let r: RenderResult = render(doc, width, { source })
|
|
308
|
+
for (let iter = 0; truncate && iter < maxIterations; iter++) {
|
|
309
|
+
display = new Map()
|
|
310
|
+
for (const a of atoms.values()) {
|
|
311
|
+
const d = displayFor(source, a, limits.get(a.id) ?? Infinity, width, maxLines)
|
|
312
|
+
if (d) display.set(a.id, d)
|
|
313
|
+
}
|
|
314
|
+
r = render(doc, width, { source, display, minWidth })
|
|
315
|
+
const byLine = new Map<number, { id: number; width: number }[]>()
|
|
316
|
+
for (const [id, pos] of r.atoms) {
|
|
317
|
+
const list = byLine.get(pos.line) ?? []
|
|
318
|
+
list.push({ id, width: pos.width })
|
|
319
|
+
byLine.set(pos.line, list)
|
|
320
|
+
}
|
|
321
|
+
let changed = false
|
|
322
|
+
r.text.split('\n').forEach((line, i) => {
|
|
323
|
+
let over = line.length - width
|
|
324
|
+
if (over <= 0) return
|
|
325
|
+
// shorten the line's atoms, most suitable first (weight × width), each down to its floor, until the overflow is covered
|
|
326
|
+
const cands = (byLine.get(i) ?? [])
|
|
327
|
+
.map((c) => ({ ...c, a: atoms.get(c.id), lo: floor.get(c.id) ?? minKeep }))
|
|
328
|
+
.filter((c) => c.a && c.a.kind !== 'hd' && c.a.weight > 0 && c.width > c.lo)
|
|
329
|
+
.sort((x, y) => y.a!.weight * y.width - x.a!.weight * x.width)
|
|
330
|
+
for (const c of cands) {
|
|
331
|
+
if (over <= 0) break
|
|
332
|
+
const limit = Math.max(c.lo, c.width - over)
|
|
333
|
+
if (limit < (limits.get(c.id) ?? Infinity) && limit < c.width) {
|
|
334
|
+
limits.set(c.id, limit)
|
|
335
|
+
over -= c.width - limit
|
|
336
|
+
changed = true
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
if (!changed) break
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const highlighted = (start: number, end: number, fallback?: string): string =>
|
|
344
|
+
hl.runs(start, end).map((run) => {
|
|
345
|
+
const cls = run.cls || fallback || ''
|
|
346
|
+
return cls ? `<span class="${cls}">${esc(run.text)}</span>` : esc(run.text)
|
|
347
|
+
}).join('')
|
|
348
|
+
|
|
349
|
+
const home = options.home ?? (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.HOME
|
|
350
|
+
const paths = options.paths === false ? new Map<number, PathInfo>() : staticPaths(script, source, options.cwd, home)
|
|
351
|
+
const TITLE_MAX = 2000
|
|
352
|
+
|
|
353
|
+
let html = ''
|
|
354
|
+
for (const p of r.pieces) {
|
|
355
|
+
const d = p.atom !== undefined ? display.get(p.atom) : undefined
|
|
356
|
+
const path = p.atom !== undefined ? paths.get(p.atom) : undefined
|
|
357
|
+
if (d || path) {
|
|
358
|
+
// one wrapper per atom: path role (whole thing underlined), tooltip, link, truncation state
|
|
359
|
+
const a = atoms.get(p.atom!)
|
|
360
|
+
const classes: string[] = []
|
|
361
|
+
if (path) {
|
|
362
|
+
const last = p.src ? hl.runs(p.src[1] - 1, p.src[1])[0]?.cls : undefined
|
|
363
|
+
classes.push('pth', last === 'dir' ? 'dir' : 'file')
|
|
364
|
+
}
|
|
365
|
+
if (d) classes.push('tc')
|
|
366
|
+
const full = a ? source.slice(a.start, a.end) : p.text
|
|
367
|
+
const title = path ? pathTitle(path, home) : full.length > TITLE_MAX ? full.slice(0, TITLE_MAX) + ELLIPSIS : full
|
|
368
|
+
const href = path?.abs ? ` data-href="${esc(fileHref(path.abs))}"` : ''
|
|
369
|
+
html += `<span class="${classes.join(' ')}" title="${esc(title)}"${href}>`
|
|
370
|
+
if (d) {
|
|
371
|
+
for (const part of d.parts) {
|
|
372
|
+
if (part.kind === 'show') html += highlighted(part.start, part.end, p.cls)
|
|
373
|
+
else if (part.kind === 'hide') html += `<span class="hid">${highlighted(part.start, part.end, p.cls)}</span>`
|
|
374
|
+
else if (part.kind === 'mark') html += `<span class="el">${esc(part.text)}</span>`
|
|
375
|
+
}
|
|
376
|
+
} else if (p.src) html += highlighted(p.src[0], p.src[1], p.cls)
|
|
377
|
+
else html += esc(p.text)
|
|
378
|
+
html += '</span>'
|
|
379
|
+
} else if (p.src) html += highlighted(p.src[0], p.src[1], p.cls)
|
|
380
|
+
else html += p.cls ? `<span class="${p.cls}">${esc(p.text)}</span>` : esc(p.text)
|
|
381
|
+
}
|
|
382
|
+
const overflowing = r.text.split('\n').filter((l) => l.length > width).length
|
|
383
|
+
return { html: `<pre class="mash">${html}</pre>`, text: r.text, truncated: display.size, overflowing }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** A DOM element, as far as the click handler needs one (no DOM types in this package). */
|
|
387
|
+
interface ElementLike {
|
|
388
|
+
closest(selector: string): (ElementLike & { dataset: Record<string, string | undefined>; classList: { toggle(c: string): void } }) | null
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Click behavior for HTML output under `root`: a statically known path opens
|
|
393
|
+
* in a new tab (`file://`; browsers allow that from file:// pages, not from
|
|
394
|
+
* http ones), any other shortened item expands or collapses.
|
|
395
|
+
*/
|
|
396
|
+
export function attachHtmlHandlers(root: { addEventListener(type: 'click', fn: (e: { target: unknown; preventDefault(): void }) => void): void }): void {
|
|
397
|
+
root.addEventListener('click', (e) => {
|
|
398
|
+
const t = e.target as ElementLike | null
|
|
399
|
+
const link = t?.closest?.('.mash [data-href]')
|
|
400
|
+
if (link) {
|
|
401
|
+
e.preventDefault()
|
|
402
|
+
;(globalThis as { open?: (url: string, target: string, features: string) => unknown }).open?.(link.dataset.href!, '_blank', 'noopener')
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
t?.closest?.('.mash .tc')?.classList.toggle('open')
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** {@link attachHtmlHandlers} as a script for pages without a module loader: `<script>${HTML_SCRIPT}</script>`. */
|
|
410
|
+
export const HTML_SCRIPT = `(${attachHtmlHandlers.toString()})(document)`
|
|
411
|
+
|
|
412
|
+
/** Default styles (light and dark). Attach the click handlers with {@link attachHtmlHandlers} or {@link HTML_SCRIPT}. */
|
|
413
|
+
export const HTML_CSS = `
|
|
414
|
+
.mash { --kw:#a626a4; --op:#0184bc; --cmd:#4078f2; --flag:#986801; --str:#50a14f; --esc:#c18401; --var:#e45649;
|
|
415
|
+
--sub:#0184bc; --ar:#986801; --asg:#e45649; --rd:#0184bc; --hd:#50a14f; --cm:#a0a1a7; --fn:#4078f2; --el:#a0a1a7;
|
|
416
|
+
--dir:#00838f; --file:#3f51b5; --re:#d84315; --code:#7b5e57; --sc:#7e3fd8;
|
|
417
|
+
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre; margin: 0; tab-size: 4; }
|
|
418
|
+
@media (prefers-color-scheme: dark) {
|
|
419
|
+
.mash { --kw:#c678dd; --op:#56b6c2; --cmd:#61afef; --flag:#d19a66; --str:#98c379; --esc:#e5c07b; --var:#e06c75;
|
|
420
|
+
--sub:#56b6c2; --ar:#d19a66; --asg:#e06c75; --rd:#56b6c2; --hd:#98c379; --cm:#7f848e; --fn:#61afef; --el:#7f848e;
|
|
421
|
+
--dir:#4dd0e1; --file:#9fa8ff; --re:#ff8a65; --code:#c8b5ad; --sc:#c49bff; }
|
|
422
|
+
}
|
|
423
|
+
.mash .kw { color: var(--kw); } .mash .op { color: var(--op); } .mash .cmd { color: var(--cmd); font-weight: 600; }
|
|
424
|
+
.mash .flag { color: var(--flag); } .mash .str { color: var(--str); } .mash .esc { color: var(--esc); }
|
|
425
|
+
.mash .var { color: var(--var); } .mash .sub { color: var(--sub); font-weight: 600; } .mash .ar, .mash .num { color: var(--ar); }
|
|
426
|
+
.mash .asg { color: var(--asg); } .mash .rd { color: var(--rd); } .mash .hd { color: var(--hd); }
|
|
427
|
+
.mash .pth { text-decoration: underline dotted; text-underline-offset: 3px; }
|
|
428
|
+
.mash .pth.dir { text-decoration-color: color-mix(in srgb, var(--dir) 55%, transparent); } .mash .pth.file { text-decoration-color: color-mix(in srgb, var(--file) 55%, transparent); }
|
|
429
|
+
.mash [data-href] { cursor: pointer; } .mash [data-href]:hover { background: color-mix(in srgb, currentColor 12%, transparent); border-radius: 3px; }
|
|
430
|
+
.mash .pth .dir, .mash .pth .file { text-decoration: none; }
|
|
431
|
+
/* paths: dotted underline (both kinds), told apart by color */
|
|
432
|
+
.mash .dir, .mash .file { text-decoration: underline dotted; text-underline-offset: 3px; }
|
|
433
|
+
.mash .dir { color: var(--dir); text-decoration-color: color-mix(in srgb, var(--dir) 45%, transparent); }
|
|
434
|
+
.mash .file { color: var(--file); text-decoration-color: color-mix(in srgb, var(--file) 45%, transparent); }
|
|
435
|
+
.mash .re { color: var(--re); } .mash .code { color: var(--code); } .mash .sc { color: var(--sc); font-weight: 600; }
|
|
436
|
+
.mash .cm { color: var(--cm); font-style: italic; } .mash .fn { color: var(--fn); font-weight: 600; } .mash .ct { color: var(--el); }
|
|
437
|
+
.mash .tc .hid { display: none; } .mash .tc.open .hid { display: inline; } .mash .tc.open > .el { display: none; }
|
|
438
|
+
.mash.sum { display: inline-block; white-space: pre; max-width: 100%; overflow: hidden; vertical-align: bottom; }
|
|
439
|
+
.mash.sum .el { cursor: help; } .mash .tr:hover { background: color-mix(in srgb, var(--el) 15%, transparent); border-radius: 3px; }
|
|
440
|
+
.mash .el { color: var(--el); cursor: pointer; border-radius: 3px; } .mash .el:hover { background: color-mix(in srgb, var(--el) 20%, transparent); }
|
|
441
|
+
`
|