@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.
Files changed (3) hide show
  1. package/README.md +58 -0
  2. package/package.json +36 -0
  3. package/src/index.js +312 -0
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ <div align="center">
2
+
3
+ [![Logo](./logo.png)](https://purus.work)
4
+
5
+ **English** | [日本語](./README-ja.md)
6
+
7
+ </div>
8
+
9
+ ---
10
+
11
+ [![npm](https://img.shields.io/npm/v/purus)](https://www.npmjs.com/package/purus)
12
+ [![npm downloads](https://img.shields.io/npm/dm/purus)](https://www.npmjs.com/package/purus)
13
+ [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/otoneko1102/purus)](https://github.com/otoneko1102/purus/pulse)
14
+ [![GitHub last commit](https://img.shields.io/github/last-commit/otoneko1102/purus)](https://github.com/otoneko1102/purus/commits/main)
15
+ ![US layout](https://img.shields.io/badge/US_layout-Supported-green)
16
+ ![JIS layout](https://img.shields.io/badge/JIS_layout-Supported-green)
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 };