@kupcad/editor 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ // KupCAD as CodeMirror reads it. Its literals are Ruby's (strings with interpolation, the single
2
+ // quoted ones, %w and %q, heredocs, symbols, comments), so CodeMirror's Ruby mode reads them and
3
+ // indents the blocks; its words are the core's (vocabulary.json, which `zig build grammar`
4
+ // writes from the lexer and the stdlib), so a word is a keyword, a function or a method as the
5
+ // language has it, not as Ruby does. Completion offers the same words.
6
+
7
+ import { completeFromList } from '@codemirror/autocomplete';
8
+ import { LanguageSupport, StreamLanguage } from '@codemirror/language';
9
+ import { ruby } from '@codemirror/legacy-modes/mode/ruby';
10
+ import vocabulary from './vocabulary.json' with { type: 'json' };
11
+
12
+ /** @import { CompletionContext, CompletionResult } from '@codemirror/autocomplete' */
13
+ /** @import { StreamParser, StringStream } from '@codemirror/language' */
14
+
15
+ const KEYWORDS = new Set(vocabulary.keywords);
16
+ const FUNCTIONS = new Set(vocabulary.functions);
17
+ const METHODS = new Set(vocabulary.methods);
18
+ /** The highlighting tags the words get: lezer's names, a modifier after a dot. */
19
+ const TOKENS = /** @type {const} */ ({
20
+ keyword: 'keyword',
21
+ function: 'variableName.function.standard',
22
+ method: 'propertyName.function.standard',
23
+ property: 'propertyName',
24
+ className: 'className',
25
+ definition: 'variableName.definition',
26
+ variable: 'variableName',
27
+ });
28
+ /** A word before the cursor, or a method's name after its dot. */
29
+ const WORD_BEFORE = /\.?[\w?!]*$/;
30
+ const METHOD_NAME = /^[\w?!]*$/;
31
+
32
+ /** @typedef {Record<string, unknown>} RubyState */
33
+ /** The Ruby mode, whose state starts and indents (its type leaves them optional). */
34
+ const rubyMode =
35
+ /** @type {Required<Pick<StreamParser<RubyState>, 'startState' | 'indent'>> & StreamParser<RubyState>} */ (
36
+ /** @type {unknown} */ (ruby)
37
+ );
38
+
39
+ /**
40
+ * The Ruby mode's state, and whether a `for` waits for its `in` (a keyword there alone).
41
+ * @typedef {{ ruby: RubyState, forOpen: boolean }} State
42
+ */
43
+
44
+ /** @type {StreamParser<State>} */
45
+ const parser = {
46
+ name: 'kupcad',
47
+ startState: (indentUnit) => ({ ruby: rubyMode.startState(indentUnit), forOpen: false }),
48
+ copyState: (state) => ({ ruby: copyFields(state.ruby), forOpen: state.forOpen }),
49
+ token(stream, state) {
50
+ if (stream.sol()) state.forOpen = false;
51
+ const style = rubyMode.token(stream, state.ruby);
52
+ return style ? styleOf(stream, style, state) : null;
53
+ },
54
+ indent: (state, textAfter, context) => rubyMode.indent(state.ruby, textAfter, context),
55
+ languageData: rubyMode.languageData,
56
+ };
57
+
58
+ /**
59
+ * What the Ruby mode called a token, as KupCAD has it: a word by the core's vocabulary.
60
+ * @param {StringStream} stream
61
+ * @param {string} style the Ruby mode's
62
+ * @param {State} state
63
+ * @returns {string}
64
+ */
65
+ function styleOf(stream, style, state) {
66
+ const word = stream.current();
67
+ switch (style) {
68
+ case 'property':
69
+ return METHODS.has(word) ? TOKENS.method : TOKENS.property;
70
+ case 'tag':
71
+ return TOKENS.className;
72
+ case 'def':
73
+ return TOKENS.definition;
74
+ case 'keyword':
75
+ case 'variable':
76
+ if (word === 'for') state.forOpen = true;
77
+ if (KEYWORDS.has(word)) return TOKENS.keyword;
78
+ if (word === 'in' && state.forOpen) {
79
+ state.forOpen = false;
80
+ return TOKENS.keyword;
81
+ }
82
+ return FUNCTIONS.has(word) ? TOKENS.function : TOKENS.variable;
83
+ default:
84
+ return style;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * A copy of a mode's state, as CodeMirror copies one whose mode has no copyState of its own (the
90
+ * Ruby mode's): its fields, an array among them copied.
91
+ * @param {RubyState} state
92
+ * @returns {RubyState}
93
+ */
94
+ function copyFields(state) {
95
+ return Object.fromEntries(
96
+ Object.entries(state).map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]),
97
+ );
98
+ }
99
+
100
+ /** KupCAD's language, for an editor to read a script by. */
101
+ export const kupcadLanguage = StreamLanguage.define(parser);
102
+
103
+ const completeKeywordOrFunction = completeFromList([
104
+ ...vocabulary.keywords.map((label) => ({ label, type: 'keyword' })),
105
+ ...vocabulary.functions.map((label) => ({ label, type: 'function' })),
106
+ ]);
107
+ const METHOD_OPTIONS = vocabulary.methods.map((label) => ({ label, type: 'method' }));
108
+
109
+ /**
110
+ * Completes a word with the keywords and the functions, and a name after a dot with the methods.
111
+ * @param {CompletionContext} context
112
+ * @returns {CompletionResult | null | Promise<CompletionResult | null>}
113
+ */
114
+ export function completeKupcad(context) {
115
+ const before = context.matchBefore(WORD_BEFORE);
116
+ if (!before || (before.from === before.to && !context.explicit)) return null;
117
+ if (!before.text.startsWith('.')) return completeKeywordOrFunction(context);
118
+ return { from: before.from + 1, options: METHOD_OPTIONS, validFor: METHOD_NAME };
119
+ }
120
+
121
+ /**
122
+ * KupCAD for an editor: the language, and completion of its words unless something else
123
+ * completes them (a language server, which knows the script's own names too).
124
+ * @param {{ complete?: boolean }} [options]
125
+ */
126
+ export function kupcad({ complete = true } = {}) {
127
+ return new LanguageSupport(
128
+ kupcadLanguage,
129
+ complete ? kupcadLanguage.data.of({ autocomplete: completeKupcad }) : [],
130
+ );
131
+ }
package/src/svelte.js ADDED
@@ -0,0 +1,4 @@
1
+ // The editor for Svelte (Editor.svelte), by a module a host imports it from: a bare specifier
2
+ // to the .svelte file itself leaves svelte-check without its types, a relative import doesn't.
3
+
4
+ export { default as Editor } from './Editor.svelte';
package/src/theme.js ADDED
@@ -0,0 +1,71 @@
1
+ // How the editor looks: by the --kc-* tokens a host sets (@kupcad/ui's tokens.css), else the
2
+ // page's own colours (CSS system colours, light or dark as the page is); the code's colours by
3
+ // the --kc-code-* tokens (keyword, string, comment, constant, function, definition), else
4
+ // GitHub's, light or dark.
5
+
6
+ import { HighlightStyle } from '@codemirror/language';
7
+ import { EditorView } from '@codemirror/view';
8
+ import { tags } from '@lezer/highlight';
9
+
10
+ /**
11
+ * The colour of a kind of code: its token's, else GitHub's for the page's scheme.
12
+ * @param {string} kind
13
+ * @param {string} light
14
+ * @param {string} dark
15
+ */
16
+ const code = (kind, light, dark) => `var(--kc-code-${kind}, light-dark(${light}, ${dark}))`;
17
+ const FG = 'var(--kc-fg, CanvasText)';
18
+ const BG = 'var(--kc-bg, Canvas)';
19
+ const MUTED = 'var(--kc-muted, GrayText)';
20
+ const BORDER = 'var(--kc-border, GrayText)';
21
+ const ACCENT = 'var(--kc-accent, Highlight)';
22
+
23
+ export const editorTheme = EditorView.theme({
24
+ '&': {
25
+ height: '100%',
26
+ color: FG,
27
+ backgroundColor: BG,
28
+ fontSize: 'var(--kc-code-font-size, var(--kc-font-size, 13px))',
29
+ // What light-dark() picks by, where the page says nothing of its scheme
30
+ colorScheme: 'light dark',
31
+ },
32
+ '.cm-scroller': {
33
+ fontFamily: 'var(--kc-code-font, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)',
34
+ lineHeight: '1.5',
35
+ },
36
+ '.cm-gutters': { color: MUTED, backgroundColor: BG, borderRight: `1px solid ${BORDER}` },
37
+ '.cm-activeLine': { backgroundColor: `color-mix(in srgb, ${ACCENT} 6%, transparent)` },
38
+ '.cm-activeLineGutter': { backgroundColor: `color-mix(in srgb, ${ACCENT} 10%, transparent)` },
39
+ '.cm-cursor, .cm-dropCursor': { borderLeftColor: FG },
40
+ '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
41
+ backgroundColor: `color-mix(in srgb, ${ACCENT} 28%, transparent)`,
42
+ },
43
+ '.cm-matchingBracket': { backgroundColor: `color-mix(in srgb, ${ACCENT} 20%, transparent)` },
44
+ '.cm-tooltip': { color: FG, backgroundColor: BG, border: `1px solid ${BORDER}` },
45
+ '.cm-tooltip-autocomplete ul li[aria-selected]': {
46
+ color: 'var(--kc-accent-fg, HighlightText)',
47
+ backgroundColor: ACCENT,
48
+ },
49
+ '.cm-panels': { color: FG, backgroundColor: BG },
50
+ });
51
+
52
+ export const highlightStyle = HighlightStyle.define([
53
+ { tag: tags.keyword, color: code('keyword', '#cf222e', '#ff7b72') },
54
+ { tag: [tags.string, tags.special(tags.string)], color: code('string', '#0a3069', '#a5d6ff') },
55
+ { tag: tags.comment, color: code('comment', '#6e7781', '#8b949e'), fontStyle: 'italic' },
56
+ {
57
+ tag: [tags.number, tags.bool, tags.null, tags.atom],
58
+ color: code('constant', '#0550ae', '#79c0ff'),
59
+ },
60
+ {
61
+ tag: [
62
+ tags.standard(tags.function(tags.variableName)),
63
+ tags.standard(tags.function(tags.propertyName)),
64
+ ],
65
+ color: code('function', '#8250df', '#d2a8ff'),
66
+ },
67
+ {
68
+ tag: [tags.definition(tags.variableName), tags.className],
69
+ color: code('definition', '#953800', '#ffa657'),
70
+ },
71
+ ]);
@@ -0,0 +1,301 @@
1
+ {
2
+ "keywords": [
3
+ "and",
4
+ "as",
5
+ "begin",
6
+ "break",
7
+ "case",
8
+ "class",
9
+ "def",
10
+ "do",
11
+ "else",
12
+ "elsif",
13
+ "end",
14
+ "ensure",
15
+ "export",
16
+ "false",
17
+ "for",
18
+ "from",
19
+ "if",
20
+ "import",
21
+ "module",
22
+ "next",
23
+ "nil",
24
+ "not",
25
+ "or",
26
+ "private",
27
+ "public",
28
+ "rescue",
29
+ "return",
30
+ "self",
31
+ "super",
32
+ "then",
33
+ "true",
34
+ "unless",
35
+ "until",
36
+ "when",
37
+ "while",
38
+ "with",
39
+ "yield"
40
+ ],
41
+ "functions": [
42
+ "arc",
43
+ "assemble",
44
+ "assert",
45
+ "batch_hull",
46
+ "benchmark",
47
+ "block_given?",
48
+ "circle",
49
+ "cube",
50
+ "cylinder",
51
+ "debugger",
52
+ "defined?",
53
+ "ghost",
54
+ "highlight",
55
+ "import_3mf",
56
+ "import_dxf",
57
+ "import_font",
58
+ "import_json",
59
+ "import_obj",
60
+ "import_step",
61
+ "import_stl",
62
+ "import_svg",
63
+ "inspect",
64
+ "line",
65
+ "param",
66
+ "polygon",
67
+ "polyhedron",
68
+ "polyline",
69
+ "print",
70
+ "puts",
71
+ "raise",
72
+ "regular_polygon",
73
+ "sphere",
74
+ "square",
75
+ "text",
76
+ "torus",
77
+ "union",
78
+ "warn"
79
+ ],
80
+ "methods": [
81
+ "abs",
82
+ "acos",
83
+ "acosd",
84
+ "align",
85
+ "all?",
86
+ "any?",
87
+ "arc_to",
88
+ "area",
89
+ "asin",
90
+ "asind",
91
+ "atan",
92
+ "atan2",
93
+ "atan2d",
94
+ "atand",
95
+ "bbox",
96
+ "between?",
97
+ "bom",
98
+ "bytes_allocated",
99
+ "cbrt",
100
+ "ceil",
101
+ "center",
102
+ "center_of_mass",
103
+ "chamfer",
104
+ "chars",
105
+ "clamp",
106
+ "class",
107
+ "clone",
108
+ "close",
109
+ "close?",
110
+ "cm",
111
+ "collect",
112
+ "compact",
113
+ "config",
114
+ "contains?",
115
+ "cos",
116
+ "cosd",
117
+ "count",
118
+ "cover?",
119
+ "current_config",
120
+ "decompose",
121
+ "deg2rad",
122
+ "delete",
123
+ "difference",
124
+ "dig",
125
+ "downcase",
126
+ "downto",
127
+ "drop",
128
+ "dup",
129
+ "each",
130
+ "each_cons",
131
+ "each_slice",
132
+ "each_with_index",
133
+ "empty?",
134
+ "end",
135
+ "end_with?",
136
+ "ends_with?",
137
+ "even?",
138
+ "exclusive?",
139
+ "exp",
140
+ "export_3mf",
141
+ "export_dxf",
142
+ "export_gltf",
143
+ "export_obj",
144
+ "export_step",
145
+ "export_stl",
146
+ "export_svg",
147
+ "extrude",
148
+ "fetch",
149
+ "fillet",
150
+ "filter",
151
+ "find",
152
+ "finite?",
153
+ "first",
154
+ "flat_map",
155
+ "flatten",
156
+ "floor",
157
+ "gcd",
158
+ "genus",
159
+ "get",
160
+ "ghost",
161
+ "group_by",
162
+ "helix",
163
+ "highlight",
164
+ "hull",
165
+ "hypot",
166
+ "inch",
167
+ "include?",
168
+ "infinite?",
169
+ "inspect",
170
+ "integer?",
171
+ "intersection",
172
+ "intersects?",
173
+ "into",
174
+ "invert",
175
+ "is_a?",
176
+ "join",
177
+ "key?",
178
+ "keys",
179
+ "last",
180
+ "lcm",
181
+ "length",
182
+ "lerp",
183
+ "line_to",
184
+ "ljust",
185
+ "loft",
186
+ "log",
187
+ "log10",
188
+ "log2",
189
+ "m",
190
+ "map",
191
+ "mass",
192
+ "material",
193
+ "max",
194
+ "max_by",
195
+ "merge",
196
+ "method",
197
+ "min",
198
+ "min_by",
199
+ "min_gap",
200
+ "minkowski",
201
+ "mirror",
202
+ "mm",
203
+ "name",
204
+ "named",
205
+ "nan?",
206
+ "negative?",
207
+ "nil?",
208
+ "none?",
209
+ "odd?",
210
+ "offset",
211
+ "parts",
212
+ "polar",
213
+ "pop",
214
+ "positive?",
215
+ "preview?",
216
+ "project",
217
+ "push",
218
+ "rad2deg",
219
+ "rand",
220
+ "ray_cast",
221
+ "reduce",
222
+ "reject",
223
+ "remap",
224
+ "repeat_linear",
225
+ "repeat_polar",
226
+ "replace",
227
+ "resize",
228
+ "respond_to?",
229
+ "reverse",
230
+ "revolve",
231
+ "rjust",
232
+ "rotate",
233
+ "round",
234
+ "sample",
235
+ "scale",
236
+ "seed",
237
+ "shell",
238
+ "shift",
239
+ "shuffle",
240
+ "sign",
241
+ "simplify",
242
+ "sin",
243
+ "sind",
244
+ "size",
245
+ "slice",
246
+ "smoothstep",
247
+ "snap",
248
+ "sort",
249
+ "sort_by",
250
+ "split",
251
+ "split_by_plane",
252
+ "sqrt",
253
+ "start",
254
+ "start_with?",
255
+ "starts_with?",
256
+ "step",
257
+ "stringify_keys",
258
+ "strip",
259
+ "stroke",
260
+ "sum",
261
+ "surface_area",
262
+ "sweep",
263
+ "symbolize_keys",
264
+ "take",
265
+ "tan",
266
+ "tand",
267
+ "tangent_arc_to",
268
+ "tap",
269
+ "times",
270
+ "to_a",
271
+ "to_f",
272
+ "to_h",
273
+ "to_i",
274
+ "to_s",
275
+ "to_sym",
276
+ "transform",
277
+ "transform_values",
278
+ "translate",
279
+ "trim",
280
+ "trim_by_plane",
281
+ "union",
282
+ "uniq",
283
+ "unshift",
284
+ "upcase",
285
+ "upto",
286
+ "values",
287
+ "volume",
288
+ "with_config",
289
+ "x_max",
290
+ "x_min",
291
+ "x_size",
292
+ "y_max",
293
+ "y_min",
294
+ "y_size",
295
+ "z_max",
296
+ "z_min",
297
+ "z_size",
298
+ "zero?",
299
+ "zip"
300
+ ]
301
+ }