aontu 0.54.0 → 0.55.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/dist/aontu.d.ts +1 -1
- package/dist/aontu.js +1 -1
- package/dist/cli.js +80 -7
- package/dist/cli.js.map +1 -1
- package/dist/hints.js +4 -0
- package/dist/hints.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/mcp.js +21 -1
- package/dist/mcp.js.map +1 -1
- package/dist/std.js +4 -1
- package/dist/std.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vet.d.ts +1 -0
- package/dist/vet.js +5 -0
- package/dist/vet.js.map +1 -1
- package/dist/view.d.ts +6 -1
- package/dist/view.js +276 -56
- package/dist/view.js.map +1 -1
- package/package.json +1 -1
- package/src/aontu.ts +1 -1
- package/src/cli.ts +89 -9
- package/src/hints.ts +4 -0
- package/src/mcp.ts +22 -1
- package/src/std.ts +4 -1
- package/src/vet.ts +5 -1
- package/src/view.ts +376 -57
package/src/view.ts
CHANGED
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
// other code makes it `lossy`, which `--strict` refuses: a figure that
|
|
39
39
|
// quietly omits things is the failure this capability exists to avoid.
|
|
40
40
|
|
|
41
|
-
import { basename, dirname, isAbsolute, relative, resolve } from 'path'
|
|
41
|
+
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path'
|
|
42
42
|
|
|
43
43
|
import { Aontu } from './aontu'
|
|
44
|
-
import { failureFinding, anchorAt } from './vet'
|
|
44
|
+
import { failureFinding, anchorAt, throughResidue } from './vet'
|
|
45
45
|
import type { VetFinding } from './vet'
|
|
46
46
|
import type { TrustOptions } from './type'
|
|
47
47
|
import { graphOf } from './graph'
|
|
@@ -57,7 +57,8 @@ import type { SubsumeProfile } from './subsume'
|
|
|
57
57
|
export type ViewVerdict = 'rendered' | 'lossy' | 'error'
|
|
58
58
|
|
|
59
59
|
export type ViewKind =
|
|
60
|
-
'tree' | 'matrix' | 'graph' | 'layer' | 'sets' | 'layers' | 'ladder'
|
|
60
|
+
'tree' | 'matrix' | 'graph' | 'layer' | 'sets' | 'layers' | 'ladder'
|
|
61
|
+
| 'poset' | 'doc'
|
|
61
62
|
|
|
62
63
|
// The target grammars. Each kind declares the profiles it can render
|
|
63
64
|
// into, and the first is its default (PROFILES below).
|
|
@@ -75,6 +76,68 @@ export type ViewOrder = 'canon' | 'partition'
|
|
|
75
76
|
// existed.
|
|
76
77
|
export type ViewEdges = 'upward' | 'all' | 'none'
|
|
77
78
|
|
|
79
|
+
// STYLING (VIEWS.0.md, "7. Styling"), which amends that note's colour
|
|
80
|
+
// boundary. Every mark a figure makes already has a reason the
|
|
81
|
+
// extractor established -- a cell is `direct` because the edge is
|
|
82
|
+
// declared, an arrow is `upward` because it runs against the bands --
|
|
83
|
+
// and the SVG profile has published those reasons as classes since it
|
|
84
|
+
// landed, because an SVG cannot be drawn without saying what each
|
|
85
|
+
// shape is. This declares the same vocabulary for the text profile and
|
|
86
|
+
// adds the one thing missing: a way to turn it on at the call.
|
|
87
|
+
//
|
|
88
|
+
// NEITHER MECHANISM STATES A COLOUR, which is what keeps the boundary
|
|
89
|
+
// intact. SGR 31 does not mean red; it means the colour the reader's
|
|
90
|
+
// terminal calls red, which the reader chose. A CSS class states
|
|
91
|
+
// nothing at all, and the stylesheet reads `var(--av-closure, ...)` so
|
|
92
|
+
// a host page's palette wins. A hex triple is the thing that cannot
|
|
93
|
+
// follow a theme, and it stays refused -- no truecolour escape, no
|
|
94
|
+
// 256-colour escape, no `classDef`.
|
|
95
|
+
export type ViewRole =
|
|
96
|
+
'label' | 'muted' | 'rule' | 'direct' | 'closure' | 'unmirrored'
|
|
97
|
+
| 'upward' | 'repeat' | 'bar' | 'hole'
|
|
98
|
+
|
|
99
|
+
// `none` is plain characters, and an SVG carrying its classes but not
|
|
100
|
+
// the embedded stylesheet -- what a host page wants once it has bound
|
|
101
|
+
// the variables and is embedding eight figures. `ansi` is the text
|
|
102
|
+
// profile's mechanism and `css` the SVG's; asking for either on the
|
|
103
|
+
// wrong profile is a usage error.
|
|
104
|
+
//
|
|
105
|
+
// `auto` IS NOT HERE ON PURPOSE. Resolving it means knowing whether
|
|
106
|
+
// the destination is a terminal, which err.ts already settles for the
|
|
107
|
+
// error frames: a library cannot see its destination and a caller who
|
|
108
|
+
// can is the only one who may decide. The CLI maps `auto`; `viewOf`
|
|
109
|
+
// takes a resolved value, so every shared-spec row is deterministic.
|
|
110
|
+
export type ViewStyle = 'none' | 'ansi' | 'css'
|
|
111
|
+
|
|
112
|
+
// The text profile's mechanism: the eight named colours, `bold` and
|
|
113
|
+
// `dim`, and nothing else. `label` is unstyled -- an entity's own name
|
|
114
|
+
// is the figure's content, not a mark about it.
|
|
115
|
+
const SGR: Record<ViewRole, string> = {
|
|
116
|
+
label: '', muted: '2', rule: '2', direct: '1', closure: '36',
|
|
117
|
+
unmirrored: '33', upward: '31', repeat: '2', bar: '36', hole: '2',
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A painter wraps a run of text in its role's mechanism. It NEVER
|
|
121
|
+
// changes the run's length in characters, so every width the renderers
|
|
122
|
+
// computed from the unpainted strings still holds.
|
|
123
|
+
type Paint = (role: ViewRole, text: string) => string
|
|
124
|
+
|
|
125
|
+
const PLAIN: Paint = (_role, text) => text
|
|
126
|
+
|
|
127
|
+
const ANSI: Paint = (role, text) =>
|
|
128
|
+
'' === SGR[role] || '' === text
|
|
129
|
+
? text : `\x1b[${SGR[role]}m${text}\x1b[0m`
|
|
130
|
+
|
|
131
|
+
const painter = (style: ViewStyle): Paint => 'ansi' === style ? ANSI : PLAIN
|
|
132
|
+
|
|
133
|
+
// The style a figure gets when the caller named none. An SVG carries
|
|
134
|
+
// its stylesheet, which is what makes it standalone and what every
|
|
135
|
+
// pinned golden holds; everything else carries no mechanism, since a
|
|
136
|
+
// library cannot see whether its output is a terminal.
|
|
137
|
+
const styleOf = (
|
|
138
|
+
style: ViewStyle | undefined, as: ViewProfile
|
|
139
|
+
): ViewStyle => style ?? ('svg' === as ? 'css' : 'none')
|
|
140
|
+
|
|
78
141
|
// One row of the loss report.
|
|
79
142
|
export type ViewLoss = {
|
|
80
143
|
code: string
|
|
@@ -190,6 +253,10 @@ export type ViewOptions = {
|
|
|
190
253
|
sets?: string
|
|
191
254
|
member?: string
|
|
192
255
|
universe?: string
|
|
256
|
+
// doc: how many levels of key below the anchor to draw. Absent
|
|
257
|
+
// means three, which is the depth at which a model's shape is
|
|
258
|
+
// legible and its data is not yet enumerated.
|
|
259
|
+
depth?: number
|
|
193
260
|
// sets: drop intersections below this degree.
|
|
194
261
|
minDegree?: number
|
|
195
262
|
// sets, layers: elide columns beyond this many, counted in the loss
|
|
@@ -208,6 +275,14 @@ export type ViewOptions = {
|
|
|
208
275
|
// document, only once every figure of the set rendered.
|
|
209
276
|
out?: string
|
|
210
277
|
|
|
278
|
+
// How the figure is styled (VIEWS.0.md, "7. Styling"). Absent means
|
|
279
|
+
// `none`: plain characters, and an SVG carrying its classes without
|
|
280
|
+
// the embedded stylesheet. THE CALLER RESOLVES `auto` -- see
|
|
281
|
+
// ViewStyle. A figure written to a file is written plain whatever
|
|
282
|
+
// this says, which the CLI enforces: a pinned golden with terminal
|
|
283
|
+
// escapes in it is not a golden anybody can read.
|
|
284
|
+
style?: ViewStyle
|
|
285
|
+
|
|
211
286
|
// The VIEW DOCUMENT (VIEWS.0.md, "6. The view document"): the path of
|
|
212
287
|
// a map whose values declare figures. `viewSet` reads it; `view`
|
|
213
288
|
// ignores it, because one call draws one figure.
|
|
@@ -219,6 +294,7 @@ export type ViewOptions = {
|
|
|
219
294
|
// global default, because there is no sensible text form of a
|
|
220
295
|
// node-link drawing and no sensible Mermaid form of a matrix.
|
|
221
296
|
const PROFILES: Record<ViewKind, ViewProfile[]> = {
|
|
297
|
+
doc: ['text', 'svg'],
|
|
222
298
|
tree: ['text', 'svg'],
|
|
223
299
|
matrix: ['text', 'svg'],
|
|
224
300
|
graph: ['mermaid', 'dot', 'er'],
|
|
@@ -229,6 +305,14 @@ const PROFILES: Record<ViewKind, ViewProfile[]> = {
|
|
|
229
305
|
poset: ['mermaid', 'dot'],
|
|
230
306
|
}
|
|
231
307
|
|
|
308
|
+
// The profile a kind draws into when none is asked for. The CLI needs
|
|
309
|
+
// it to resolve `--style auto` BEFORE the library runs, since the
|
|
310
|
+
// mechanism is the profile's.
|
|
311
|
+
export function viewDefaultProfile(kind: ViewKind): ViewProfile | undefined {
|
|
312
|
+
return PROFILES[kind]?.[0]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
232
316
|
// Loss codes that describe the drawing rather than a gap in it.
|
|
233
317
|
const INFORMATIONAL = ['edges_deduped', 'inverse_suppressed', 'crossings']
|
|
234
318
|
|
|
@@ -564,11 +648,18 @@ const svgEsc = (s: string): string => escape(s, SVG_ESC)
|
|
|
564
648
|
|
|
565
649
|
// The document: a viewBox the size of the figure, the style, and the
|
|
566
650
|
// parts, one per line, so the bytes read as a figure and diff as one.
|
|
567
|
-
function svgDoc(
|
|
651
|
+
function svgDoc(
|
|
652
|
+
w: number, h: number, about: string, parts: string[], style: ViewStyle
|
|
653
|
+
): string {
|
|
654
|
+
// The CLASSES are structure and are always written -- a rect that
|
|
655
|
+
// does not say whether it is a direct cell or a closure cell is not
|
|
656
|
+
// a figure. What `--style none` drops is the STYLESHEET, for a host
|
|
657
|
+
// page that has already bound the variables and would otherwise
|
|
658
|
+
// carry one copy of these rules per embedded figure.
|
|
568
659
|
return [
|
|
569
660
|
`<svg xmlns="http://www.w3.org/2000/svg" class="av" viewBox="0 0 ${w} ${h}" ` +
|
|
570
661
|
`width="${w}" height="${h}" role="img" aria-label="${svgEsc(about)}">`,
|
|
571
|
-
SVG_STYLE,
|
|
662
|
+
...('css' === style ? [SVG_STYLE] : []),
|
|
572
663
|
...parts,
|
|
573
664
|
'</svg>',
|
|
574
665
|
].join('\n')
|
|
@@ -688,8 +779,9 @@ type TreeRow = { depth: number, text: string, mark: string, parent: number }
|
|
|
688
779
|
|
|
689
780
|
function drawTree(
|
|
690
781
|
all: Drawn[], relation: string | undefined, roots: string[], max: number,
|
|
691
|
-
as: ViewProfile
|
|
782
|
+
as: ViewProfile, style: ViewStyle
|
|
692
783
|
): Figure {
|
|
784
|
+
const paint = painter(style)
|
|
693
785
|
// With a relation named, the tree is OVER THAT RELATION. A node-link
|
|
694
786
|
// diagram can label each edge and so draw every relation at once; a
|
|
695
787
|
// tree cannot without becoming unreadable, and walking two relations
|
|
@@ -785,7 +877,8 @@ function drawTree(
|
|
|
785
877
|
const grown = 0 < (kids.get(edge.to) as Kid[]).length
|
|
786
878
|
const text = label(edge.to) + (many ? ' (' + edge.label + ')' : '')
|
|
787
879
|
const mark = loop ? ' (cycle)' : (seen && grown ? ' (*)' : '')
|
|
788
|
-
out.push(frame.prefix + (last ? '└── ' : '├── ')
|
|
880
|
+
out.push(paint('rule', frame.prefix + (last ? '└── ' : '├── '))
|
|
881
|
+
+ text + paint('repeat', mark))
|
|
789
882
|
rows.push({ depth: stack.length, text, mark, parent: frame.row })
|
|
790
883
|
if (loop || seen) {
|
|
791
884
|
continue
|
|
@@ -820,7 +913,11 @@ function drawTree(
|
|
|
820
913
|
}
|
|
821
914
|
}
|
|
822
915
|
|
|
823
|
-
return {
|
|
916
|
+
return {
|
|
917
|
+
text: 'svg' === as
|
|
918
|
+
? treeSvg(rows, `Dependency tree: ${nodes.length} nodes`, style)
|
|
919
|
+
: out.join('\n'),
|
|
920
|
+
}
|
|
824
921
|
}
|
|
825
922
|
|
|
826
923
|
|
|
@@ -828,7 +925,9 @@ function drawTree(
|
|
|
828
925
|
// depth, joined to its parent by a path that drops from the parent's
|
|
829
926
|
// row and turns in to the child. The marks are muted text after the
|
|
830
927
|
// label.
|
|
831
|
-
function treeSvg(
|
|
928
|
+
function treeSvg(
|
|
929
|
+
rows: (TreeRow | null)[], about: string, style: ViewStyle
|
|
930
|
+
): string {
|
|
832
931
|
const U = 24
|
|
833
932
|
const parts: string[] = []
|
|
834
933
|
let width = 0
|
|
@@ -848,11 +947,162 @@ function treeSvg(rows: (TreeRow | null)[], count: number): string {
|
|
|
848
947
|
`</tspan><tspan class="av-m">${svgEsc(r.mark)}</tspan></text>`)
|
|
849
948
|
width = Math.max(width, x + (r.text.length + r.mark.length) * CH)
|
|
850
949
|
})
|
|
851
|
-
return svgDoc(width + PAD, rows.length * LH + PAD,
|
|
852
|
-
`Dependency tree: ${count} nodes`, parts)
|
|
950
|
+
return svgDoc(width + PAD, rows.length * LH + PAD, about, parts, style)
|
|
853
951
|
}
|
|
854
952
|
|
|
855
953
|
|
|
954
|
+
// ---------------------------------------------------------------------
|
|
955
|
+
// The document tree
|
|
956
|
+
|
|
957
|
+
// THE SHAPE OF THE MODEL ITSELF, which no other kind draws. Every
|
|
958
|
+
// other figure here reads a REPORT -- the edge set, the provenance
|
|
959
|
+
// record, the subsumption order -- and so can only draw a document
|
|
960
|
+
// that has links, contributions or peers. A reader meeting a model for
|
|
961
|
+
// the first time wants the plainer thing first: what is in it, and how
|
|
962
|
+
// it is arranged.
|
|
963
|
+
//
|
|
964
|
+
// This is `get --keys --types` as a picture, and it reads the same
|
|
965
|
+
// walk: map keys in code-point order, list indices in order, and a
|
|
966
|
+
// leaf's KIND rather than its value -- the canon of a scalar's type,
|
|
967
|
+
// not the scalar. Values are what the document is for; the shape is
|
|
968
|
+
// what a reader needs before any of them mean anything.
|
|
969
|
+
//
|
|
970
|
+
// DEPTH IS A BOUND, NOT AN ELISION MARK. Below it the subtree is not
|
|
971
|
+
// drawn and the row says how many keys were not drawn, because a tree
|
|
972
|
+
// that stops without saying so is the one thing a structural drawing
|
|
973
|
+
// must not be.
|
|
974
|
+
|
|
975
|
+
const DEFAULT_DOC_DEPTH = 3
|
|
976
|
+
|
|
977
|
+
// A node's own children, as the anchor walk sees them: map keys sorted
|
|
978
|
+
// by code point, list indices in order, and nothing for a leaf.
|
|
979
|
+
function docKids(v: any): string[] {
|
|
980
|
+
const node: any = throughDoc(v)
|
|
981
|
+
if (true === node?.isMap) {
|
|
982
|
+
// AN ALIAS DECLARATION IS NOT PART OF THE DOCUMENT
|
|
983
|
+
// (docs/reference-language.md, "Aliases"): it does not generate
|
|
984
|
+
// and it does not appear in canon. It IS a key of the root map in
|
|
985
|
+
// the value tree, which `get --keys` reports and this does not --
|
|
986
|
+
// a figure of the document's shape that showed `%Cents` beside
|
|
987
|
+
// `customers` would be drawing the declaration as data
|
|
988
|
+
// (use-cases/BUGS.md 74).
|
|
989
|
+
return Object.keys(node.peg)
|
|
990
|
+
.filter((k) => !k.startsWith('%')).sort(cmpCodePoint)
|
|
991
|
+
}
|
|
992
|
+
if (true === node?.isList) {
|
|
993
|
+
return Object.keys(node.peg).filter((k) => /^[0-9]+$/.test(k))
|
|
994
|
+
}
|
|
995
|
+
return []
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// A preference wraps its value without being a level of its own, and
|
|
999
|
+
// `anchorAt` already steps through a sizing residue; this is the same
|
|
1000
|
+
// unwrapping, for the shape walk.
|
|
1001
|
+
function throughDoc(v: any): any {
|
|
1002
|
+
// Every caller reaches this with a Val the anchor walk handed over,
|
|
1003
|
+
// so the node is never absent and the optional chain that would say
|
|
1004
|
+
// otherwise is an arm no test can take.
|
|
1005
|
+
const node = throughResidue(v)
|
|
1006
|
+
return true === node.isPref ? throughDoc(node.peg) : node
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// What a leaf IS, in one short word: its canon, which for a constraint
|
|
1010
|
+
// is the constraint and for a scalar its value. Long canons are cut,
|
|
1011
|
+
// since the figure is the shape and not the data.
|
|
1012
|
+
function docLeaf(v: any): string {
|
|
1013
|
+
// A CONTAINER WITH NOTHING IN IT IS NOT A LEAF, and calling it one
|
|
1014
|
+
// by writing nothing after the key would make it read as a value the
|
|
1015
|
+
// figure declined to describe. Its canon says what it is -- `{}`,
|
|
1016
|
+
// `[]`, or a template a spread wrote and no member filled.
|
|
1017
|
+
//
|
|
1018
|
+
// `canon` is a string on every Val, so there is no other-type arm to
|
|
1019
|
+
// take; the cut is the only decision here.
|
|
1020
|
+
const canon: string = throughDoc(v).canon
|
|
1021
|
+
return 32 < canon.length ? canon.slice(0, 29) + '...' : canon
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
function drawDoc(
|
|
1026
|
+
root: any,
|
|
1027
|
+
o: { at?: string, depth?: number, as: ViewProfile, style: ViewStyle },
|
|
1028
|
+
max: number, loss: ViewLoss[]
|
|
1029
|
+
): Figure {
|
|
1030
|
+
const paint = painter(o.style)
|
|
1031
|
+
const at = o.at ?? '$'
|
|
1032
|
+
const anchor = anchorAt(root, at)
|
|
1033
|
+
if (null == anchor) {
|
|
1034
|
+
return {
|
|
1035
|
+
// The same code and the same sentence `get` answers with: the
|
|
1036
|
+
// question is identical, so a caller that already handles one
|
|
1037
|
+
// handles the other.
|
|
1038
|
+
errors: [finding('no_path', 'reference', at,
|
|
1039
|
+
`The path ${at} names nothing in this document.`)],
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const depth = o.depth ?? DEFAULT_DOC_DEPTH
|
|
1043
|
+
const out: string[] = []
|
|
1044
|
+
const rows: (TreeRow | null)[] = []
|
|
1045
|
+
let elided = 0
|
|
1046
|
+
|
|
1047
|
+
out.push(at)
|
|
1048
|
+
rows.push({ depth: 0, text: at, mark: '', parent: 0 })
|
|
1049
|
+
|
|
1050
|
+
// ITERATIVE, like the dependency tree's walk and for the same
|
|
1051
|
+
// reason: a deep model is a real shape, and the drawing of one must
|
|
1052
|
+
// not depend on how deep the interpreter lets a recursion go.
|
|
1053
|
+
type Frame = { node: any, kids: string[], at: number, prefix: string, row: number }
|
|
1054
|
+
const stack: Frame[] = [
|
|
1055
|
+
{ node: anchor, kids: docKids(anchor), at: 0, prefix: '', row: 0 },
|
|
1056
|
+
]
|
|
1057
|
+
while (0 < stack.length) {
|
|
1058
|
+
const frame = stack[stack.length - 1]
|
|
1059
|
+
if (frame.at >= frame.kids.length) {
|
|
1060
|
+
stack.pop()
|
|
1061
|
+
continue
|
|
1062
|
+
}
|
|
1063
|
+
const key = frame.kids[frame.at++]
|
|
1064
|
+
const last = frame.at === frame.kids.length
|
|
1065
|
+
const child = throughDoc(throughDoc(frame.node).peg[key])
|
|
1066
|
+
const kids = docKids(child)
|
|
1067
|
+
const under = stack.length < depth
|
|
1068
|
+
// A container the depth bound stops at says how many keys are not
|
|
1069
|
+
// drawn; a leaf says what it is.
|
|
1070
|
+
// A leaf says what it is and a stopped container says how many
|
|
1071
|
+
// keys it holds; both are written after the key with one space,
|
|
1072
|
+
// and neither is ever empty (a canon has at least one character).
|
|
1073
|
+
const mark = 0 === kids.length ? ' ' + docLeaf(child)
|
|
1074
|
+
: under ? '' : ` (${kids.length})`
|
|
1075
|
+
if (0 < kids.length && !under) {
|
|
1076
|
+
elided += kids.length
|
|
1077
|
+
}
|
|
1078
|
+
out.push(paint('rule', frame.prefix + (last ? '└── ' : '├── ')) + key +
|
|
1079
|
+
paint('muted', mark))
|
|
1080
|
+
rows.push({ depth: stack.length, text: key, mark, parent: frame.row })
|
|
1081
|
+
if (max < rows.length) {
|
|
1082
|
+
return {
|
|
1083
|
+
errors: [rowsFinding(rows.length, max, '--at or --depth')],
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (0 < kids.length && under) {
|
|
1087
|
+
stack.push({
|
|
1088
|
+
node: child, kids, at: 0,
|
|
1089
|
+
prefix: frame.prefix + (last ? ' ' : '│ '),
|
|
1090
|
+
row: rows.length - 1,
|
|
1091
|
+
})
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (0 < elided) {
|
|
1095
|
+
loss.push({ code: 'depth_elided', count: elided })
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
text: 'svg' === o.as
|
|
1099
|
+
? treeSvg(rows,
|
|
1100
|
+
`Document tree at ${at}: ${rows.length - 1} keys to depth ${depth}`,
|
|
1101
|
+
o.style)
|
|
1102
|
+
: out.join('\n'),
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
856
1106
|
// ---------------------------------------------------------------------
|
|
857
1107
|
// The matrix (Ghoniem et al. 2004; Sangal et al. 2005)
|
|
858
1108
|
|
|
@@ -925,9 +1175,13 @@ function pickRelation(
|
|
|
925
1175
|
|
|
926
1176
|
function drawMatrix(
|
|
927
1177
|
triples: Triple[], decls: RelDecls,
|
|
928
|
-
o: {
|
|
1178
|
+
o: {
|
|
1179
|
+
relation?: string, order: ViewOrder, closure: boolean, as: ViewProfile,
|
|
1180
|
+
style: ViewStyle,
|
|
1181
|
+
},
|
|
929
1182
|
max: number, loss: ViewLoss[]
|
|
930
1183
|
): Figure {
|
|
1184
|
+
const paint = painter(o.style)
|
|
931
1185
|
const picked = pickRelation(o.relation, keysOf(triples))
|
|
932
1186
|
if (undefined !== picked.error) {
|
|
933
1187
|
return { errors: [picked.error] }
|
|
@@ -972,7 +1226,7 @@ function drawMatrix(
|
|
|
972
1226
|
// column stays one character wide.
|
|
973
1227
|
for (let d = 0; d < iw; d++) {
|
|
974
1228
|
lines.push(' '.repeat(w + 1 + iw + 1) +
|
|
975
|
-
idx.map((s) => lpad(s, iw)[d]).join(' '))
|
|
1229
|
+
paint('muted', idx.map((s) => lpad(s, iw)[d]).join(' ')))
|
|
976
1230
|
}
|
|
977
1231
|
|
|
978
1232
|
let above = 0
|
|
@@ -992,15 +1246,16 @@ function drawMatrix(
|
|
|
992
1246
|
})
|
|
993
1247
|
grid.push(cells)
|
|
994
1248
|
lines.push(
|
|
995
|
-
pad(label(r), w) + ' ' + lpad(idx[ri], iw) + ' ' +
|
|
1249
|
+
pad(label(r), w) + ' ' + paint('muted', lpad(idx[ri], iw)) + ' ' +
|
|
1250
|
+
cells.map((g) => paint(CELL_ROLE[g], g)).join(' '))
|
|
996
1251
|
})
|
|
997
1252
|
const footer = `# above-diagonal direct cells: ${above}`
|
|
998
|
-
lines.push(footer)
|
|
1253
|
+
lines.push(paint('muted', footer))
|
|
999
1254
|
if ('svg' === o.as) {
|
|
1000
1255
|
return {
|
|
1001
1256
|
text: matrixSvg(order.map(label), idx, grid, footer,
|
|
1002
1257
|
`Dependency matrix${over(relation)}: ${order.length} rows, ` +
|
|
1003
|
-
`${above} direct cells above the diagonal
|
|
1258
|
+
`${above} direct cells above the diagonal`, o.style),
|
|
1004
1259
|
}
|
|
1005
1260
|
}
|
|
1006
1261
|
return { text: lines.join('\n') }
|
|
@@ -1014,9 +1269,19 @@ const CELL_CLASS: Record<string, string> = {
|
|
|
1014
1269
|
'.': 'av-cell', '\\': 'av-cell',
|
|
1015
1270
|
}
|
|
1016
1271
|
|
|
1272
|
+
// The same five states as ROLES, for the text profile. One table per
|
|
1273
|
+
// mechanism rather than one shared one, because the two vocabularies
|
|
1274
|
+
// are not in step: SVG needs a class for the empty cell (it draws a
|
|
1275
|
+
// rect there) and the text profile has nothing to say about a `.`
|
|
1276
|
+
// beyond that it is not a mark.
|
|
1277
|
+
const CELL_ROLE: Record<string, ViewRole> = {
|
|
1278
|
+
X: 'direct', '!': 'unmirrored', '+': 'closure',
|
|
1279
|
+
'.': 'muted', '\\': 'rule',
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1017
1282
|
function matrixSvg(
|
|
1018
1283
|
labels: string[], idx: string[], grid: string[][], footer: string,
|
|
1019
|
-
about: string
|
|
1284
|
+
about: string, style: ViewStyle
|
|
1020
1285
|
): string {
|
|
1021
1286
|
const S = 20
|
|
1022
1287
|
const w = widest(labels)
|
|
@@ -1042,7 +1307,7 @@ function matrixSvg(
|
|
|
1042
1307
|
const n = labels.length
|
|
1043
1308
|
parts.push(svgText(4, y0 + n * S + 16, 'av-m', footer))
|
|
1044
1309
|
const width = Math.max(gutter + n * S, 4 + footer.length * CH) + PAD
|
|
1045
|
-
return svgDoc(width, y0 + n * S + LH + PAD, about, parts)
|
|
1310
|
+
return svgDoc(width, y0 + n * S + LH + PAD, about, parts, style)
|
|
1046
1311
|
}
|
|
1047
1312
|
|
|
1048
1313
|
|
|
@@ -1272,7 +1537,7 @@ function drawLayer(
|
|
|
1272
1537
|
triples: Triple[], root: any,
|
|
1273
1538
|
o: {
|
|
1274
1539
|
relation?: string, groupBy?: string, layers: string[],
|
|
1275
|
-
edges?: ViewEdges, as: ViewProfile,
|
|
1540
|
+
edges?: ViewEdges, as: ViewProfile, style: ViewStyle,
|
|
1276
1541
|
},
|
|
1277
1542
|
max: number, loss: ViewLoss[]
|
|
1278
1543
|
): Figure {
|
|
@@ -1400,20 +1665,35 @@ function drawLayer(
|
|
|
1400
1665
|
: `${upward} upward edges`
|
|
1401
1666
|
return {
|
|
1402
1667
|
text: layerSvg(bands, shown, footer,
|
|
1403
|
-
`Architecture layers${over(relation)}: ${bands.length} bands, ${drew}
|
|
1668
|
+
`Architecture layers${over(relation)}: ${bands.length} bands, ${drew}`,
|
|
1669
|
+
o.style),
|
|
1404
1670
|
}
|
|
1405
1671
|
}
|
|
1406
1672
|
if ('text' === o.as) {
|
|
1673
|
+
const paint = painter(o.style)
|
|
1407
1674
|
const w = widest(bands.map((b) => b.name))
|
|
1408
1675
|
const rows = bands.map((b) =>
|
|
1409
|
-
pad(b.name, w) + ' ' +
|
|
1410
|
-
|
|
1411
|
-
const
|
|
1676
|
+
paint('muted', pad(b.name, w)) + ' ' +
|
|
1677
|
+
b.nodes.map((n) => n.label).join(' '))
|
|
1678
|
+
const inner = widest(bands.map((b) =>
|
|
1679
|
+
pad(b.name, w) + ' ' + b.nodes.map((n) => n.label).join(' ')))
|
|
1680
|
+
const rule = paint('rule', '+' + '-'.repeat(inner + 2) + '+')
|
|
1412
1681
|
out.push(rule)
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1682
|
+
rows.forEach((row, i) => {
|
|
1683
|
+
// The row was padded from its UNPAINTED width, which the band
|
|
1684
|
+
// name's escapes do not change; `pad` would count them, so the
|
|
1685
|
+
// padding is computed here and appended.
|
|
1686
|
+
const bare = pad(bands[i].name, w) + ' ' +
|
|
1687
|
+
bands[i].nodes.map((n) => n.label).join(' ')
|
|
1688
|
+
out.push(paint('rule', '|') + ' ' + row +
|
|
1689
|
+
' '.repeat(inner - bare.length) + ' ' + paint('rule', '|'), rule)
|
|
1690
|
+
})
|
|
1691
|
+
// The first footer line counts; the rest name one edge each, and
|
|
1692
|
+
// an upward edge is the violation the bands cannot show.
|
|
1693
|
+
out.push(paint('muted', footer[0]))
|
|
1694
|
+
footer.slice(1).forEach((f, i) => {
|
|
1695
|
+
out.push(paint('upward' === shown[i].way ? 'upward' : 'muted', f))
|
|
1696
|
+
})
|
|
1417
1697
|
}
|
|
1418
1698
|
else {
|
|
1419
1699
|
const esc = (s: string): string => escape(s, MERMAID_ESC)
|
|
@@ -1448,7 +1728,8 @@ type Drawing = { edge: GEdge, way: 'downward' | 'sideways' | 'upward' }
|
|
|
1448
1728
|
// same line and a straight edge between them would cross whatever
|
|
1449
1729
|
// stands between.
|
|
1450
1730
|
function layerSvg(
|
|
1451
|
-
bands: Band[], shown: Drawing[], footer: string[], about: string
|
|
1731
|
+
bands: Band[], shown: Drawing[], footer: string[], about: string,
|
|
1732
|
+
style: ViewStyle
|
|
1452
1733
|
): string {
|
|
1453
1734
|
const BH = 44
|
|
1454
1735
|
const gutter = widest(bands.map((b) => b.name)) * CH + 16
|
|
@@ -1512,7 +1793,7 @@ function layerSvg(
|
|
|
1512
1793
|
footer.forEach((f, i) => {
|
|
1513
1794
|
parts.push(svgText(4, y1 + i * LH + 14, 'av-m', f))
|
|
1514
1795
|
})
|
|
1515
|
-
return svgDoc(width, y1 + footer.length * LH + PAD, about, parts)
|
|
1796
|
+
return svgDoc(width, y1 + footer.length * LH + PAD, about, parts, style)
|
|
1516
1797
|
}
|
|
1517
1798
|
|
|
1518
1799
|
|
|
@@ -1566,38 +1847,47 @@ function columnsOf(
|
|
|
1566
1847
|
}
|
|
1567
1848
|
|
|
1568
1849
|
|
|
1569
|
-
function renderPanel(p: Panel): string {
|
|
1850
|
+
function renderPanel(p: Panel, style: ViewStyle): string {
|
|
1851
|
+
const paint = painter(style)
|
|
1570
1852
|
const w = widest(p.names)
|
|
1571
|
-
const out: string[] = [p.header, '']
|
|
1853
|
+
const out: string[] = [paint('muted', p.header), '']
|
|
1572
1854
|
const most = p.sizes.reduce((m, n) => Math.max(m, n), 0)
|
|
1573
1855
|
p.names.forEach((n, i) => {
|
|
1856
|
+
// The bar is padded to `most` from its own length, so the pad is
|
|
1857
|
+
// written outside the painted run rather than counted inside it.
|
|
1858
|
+
const bar = '#'.repeat(p.sizes[i])
|
|
1574
1859
|
out.push(pad(n, w) + ' ' +
|
|
1575
|
-
(p.bars ?
|
|
1576
|
-
String(p.sizes[i]))
|
|
1860
|
+
(p.bars ? paint('bar', bar) + ' '.repeat(most - bar.length) + ' ' : '') +
|
|
1861
|
+
paint('muted', String(p.sizes[i])))
|
|
1577
1862
|
})
|
|
1578
1863
|
out.push('')
|
|
1579
1864
|
p.names.forEach((n, i) => {
|
|
1580
|
-
out.push(pad(n, w) + ' | ' +
|
|
1581
|
-
p.cols.map((c) => c.sig[i] ? '*' : '
|
|
1865
|
+
out.push(pad(n, w) + ' ' + paint('rule', '|') + ' ' +
|
|
1866
|
+
p.cols.map((c) => c.sig[i] ? paint('direct', '*') : paint('hole', '.'))
|
|
1867
|
+
.join(' '))
|
|
1582
1868
|
})
|
|
1583
|
-
out.push(pad('', w) + ' +' + '-'.repeat(2 * p.cols.length))
|
|
1869
|
+
out.push(pad('', w) + ' ' + paint('rule', '+' + '-'.repeat(2 * p.cols.length)))
|
|
1584
1870
|
if (p.bars) {
|
|
1585
1871
|
const tallest = p.cols.reduce((m, c) => Math.max(m, c.items.length), 0)
|
|
1586
|
-
// The bars, tallest column first; a line ends at its last bar.
|
|
1872
|
+
// The bars, tallest column first; a line ends at its last bar. The
|
|
1873
|
+
// trailing blanks are trimmed BEFORE painting, so an escape can
|
|
1874
|
+
// never be what the trim leaves behind.
|
|
1587
1875
|
for (let h = tallest; 0 < h; h--) {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1876
|
+
const cells = p.cols.map((c) => h <= c.items.length ? ' #' : ' ')
|
|
1877
|
+
.join('').replace(/ +$/, '')
|
|
1878
|
+
out.push(pad('', w) + ' ' + paint('rule', '|') +
|
|
1879
|
+
cells.replace(/#/g, () => paint('bar', '#')))
|
|
1591
1880
|
}
|
|
1592
1881
|
}
|
|
1593
1882
|
out.push(pad('', w) + ' ' +
|
|
1594
|
-
p.cols.map((c) => String(c.items.length)).join(' '))
|
|
1883
|
+
paint('muted', p.cols.map((c) => String(c.items.length)).join(' ')))
|
|
1595
1884
|
out.push('')
|
|
1596
1885
|
p.cols.forEach((c, i) => {
|
|
1597
1886
|
const shown = 4 < c.items.length && !p.bars
|
|
1598
1887
|
? c.items.slice(0, 3).join(' ') + ' ...' : c.items.join(' ')
|
|
1599
|
-
out.push(
|
|
1600
|
-
|
|
1888
|
+
out.push(paint('muted',
|
|
1889
|
+
` col ${i + 1}${p.bars ? '' : ` (${c.items.length})`}:`) +
|
|
1890
|
+
` ${shown}` + (c.sig.some((b) => b) ? '' : paint('muted', p.none)))
|
|
1601
1891
|
})
|
|
1602
1892
|
return out.join('\n')
|
|
1603
1893
|
}
|
|
@@ -1606,7 +1896,7 @@ function renderPanel(p: Panel): string {
|
|
|
1606
1896
|
// The panel as SVG: the set sizes as bars, the intersections as a dot
|
|
1607
1897
|
// matrix (a filled dot where the set lies in the column), the column
|
|
1608
1898
|
// cardinalities as bars under it, and the columns' elements as text.
|
|
1609
|
-
function panelSvg(p: Panel, about: string): string {
|
|
1899
|
+
function panelSvg(p: Panel, about: string, style: ViewStyle): string {
|
|
1610
1900
|
const w = widest(p.names)
|
|
1611
1901
|
const most = p.sizes.reduce((m, n) => Math.max(m, n), 0)
|
|
1612
1902
|
const parts: string[] = [svgText(4, 14, 'av-m', p.header)]
|
|
@@ -1651,7 +1941,7 @@ function panelSvg(p: Panel, about: string): string {
|
|
|
1651
1941
|
const width = Math.max(gx + p.cols.length * 20,
|
|
1652
1942
|
gx + (p.bars ? most * 10 + 8 : 0) + 3 * CH,
|
|
1653
1943
|
4 + widest(lines) * CH, 4 + p.header.length * CH) + PAD
|
|
1654
|
-
return svgDoc(width, yI + lines.length * LH + PAD, about, parts)
|
|
1944
|
+
return svgDoc(width, yI + lines.length * LH + PAD, about, parts, style)
|
|
1655
1945
|
}
|
|
1656
1946
|
|
|
1657
1947
|
|
|
@@ -1695,7 +1985,7 @@ function drawSets(
|
|
|
1695
1985
|
gen: any,
|
|
1696
1986
|
o: {
|
|
1697
1987
|
sets: string, member: string, universe?: string,
|
|
1698
|
-
minDegree?: number, maxCols?: number, as: ViewProfile,
|
|
1988
|
+
minDegree?: number, maxCols?: number, as: ViewProfile, style: ViewStyle,
|
|
1699
1989
|
},
|
|
1700
1990
|
max: number, loss: ViewLoss[]
|
|
1701
1991
|
): Figure {
|
|
@@ -1772,8 +2062,8 @@ function drawSets(
|
|
|
1772
2062
|
return {
|
|
1773
2063
|
text: 'svg' === o.as
|
|
1774
2064
|
? panelSvg(panel, `Set panel over ${o.sets}: ${names.length} sets, ` +
|
|
1775
|
-
`${elements.size} elements, ${cols.length} intersections
|
|
1776
|
-
: renderPanel(panel),
|
|
2065
|
+
`${elements.size} elements, ${cols.length} intersections`, o.style)
|
|
2066
|
+
: renderPanel(panel, o.style),
|
|
1777
2067
|
}
|
|
1778
2068
|
}
|
|
1779
2069
|
|
|
@@ -1791,7 +2081,10 @@ function docName(file: string, entry: string | undefined): string {
|
|
|
1791
2081
|
|
|
1792
2082
|
function drawLayers(
|
|
1793
2083
|
prov: Provenance, root: any, entry: string | undefined,
|
|
1794
|
-
o: {
|
|
2084
|
+
o: {
|
|
2085
|
+
at?: string, minSize?: number, maxCols?: number, as: ViewProfile,
|
|
2086
|
+
style: ViewStyle,
|
|
2087
|
+
},
|
|
1795
2088
|
max: number, loss: ViewLoss[]
|
|
1796
2089
|
): Figure {
|
|
1797
2090
|
// Every path something met at AND THE DOCUMENT HAS A VALUE AT,
|
|
@@ -1849,8 +2142,8 @@ function drawLayers(
|
|
|
1849
2142
|
return {
|
|
1850
2143
|
text: 'svg' === o.as
|
|
1851
2144
|
? panelSvg(panel, `Document layers: ${names.length} documents, ` +
|
|
1852
|
-
`${paths.length} paths, ${cols.length} intersections
|
|
1853
|
-
: renderPanel(panel),
|
|
2145
|
+
`${paths.length} paths, ${cols.length} intersections`, o.style)
|
|
2146
|
+
: renderPanel(panel, o.style),
|
|
1854
2147
|
}
|
|
1855
2148
|
}
|
|
1856
2149
|
|
|
@@ -2209,6 +2502,28 @@ export function view(
|
|
|
2209
2502
|
`profiles: ${profiles.join(', ')}`)],
|
|
2210
2503
|
})
|
|
2211
2504
|
}
|
|
2505
|
+
// ONE MECHANISM PER PROFILE (VIEWS.0.md, "7. Styling"). `ansi` is
|
|
2506
|
+
// the text profile's and `css` the SVG's; asking for one on a
|
|
2507
|
+
// profile that has no way to carry it is a usage error rather than a
|
|
2508
|
+
// silent no-op, so a script that asks for colour and gets none is
|
|
2509
|
+
// told why. `none` is always available -- it is the absence of a
|
|
2510
|
+
// mechanism.
|
|
2511
|
+
const style: ViewStyle = styleOf(options.style, as)
|
|
2512
|
+
const carrier: Record<string, ViewProfile> = { ansi: 'text', css: 'svg' }
|
|
2513
|
+
if (undefined !== carrier[style] && carrier[style] !== as) {
|
|
2514
|
+
return done({
|
|
2515
|
+
errors: [finding('view_style_profile', 'reference', '$',
|
|
2516
|
+
`The ${as} profile cannot carry --style ${style}.`,
|
|
2517
|
+
`${style} is the ${carrier[style]} profile's mechanism`)],
|
|
2518
|
+
})
|
|
2519
|
+
}
|
|
2520
|
+
if (undefined === carrier[style] && 'none' !== style) {
|
|
2521
|
+
return done({
|
|
2522
|
+
errors: [finding('view_style_unknown', 'reference', '$',
|
|
2523
|
+
`${style} is not a style.`, 'styles: auto, none, ansi, css')],
|
|
2524
|
+
})
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2212
2527
|
// Zero means the default, in both ports.
|
|
2213
2528
|
const max = options.maxRows || DEFAULT_MAX_ROWS
|
|
2214
2529
|
|
|
@@ -2249,9 +2564,13 @@ function drawLoaded(
|
|
|
2249
2564
|
kind: ViewKind, as: ViewProfile, options: ViewOptions,
|
|
2250
2565
|
max: number, loss: ViewLoss[]
|
|
2251
2566
|
): Figure {
|
|
2567
|
+
const style = styleOf(options.style, as)
|
|
2568
|
+
if ('doc' === kind) {
|
|
2569
|
+
return drawDoc(root, { ...options, as, style }, max, loss)
|
|
2570
|
+
}
|
|
2252
2571
|
if ('layers' === kind) {
|
|
2253
2572
|
return drawLayers(prov as Provenance, root, options.path,
|
|
2254
|
-
{ ...options, as }, max, loss)
|
|
2573
|
+
{ ...options, as, style }, max, loss)
|
|
2255
2574
|
}
|
|
2256
2575
|
if ('sets' === kind) {
|
|
2257
2576
|
if (undefined === options.sets || undefined === options.member) {
|
|
@@ -2277,7 +2596,7 @@ function drawLoaded(
|
|
|
2277
2596
|
}
|
|
2278
2597
|
return drawSets(value, {
|
|
2279
2598
|
sets: options.sets, member: options.member, universe: options.universe,
|
|
2280
|
-
minDegree: options.minDegree, maxCols: options.maxCols, as,
|
|
2599
|
+
minDegree: options.minDegree, maxCols: options.maxCols, as, style,
|
|
2281
2600
|
}, max, loss)
|
|
2282
2601
|
}
|
|
2283
2602
|
|
|
@@ -2289,7 +2608,7 @@ function drawLoaded(
|
|
|
2289
2608
|
if ('matrix' === kind) {
|
|
2290
2609
|
return drawMatrix(triples, decls, {
|
|
2291
2610
|
relation, order: options.order ?? 'canon', closure: true === options.closure,
|
|
2292
|
-
as,
|
|
2611
|
+
as, style,
|
|
2293
2612
|
}, max, loss)
|
|
2294
2613
|
}
|
|
2295
2614
|
if ('graph' === kind) {
|
|
@@ -2301,11 +2620,11 @@ function drawLoaded(
|
|
|
2301
2620
|
if ('layer' === kind) {
|
|
2302
2621
|
return drawLayer(triples, root, {
|
|
2303
2622
|
relation, groupBy: options.groupBy, layers: options.layers ?? [],
|
|
2304
|
-
edges: options.edges, as,
|
|
2623
|
+
edges: options.edges, as, style,
|
|
2305
2624
|
}, max, loss)
|
|
2306
2625
|
}
|
|
2307
2626
|
return drawTree(
|
|
2308
|
-
collapse(triples, relation), relation, options.roots ?? [], max, as)
|
|
2627
|
+
collapse(triples, relation), relation, options.roots ?? [], max, as, style)
|
|
2309
2628
|
}
|
|
2310
2629
|
|
|
2311
2630
|
|
|
@@ -2341,7 +2660,7 @@ const DECL_ENUM: Record<string, string[]> = {
|
|
|
2341
2660
|
order: ['canon', 'partition'],
|
|
2342
2661
|
edges: ['upward', 'all', 'none'],
|
|
2343
2662
|
}
|
|
2344
|
-
const DECL_COUNT = ['maxRows', 'maxCols', 'minDegree', 'minSize']
|
|
2663
|
+
const DECL_COUNT = ['maxRows', 'maxCols', 'minDegree', 'minSize', 'depth']
|
|
2345
2664
|
const DECL_FLAG = ['closure']
|
|
2346
2665
|
const DECL_LIST = ['roots', 'relations', 'layers']
|
|
2347
2666
|
|