@uwdata/mosaic-spec 0.5.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.
@@ -0,0 +1,34 @@
1
+ import { ASTNode } from './ASTNode.js';
2
+ import { INTERACTOR, SELECT } from '../constants.js';
3
+ import { parseOptions } from './OptionsNode.js';
4
+
5
+ export function parseInteractor(spec, ctx) {
6
+ const { [SELECT]: name, ...options } = spec;
7
+ if (!ctx.plot?.interactors?.has(name)) {
8
+ ctx.error(`Unrecognized interactor type: ${name}`, spec);
9
+ }
10
+ return new PlotInteractorNode(name, parseOptions(options, ctx));
11
+ }
12
+
13
+ export class PlotInteractorNode extends ASTNode {
14
+ constructor(name, options) {
15
+ super(INTERACTOR);
16
+ this.name = name;
17
+ this.options = options;
18
+ }
19
+
20
+ instantiate(ctx) {
21
+ const fn = ctx.api[this.name];
22
+ return fn(this.options.instantiate(ctx));
23
+ }
24
+
25
+ codegen(ctx) {
26
+ const opt = this.options.codegen(ctx);
27
+ return `${ctx.tab()}${ctx.ns()}${this.name}(${opt})`;
28
+ }
29
+
30
+ toJSON() {
31
+ const { name, options } = this;
32
+ return { [SELECT]: name, ...options.toJSON() };
33
+ }
34
+ }
@@ -0,0 +1,36 @@
1
+ import { ASTNode } from './ASTNode.js';
2
+ import { LEGEND } from '../constants.js';
3
+ import { parseOptions } from './OptionsNode.js';
4
+
5
+ export function parseLegend(spec, ctx) {
6
+ const { [LEGEND]: name, ...options } = spec;
7
+ const key = `${name}Legend`;
8
+ if (!ctx.plot?.legends?.has(key)) {
9
+ ctx.error(`Unrecognized legend type: ${name}`, spec);
10
+ }
11
+ return new PlotLegendNode(key, name, parseOptions(options, ctx));
12
+ }
13
+
14
+ export class PlotLegendNode extends ASTNode {
15
+ constructor(key, name, options) {
16
+ super(LEGEND);
17
+ this.key = key;
18
+ this.name = name;
19
+ this.options = options;
20
+ }
21
+
22
+ instantiate(ctx) {
23
+ const fn = ctx.api[this.key];
24
+ return fn(this.options.instantiate(ctx));
25
+ }
26
+
27
+ codegen(ctx) {
28
+ const opt = this.options.codegen(ctx);
29
+ return `${ctx.tab()}${ctx.ns()}${this.key}(${opt})`;
30
+ }
31
+
32
+ toJSON() {
33
+ const { type, name, options } = this;
34
+ return { [type]: name, ...options.toJSON() };
35
+ }
36
+ }
@@ -0,0 +1,75 @@
1
+ import { AGG, MARK, SQL } from '../constants.js';
2
+ import { isObject } from '../util.js';
3
+ import { ASTNode } from './ASTNode.js';
4
+ import { parseExpression } from './ExpressionNode.js';
5
+ import { OptionsNode } from './OptionsNode.js';
6
+ import { parseMarkData } from './PlotFromNode.js';
7
+ import { parseTransform } from './TransformNode.js';
8
+
9
+ function maybeTransform(value, ctx) {
10
+ if (isObject(value)) {
11
+ return (value[SQL] || value[AGG])
12
+ ? parseExpression(value, ctx)
13
+ : parseTransform(value, ctx);
14
+ }
15
+ }
16
+
17
+ export function parseMark(spec, ctx) {
18
+ const { mark, data, ...options } = spec;
19
+ if (!ctx.plot?.marks?.has(mark)) {
20
+ ctx.error(`Unrecognized mark type: ${mark}`, spec);
21
+ }
22
+
23
+ const input = parseMarkData(data, ctx);
24
+
25
+ const opt = {};
26
+ for (const key in options) {
27
+ const value = options[key];
28
+ opt[key] = maybeTransform(value, ctx) || ctx.maybeParam(value);
29
+ }
30
+
31
+ return new PlotMarkNode(mark, input, new OptionsNode(opt));
32
+ }
33
+
34
+ export class PlotMarkNode extends ASTNode {
35
+ constructor(name, data, options) {
36
+ super(MARK);
37
+ this.name = name;
38
+ this.data = data;
39
+ this.options = options;
40
+ }
41
+
42
+ instantiate(ctx) {
43
+ const { name, data, options } = this;
44
+ const fn = ctx.api[name];
45
+ const opt = options.instantiate(ctx);
46
+ return data ? fn(data.instantiate(ctx), opt) : fn(opt);
47
+ }
48
+
49
+ codegen(ctx) {
50
+ const { name, data, options } = this;
51
+ const d = data ? data.codegen(ctx) : '';
52
+ const o = options.codegen(ctx);
53
+
54
+ let arg;
55
+ if (d && o) {
56
+ ctx.indent();
57
+ const opt = options.codegen(ctx);
58
+ arg = `\n${ctx.tab()}${d},\n${ctx.tab()}${opt}\n`;
59
+ ctx.undent();
60
+ arg += ctx.tab();
61
+ } else {
62
+ arg = `${d}${o}`;
63
+ }
64
+ return `${ctx.tab()}${ctx.ns()}${name}(${arg})`;
65
+ }
66
+
67
+ toJSON() {
68
+ const { type, name, data, options } = this;
69
+ return {
70
+ [type]: name,
71
+ ...(data ? { data: data.toJSON() } : {}),
72
+ ...options.toJSON()
73
+ };
74
+ }
75
+ }
@@ -0,0 +1,66 @@
1
+ import { isString } from '../util.js';
2
+ import { ASTNode } from './ASTNode.js';
3
+ import { PLOT } from '../constants.js';
4
+ import { parseAttribute } from './PlotAttributeNode.js';
5
+ import { parseInteractor } from './PlotInteractorNode.js';
6
+ import { parseLegend } from './PlotLegendNode.js';
7
+ import { parseMark } from './PlotMarkNode.js';
8
+
9
+ export function parseTopLevelMark(spec, ctx) {
10
+ return parsePlot({ plot: [ spec ] }, ctx);
11
+ }
12
+
13
+ export function parsePlot(spec, ctx) {
14
+ const { [PLOT]: entries, ...attrs } = spec;
15
+ const attributes = Object.entries(attrs)
16
+ .map(([key, value]) => parseAttribute(key, value, ctx));
17
+
18
+ const children = entries.map(spec => {
19
+ return isString(spec.mark) ? parseMark(spec, ctx)
20
+ : isString(spec.legend) ? parseLegend(spec, ctx)
21
+ : isString(spec.select) ? parseInteractor(spec, ctx)
22
+ : ctx.error(`Invalid plot entry.`, spec);
23
+ });
24
+
25
+ return new PlotNode(children, attributes);
26
+ }
27
+
28
+ export class PlotNode extends ASTNode {
29
+ constructor(children, attributes) {
30
+ super(PLOT, children);
31
+ this.attributes = attributes;
32
+ }
33
+
34
+ instantiate(ctx) {
35
+ const attrs = [
36
+ ...(ctx.plotDefaults || []),
37
+ ...(this.attributes || [])
38
+ ];
39
+ return ctx.api[PLOT](
40
+ this.children.map(c => c.instantiate(ctx)),
41
+ attrs.map(a => a.instantiate(ctx))
42
+ );
43
+ }
44
+
45
+ codegen(ctx) {
46
+ const { type, children, attributes } = this;
47
+ ctx.indent();
48
+ const entries = [
49
+ ...children.map(c => c.codegen(ctx)),
50
+ ...(ctx.plotDefaults?.length ? [`${ctx.tab()}...defaultAttributes`] : []),
51
+ ...attributes.map(a => a.codegen(ctx))
52
+ ].join(',\n');
53
+ ctx.undent();
54
+
55
+ return `${ctx.tab()}${ctx.ns()}${type}(\n${entries}\n${ctx.tab()})`;
56
+ }
57
+
58
+ toJSON() {
59
+ const { type, children, attributes } = this;
60
+ const plot = { [type]: children.map(c => c.toJSON()) };
61
+ for (const a of attributes) {
62
+ Object.assign(plot, a.toJSON());
63
+ }
64
+ return plot;
65
+ }
66
+ }
@@ -0,0 +1,26 @@
1
+ import { ASTNode } from './ASTNode.js';
2
+ import { INTERSECT, SELECTION } from '../constants.js';
3
+
4
+ export class SelectionNode extends ASTNode {
5
+ constructor(select = INTERSECT, cross) {
6
+ super(SELECTION);
7
+ this.select = select;
8
+ this.cross = cross;
9
+ }
10
+
11
+ instantiate(ctx) {
12
+ const { select, cross } = this;
13
+ return ctx.api.Selection[select]({ cross });
14
+ }
15
+
16
+ codegen(ctx) {
17
+ const { select, cross } = this;
18
+ const arg = cross != null ? `{ cross: ${cross} }` : '';
19
+ return `${ctx.ns()}Selection.${select}(${arg})`;
20
+ }
21
+
22
+ toJSON() {
23
+ const { select, cross } = this;
24
+ return { select, cross };
25
+ }
26
+ }
@@ -0,0 +1,47 @@
1
+ import { SPEC } from '../constants.js';
2
+ import { ASTNode } from './ASTNode.js';
3
+
4
+ export class SpecNode extends ASTNode {
5
+ constructor(root, meta, config, data, params, plotDefaults) {
6
+ super(SPEC, [root]);
7
+ this.root = root;
8
+ this.meta = meta;
9
+ this.config = config;
10
+ this.data = data;
11
+ this.params = params;
12
+ this.plotDefaults = plotDefaults;
13
+ }
14
+
15
+ toJSON() {
16
+ const { root, meta, config, plotDefaults } = this;
17
+ const dataDefs = new Map(Object.entries(this.data));
18
+ const paramDefs = new Map(Object.entries(this.params));
19
+ const spec = {};
20
+
21
+ if (meta) spec.meta = { ...meta };
22
+ if (config) spec.config = { ...config };
23
+
24
+ if (dataDefs?.size) {
25
+ const data = spec.data = {};
26
+ for (const [name, node] of dataDefs) {
27
+ data[name] = node.toJSON();
28
+ }
29
+ }
30
+
31
+ if (paramDefs?.size) {
32
+ const params = spec.params = {};
33
+ for (const [name, node] of paramDefs) {
34
+ params[name] = node.toJSON();
35
+ }
36
+ }
37
+
38
+ if (plotDefaults?.length) {
39
+ const defaults = spec.plotDefaults = {};
40
+ for (const node of plotDefaults) {
41
+ Object.assign(defaults, node.toJSON());
42
+ }
43
+ }
44
+
45
+ return Object.assign(spec, root.toJSON());
46
+ }
47
+ }
@@ -0,0 +1,118 @@
1
+ import { ASTNode } from './ASTNode.js';
2
+ import { TRANSFORM } from '../constants.js';
3
+
4
+ function toArray(value) {
5
+ return value == null ? [] : [value].flat();
6
+ }
7
+
8
+ export function parseTransform(spec, ctx) {
9
+ let name;
10
+ for (const key in spec) {
11
+ if (ctx.transforms.has(key)) {
12
+ name = key;
13
+ }
14
+ }
15
+
16
+ if (!name) {
17
+ return; // return undefined to signal no transform!
18
+ }
19
+
20
+ const args = name === 'count' || name == null ? [] : toArray(spec[name]);
21
+ const options = {
22
+ distinct: spec.distinct,
23
+ orderby: toArray(spec.orderby).map(v => ctx.maybeParam(v)),
24
+ partitionby: toArray(spec.partitionby).map(v => ctx.maybeParam(v)),
25
+ rows: spec.rows ? ctx.maybeParam(spec.rows) : null,
26
+ range: spec.range ? ctx.maybeParam(spec.range) : null
27
+ };
28
+
29
+ return new TransformNode(name, args, options);
30
+ }
31
+
32
+ export class TransformNode extends ASTNode {
33
+ constructor(name, args, options) {
34
+ super(TRANSFORM);
35
+ this.name = name;
36
+ this.args = args;
37
+ this.options = options;
38
+ }
39
+
40
+ instantiate(ctx) {
41
+ const { name, args, options } = this;
42
+ const { distinct, orderby, partitionby, rows, range } = options;
43
+
44
+ const fn = ctx.api[name];
45
+ let expr = fn(...args);
46
+ if (distinct) {
47
+ expr = expr.distinct();
48
+ }
49
+ if (orderby.length) {
50
+ expr = expr.orderby(orderby.map(v => v.instantiate(ctx)));
51
+ }
52
+ if (partitionby.length) {
53
+ expr = expr.partitionby(partitionby.map(v => v.instantiate(ctx)));
54
+ }
55
+ if (rows != null) {
56
+ expr = expr.rows(rows.instantiate(ctx));
57
+ } else if (range != null) {
58
+ expr = expr.range(range.instantiate(ctx));
59
+ }
60
+ return expr;
61
+ }
62
+
63
+ codegen(ctx) {
64
+ const { name, args, options } = this;
65
+ const { distinct, orderby, partitionby, rows, range } = options;
66
+
67
+ let str = `${ctx.ns()}${name}(`
68
+ + args.map(v => JSON.stringify(v)).join(', ')
69
+ + ')';
70
+
71
+ if (distinct) {
72
+ str += '.distinct()'
73
+ }
74
+ if (orderby.length) {
75
+ const p = orderby.map(v => v.codegen(ctx));
76
+ str += `.orderby(${p.join(', ')})`;
77
+ }
78
+ if (partitionby.length) {
79
+ const p = partitionby.map(v => v.codegen(ctx));
80
+ str += `.partitionby(${p.join(', ')})`;
81
+ }
82
+ if (rows) {
83
+ str += `.rows(${rows.codegen(ctx)})`;
84
+ } else if (range) {
85
+ str += `.range(${range.codegen(ctx)})`;
86
+ }
87
+
88
+ return str;
89
+ }
90
+
91
+ toJSON() {
92
+ const { name, args, options } = this;
93
+ const { distinct, orderby, partitionby, rows, range } = options;
94
+
95
+ const json = { [name]: simplify(args) };
96
+
97
+ if (distinct) {
98
+ json.distinct = true;
99
+ }
100
+ if (orderby.length) {
101
+ json.orderby = simplify(orderby.map(v => v.toJSON()));
102
+ }
103
+ if (partitionby.length) {
104
+ json.partitionby = simplify(partitionby.map(v => v.toJSON()));
105
+ }
106
+ if (rows) {
107
+ json.rows = rows.toJSON();
108
+ } else if (range) {
109
+ json.range = range.toJSON();
110
+ }
111
+
112
+ return json;
113
+ }
114
+ }
115
+
116
+ function simplify(array) {
117
+ return array.length === 0 ? '' : array.length === 1 ? array[0] : array;
118
+ }
@@ -0,0 +1,28 @@
1
+ import { ASTNode } from './ASTNode.js';
2
+ import { VCONCAT } from '../constants.js';
3
+
4
+ export function parseVConcat(spec, ctx) {
5
+ const children = spec[VCONCAT].map(s => ctx.parseComponent(s));
6
+ return new VConcatNode(children);
7
+ }
8
+
9
+ export class VConcatNode extends ASTNode {
10
+ constructor(children) {
11
+ super(VCONCAT, children);
12
+ }
13
+
14
+ instantiate(ctx) {
15
+ return ctx.api[VCONCAT](this.children.map(c => c.instantiate(ctx)));
16
+ }
17
+
18
+ codegen(ctx) {
19
+ ctx.indent();
20
+ const items = this.children.map(c => c.codegen(ctx));
21
+ ctx.undent();
22
+ return `${ctx.tab()}${ctx.ns()}${this.type}(\n${items.join(',\n')}\n${ctx.tab()})`;
23
+ }
24
+
25
+ toJSON() {
26
+ return { [this.type]: this.children.map(c => c.toJSON()) };
27
+ }
28
+ }
@@ -0,0 +1,25 @@
1
+ import { ASTNode } from './ASTNode.js';
2
+ import { VSPACE } from '../constants.js';
3
+
4
+ export function parseVSpace(spec) {
5
+ return new VSpaceNode(spec[VSPACE]);
6
+ }
7
+
8
+ export class VSpaceNode extends ASTNode {
9
+ constructor(value) {
10
+ super(VSPACE);
11
+ this.value = value;
12
+ }
13
+
14
+ instantiate(ctx) {
15
+ return ctx.api[VSPACE](this.value);
16
+ }
17
+
18
+ codegen(ctx) {
19
+ return `${ctx.tab()}${ctx.ns()}${this.type}(${this.value})`;
20
+ }
21
+
22
+ toJSON() {
23
+ return { [this.type]: this.value };
24
+ }
25
+ }
@@ -0,0 +1,64 @@
1
+ import { createAPIContext, loadExtension } from '@uwdata/vgplot';
2
+ import { SpecNode } from './ast/SpecNode.js';
3
+ import { resolveExtensions } from './config/extensions.js';
4
+ import { error } from './util.js';
5
+
6
+ /**
7
+ * Generate running web application (DOM content) for a Mosaic spec AST.
8
+ * @param {SpecNode} ast Mosaic AST root node.
9
+ * @param {object} [options] Instantiation options.
10
+ * @param {string} [options.baseURL] The base URL for loading data files.
11
+ * @returns {object} An object with the resulting DOM element, and
12
+ * a map of named parameters (Param and Selection instances).
13
+ */
14
+ export async function astToDOM(ast, options) {
15
+ const { data, params, plotDefaults } = ast;
16
+ const ctx = new InstantiateContext({ plotDefaults, ...options });
17
+
18
+ const queries = [];
19
+
20
+ // process database extensions
21
+ const exts = resolveExtensions(ast);
22
+ queries.push(...Array.from(exts).map(name => loadExtension(name)));
23
+
24
+ // process data definitions
25
+ for (const node of Object.values(data)) {
26
+ const query = node.instantiate(ctx);
27
+ if (query) queries.push(query);
28
+ }
29
+
30
+ // perform extension and data loading, if needed
31
+ if (queries.length > 0) {
32
+ await ctx.coordinator.exec(queries);
33
+ }
34
+
35
+ // process param/selection definitions
36
+ for (const [name, node] of Object.entries(params)) {
37
+ const param = node.instantiate(ctx);
38
+ ctx.activeParams.set(name, param);
39
+ }
40
+
41
+ return {
42
+ element: ast.root.instantiate(ctx),
43
+ params: ctx.activeParams
44
+ };
45
+ }
46
+
47
+ export class InstantiateContext {
48
+ constructor({
49
+ api = createAPIContext(),
50
+ plotDefaults = [],
51
+ activeParams = new Map,
52
+ baseURL = null
53
+ } = {}) {
54
+ this.api = api;
55
+ this.plotDefaults = plotDefaults;
56
+ this.activeParams = activeParams;
57
+ this.baseURL = baseURL;
58
+ this.coordinator = api.context.coordinator;
59
+ }
60
+
61
+ error(message, data) {
62
+ error(message, data);
63
+ }
64
+ }
@@ -0,0 +1,178 @@
1
+ import { SpecNode } from './ast/SpecNode.js';
2
+ import { resolveExtensions } from './config/extensions.js';
3
+ import { error, isArray, isObject, isString, toParamRef } from './util.js';
4
+
5
+ /**
6
+ * Generate ESM code for a Mosaic spec AST.
7
+ * @param {SpecNode} ast Mosaic AST root node.
8
+ * @param {object} [options] Code generation options.
9
+ * @param {string} [options.namespace='vg'] The vgplot API namespace object.
10
+ * @param {string} [options.baseURL] The base URL for loading data files.
11
+ * @param {number} [options.depth=0] The starting indentation depth.
12
+ * @param {Map<string,string>} [options.imports] A Map of ESM imports to
13
+ * include in generated code. Keys are packages (e.g., '@uwdata/vgplot')
14
+ * and values indicate what to import (e.g., '* as vg').
15
+ * @returns {string} Generated ESM code using the vgplot API.
16
+ */
17
+ export function astToESM(ast, options) {
18
+ const { root, data, params, plotDefaults } = ast;
19
+ const ctx = new CodegenContext({ plotDefaults, ...options });
20
+
21
+ // generate package imports
22
+ const importsCode = [];
23
+ for (const [pkg, methods] of ctx.imports) {
24
+ importsCode.push(
25
+ isString(methods)
26
+ ? `import ${methods} from "${pkg}";`
27
+ : `import { ${methods.join(', ')} } from "${pkg}";`
28
+ );
29
+ }
30
+
31
+ // generate database connector code
32
+ const connectorCode = [];
33
+ if (ctx.connector) {
34
+ const con = `${ctx.ns()}${ctx.connector}Connector()`;
35
+ connectorCode.push(
36
+ `${ctx.tab()}${ctx.ns()}coordinator().databaseConnector(${con});`
37
+ );
38
+ }
39
+
40
+ // process extensions and data definitions
41
+ const queries = [];
42
+ for (const name of resolveExtensions(ast)) {
43
+ queries.push(`${ctx.ns()}loadExtension("${name}")`);
44
+ }
45
+ for (const node of Object.values(data)) {
46
+ const load = node.codegen(ctx);
47
+ if (load) queries.push(load);
48
+ }
49
+
50
+ // perform extension and data loading
51
+ const dataCode = [];
52
+ if (queries.length) {
53
+ dataCode.push(`${ctx.tab()}await ${ctx.ns()}coordinator().exec([`);
54
+ ctx.indent();
55
+ dataCode.push(queries.map(q => `${ctx.tab()}${q}`).join(',\n'));
56
+ ctx.undent();
57
+ dataCode.push(`${ctx.tab()}]);`);
58
+ }
59
+
60
+ // generate params / selections
61
+ const paramCode = [];
62
+ for (const [key, value] of Object.entries(params)) {
63
+ paramCode.push(`const ${toParamRef(key)} = ${value.codegen(ctx)};`);
64
+ }
65
+
66
+ // generate default attributes
67
+ let defaultCode = [];
68
+ const defaultList = plotDefaults;
69
+ if (defaultList.length) {
70
+ defaultCode = [
71
+ 'const defaultAttributes = [',
72
+ defaultList.map(a => ' ' + a.codegen(ctx)).join(',\n'),
73
+ '];'
74
+ ];
75
+ }
76
+
77
+ // generate specification tree
78
+ const specCode = [
79
+ `export default ${root.codegen(ctx)};`
80
+ ];
81
+
82
+ return [
83
+ ...importsCode,
84
+ ...maybeNewline(importsCode),
85
+ ...connectorCode,
86
+ ...maybeNewline(connectorCode),
87
+ ...dataCode,
88
+ ...maybeNewline(dataCode),
89
+ ...paramCode,
90
+ ...maybeNewline(paramCode),
91
+ ...defaultCode,
92
+ ...maybeNewline(defaultCode),
93
+ ...specCode
94
+ ].join('\n');
95
+ }
96
+
97
+ export class CodegenContext {
98
+ constructor({
99
+ plotDefaults = null,
100
+ namespace = 'vg',
101
+ connector = null,
102
+ imports = new Map([['@uwdata/vgplot', '* as vg']]),
103
+ baseURL = null,
104
+ depth = 0
105
+ } = {}) {
106
+ this.plotDefaults = plotDefaults;
107
+ this.namespace = `${namespace}.`;
108
+ this.connector = connector;
109
+ this.imports = imports;
110
+ this.baseURL = baseURL;
111
+ this.depth = depth;
112
+ }
113
+
114
+ addImport(pkg, method) {
115
+ if (!this.imports.has(pkg)) {
116
+ this.imports.set(pkg, []);
117
+ }
118
+ this.imports.get(pkg).push(method);
119
+ }
120
+
121
+ setImports(pkg, all) {
122
+ this.imports.set(pkg, all);
123
+ }
124
+
125
+ ns() {
126
+ return this.namespace;
127
+ }
128
+
129
+ indent() {
130
+ this.depth += 1;
131
+ }
132
+
133
+ undent() {
134
+ this.depth -= 1;
135
+ }
136
+
137
+ tab() {
138
+ return Array.from({ length: this.depth }, () => ' ').join('');
139
+ }
140
+
141
+ stringify(value) {
142
+ if (isArray(value)) {
143
+ const items = value.map(v => this.stringify(v));
144
+ return `[${this.maybeLineWrap(items)}]`;
145
+ } else if (isObject(value)) {
146
+ const props = Object.entries(value)
147
+ .map(([k, v]) => `${maybeQuote(k)}: ${this.stringify(v)}`);
148
+ return `{${this.maybeLineWrap(props)}}`;
149
+ } else {
150
+ return JSON.stringify(value);
151
+ }
152
+ }
153
+
154
+ maybeLineWrap(spans) {
155
+ const limit = 80 - 2 * this.depth;
156
+ const chars = 2 * spans.length + spans.reduce((a, b) => a + b.length, 0);
157
+ if (chars > limit) {
158
+ this.indent();
159
+ const str = spans.map(s => `\n${this.tab()}${s}`).join(',')
160
+ this.undent();
161
+ return str + '\n' + this.tab();
162
+ } else {
163
+ return spans.join(', ');
164
+ }
165
+ }
166
+
167
+ error(message, data) {
168
+ error(message, data);
169
+ }
170
+ }
171
+
172
+ function maybeQuote(str) {
173
+ return /^[A-Za-z_$]\w*/.test(str) ? str : JSON.stringify(str);
174
+ }
175
+
176
+ function maybeNewline(entry) {
177
+ return entry?.length ? [''] : [];
178
+ }