@forming/tools 0.0.0-stage → 0.1.0-dev.3.1
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 +86 -0
- package/NOTICE.md +18 -0
- package/README.md +77 -2
- package/dist/completions.mjs +419 -0
- package/dist/tokens.mjs +216 -0
- package/package.json +26 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Forming Source Available License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Forming contributors.
|
|
4
|
+
|
|
5
|
+
1. Scope and definitions
|
|
6
|
+
|
|
7
|
+
This license covers the Forming implementation, tools, examples and other
|
|
8
|
+
original material supplied with this notice (the Software). Each copyright
|
|
9
|
+
holder grants rights only to material they own or are authorized to license.
|
|
10
|
+
Third-party components and material expressly offered under another license
|
|
11
|
+
remain governed by those terms. See NOTICE.md and the attribution notices.
|
|
12
|
+
|
|
13
|
+
An Internal Use is use by your employees or contractors solely for your own
|
|
14
|
+
organization's operations. Customers and other external users are not internal
|
|
15
|
+
users. A Commercial Product is a product or service sold, licensed for a fee,
|
|
16
|
+
monetized through advertising, or offered to support a paid business offering,
|
|
17
|
+
including its free tiers, trials and bundled features.
|
|
18
|
+
|
|
19
|
+
2. Permission
|
|
20
|
+
|
|
21
|
+
Subject to the conditions below, you may use, inspect, copy and modify the
|
|
22
|
+
Software for personal, educational, noncommercial and Internal Use. Internal
|
|
23
|
+
Use by a commercial business is permitted without payment.
|
|
24
|
+
|
|
25
|
+
You may also use the Software on the backend of a Commercial Product to compile
|
|
26
|
+
definitions, validate data or process results, provided that you do not offer
|
|
27
|
+
the Software's capabilities as a service or use the Software in that product's
|
|
28
|
+
external user interface as described in section 3.
|
|
29
|
+
|
|
30
|
+
You may redistribute unmodified or modified copies without charge for uses
|
|
31
|
+
permitted by this license. You must include this license and all applicable
|
|
32
|
+
copyright, attribution and third-party notices. Identify changes you make.
|
|
33
|
+
Recipients receive their rights from the applicable copyright holders under
|
|
34
|
+
these same terms; you may not grant broader rights to the Software.
|
|
35
|
+
|
|
36
|
+
3. Uses requiring separate written permission
|
|
37
|
+
|
|
38
|
+
Without a separate written agreement from the applicable copyright holders,
|
|
39
|
+
you may not, directly or through another party:
|
|
40
|
+
|
|
41
|
+
(a) sell, rent, sublicense for a fee, or otherwise charge for copies of or
|
|
42
|
+
access to the Software, including modified, rebranded or bundled versions;
|
|
43
|
+
|
|
44
|
+
(b) offer the Software or its form authoring, rendering, compilation, validation
|
|
45
|
+
or runtime capabilities to external users as a hosted, managed, API or other
|
|
46
|
+
commercial service, including through a wrapper or a free tier of that service;
|
|
47
|
+
|
|
48
|
+
(c) embed, distribute or otherwise use the Software to provide any part of a
|
|
49
|
+
Commercial Product's external user interface, including ordinary rendered
|
|
50
|
+
forms, widgets, editors, visual builders and white-label interfaces.
|
|
51
|
+
|
|
52
|
+
These restrictions apply even if the Software is only one part of an offering
|
|
53
|
+
or is not separately priced. They do not prohibit Internal Use or the backend
|
|
54
|
+
integration expressly allowed in section 2. Charging for consulting, installation
|
|
55
|
+
or support is allowed when the resulting use itself complies with this license.
|
|
56
|
+
|
|
57
|
+
4. Your material and other rights
|
|
58
|
+
|
|
59
|
+
You retain ownership of your independently authored form definitions, submitted
|
|
60
|
+
data and other original output. This license does not require you to disclose
|
|
61
|
+
your application's source code. Output that contains Software code remains
|
|
62
|
+
subject to this license to the extent it contains that code. No trademark rights
|
|
63
|
+
are granted. Rights not expressly granted are reserved.
|
|
64
|
+
|
|
65
|
+
5. Termination
|
|
66
|
+
|
|
67
|
+
Your permissions end if you violate this license. If you correct your first
|
|
68
|
+
violation within 30 days of receiving written notice from a copyright holder,
|
|
69
|
+
your permissions are restored. Further violations require express written
|
|
70
|
+
reinstatement. Termination does not remove obligations that apply to copies
|
|
71
|
+
already distributed.
|
|
72
|
+
|
|
73
|
+
6. Warranty and liability
|
|
74
|
+
|
|
75
|
+
TO THE EXTENT PERMITTED BY LAW, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT
|
|
76
|
+
WARRANTIES, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
77
|
+
NONINFRINGEMENT. COPYRIGHT HOLDERS AND CONTRIBUTORS ARE NOT LIABLE FOR CLAIMS,
|
|
78
|
+
DAMAGES OR OTHER LIABILITY ARISING FROM THE SOFTWARE OR ITS USE, WHETHER IN
|
|
79
|
+
CONTRACT, TORT OR OTHERWISE.
|
|
80
|
+
|
|
81
|
+
7. Additional permission
|
|
82
|
+
|
|
83
|
+
Contact the project maintainers through the repository from which you obtained
|
|
84
|
+
the Software to request a commercial agreement. An unanswered request does not
|
|
85
|
+
grant permission. A separate written agreement controls only to the extent it
|
|
86
|
+
expressly grants different rights.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Licensing notices
|
|
2
|
+
|
|
3
|
+
The Forming implementation is covered by the [Forming Source Available License](LICENSE).
|
|
4
|
+
This license permits internal business use and commercial backend integration,
|
|
5
|
+
but requires separate written permission for resale, commercial hosted Forming
|
|
6
|
+
services and external UI use in commercial products. It is not an open-source
|
|
7
|
+
license. The license text controls over this summary.
|
|
8
|
+
|
|
9
|
+
The specifications and adapted specification text retain their existing
|
|
10
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) terms and attribution.
|
|
11
|
+
See `core/ATTRIBUTION.md` and `website/ATTRIBUTION.md` in the source repository.
|
|
12
|
+
The implementation license does not replace those rights or revoke rights
|
|
13
|
+
granted under earlier licenses.
|
|
14
|
+
|
|
15
|
+
Dependencies retain their own licenses. Rust dependency notices are collected
|
|
16
|
+
under `tools/skills/notices/` in the source repository and included with packaged
|
|
17
|
+
WASM distributions. JavaScript and Go dependency notices are supplied by their
|
|
18
|
+
respective packages. Preserve applicable notices when redistributing bundles.
|
package/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Forming tools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
JavaScript tooling for writing Forming: autocomplete, editor support and the language server. The lint rules, `forming check` and `forming fmt` live in the [Rust core](../core/spec/lint.md), so every tool reports the same errors and warnings.
|
|
4
|
+
|
|
5
|
+
| Folder | Package | What it is |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `.` (this folder) | `@forming/tools` | Completion engine and tokenizer shared by the playground and editors |
|
|
8
|
+
| [`language-server/`](language-server) | `@forming/language-server` | Errors, lint warnings, autocomplete and formatting over the Language Server Protocol: VS Code, Neovim, Zed, Helix and other LSP clients |
|
|
9
|
+
| [`vscode/`](vscode) | VS Code extension | `.forming` files, syntax highlighting, comments and indentation, and the language server |
|
|
10
|
+
| [`skills/`](skills) | `@forming/skills` (local package prototype) | Installable Codex/Claude skill with an offline WASM validator and extension references; not yet published |
|
|
11
|
+
|
|
12
|
+
## @forming/tools
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import {complete, importKinds} from '@forming/tools/completions';
|
|
16
|
+
|
|
17
|
+
const kinds = importKinds(source, packageManifests); // which imports are uploads, options, annotations...
|
|
18
|
+
const result = complete(source, offset, {kinds}); // {from, to, options: [{label, type, apply}]}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- Suggests keywords, types, settings, rules, field paths, pages and functions for the line being edited.
|
|
22
|
+
- With package manifests, imports are offered only where their kind fits: uploads after `upload with`, option lists after `options from`, validators after `validate with`, actions after `invoke`, and annotations beneath fields and fragment uses. Without manifests every import is offered.
|
|
23
|
+
- `formingCompletions(context, {kinds})` is a CodeMirror completion source; `complete(text, offset)` works on plain text for any editor.
|
|
24
|
+
- `@forming/tools/tokens` exports the tokenizer and its word lists (`WORDS`), from which the VS Code grammar is generated.
|
|
25
|
+
|
|
26
|
+
The website vendors a copy (`npm run sync:runtime` in `website/`) so it builds on its own.
|
|
27
|
+
|
|
28
|
+
## Editors
|
|
29
|
+
|
|
30
|
+
Both the server and the extension read package manifests from the nearest `forming.packages.json` beside your forms, so imports resolve and completions know what each import is:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
[{"profile": "forming.packages/1", "name": "hidden", "version": "1.0.0", "exports": {"visibleTo": {"kind": "annotation", "valueSchema": {"type": "array", "items": {"type": "string"}}}}}]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Copy each plugin's `manifest.json` (for example `extensions/hidden/manifest.json`) into that array.
|
|
37
|
+
|
|
38
|
+
### VS Code
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
cd tools/language-server && npm ci # needs js-sdk built (npm run build:wasm && npm run build)
|
|
42
|
+
cd ../vscode && npm ci && npm run package # bundles, then: code --install-extension forming-*.vsix
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`npm run package` bundles the extension and server into `dist/` with the core's WebAssembly in `wasm/`, so the `.vsix` carries no `node_modules`. The extension is not yet on the Marketplace.
|
|
46
|
+
|
|
47
|
+
### Other editors
|
|
48
|
+
|
|
49
|
+
Run the server with `--stdio`:
|
|
50
|
+
|
|
51
|
+
```lua
|
|
52
|
+
-- Neovim
|
|
53
|
+
vim.lsp.start({name = 'forming', cmd = {'forming-language-server', '--stdio'}, root_dir = vim.fn.getcwd()})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
npm test # @forming/tools
|
|
60
|
+
npm --prefix language-server test # diagnostics, completion, formatting and a real stdio LSP session
|
|
61
|
+
npm --prefix vscode test # grammar, activation, and the bundled server answering LSP
|
|
62
|
+
npm --prefix vscode run grammar # regenerate syntaxes/forming.tmLanguage.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Automatic npm publishing
|
|
66
|
+
|
|
67
|
+
Publishes `@forming/tools` from `.`.
|
|
68
|
+
|
|
69
|
+
`.github/workflows/publish.yml` publishes the JavaScript package on pushes to
|
|
70
|
+
`dev` (base version plus `-dev.<run>.<attempt>`, npm tag `dev`) and `main`
|
|
71
|
+
(stable package.json version, npm tag `latest`). Bump package.json and its lockfile
|
|
72
|
+
before another stable release. Manual runs support only these two branches.
|
|
73
|
+
The workflow creates no commits, Git tags or GitHub releases.
|
|
74
|
+
|
|
75
|
+
Set Actions variable `NPM_ACCESS` to `public` or `restricted`. Configure secret
|
|
76
|
+
`NPM_TOKEN`, or npm trusted publishing for this repository and workflow
|
|
77
|
+
`publish.yml` after the first publish. Private repositories publish with provenance
|
|
78
|
+
disabled. Packages exclude tests, source directories, build scripts and source maps.
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import {scriptTokens} from './tokens.mjs';
|
|
2
|
+
|
|
3
|
+
const NAME = '[A-Za-z_][A-Za-z0-9_]*';
|
|
4
|
+
const FIELD = new RegExp(`^(${NAME})\\s*:\\s*(.+)?$`);
|
|
5
|
+
const TYPES = [
|
|
6
|
+
'string',
|
|
7
|
+
'integer',
|
|
8
|
+
'number',
|
|
9
|
+
'boolean',
|
|
10
|
+
'email',
|
|
11
|
+
'url',
|
|
12
|
+
'phone',
|
|
13
|
+
'date',
|
|
14
|
+
'time',
|
|
15
|
+
'datetime',
|
|
16
|
+
'file',
|
|
17
|
+
'object',
|
|
18
|
+
'enum(',
|
|
19
|
+
];
|
|
20
|
+
const VIEWS = ['section', 'group', 'stack', 'columns', 'grid', 'repeat', 'display', 'use'];
|
|
21
|
+
const RULES = ['show when ', 'enable when ', 'readonly when '];
|
|
22
|
+
const FUNCTIONS = {
|
|
23
|
+
has: 'has(path): is an answer present?',
|
|
24
|
+
default: 'default(value, fallback)',
|
|
25
|
+
size: 'size(text | list)',
|
|
26
|
+
startsWith: 'startsWith(text, prefix)',
|
|
27
|
+
endsWith: 'endsWith(text, suffix)',
|
|
28
|
+
contains: 'contains(text, part)',
|
|
29
|
+
sum: 'sum(list, key?)',
|
|
30
|
+
count: 'count(list, key)',
|
|
31
|
+
every: 'every(list, key)',
|
|
32
|
+
some: 'some(list, key)',
|
|
33
|
+
};
|
|
34
|
+
const OPERATORS = ['and', 'or', 'in', '??', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '%', '?'];
|
|
35
|
+
const numeric = ['min ', 'max ', 'multipleOf '];
|
|
36
|
+
const strings = ['minLength ', 'maxLength ', 'pattern '];
|
|
37
|
+
const arrays = ['minItems ', 'maxItems ', 'uniqueItems'];
|
|
38
|
+
const constraints = type => [
|
|
39
|
+
'required',
|
|
40
|
+
'default ',
|
|
41
|
+
'id ',
|
|
42
|
+
...(type.includes('[]')
|
|
43
|
+
? arrays
|
|
44
|
+
: /^(number|integer)\b/.test(type)
|
|
45
|
+
? numeric
|
|
46
|
+
: /^(string|email|url|phone|date|time|datetime|enum)\b/.test(type)
|
|
47
|
+
? strings
|
|
48
|
+
: []),
|
|
49
|
+
...(!type.includes('[]') && !/^(object|file)\b/.test(type) ? ['mustBe '] : []),
|
|
50
|
+
...(type.startsWith('phone') ? ['region '] : []),
|
|
51
|
+
...(type.startsWith('file') ? ['accept ', 'maxSize ', ...(type.includes('[]') ? ['maxTotalSize '] : [])] : []),
|
|
52
|
+
];
|
|
53
|
+
const controls = type =>
|
|
54
|
+
type.includes('[]')
|
|
55
|
+
? ['multichoice', 'hidden']
|
|
56
|
+
: /^(string|email|url|phone)\b/.test(type)
|
|
57
|
+
? ['textarea', 'password', 'choice', 'hidden']
|
|
58
|
+
: /^(date|time|datetime)\b/.test(type)
|
|
59
|
+
? [type.match(/^\w+/)[0], 'hidden']
|
|
60
|
+
: ['choice', 'hidden'];
|
|
61
|
+
const choices = (labels, type = 'keyword') =>
|
|
62
|
+
labels.map(label => (typeof label === 'string' ? {label, type} : {...label, type}));
|
|
63
|
+
const clean = text =>
|
|
64
|
+
scriptTokens(text)
|
|
65
|
+
.filter(token => token.type !== 'lineComment')
|
|
66
|
+
.map(token => token.text)
|
|
67
|
+
.join('');
|
|
68
|
+
const masked = text =>
|
|
69
|
+
scriptTokens(text)
|
|
70
|
+
.map(token =>
|
|
71
|
+
token.type === 'string' || token.type === 'lineComment' ? ' '.repeat(token.text.length) : token.text
|
|
72
|
+
)
|
|
73
|
+
.join('');
|
|
74
|
+
|
|
75
|
+
// Recover indentation scopes from incomplete source. Blank/comment lines don't
|
|
76
|
+
// close blocks. Keep following declarations too, for forward references and
|
|
77
|
+
// duplicate settings while editing in the middle of a document.
|
|
78
|
+
function documentLines(text) {
|
|
79
|
+
const stack = [],
|
|
80
|
+
records = [];
|
|
81
|
+
let offset = 0;
|
|
82
|
+
for (const raw of text.split('\n')) {
|
|
83
|
+
const code = clean(raw).trim();
|
|
84
|
+
const indent = raw.match(/^ */)[0].length;
|
|
85
|
+
if (code) {
|
|
86
|
+
while (stack.length && stack.at(-1).indent >= indent) stack.pop();
|
|
87
|
+
const field = code.match(FIELD);
|
|
88
|
+
const record = {
|
|
89
|
+
code,
|
|
90
|
+
indent,
|
|
91
|
+
from: offset,
|
|
92
|
+
parents: [...stack],
|
|
93
|
+
field,
|
|
94
|
+
kind: field ? 'field' : code.split(/\s/)[0],
|
|
95
|
+
};
|
|
96
|
+
records.push(record);
|
|
97
|
+
stack.push(record);
|
|
98
|
+
}
|
|
99
|
+
offset += raw.length + 1;
|
|
100
|
+
}
|
|
101
|
+
return records;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function symbols(records, parents) {
|
|
105
|
+
const fragment = parents.find(record => record.kind === 'fragment');
|
|
106
|
+
const fields = [],
|
|
107
|
+
inputs = [],
|
|
108
|
+
computed = [];
|
|
109
|
+
for (const record of records) {
|
|
110
|
+
const owner = record.parents.find(parent => parent.kind === 'fragment');
|
|
111
|
+
if (owner !== fragment) continue;
|
|
112
|
+
if (record.field) {
|
|
113
|
+
const prefix = record.parents.flatMap(parent =>
|
|
114
|
+
parent.field ? [parent.field[1]] : parent.kind === 'repeat' ? [parent.code.split(/\s+/)[1]] : []
|
|
115
|
+
);
|
|
116
|
+
fields.push([...prefix, record.field[1]].join('.'));
|
|
117
|
+
}
|
|
118
|
+
const input = record.code.match(/^input\s+(\w+)\s*:/);
|
|
119
|
+
if (input) inputs.push(input[1]);
|
|
120
|
+
const calc = record.code.match(/^(\w+)\s*=/);
|
|
121
|
+
if (calc && record.parents.at(-1)?.kind === 'computed') computed.push(calc[1]);
|
|
122
|
+
}
|
|
123
|
+
return {fields, inputs, computed};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* CodeMirror-compatible completion source. `kinds` maps imported names to
|
|
128
|
+
* their export kind ("upload", "options", "annotation", ...) when manifests
|
|
129
|
+
* are known; see importKinds. Without it every import is offered everywhere.
|
|
130
|
+
*/
|
|
131
|
+
export function formingCompletions(context, {kinds} = {}) {
|
|
132
|
+
const doc = context.state.doc;
|
|
133
|
+
const line = doc.lineAt(context.pos);
|
|
134
|
+
const column = context.pos - line.from;
|
|
135
|
+
const before = line.text.slice(0, column);
|
|
136
|
+
const tokens = scriptTokens(line.text);
|
|
137
|
+
// No suggestions in comments or quoted labels, even on explicit Ctrl+Space.
|
|
138
|
+
if (
|
|
139
|
+
tokens.some(
|
|
140
|
+
token =>
|
|
141
|
+
token.from < column &&
|
|
142
|
+
(token.type === 'lineComment' ||
|
|
143
|
+
(token.type === 'string' &&
|
|
144
|
+
(column < token.to || (column === token.to && !/^"(?:\\.|[^"\\])*"$/.test(token.text)))))
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
return null;
|
|
148
|
+
const word = before.match(/[A-Za-z_][A-Za-z0-9_-]*$/)?.[0] ?? '';
|
|
149
|
+
const from = context.pos - word.length;
|
|
150
|
+
const suffix = line.text.slice(column).match(/^[A-Za-z0-9_-]*/)[0];
|
|
151
|
+
const to = context.pos + suffix.length;
|
|
152
|
+
const head = before.slice(0, before.length - word.length);
|
|
153
|
+
const after = line.text.slice(to - line.from);
|
|
154
|
+
if (!word && !context.explicit && !/[\s:.]$/.test(before)) return null;
|
|
155
|
+
const done = list => {
|
|
156
|
+
const unique = [...new Map(list.map(option => [option.label, option])).values()];
|
|
157
|
+
return unique.length
|
|
158
|
+
? {
|
|
159
|
+
from,
|
|
160
|
+
to,
|
|
161
|
+
options: unique.map(option => {
|
|
162
|
+
let apply = option.apply ?? option.label;
|
|
163
|
+
if (typeof apply === 'string') {
|
|
164
|
+
if (/\s$/.test(apply) && /^\s/.test(after)) apply = apply.trimEnd();
|
|
165
|
+
if (apply.endsWith('(') && /^\s*\(/.test(after)) apply = apply.slice(0, -1);
|
|
166
|
+
}
|
|
167
|
+
return {...option, apply};
|
|
168
|
+
}),
|
|
169
|
+
validFor: /^[A-Za-z0-9_-]*$/,
|
|
170
|
+
}
|
|
171
|
+
: null;
|
|
172
|
+
};
|
|
173
|
+
const text = doc.toString();
|
|
174
|
+
const records = documentLines(text);
|
|
175
|
+
const indent = line.text.match(/^ */)[0].length;
|
|
176
|
+
const current = records.find(record => record.from === line.from);
|
|
177
|
+
const previous = records.filter(record => record.from < line.from).at(-1);
|
|
178
|
+
const parents = previous ? [...previous.parents, previous].filter(record => record.indent < indent) : [];
|
|
179
|
+
const parent = parents.at(-1);
|
|
180
|
+
const names = symbols(records, parents);
|
|
181
|
+
const imported = records
|
|
182
|
+
.filter(record => record.kind === 'import')
|
|
183
|
+
.flatMap(record =>
|
|
184
|
+
(record.code.match(/\{([^}]*)\}/)?.[1] ?? '')
|
|
185
|
+
.split(',')
|
|
186
|
+
.map(name =>
|
|
187
|
+
name
|
|
188
|
+
.trim()
|
|
189
|
+
.split(/\s+as\s+/)
|
|
190
|
+
.at(-1)
|
|
191
|
+
)
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
);
|
|
194
|
+
const kindOf = name => kinds?.get?.(name) ?? kinds?.[name];
|
|
195
|
+
// Imports of the wanted kinds; a name whose kind is unknown stays on offer.
|
|
196
|
+
const importedOf = (...wanted) =>
|
|
197
|
+
imported.filter(name => kindOf(name) === undefined || wanted.includes(kindOf(name)));
|
|
198
|
+
const annotations = imported.filter(name => kindOf(name) === 'annotation');
|
|
199
|
+
const declared = kind =>
|
|
200
|
+
records
|
|
201
|
+
.filter(record => record.kind === kind && record.indent === 0)
|
|
202
|
+
.map(record => record.code.split(/\s+/)[1])
|
|
203
|
+
.filter(Boolean);
|
|
204
|
+
const field = current?.field ? current : [...parents].reverse().find(record => record.field);
|
|
205
|
+
const type = field?.field[2]?.split(/\s+"/)[0] ?? 'string';
|
|
206
|
+
const inData = parents.some(record => record.kind === 'data');
|
|
207
|
+
const inActions = parent?.kind === 'actions';
|
|
208
|
+
const inFragment = parents.some(record => record.kind === 'fragment');
|
|
209
|
+
const repeats = parents.filter(record => record.kind === 'repeat');
|
|
210
|
+
const namespaces = [
|
|
211
|
+
'data',
|
|
212
|
+
'computed',
|
|
213
|
+
...(inFragment ? ['inputs', 'local'] : []),
|
|
214
|
+
...(repeats.length ? ['item'] : []),
|
|
215
|
+
...(repeats.length > 1 ? ['parent'] : []),
|
|
216
|
+
];
|
|
217
|
+
const target = labels => done(choices(labels, 'variable'));
|
|
218
|
+
const words = labels => done(choices(labels));
|
|
219
|
+
const unused = (labels, owner = field) => {
|
|
220
|
+
// Include both sides of the cursor and settings elsewhere in this block.
|
|
221
|
+
const related = owner ? records.filter(record => record === owner || record.parents.at(-1) === owner) : [];
|
|
222
|
+
const occupied = related
|
|
223
|
+
.map(record => (record.from === line.from ? head + after : record.code))
|
|
224
|
+
.map(masked)
|
|
225
|
+
.join('\n');
|
|
226
|
+
return labels.filter(label => !new RegExp(`\\b${label.trim().split(/\s/)[0]}\\b`).test(occupied));
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (/^\s*(?:input\s+)?\w+\s*:\s*(?:\(\s*)?$/.test(head)) return done(choices(TYPES, 'type'));
|
|
230
|
+
if (/^\s*forming\s+$/.test(head)) return words(['1']);
|
|
231
|
+
if (/^\s*import\s+\{[^}]*\}\s*$/.test(head)) return words(['from ']);
|
|
232
|
+
if (/^\s*options\s+from\s+$/.test(head)) return target([...declared('options'), ...importedOf('options')]);
|
|
233
|
+
if (/^\s*options\s+$/.test(head) && field) return words(['from ']);
|
|
234
|
+
if (/^\s*data\s+$/.test(head)) return words(['from ']);
|
|
235
|
+
if (/^\s*data\s+from\s+$/.test(head)) return target(importedOf('schema'));
|
|
236
|
+
if (/^\s*use\s+$/.test(head)) return target([...declared('fragment'), ...importedOf('fragment')]);
|
|
237
|
+
if (/^\s*use\s+\w+\s+$/.test(head)) return words(['at ']);
|
|
238
|
+
if (/^\s*(?:use\s+\w+\s+at|repeat|set|clear)\s+$/.test(head)) return target(names.fields);
|
|
239
|
+
if (/^\s*next\s+$/.test(head) && inActions) return target(declared('page'));
|
|
240
|
+
if (/^\s*validate\s+with\s+$/.test(head)) return target(importedOf('validator'));
|
|
241
|
+
if (/^\s*upload\s+with\s+$/.test(head)) return target(importedOf('upload'));
|
|
242
|
+
if (/^\s*invoke\s+$/.test(head)) return target(importedOf('action'));
|
|
243
|
+
if (/^\s*upload\s+$/.test(head)) return words(['with ']);
|
|
244
|
+
if (/\bas\s+$/.test(masked(head)) && field) return done(choices(controls(type), 'enum'));
|
|
245
|
+
if (/^\s*(?:show|enable|readonly|required)\s+$/.test(head)) return words(['when ']);
|
|
246
|
+
if (/^\s*with\s+$/.test(head) && parent?.kind === 'set') return words(['value = ']);
|
|
247
|
+
if (/^\s*with\s+\w+\s*$/.test(head)) return words(['= ']);
|
|
248
|
+
|
|
249
|
+
const expressionMatch = masked(head).match(
|
|
250
|
+
/(?:\bwhen\s+|^\s*validate\s+(?!with\b)|^\s*computed\s+|(?<![=!<>])=(?!=)\s*)(.*)$/
|
|
251
|
+
);
|
|
252
|
+
if (expressionMatch && !(inActions && /^\s*validate\s/.test(head))) {
|
|
253
|
+
const expression = head.slice(expressionMatch.index + expressionMatch[0].length - expressionMatch[1].length);
|
|
254
|
+
const member = expression.match(/((?:[A-Za-z_][A-Za-z0-9_]*\.)+)$/)?.[1];
|
|
255
|
+
if (member) {
|
|
256
|
+
const [namespace, ...path] = member.slice(0, -1).split('.');
|
|
257
|
+
let paths = names.fields;
|
|
258
|
+
if (namespace === 'computed') paths = names.computed;
|
|
259
|
+
else if (namespace === 'inputs') paths = names.inputs;
|
|
260
|
+
else if (namespace === 'item' || namespace === 'parent') {
|
|
261
|
+
const repeat = repeats.at(namespace === 'item' ? -1 : -2);
|
|
262
|
+
if (!repeat) return null;
|
|
263
|
+
const prefix = repeat.code.split(/\s+/)[1] + '.';
|
|
264
|
+
paths = paths.filter(name => name.startsWith(prefix)).map(name => name.slice(prefix.length));
|
|
265
|
+
} else if (!['data', 'local'].includes(namespace)) path.unshift(namespace);
|
|
266
|
+
const prefix = path.length ? path.join('.') + '.' : '';
|
|
267
|
+
return target(paths.filter(name => name.startsWith(prefix)).map(name => name.slice(prefix.length).split('.')[0]));
|
|
268
|
+
}
|
|
269
|
+
const significant = scriptTokens(expression).filter(token => token.text.trim());
|
|
270
|
+
const last = significant.at(-1);
|
|
271
|
+
const operand =
|
|
272
|
+
!last ||
|
|
273
|
+
/^(and|or|not|in)$/.test(last.text) ||
|
|
274
|
+
(last.type === 'operator' && last.text !== '.') ||
|
|
275
|
+
/^[([,:]$/.test(last.text);
|
|
276
|
+
if (!operand) return words(OPERATORS);
|
|
277
|
+
return done([
|
|
278
|
+
...choices(
|
|
279
|
+
names.fields.filter(name => !name.includes('.')),
|
|
280
|
+
'variable'
|
|
281
|
+
),
|
|
282
|
+
...choices(namespaces, 'namespace'),
|
|
283
|
+
...choices(
|
|
284
|
+
Object.entries(FUNCTIONS).map(([label, detail]) => ({label, detail, apply: label + '('})),
|
|
285
|
+
'function'
|
|
286
|
+
),
|
|
287
|
+
...choices(['true', 'false', 'null', 'not', ...(/^\s*validate\s+$/.test(head) ? ['with '] : [])]),
|
|
288
|
+
]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Value slots have their own vocabulary; structural keywords cannot fill them.
|
|
292
|
+
const valueSlot = head.match(
|
|
293
|
+
/\b(default|mustBe|min|max|multipleOf|minLength|maxLength|minItems|maxItems|pattern|region|accept|maxSize|maxTotalSize|description|placeholder|message|id)\s*$/
|
|
294
|
+
)?.[1];
|
|
295
|
+
if (valueSlot)
|
|
296
|
+
return ['default', 'mustBe'].includes(valueSlot)
|
|
297
|
+
? words(
|
|
298
|
+
['true', 'false', 'null'].filter(value =>
|
|
299
|
+
value === 'null' ? type.includes('?') : type.startsWith('boolean')
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
: null;
|
|
303
|
+
if (/^\s*next\s+\S+\s+"(?:\\.|[^"\\])*"\s+$/.test(head)) return words(['when ']);
|
|
304
|
+
if (current?.field && /"(?:\\.|[^"\\])*"\s/.test(head))
|
|
305
|
+
return words(unused([...constraints(type), ...(!inData ? ['as '] : [])]));
|
|
306
|
+
if (head.trim()) return null;
|
|
307
|
+
|
|
308
|
+
if (inActions)
|
|
309
|
+
return words([
|
|
310
|
+
'submit ',
|
|
311
|
+
'save ',
|
|
312
|
+
'reset ',
|
|
313
|
+
'validate ',
|
|
314
|
+
'set ',
|
|
315
|
+
'clear ',
|
|
316
|
+
'invoke ',
|
|
317
|
+
...(parents.some(record => record.kind === 'page') ? ['next ', 'back '] : []),
|
|
318
|
+
]);
|
|
319
|
+
if (parent?.kind === 'options') {
|
|
320
|
+
const provider = parent.code.match(/^options\s+from\s+(\w+)/)?.[1];
|
|
321
|
+
return provider ? (imported.includes(provider) ? words(['with ']) : null) : words(['option ']);
|
|
322
|
+
}
|
|
323
|
+
if (parent?.field) {
|
|
324
|
+
return words(
|
|
325
|
+
unused([
|
|
326
|
+
...annotations,
|
|
327
|
+
...constraints(type),
|
|
328
|
+
'description ',
|
|
329
|
+
'schema ',
|
|
330
|
+
...(!inData
|
|
331
|
+
? [
|
|
332
|
+
'as ',
|
|
333
|
+
'placeholder ',
|
|
334
|
+
'options',
|
|
335
|
+
'validate ',
|
|
336
|
+
...RULES,
|
|
337
|
+
'required when ',
|
|
338
|
+
...(type.startsWith('file') ? ['upload with '] : []),
|
|
339
|
+
...(!/^(object|file)\b/.test(type) && !type.includes('[]') ? ['computed '] : []),
|
|
340
|
+
]
|
|
341
|
+
: []),
|
|
342
|
+
...(type.startsWith('object') && !inData ? VIEWS : []),
|
|
343
|
+
])
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (parent?.kind === 'data' || parent?.kind === 'computed') return null;
|
|
347
|
+
if (parent?.kind === 'set') return words(['with value = ']);
|
|
348
|
+
if (parent && ['invoke', 'upload', 'as'].includes(parent.kind)) return words(['with ']);
|
|
349
|
+
if (parent?.kind === 'validate')
|
|
350
|
+
return words(unused(['message ', 'when ', ...(parent.code.includes(' with ') ? ['with '] : [])], parent));
|
|
351
|
+
if (parent && ['use', 'display'].includes(parent.kind))
|
|
352
|
+
return words([
|
|
353
|
+
'with ',
|
|
354
|
+
...RULES,
|
|
355
|
+
...(parent.kind === 'use' ? ['required when '] : []),
|
|
356
|
+
...unused(annotations, parent),
|
|
357
|
+
]);
|
|
358
|
+
if (
|
|
359
|
+
parent &&
|
|
360
|
+
['page', ...VIEWS.filter(name => name !== 'use' && name !== 'display'), 'fragment'].includes(parent.kind)
|
|
361
|
+
) {
|
|
362
|
+
return words([
|
|
363
|
+
...VIEWS,
|
|
364
|
+
...(!inFragment ? RULES : []),
|
|
365
|
+
...(parent.kind === 'page' ? ['actions'] : []),
|
|
366
|
+
...(parent.kind === 'repeat' ? ['computed', 'required when ', 'id '] : []),
|
|
367
|
+
...(parent.kind === 'fragment' ? ['input '] : []),
|
|
368
|
+
]);
|
|
369
|
+
}
|
|
370
|
+
if (indent > 0) return null;
|
|
371
|
+
const earlier = records.filter(record => record.from < line.from);
|
|
372
|
+
if (!earlier.some(record => record.kind === 'forming')) return words(['forming ']);
|
|
373
|
+
if (!earlier.some(record => record.kind === 'form'))
|
|
374
|
+
return words([
|
|
375
|
+
'import',
|
|
376
|
+
'options',
|
|
377
|
+
'fragment',
|
|
378
|
+
...(!records.some(record => record !== current && record.kind === 'form') ? ['form'] : []),
|
|
379
|
+
]);
|
|
380
|
+
const existing = records.filter(record => record !== current && record.indent === 0).map(record => record.kind);
|
|
381
|
+
return done([
|
|
382
|
+
...choices(['page', ...VIEWS, ...['data', 'computed', 'actions'].filter(kind => !existing.includes(kind))]),
|
|
383
|
+
...choices(names.fields, 'variable'),
|
|
384
|
+
]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Map each imported name to its export kind, using the supplied package manifests. */
|
|
388
|
+
export function importKinds(source, packages = []) {
|
|
389
|
+
const manifests = new Map(
|
|
390
|
+
(typeof packages === 'string' ? JSON.parse(packages) : packages).map(p => [`${p.name}@${p.version}`, p])
|
|
391
|
+
);
|
|
392
|
+
const kinds = new Map();
|
|
393
|
+
for (const match of source.matchAll(/^import\s*\{([^}]*)\}\s*from\s*"([^"]+)"/gm)) {
|
|
394
|
+
const manifest = manifests.get(match[2]);
|
|
395
|
+
for (const part of match[1].split(',')) {
|
|
396
|
+
const [name, alias = name] = part.trim().split(/\s+as\s+/);
|
|
397
|
+
const kind = manifest?.exports?.[name]?.kind;
|
|
398
|
+
if (name && kind) kinds.set(alias, kind);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return kinds;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Completions for plain text at a UTF-16 offset, for editors and language servers. */
|
|
405
|
+
export function complete(text, offset, {explicit = true, kinds} = {}) {
|
|
406
|
+
const starts = [0];
|
|
407
|
+
for (let i = 0; i < text.length; i++) if (text[i] === '\n') starts.push(i + 1);
|
|
408
|
+
const doc = {
|
|
409
|
+
toString: () => text,
|
|
410
|
+
lineAt(pos) {
|
|
411
|
+
let index = starts.length - 1;
|
|
412
|
+
while (starts[index] > pos) index--;
|
|
413
|
+
const from = starts[index];
|
|
414
|
+
const end = text.indexOf('\n', from);
|
|
415
|
+
return {from, text: text.slice(from, end < 0 ? text.length : end), number: index + 1};
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
return formingCompletions({state: {doc}, pos: offset, explicit}, {kinds});
|
|
419
|
+
}
|
package/dist/tokens.mjs
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Structural words and namespaces reserved by the grammar (spec/grammar.md).
|
|
2
|
+
const STRUCTURE = new Set([
|
|
3
|
+
'forming',
|
|
4
|
+
'form',
|
|
5
|
+
'page',
|
|
6
|
+
'fragment',
|
|
7
|
+
'input',
|
|
8
|
+
'use',
|
|
9
|
+
'actions',
|
|
10
|
+
'options',
|
|
11
|
+
'option',
|
|
12
|
+
'schema',
|
|
13
|
+
'import',
|
|
14
|
+
'from',
|
|
15
|
+
'with',
|
|
16
|
+
'section',
|
|
17
|
+
'group',
|
|
18
|
+
'stack',
|
|
19
|
+
'columns',
|
|
20
|
+
'grid',
|
|
21
|
+
'repeat',
|
|
22
|
+
'display',
|
|
23
|
+
'data',
|
|
24
|
+
'computed',
|
|
25
|
+
'upload',
|
|
26
|
+
]);
|
|
27
|
+
const NAMESPACES = new Set(['data', 'context', 'item', 'parent', 'computed', 'inputs', 'local', 'node']);
|
|
28
|
+
const ACTIONS = new Set(['next', 'back', 'save', 'submit', 'reset', 'set', 'clear', 'invoke', 'validate']);
|
|
29
|
+
const MODIFIERS = new Set([
|
|
30
|
+
'required',
|
|
31
|
+
'show',
|
|
32
|
+
'enable',
|
|
33
|
+
'readonly',
|
|
34
|
+
'when',
|
|
35
|
+
'message',
|
|
36
|
+
'default',
|
|
37
|
+
'description',
|
|
38
|
+
'placeholder',
|
|
39
|
+
'as',
|
|
40
|
+
'at',
|
|
41
|
+
'id',
|
|
42
|
+
'region',
|
|
43
|
+
'accept',
|
|
44
|
+
'maxSize',
|
|
45
|
+
'maxTotalSize',
|
|
46
|
+
'min',
|
|
47
|
+
'max',
|
|
48
|
+
'minLength',
|
|
49
|
+
'maxLength',
|
|
50
|
+
'minItems',
|
|
51
|
+
'maxItems',
|
|
52
|
+
'multipleOf',
|
|
53
|
+
'mustBe',
|
|
54
|
+
'uniqueItems',
|
|
55
|
+
'pattern',
|
|
56
|
+
'format',
|
|
57
|
+
'hide',
|
|
58
|
+
'disable',
|
|
59
|
+
]);
|
|
60
|
+
const TYPES = new Set([
|
|
61
|
+
'string',
|
|
62
|
+
'integer',
|
|
63
|
+
'number',
|
|
64
|
+
'boolean',
|
|
65
|
+
'object',
|
|
66
|
+
'email',
|
|
67
|
+
'url',
|
|
68
|
+
'phone',
|
|
69
|
+
'date',
|
|
70
|
+
'time',
|
|
71
|
+
'datetime',
|
|
72
|
+
'file',
|
|
73
|
+
'enum',
|
|
74
|
+
]);
|
|
75
|
+
const FUNCTIONS = new Set([
|
|
76
|
+
'has',
|
|
77
|
+
'size',
|
|
78
|
+
'sum',
|
|
79
|
+
'default',
|
|
80
|
+
'startsWith',
|
|
81
|
+
'endsWith',
|
|
82
|
+
'contains',
|
|
83
|
+
'count',
|
|
84
|
+
'every',
|
|
85
|
+
'some',
|
|
86
|
+
]);
|
|
87
|
+
const LOGIC = new Set(['and', 'or', 'not', 'in']);
|
|
88
|
+
const LITERALS = new Set(['true', 'false', 'null']);
|
|
89
|
+
|
|
90
|
+
const NAME = /[A-Za-z_][A-Za-z0-9_-]*/;
|
|
91
|
+
const NUMBER = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/;
|
|
92
|
+
|
|
93
|
+
export const scriptTokenizer = {
|
|
94
|
+
name: 'forming',
|
|
95
|
+
startState: () => ({expectType: false}),
|
|
96
|
+
token(stream, state) {
|
|
97
|
+
if (stream.sol()) state.expectType = false;
|
|
98
|
+
if (stream.eatSpace()) return null;
|
|
99
|
+
if (stream.peek() === '#') {
|
|
100
|
+
stream.skipToEnd();
|
|
101
|
+
return 'lineComment';
|
|
102
|
+
}
|
|
103
|
+
if (stream.peek() === '"') {
|
|
104
|
+
stream.next();
|
|
105
|
+
while (!stream.eol()) {
|
|
106
|
+
const ch = stream.next();
|
|
107
|
+
if (ch === '\\') stream.next();
|
|
108
|
+
else if (ch === '"') break;
|
|
109
|
+
}
|
|
110
|
+
state.expectType = false;
|
|
111
|
+
return 'string';
|
|
112
|
+
}
|
|
113
|
+
if (stream.match(NUMBER)) {
|
|
114
|
+
state.expectType = false;
|
|
115
|
+
return 'number';
|
|
116
|
+
}
|
|
117
|
+
if (stream.match(NAME)) {
|
|
118
|
+
const word = stream.current();
|
|
119
|
+
const expectType = state.expectType;
|
|
120
|
+
state.expectType = false;
|
|
121
|
+
if (stream.match(/^\s*:/, false)) {
|
|
122
|
+
state.expectType = true;
|
|
123
|
+
return 'variableName.definition';
|
|
124
|
+
}
|
|
125
|
+
if (expectType && TYPES.has(word)) return 'typeName';
|
|
126
|
+
if (stream.match(/^\(/, false) && FUNCTIONS.has(word)) return 'variableName.function';
|
|
127
|
+
if (LITERALS.has(word)) return word === 'null' ? 'null' : 'bool';
|
|
128
|
+
if (LOGIC.has(word)) return 'logicOperator';
|
|
129
|
+
if (STRUCTURE.has(word)) return 'keyword';
|
|
130
|
+
if (ACTIONS.has(word)) return 'keyword';
|
|
131
|
+
if (NAMESPACES.has(word)) return 'namespace';
|
|
132
|
+
if (MODIFIERS.has(word)) return 'modifier';
|
|
133
|
+
if (TYPES.has(word)) return 'typeName';
|
|
134
|
+
return 'variableName';
|
|
135
|
+
}
|
|
136
|
+
const ch = stream.next();
|
|
137
|
+
if (ch === ':') {
|
|
138
|
+
state.expectType = true;
|
|
139
|
+
return 'punctuation';
|
|
140
|
+
}
|
|
141
|
+
if (ch === '?' && stream.peek() === '?') {
|
|
142
|
+
stream.next();
|
|
143
|
+
return 'operator';
|
|
144
|
+
}
|
|
145
|
+
if ('=!<>'.includes(ch) && stream.peek() === '=') {
|
|
146
|
+
stream.next();
|
|
147
|
+
return 'operator';
|
|
148
|
+
}
|
|
149
|
+
if ('=!<>+-*/%?.'.includes(ch)) return 'operator';
|
|
150
|
+
if ('()[],'.includes(ch)) return 'punctuation';
|
|
151
|
+
return null;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Minimal stream interface for static snippets. No editor or browser is needed.
|
|
156
|
+
export function scriptTokens(source) {
|
|
157
|
+
const result = [];
|
|
158
|
+
const state = scriptTokenizer.startState();
|
|
159
|
+
let offset = 0;
|
|
160
|
+
for (const line of source.split('\n')) {
|
|
161
|
+
const stream = {
|
|
162
|
+
pos: 0,
|
|
163
|
+
start: 0,
|
|
164
|
+
sol() {
|
|
165
|
+
return this.pos === 0;
|
|
166
|
+
},
|
|
167
|
+
eol() {
|
|
168
|
+
return this.pos >= line.length;
|
|
169
|
+
},
|
|
170
|
+
peek() {
|
|
171
|
+
return line[this.pos];
|
|
172
|
+
},
|
|
173
|
+
next() {
|
|
174
|
+
return line[this.pos++];
|
|
175
|
+
},
|
|
176
|
+
current() {
|
|
177
|
+
return line.slice(this.start, this.pos);
|
|
178
|
+
},
|
|
179
|
+
skipToEnd() {
|
|
180
|
+
this.pos = line.length;
|
|
181
|
+
},
|
|
182
|
+
eatSpace() {
|
|
183
|
+
return this.match(/^\s+/);
|
|
184
|
+
},
|
|
185
|
+
match(pattern, consume = true) {
|
|
186
|
+
const match = line.slice(this.pos).match(pattern);
|
|
187
|
+
if (!match || match.index !== 0) return false;
|
|
188
|
+
if (consume) this.pos += match[0].length;
|
|
189
|
+
return match;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
while (!stream.eol()) {
|
|
193
|
+
stream.start = stream.pos;
|
|
194
|
+
const type = scriptTokenizer.token(stream, state);
|
|
195
|
+
// An unfinished escape can advance past the final character.
|
|
196
|
+
stream.pos = Math.min(stream.pos, line.length);
|
|
197
|
+
result.push({from: offset + stream.start, to: offset + stream.pos, type, text: stream.current()});
|
|
198
|
+
}
|
|
199
|
+
if (offset + line.length < source.length)
|
|
200
|
+
result.push({from: offset + line.length, to: offset + line.length + 1, type: null, text: '\n'});
|
|
201
|
+
offset += line.length + 1;
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The tokenizer's word lists, for editors that build their own highlighting. */
|
|
207
|
+
export const WORDS = Object.freeze({
|
|
208
|
+
structure: [...STRUCTURE],
|
|
209
|
+
namespaces: [...NAMESPACES],
|
|
210
|
+
actions: [...ACTIONS],
|
|
211
|
+
modifiers: [...MODIFIERS],
|
|
212
|
+
types: [...TYPES],
|
|
213
|
+
functions: [...FUNCTIONS],
|
|
214
|
+
logic: [...LOGIC],
|
|
215
|
+
literals: [...LITERALS],
|
|
216
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forming/tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-dev.3.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
"./completions": "./dist/completions.mjs",
|
|
7
|
+
"./tokens": "./dist/tokens.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"NOTICE.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test test/*.test.mjs",
|
|
17
|
+
"build": "node build-package.mjs",
|
|
18
|
+
"prepack": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"registry": "https://registry.npmjs.org/"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/forming-info/tools.git"
|
|
27
|
+
}
|
|
28
|
+
}
|