@puruslang/prettier-plugin-purus 0.0.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/README.md +58 -0
- package/package.json +36 -0
- package/src/index.js +312 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
[](https://purus.work)
|
|
4
|
+
|
|
5
|
+
**English** | [日本語](./README-ja.md)
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/purus)
|
|
12
|
+
[](https://www.npmjs.com/package/purus)
|
|
13
|
+
[](https://github.com/otoneko1102/purus/pulse)
|
|
14
|
+
[](https://github.com/otoneko1102/purus/commits/main)
|
|
15
|
+

|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
Purus - _/ˈpuː.rus/_ _**means pure✨ in Latin**_ - is a beautiful, simple, and easy-to-use language. It compiles to _JavaScript_.
|
|
19
|
+
|
|
20
|
+
**It makes your fingers free from the _Shift key_.**
|
|
21
|
+
|
|
22
|
+
With Purus, you can write code almost without pressing the _Shift key_.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# Global
|
|
28
|
+
npm install -g purus
|
|
29
|
+
|
|
30
|
+
# or Local
|
|
31
|
+
npm install -D purus
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## File Extensions
|
|
35
|
+
|
|
36
|
+
| Extension | Output |
|
|
37
|
+
| --------- | ------ |
|
|
38
|
+
| `.purus` | `.js` |
|
|
39
|
+
| `.cpurus` | `.cjs` |
|
|
40
|
+
| `.mpurus` | `.mjs` |
|
|
41
|
+
|
|
42
|
+
## Tooling
|
|
43
|
+
|
|
44
|
+
- **VS Code Extension** — [Marketplace](https://marketplace.visualstudio.com/items?itemName=otoneko1102.purus): Syntax highlighting, snippets, file icons
|
|
45
|
+
- **Linter** — [`@puruslang/linter`](https://www.npmjs.com/package/@puruslang/linter): Static analysis for Purus
|
|
46
|
+
- **Prettier Plugin** — [`@puruslang/prettier-plugin-purus`](https://www.npmjs.com/package/@puruslang/prettier-plugin-purus): Code formatting
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
The documentation is available on [purus.work](https://purus.work).
|
|
51
|
+
|
|
52
|
+
## Author
|
|
53
|
+
|
|
54
|
+
otoneko. https://github.com/otoneko1102
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
Distributed under the Apache 2.0 License. See [LICENSE](./LICENSE) for more information.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@puruslang/prettier-plugin-purus",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Prettier plugin for the Purus language",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"prettier",
|
|
9
|
+
"plugin",
|
|
10
|
+
"purus",
|
|
11
|
+
"altjs",
|
|
12
|
+
"lang",
|
|
13
|
+
"language",
|
|
14
|
+
"noshift",
|
|
15
|
+
"noshift.js"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"prettier": "^3.0.0"
|
|
19
|
+
},
|
|
20
|
+
"author": "otoneko1102 (https://github.com/otoneko1102)",
|
|
21
|
+
"homepage": "https://purus.work",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/otoneko1102/purus",
|
|
25
|
+
"directory": "prettier-plugin"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/otoneko1102/purus/issues"
|
|
29
|
+
},
|
|
30
|
+
"funding": {
|
|
31
|
+
"url": "https://github.com/sponsors/otoneko1102"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const KEYWORDS = new Set([
|
|
4
|
+
"const", "let", "var", "be",
|
|
5
|
+
"fn", "async", "return", "to", "gives",
|
|
6
|
+
"if", "elif", "else", "unless", "then",
|
|
7
|
+
"while", "until", "for", "in", "range",
|
|
8
|
+
"match", "when",
|
|
9
|
+
"try", "catch", "finally", "throw",
|
|
10
|
+
"import", "from", "export", "default", "require", "use", "mod", "pub", "all",
|
|
11
|
+
"add", "sub", "mul", "div", "mod", "neg",
|
|
12
|
+
"eq", "ne", "lt", "gt", "le", "ge",
|
|
13
|
+
"and", "or", "not", "pipe",
|
|
14
|
+
"is", "as", "of", "typeof", "instanceof", "type",
|
|
15
|
+
"new", "delete", "this", "await",
|
|
16
|
+
"true", "false", "null", "nil", "undefined",
|
|
17
|
+
"break", "continue",
|
|
18
|
+
"list", "object",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const BLOCK_STARTERS = new Set([
|
|
22
|
+
"fn", "if", "elif", "else", "unless",
|
|
23
|
+
"while", "until", "for",
|
|
24
|
+
"match", "when",
|
|
25
|
+
"try", "catch", "finally",
|
|
26
|
+
"mod",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function tokenize(source) {
|
|
30
|
+
const tokens = [];
|
|
31
|
+
let i = 0;
|
|
32
|
+
const len = source.length;
|
|
33
|
+
|
|
34
|
+
while (i < len) {
|
|
35
|
+
// Shebang
|
|
36
|
+
if (i === 0 && source[i] === "#" && source[i + 1] === "!") {
|
|
37
|
+
let end = source.indexOf("\n", i);
|
|
38
|
+
if (end === -1) end = len;
|
|
39
|
+
tokens.push({ type: "shebang", value: source.slice(i, end) });
|
|
40
|
+
i = end;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Newline
|
|
45
|
+
if (source[i] === "\n") {
|
|
46
|
+
tokens.push({ type: "newline", value: "\n" });
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Carriage return
|
|
52
|
+
if (source[i] === "\r") {
|
|
53
|
+
i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Whitespace (not newline)
|
|
58
|
+
if (source[i] === " " || source[i] === "\t") {
|
|
59
|
+
let start = i;
|
|
60
|
+
while (i < len && (source[i] === " " || source[i] === "\t")) i++;
|
|
61
|
+
tokens.push({ type: "whitespace", value: source.slice(start, i) });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Block comment ---
|
|
66
|
+
if (source[i] === "-" && source[i + 1] === "-" && source[i + 2] === "-") {
|
|
67
|
+
let end = source.indexOf("---", i + 3);
|
|
68
|
+
if (end === -1) end = len;
|
|
69
|
+
else end += 3;
|
|
70
|
+
tokens.push({ type: "block-comment", value: source.slice(i, end) });
|
|
71
|
+
i = end;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Line comment --
|
|
76
|
+
if (source[i] === "-" && source[i + 1] === "-") {
|
|
77
|
+
let end = source.indexOf("\n", i);
|
|
78
|
+
if (end === -1) end = len;
|
|
79
|
+
tokens.push({ type: "comment", value: source.slice(i, end) });
|
|
80
|
+
i = end;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// String ///
|
|
85
|
+
if (source[i] === "/" && source[i + 1] === "/" && source[i + 2] === "/") {
|
|
86
|
+
let j = i + 3;
|
|
87
|
+
while (j < len) {
|
|
88
|
+
if (source[j] === "\\" && j + 1 < len) {
|
|
89
|
+
j += 2;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (source[j] === "/" && source[j + 1] === "/" && source[j + 2] === "/") {
|
|
93
|
+
j += 3;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
j++;
|
|
97
|
+
}
|
|
98
|
+
tokens.push({ type: "string", value: source.slice(i, j) });
|
|
99
|
+
i = j;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Punctuation
|
|
104
|
+
if ("[],;.".includes(source[i])) {
|
|
105
|
+
tokens.push({ type: "punct", value: source[i] });
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Word (identifier or keyword)
|
|
111
|
+
if (/[a-zA-Z]/.test(source[i])) {
|
|
112
|
+
let start = i;
|
|
113
|
+
while (i < len && /[a-zA-Z0-9-]/.test(source[i])) i++;
|
|
114
|
+
const word = source.slice(start, i);
|
|
115
|
+
tokens.push({ type: KEYWORDS.has(word) ? "keyword" : "ident", value: word });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Number
|
|
120
|
+
if (/[0-9]/.test(source[i])) {
|
|
121
|
+
let start = i;
|
|
122
|
+
while (i < len && /[0-9]/.test(source[i])) i++;
|
|
123
|
+
if (i < len && source[i] === "." && i + 1 < len && /[0-9]/.test(source[i + 1])) {
|
|
124
|
+
i++;
|
|
125
|
+
while (i < len && /[0-9]/.test(source[i])) i++;
|
|
126
|
+
}
|
|
127
|
+
tokens.push({ type: "number", value: source.slice(start, i) });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Regex /pattern/flags
|
|
132
|
+
if (source[i] === "/" && source[i + 1] !== "/") {
|
|
133
|
+
let j = i + 1;
|
|
134
|
+
while (j < len && source[j] !== "/" && source[j] !== "\n") {
|
|
135
|
+
if (source[j] === "\\") j++;
|
|
136
|
+
j++;
|
|
137
|
+
}
|
|
138
|
+
if (j < len && source[j] === "/") {
|
|
139
|
+
j++;
|
|
140
|
+
while (j < len && /[gimsuy]/.test(source[j])) j++;
|
|
141
|
+
tokens.push({ type: "regex", value: source.slice(i, j) });
|
|
142
|
+
i = j;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Other characters
|
|
148
|
+
tokens.push({ type: "other", value: source[i] });
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return tokens;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseLinesFromTokens(tokens) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
let current = [];
|
|
158
|
+
|
|
159
|
+
for (const tok of tokens) {
|
|
160
|
+
if (tok.type === "newline") {
|
|
161
|
+
lines.push(current);
|
|
162
|
+
current = [];
|
|
163
|
+
} else {
|
|
164
|
+
current.push(tok);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (current.length > 0) lines.push(current);
|
|
168
|
+
return lines;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getLineIndent(lineTokens) {
|
|
172
|
+
if (lineTokens.length === 0) return 0;
|
|
173
|
+
if (lineTokens[0].type === "whitespace") {
|
|
174
|
+
let count = 0;
|
|
175
|
+
for (const ch of lineTokens[0].value) {
|
|
176
|
+
count += ch === "\t" ? 2 : 1;
|
|
177
|
+
}
|
|
178
|
+
return count;
|
|
179
|
+
}
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getFirstWord(lineTokens) {
|
|
184
|
+
for (const tok of lineTokens) {
|
|
185
|
+
if (tok.type === "whitespace") continue;
|
|
186
|
+
if (tok.type === "keyword" || tok.type === "ident") return tok.value;
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isEmptyLine(lineTokens) {
|
|
193
|
+
return lineTokens.every(t => t.type === "whitespace");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatPurus(source, options = {}) {
|
|
197
|
+
const indent = options.tabWidth || 2;
|
|
198
|
+
const useTabs = options.useTabs || false;
|
|
199
|
+
const indentStr = useTabs ? "\t" : " ".repeat(indent);
|
|
200
|
+
|
|
201
|
+
const tokens = tokenize(source);
|
|
202
|
+
const lines = parseLinesFromTokens(tokens);
|
|
203
|
+
|
|
204
|
+
const result = [];
|
|
205
|
+
|
|
206
|
+
for (let li = 0; li < lines.length; li++) {
|
|
207
|
+
const line = lines[li];
|
|
208
|
+
|
|
209
|
+
if (isEmptyLine(line)) {
|
|
210
|
+
result.push("");
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get original indent level
|
|
215
|
+
const origIndent = getLineIndent(line);
|
|
216
|
+
const indentLevel = Math.round(origIndent / indent);
|
|
217
|
+
|
|
218
|
+
// Remove leading whitespace from tokens
|
|
219
|
+
const contentTokens = line.filter(t => t.type !== "whitespace" || line.indexOf(t) !== 0);
|
|
220
|
+
// Actually, remove ALL leading whitespace
|
|
221
|
+
let startIdx = 0;
|
|
222
|
+
while (startIdx < line.length && line[startIdx].type === "whitespace") startIdx++;
|
|
223
|
+
const content = line.slice(startIdx);
|
|
224
|
+
|
|
225
|
+
if (content.length === 0) {
|
|
226
|
+
result.push("");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Rebuild line with normalized indent
|
|
231
|
+
const prefix = indentStr.repeat(indentLevel);
|
|
232
|
+
|
|
233
|
+
// Normalize spacing within content
|
|
234
|
+
let lineStr = "";
|
|
235
|
+
for (let ti = 0; ti < content.length; ti++) {
|
|
236
|
+
const tok = content[ti];
|
|
237
|
+
if (tok.type === "whitespace") {
|
|
238
|
+
// Normalize to single space between tokens, but not before [ or after [, or before ]
|
|
239
|
+
const next = content[ti + 1];
|
|
240
|
+
const prevChar = lineStr.length > 0 ? lineStr[lineStr.length - 1] : "";
|
|
241
|
+
if (lineStr.length > 0 && next && next.value !== "]" && next.value !== "[" && prevChar !== "[") {
|
|
242
|
+
lineStr += " ";
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
if (ti > 0 && content[ti - 1].type !== "whitespace" && lineStr.length > 0) {
|
|
246
|
+
// Adjacent non-whitespace tokens - check if space needed
|
|
247
|
+
const prev = lineStr[lineStr.length - 1];
|
|
248
|
+
if (tok.value === "." || prev === ".") {
|
|
249
|
+
// No space around dots
|
|
250
|
+
} else if (tok.value === "," || tok.value === ";") {
|
|
251
|
+
// No space before comma/semicolon
|
|
252
|
+
} else if (tok.value === "[" || tok.value === "]" || prev === "[") {
|
|
253
|
+
// No space around brackets (function call syntax)
|
|
254
|
+
} else {
|
|
255
|
+
lineStr += " ";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lineStr += tok.value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Ensure space after comma/semicolon
|
|
263
|
+
lineStr = lineStr.replace(/,(?!\s)/g, ", ");
|
|
264
|
+
lineStr = lineStr.replace(/;(?!\s)/g, "; ");
|
|
265
|
+
|
|
266
|
+
// No trailing whitespace
|
|
267
|
+
lineStr = lineStr.trimEnd();
|
|
268
|
+
|
|
269
|
+
result.push(prefix + lineStr);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Ensure trailing newline
|
|
273
|
+
let output = result.join("\n");
|
|
274
|
+
if (!output.endsWith("\n")) output += "\n";
|
|
275
|
+
|
|
276
|
+
return output;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Prettier plugin interface
|
|
280
|
+
const languages = [
|
|
281
|
+
{
|
|
282
|
+
name: "Purus",
|
|
283
|
+
parsers: ["purus"],
|
|
284
|
+
extensions: [".purus", ".cpurus", ".mpurus"],
|
|
285
|
+
vscodeLanguageIds: ["purus"],
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const parsers = {
|
|
290
|
+
purus: {
|
|
291
|
+
parse(text) {
|
|
292
|
+
return { type: "purus-root", body: text };
|
|
293
|
+
},
|
|
294
|
+
astFormat: "purus-ast",
|
|
295
|
+
locStart: () => 0,
|
|
296
|
+
locEnd: (node) => (node.body ? node.body.length : 0),
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const printers = {
|
|
301
|
+
"purus-ast": {
|
|
302
|
+
print(path, options) {
|
|
303
|
+
const node = path.getValue();
|
|
304
|
+
return formatPurus(node.body, {
|
|
305
|
+
tabWidth: options.tabWidth,
|
|
306
|
+
useTabs: options.useTabs,
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
module.exports = { languages, parsers, printers };
|