@unocss/autocomplete 0.29.0 → 0.29.3

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/README.md CHANGED
@@ -1,8 +1,72 @@
1
1
  # @unocss/autocomplete
2
2
 
3
- Autocomplete utils for UnoCSS.
3
+ Autocomplete utils for UnoCSS. This is embedded in [the playground](https://unocss.antfu.me/) and [the VS Code extension](https://github.com/unocss/unocss/tree/main/packages/vscode).
4
4
 
5
- > WIP
5
+ ## Syntax
6
+
7
+ To add autocomplete support to your custom rules:
8
+
9
+ ### Static Rules
10
+
11
+ Static rules like this will just works without any configuration.
12
+
13
+ ```ts
14
+ rules: [
15
+ ['flex', { display: 'flex' }]
16
+ ]
17
+ ```
18
+
19
+ ### Dynamic Rules
20
+
21
+ For dynamic rules, you can provide an extra `meta` object to the rule and specify the autocomplete template.
22
+
23
+ ```ts
24
+ rules: [
25
+ [
26
+ /^m-(\d)$/,
27
+ ([, d]) => ({ margin: `${d / 4}rem` }),
28
+ { autocomplete: 'm-<num>' }, // <-- this
29
+ ],
30
+ ]
31
+ ```
32
+
33
+ The template uses a simle DSL to specify the autocomplete suggestions. The syntax is:
34
+
35
+ - `(...|...)`: logic OR groups. `|` as the separator. Will be used as suggestions when the some of the groups match.
36
+ - `<...>`: built-in short hands. currently supports `<num>`, `<percent>` and `<directions>`
37
+ - `$...`: theme infering. for example, `$colors` will list all the properties of the `colors` object of the theme.
38
+
39
+ For examples:
40
+
41
+ ###### Example 1
42
+
43
+ - **Template**: `(border|b)-(solid|dashed|dotted|double|hidden|none)`
44
+ - **Input**: `b-do`
45
+ - **Suggestions**: `b-dashed`, `b-double`
46
+
47
+ ###### Example 2
48
+
49
+ - **Template**: `m-<num>`
50
+ - **Input**: `m-`
51
+ - **Suggestions**: `m-1`, `m-2`, `m-3` ...
52
+
53
+ ###### Example 3
54
+
55
+ - **Template**: `text-$colors`
56
+ - **Input**: `text-r`
57
+ - **Suggestions**: `text-red`, `text-rose` ...
58
+
59
+ ###### Example 4
60
+
61
+ For multiple templates
62
+
63
+ - **Template**: `['(border|b)-<num>', '(border|b)-<directions>-<num>']`
64
+
65
+ - **Input**: `b-`
66
+ - **Suggestions**: `b-x`, `b-y`, `b-1`, `b-2` ...
67
+
68
+ - **Input**: `b-x-`
69
+ - **Suggestions**: `b-x-1`, `b-x-2` ...
6
70
 
7
71
  ## License
8
72
 
package/dist/index.cjs CHANGED
@@ -11,9 +11,10 @@ const LRU__default = /*#__PURE__*/_interopDefaultLegacy(LRU);
11
11
 
12
12
  const shorthands = {
13
13
  num: `(${[0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 24, 36].join("|")})`,
14
- precent: `(${[0, 25, 50, 45, 100].join("|")})`,
14
+ percent: `(${Array.from({ length: 11 }, (_, i) => i * 10).join("|")})`,
15
15
  directions: "(x|y|t|b|l|r|s|e)"
16
16
  };
17
+ const ignoredThemeKeys = ["DEFAULT"];
17
18
  function handleRegexMatch(str, regex, onMatched, onNotMatched) {
18
19
  let lastIndex = 0;
19
20
  Array.from(str.matchAll(regex)).forEach((m) => {
@@ -40,10 +41,9 @@ function parseAutocomplete(template, theme = {}) {
40
41
  };
41
42
  function handleNonGroup(input) {
42
43
  handleRegexMatch(input, /\$([\w\|]+)/g, (m) => {
43
- const keys = m[1].split("|");
44
44
  parts.push({
45
45
  type: "theme",
46
- objects: keys.map((i) => {
46
+ objects: m[1].split("|").map((i) => {
47
47
  if (!i || !theme[i])
48
48
  throw new Error(`Invalid theme key ${i}`);
49
49
  return theme[i];
@@ -74,8 +74,14 @@ function parseAutocomplete(template, theme = {}) {
74
74
  while (tempParts.length) {
75
75
  const part = tempParts.shift();
76
76
  if (part.type === "static") {
77
- if (!rest.startsWith(part.value))
77
+ if (combinations.length)
78
+ combinations = combinations.map((i) => i + part.value);
79
+ if (part.value.startsWith(rest) && part.value !== rest && !combinations.length) {
80
+ combinations = [part.value];
78
81
  break;
82
+ } else if (!rest.startsWith(part.value)) {
83
+ break;
84
+ }
79
85
  matched += part.value;
80
86
  rest = rest.slice(part.value.length);
81
87
  } else if (part.type === "group") {
@@ -85,10 +91,11 @@ function parseAutocomplete(template, theme = {}) {
85
91
  rest = rest.slice(fullMatched.length);
86
92
  } else {
87
93
  combinations = part.values.filter((i) => i.startsWith(rest));
88
- break;
94
+ if (tempParts[0]?.type !== "static")
95
+ break;
89
96
  }
90
97
  } else if (part.type === "theme") {
91
- const keys = part.objects.flatMap((i) => Object.keys(i));
98
+ const keys = part.objects.flatMap((i) => Object.keys(i)).filter((i) => i && !ignoredThemeKeys.includes(i) && i[0] !== "_");
92
99
  const fullMatched = keys.find((i) => i && rest.startsWith(i));
93
100
  if (fullMatched != null) {
94
101
  matched += fullMatched;
@@ -105,12 +112,11 @@ function parseAutocomplete(template, theme = {}) {
105
112
  }
106
113
  } else {
107
114
  combinations = keys.filter((i) => i.startsWith(rest));
108
- break;
115
+ if (tempParts[0]?.type !== "static")
116
+ break;
109
117
  }
110
118
  }
111
119
  }
112
- if (!matched)
113
- return [];
114
120
  if (combinations.length === 0)
115
121
  combinations.push("");
116
122
  return combinations.map((i) => matched + i).filter((i) => i.length >= input.length);
@@ -121,7 +127,14 @@ function createAutocomplete(uno) {
121
127
  const templateCache = /* @__PURE__ */ new Map();
122
128
  const cache = new LRU__default({ max: 1e3 });
123
129
  let staticUtils = [];
124
- let templates = [];
130
+ const templates = [];
131
+ reset();
132
+ return {
133
+ suggest,
134
+ templates,
135
+ cache,
136
+ reset
137
+ };
125
138
  function getParsed(template) {
126
139
  if (!templateCache.has(template))
127
140
  templateCache.set(template, parseAutocomplete(template, uno.config.theme));
@@ -132,11 +145,18 @@ function createAutocomplete(uno) {
132
145
  return [];
133
146
  if (cache.has(input))
134
147
  return cache.get(input);
135
- const result = await Promise.all([
136
- suggestSelf(input),
137
- suggestStatic(input),
138
- ...suggestFromPreset(input)
139
- ]).then((i) => core.uniq(i.flat()).sort().filter(Boolean));
148
+ const [, processed, , variants] = uno.matchVariants(input);
149
+ const idx = input.search(processed);
150
+ if (idx === -1)
151
+ return [];
152
+ const variantPrefix = input.slice(0, idx);
153
+ const variantPostfix = input.slice(idx + input.length);
154
+ const result = processSuggestions(await Promise.all([
155
+ suggestSelf(processed),
156
+ suggestStatic(processed),
157
+ ...suggestFromPreset(processed),
158
+ ...suggestVariant(processed, variants)
159
+ ]), variantPrefix, variantPostfix);
140
160
  cache.set(input, result);
141
161
  return result;
142
162
  }
@@ -150,30 +170,34 @@ function createAutocomplete(uno) {
150
170
  function suggestFromPreset(input) {
151
171
  return templates.map((fn) => typeof fn === "function" ? fn(input) : getParsed(fn)(input)) || [];
152
172
  }
173
+ function suggestVariant(input, used) {
174
+ return uno.config.variants.filter((v) => v.autocomplete && (v.multiPass || !used.has(v))).flatMap((v) => core.toArray(v.autocomplete || [])).map((fn) => typeof fn === "function" ? fn(input) : getParsed(fn)(input));
175
+ }
153
176
  function reset() {
154
177
  templateCache.clear();
155
178
  cache.clear();
156
179
  staticUtils = Object.keys(uno.config.rulesStaticMap);
157
- templates = [
158
- ...uno.config.autocomplete || [],
159
- ...uno.config.rulesDynamic.flatMap((i) => core.toArray(i?.[2]?.autocomplete || []))
160
- ];
180
+ templates.length = 0;
181
+ templates.push(...uno.config.autocomplete || [], ...uno.config.rulesDynamic.flatMap((i) => core.toArray(i?.[2]?.autocomplete || [])));
182
+ }
183
+ function processSuggestions(suggestions, prefix = "", suffix = "") {
184
+ return core.uniq(suggestions.flat()).filter((i) => !!(i && !i.match(/-$/))).sort((a, b) => {
185
+ const numA = +(a.match(/\d+$/)?.[0] || NaN);
186
+ const numB = +(b.match(/\d+$/)?.[0] || NaN);
187
+ if (!Number.isNaN(numA) && !Number.isNaN(numB))
188
+ return numA - numB;
189
+ return a.localeCompare(b);
190
+ }).map((i) => prefix + i + suffix);
161
191
  }
162
- reset();
163
- return {
164
- suggest,
165
- templates,
166
- cache,
167
- reset
168
- };
169
192
  }
170
193
 
171
194
  function searchUsageBoundary(line, index) {
172
195
  let start = index;
173
196
  let end = index;
174
- while (start && /[^\s"']/.test(line.charAt(start - 1)))
197
+ const regex = /[^\s>"'`]/;
198
+ while (start && regex.test(line.charAt(start - 1)))
175
199
  --start;
176
- while (end < line.length && /[^\s"']/.test(line.charAt(end)))
200
+ while (end < line.length && regex.test(line.charAt(end)))
177
201
  ++end;
178
202
  return {
179
203
  content: line.slice(start, end),
@@ -183,5 +207,7 @@ function searchUsageBoundary(line, index) {
183
207
  }
184
208
 
185
209
  exports.createAutocomplete = createAutocomplete;
210
+ exports.ignoredThemeKeys = ignoredThemeKeys;
186
211
  exports.parseAutocomplete = parseAutocomplete;
187
212
  exports.searchUsageBoundary = searchUsageBoundary;
213
+ exports.shorthands = shorthands;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { UnoGenerator, AutoCompleteFunction } from '@unocss/core';
2
2
  import LRU from 'lru-cache';
3
3
 
4
4
  declare function createAutocomplete(uno: UnoGenerator): {
5
- suggest: (input: string) => Promise<string[] | undefined>;
5
+ suggest: (input: string) => Promise<string[]>;
6
6
  templates: (string | AutoCompleteFunction)[];
7
7
  cache: LRU<string, string[]>;
8
8
  reset: () => void;
@@ -26,6 +26,8 @@ interface ParsedAutocompleteTemplate {
26
26
  suggest(input: string): string[] | undefined;
27
27
  }
28
28
 
29
+ declare const shorthands: Record<string, string>;
30
+ declare const ignoredThemeKeys: string[];
29
31
  declare function parseAutocomplete(template: string, theme?: any): ParsedAutocompleteTemplate;
30
32
 
31
33
  declare function searchUsageBoundary(line: string, index: number): {
@@ -34,4 +36,4 @@ declare function searchUsageBoundary(line: string, index: number): {
34
36
  end: number;
35
37
  };
36
38
 
37
- export { AutocompleteTemplateGroup, AutocompleteTemplatePart, AutocompleteTemplateStatic, AutocompleteTemplateTheme, ParsedAutocompleteTemplate, createAutocomplete, parseAutocomplete, searchUsageBoundary };
39
+ export { AutocompleteTemplateGroup, AutocompleteTemplatePart, AutocompleteTemplateStatic, AutocompleteTemplateTheme, ParsedAutocompleteTemplate, createAutocomplete, ignoredThemeKeys, parseAutocomplete, searchUsageBoundary, shorthands };
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
- import { uniq, toArray } from '@unocss/core';
1
+ import { toArray, uniq } from '@unocss/core';
2
2
  import LRU from 'lru-cache';
3
3
 
4
4
  const shorthands = {
5
5
  num: `(${[0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 24, 36].join("|")})`,
6
- precent: `(${[0, 25, 50, 45, 100].join("|")})`,
6
+ percent: `(${Array.from({ length: 11 }, (_, i) => i * 10).join("|")})`,
7
7
  directions: "(x|y|t|b|l|r|s|e)"
8
8
  };
9
+ const ignoredThemeKeys = ["DEFAULT"];
9
10
  function handleRegexMatch(str, regex, onMatched, onNotMatched) {
10
11
  let lastIndex = 0;
11
12
  Array.from(str.matchAll(regex)).forEach((m) => {
@@ -32,10 +33,9 @@ function parseAutocomplete(template, theme = {}) {
32
33
  };
33
34
  function handleNonGroup(input) {
34
35
  handleRegexMatch(input, /\$([\w\|]+)/g, (m) => {
35
- const keys = m[1].split("|");
36
36
  parts.push({
37
37
  type: "theme",
38
- objects: keys.map((i) => {
38
+ objects: m[1].split("|").map((i) => {
39
39
  if (!i || !theme[i])
40
40
  throw new Error(`Invalid theme key ${i}`);
41
41
  return theme[i];
@@ -66,8 +66,14 @@ function parseAutocomplete(template, theme = {}) {
66
66
  while (tempParts.length) {
67
67
  const part = tempParts.shift();
68
68
  if (part.type === "static") {
69
- if (!rest.startsWith(part.value))
69
+ if (combinations.length)
70
+ combinations = combinations.map((i) => i + part.value);
71
+ if (part.value.startsWith(rest) && part.value !== rest && !combinations.length) {
72
+ combinations = [part.value];
70
73
  break;
74
+ } else if (!rest.startsWith(part.value)) {
75
+ break;
76
+ }
71
77
  matched += part.value;
72
78
  rest = rest.slice(part.value.length);
73
79
  } else if (part.type === "group") {
@@ -77,10 +83,11 @@ function parseAutocomplete(template, theme = {}) {
77
83
  rest = rest.slice(fullMatched.length);
78
84
  } else {
79
85
  combinations = part.values.filter((i) => i.startsWith(rest));
80
- break;
86
+ if (tempParts[0]?.type !== "static")
87
+ break;
81
88
  }
82
89
  } else if (part.type === "theme") {
83
- const keys = part.objects.flatMap((i) => Object.keys(i));
90
+ const keys = part.objects.flatMap((i) => Object.keys(i)).filter((i) => i && !ignoredThemeKeys.includes(i) && i[0] !== "_");
84
91
  const fullMatched = keys.find((i) => i && rest.startsWith(i));
85
92
  if (fullMatched != null) {
86
93
  matched += fullMatched;
@@ -97,12 +104,11 @@ function parseAutocomplete(template, theme = {}) {
97
104
  }
98
105
  } else {
99
106
  combinations = keys.filter((i) => i.startsWith(rest));
100
- break;
107
+ if (tempParts[0]?.type !== "static")
108
+ break;
101
109
  }
102
110
  }
103
111
  }
104
- if (!matched)
105
- return [];
106
112
  if (combinations.length === 0)
107
113
  combinations.push("");
108
114
  return combinations.map((i) => matched + i).filter((i) => i.length >= input.length);
@@ -113,7 +119,14 @@ function createAutocomplete(uno) {
113
119
  const templateCache = /* @__PURE__ */ new Map();
114
120
  const cache = new LRU({ max: 1e3 });
115
121
  let staticUtils = [];
116
- let templates = [];
122
+ const templates = [];
123
+ reset();
124
+ return {
125
+ suggest,
126
+ templates,
127
+ cache,
128
+ reset
129
+ };
117
130
  function getParsed(template) {
118
131
  if (!templateCache.has(template))
119
132
  templateCache.set(template, parseAutocomplete(template, uno.config.theme));
@@ -124,11 +137,18 @@ function createAutocomplete(uno) {
124
137
  return [];
125
138
  if (cache.has(input))
126
139
  return cache.get(input);
127
- const result = await Promise.all([
128
- suggestSelf(input),
129
- suggestStatic(input),
130
- ...suggestFromPreset(input)
131
- ]).then((i) => uniq(i.flat()).sort().filter(Boolean));
140
+ const [, processed, , variants] = uno.matchVariants(input);
141
+ const idx = input.search(processed);
142
+ if (idx === -1)
143
+ return [];
144
+ const variantPrefix = input.slice(0, idx);
145
+ const variantPostfix = input.slice(idx + input.length);
146
+ const result = processSuggestions(await Promise.all([
147
+ suggestSelf(processed),
148
+ suggestStatic(processed),
149
+ ...suggestFromPreset(processed),
150
+ ...suggestVariant(processed, variants)
151
+ ]), variantPrefix, variantPostfix);
132
152
  cache.set(input, result);
133
153
  return result;
134
154
  }
@@ -142,30 +162,34 @@ function createAutocomplete(uno) {
142
162
  function suggestFromPreset(input) {
143
163
  return templates.map((fn) => typeof fn === "function" ? fn(input) : getParsed(fn)(input)) || [];
144
164
  }
165
+ function suggestVariant(input, used) {
166
+ return uno.config.variants.filter((v) => v.autocomplete && (v.multiPass || !used.has(v))).flatMap((v) => toArray(v.autocomplete || [])).map((fn) => typeof fn === "function" ? fn(input) : getParsed(fn)(input));
167
+ }
145
168
  function reset() {
146
169
  templateCache.clear();
147
170
  cache.clear();
148
171
  staticUtils = Object.keys(uno.config.rulesStaticMap);
149
- templates = [
150
- ...uno.config.autocomplete || [],
151
- ...uno.config.rulesDynamic.flatMap((i) => toArray(i?.[2]?.autocomplete || []))
152
- ];
172
+ templates.length = 0;
173
+ templates.push(...uno.config.autocomplete || [], ...uno.config.rulesDynamic.flatMap((i) => toArray(i?.[2]?.autocomplete || [])));
174
+ }
175
+ function processSuggestions(suggestions, prefix = "", suffix = "") {
176
+ return uniq(suggestions.flat()).filter((i) => !!(i && !i.match(/-$/))).sort((a, b) => {
177
+ const numA = +(a.match(/\d+$/)?.[0] || NaN);
178
+ const numB = +(b.match(/\d+$/)?.[0] || NaN);
179
+ if (!Number.isNaN(numA) && !Number.isNaN(numB))
180
+ return numA - numB;
181
+ return a.localeCompare(b);
182
+ }).map((i) => prefix + i + suffix);
153
183
  }
154
- reset();
155
- return {
156
- suggest,
157
- templates,
158
- cache,
159
- reset
160
- };
161
184
  }
162
185
 
163
186
  function searchUsageBoundary(line, index) {
164
187
  let start = index;
165
188
  let end = index;
166
- while (start && /[^\s"']/.test(line.charAt(start - 1)))
189
+ const regex = /[^\s>"'`]/;
190
+ while (start && regex.test(line.charAt(start - 1)))
167
191
  --start;
168
- while (end < line.length && /[^\s"']/.test(line.charAt(end)))
192
+ while (end < line.length && regex.test(line.charAt(end)))
169
193
  ++end;
170
194
  return {
171
195
  content: line.slice(start, end),
@@ -174,4 +198,4 @@ function searchUsageBoundary(line, index) {
174
198
  };
175
199
  }
176
200
 
177
- export { createAutocomplete, parseAutocomplete, searchUsageBoundary };
201
+ export { createAutocomplete, ignoredThemeKeys, parseAutocomplete, searchUsageBoundary, shorthands };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unocss/autocomplete",
3
- "version": "0.29.0",
3
+ "version": "0.29.3",
4
4
  "description": "Autocomplete utils for UnoCSS",
5
5
  "keywords": [
6
6
  "unocss",
@@ -36,11 +36,11 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/lru-cache": "^7.4.0",
39
- "@unocss/core": "0.29.0"
39
+ "@unocss/core": "0.29.3"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "unbuild",
43
43
  "stub": "unbuild --stub"
44
44
  },
45
- "readme": "# @unocss/autocomplete\n\nAutocomplete utils for UnoCSS.\n\n> WIP\n\n## License\n\nMIT License © 2021-PRESENT [Anthony Fu](https://github.com/antfu)\n"
45
+ "readme": "# @unocss/autocomplete\n\nAutocomplete utils for UnoCSS. This is embedded in [the playground](https://unocss.antfu.me/) and [the VS Code extension](https://github.com/unocss/unocss/tree/main/packages/vscode).\n\n## Syntax\n\nTo add autocomplete support to your custom rules:\n\n### Static Rules\n\nStatic rules like this will just works without any configuration.\n\n```ts\nrules: [\n ['flex', { display: 'flex' }]\n]\n```\n\n### Dynamic Rules\n\nFor dynamic rules, you can provide an extra `meta` object to the rule and specify the autocomplete template.\n\n```ts\nrules: [\n [\n /^m-(\\d)$/,\n ([, d]) => ({ margin: `${d / 4}rem` }),\n { autocomplete: 'm-<num>' }, // <-- this\n ],\n]\n```\n\nThe template uses a simle DSL to specify the autocomplete suggestions. The syntax is:\n\n- `(...|...)`: logic OR groups. `|` as the separator. Will be used as suggestions when the some of the groups match.\n- `<...>`: built-in short hands. currently supports `<num>`, `<percent>` and `<directions>`\n- `$...`: theme infering. for example, `$colors` will list all the properties of the `colors` object of the theme.\n\nFor examples:\n\n###### Example 1\n\n- **Template**: `(border|b)-(solid|dashed|dotted|double|hidden|none)`\n- **Input**: `b-do`\n- **Suggestions**: `b-dashed`, `b-double`\n\n###### Example 2\n\n- **Template**: `m-<num>`\n- **Input**: `m-`\n- **Suggestions**: `m-1`, `m-2`, `m-3` ...\n\n###### Example 3\n\n- **Template**: `text-$colors`\n- **Input**: `text-r`\n- **Suggestions**: `text-red`, `text-rose` ...\n\n###### Example 4\n\nFor multiple templates\n\n- **Template**: `['(border|b)-<num>', '(border|b)-<directions>-<num>']`\n\n- **Input**: `b-`\n- **Suggestions**: `b-x`, `b-y`, `b-1`, `b-2` ...\n\n- **Input**: `b-x-`\n- **Suggestions**: `b-x-1`, `b-x-2` ...\n\n## License\n\nMIT License © 2021-PRESENT [Anthony Fu](https://github.com/antfu)\n"
46
46
  }