@hekana/prettier-plugin-sneferu 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.
- package/LICENSE +1 -0
- package/README.md +14 -2
- package/dist/index.cjs +646 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +614 -0
- package/package.json +49 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
// src/parse.ts
|
|
2
|
+
import { VOID_ELEMENTS } from "@hekana/sneferu-compiler";
|
|
3
|
+
var SneferuSyntaxError = class extends Error {
|
|
4
|
+
loc;
|
|
5
|
+
constructor(message, src, offset) {
|
|
6
|
+
const before = src.slice(0, offset).split("\n");
|
|
7
|
+
const line3 = before.length;
|
|
8
|
+
const column = before[before.length - 1].length + 1;
|
|
9
|
+
super(`${message} (${line3}:${column})`);
|
|
10
|
+
this.loc = { start: { line: line3, column } };
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function lex(src) {
|
|
14
|
+
const toks = [];
|
|
15
|
+
let i = 0;
|
|
16
|
+
let text = "";
|
|
17
|
+
let textStart = 0;
|
|
18
|
+
const fail = (m, at2 = i) => {
|
|
19
|
+
throw new SneferuSyntaxError(m, src, at2);
|
|
20
|
+
};
|
|
21
|
+
const flush = () => {
|
|
22
|
+
if (text) toks.push({ t: "text", v: text, start: textStart, end: i });
|
|
23
|
+
text = "";
|
|
24
|
+
};
|
|
25
|
+
const at = (s) => src.startsWith(s, i);
|
|
26
|
+
while (i < src.length) {
|
|
27
|
+
if (at("<!--")) {
|
|
28
|
+
flush();
|
|
29
|
+
const end = src.indexOf("-->", i);
|
|
30
|
+
if (end === -1) fail("unterminated <!-- comment");
|
|
31
|
+
toks.push({ t: "comment", v: src.slice(i, end + 3), start: i, end: end + 3 });
|
|
32
|
+
i = end + 3;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (at("{{") || at("{%")) {
|
|
36
|
+
flush();
|
|
37
|
+
const isInterp = at("{{");
|
|
38
|
+
const end = src.indexOf(isInterp ? "}}" : "%}", i + 2);
|
|
39
|
+
if (end === -1) fail(`unterminated ${isInterp ? "{{" : "{%"}`);
|
|
40
|
+
const inner = src.slice(i + 2, end).trim();
|
|
41
|
+
toks.push(isInterp ? { t: "interp", src: inner, start: i, end: end + 2 } : { t: "dir", src: inner, start: i, end: end + 2 });
|
|
42
|
+
i = end + 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (at("<hk-schema>")) {
|
|
46
|
+
flush();
|
|
47
|
+
const end = src.indexOf("</hk-schema>", i);
|
|
48
|
+
if (end === -1) fail("unterminated <hk-schema>");
|
|
49
|
+
const raw = src.slice(i + "<hk-schema>".length, end);
|
|
50
|
+
toks.push({ t: "schema", json: raw.trim(), raw, start: i, end: end + "</hk-schema>".length });
|
|
51
|
+
i = end + "</hk-schema>".length;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (at("</")) {
|
|
55
|
+
const m = /^<\/([a-zA-Z][a-zA-Z0-9-]*)\s*>/.exec(src.slice(i));
|
|
56
|
+
if (m) {
|
|
57
|
+
flush();
|
|
58
|
+
toks.push({ t: "close", name: m[1], start: i, end: i + m[0].length });
|
|
59
|
+
i += m[0].length;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (src[i] === "<" && /[a-zA-Z]/.test(src[i + 1] ?? "")) {
|
|
64
|
+
flush();
|
|
65
|
+
const start = i;
|
|
66
|
+
i++;
|
|
67
|
+
let name = "";
|
|
68
|
+
while (i < src.length && /[a-zA-Z0-9-]/.test(src[i])) name += src[i++];
|
|
69
|
+
const attrs = [];
|
|
70
|
+
let selfClosing = false;
|
|
71
|
+
let closed = false;
|
|
72
|
+
while (i < src.length) {
|
|
73
|
+
while (/\s/.test(src[i] ?? "")) i++;
|
|
74
|
+
if (at("/>")) {
|
|
75
|
+
selfClosing = true;
|
|
76
|
+
i += 2;
|
|
77
|
+
closed = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (src[i] === ">") {
|
|
81
|
+
i++;
|
|
82
|
+
closed = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
let aName = "";
|
|
86
|
+
while (i < src.length && /[^\s=/>"']/.test(src[i])) aName += src[i++];
|
|
87
|
+
if (!aName) fail(`unexpected '${src[i]}' in <${name}>`);
|
|
88
|
+
let value = null;
|
|
89
|
+
let quote = "";
|
|
90
|
+
if (src[i] === "=") {
|
|
91
|
+
i++;
|
|
92
|
+
const q = src[i];
|
|
93
|
+
if (q === '"' || q === "'") {
|
|
94
|
+
const end = src.indexOf(q, i + 1);
|
|
95
|
+
if (end === -1) fail(`unterminated value for ${aName}`);
|
|
96
|
+
value = src.slice(i + 1, end);
|
|
97
|
+
quote = q;
|
|
98
|
+
i = end + 1;
|
|
99
|
+
} else {
|
|
100
|
+
value = "";
|
|
101
|
+
while (i < src.length && /[^\s>]/.test(src[i])) value += src[i++];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
attrs.push({ name: aName, value, quote });
|
|
105
|
+
}
|
|
106
|
+
if (!closed) fail(`unterminated tag <${name}>`, start);
|
|
107
|
+
toks.push({ t: "open", name, attrs, selfClosing, start, end: i });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!text) textStart = i;
|
|
111
|
+
text += src[i++];
|
|
112
|
+
}
|
|
113
|
+
flush();
|
|
114
|
+
return toks;
|
|
115
|
+
}
|
|
116
|
+
var keywordOf = (src) => src.split(/\s+/)[0] ?? "";
|
|
117
|
+
function parse(src) {
|
|
118
|
+
const root = { type: "root", children: [], start: 0, end: src.length, source: src };
|
|
119
|
+
const stack = [{ kind: "root", body: root.children }];
|
|
120
|
+
const top = () => stack[stack.length - 1];
|
|
121
|
+
const fail = (m, at) => {
|
|
122
|
+
throw new SneferuSyntaxError(m, src, at);
|
|
123
|
+
};
|
|
124
|
+
for (const tok of lex(src)) {
|
|
125
|
+
switch (tok.t) {
|
|
126
|
+
case "text":
|
|
127
|
+
top().body.push({ type: "text", value: tok.v, start: tok.start, end: tok.end });
|
|
128
|
+
break;
|
|
129
|
+
case "comment":
|
|
130
|
+
top().body.push({ type: "comment", value: tok.v, start: tok.start, end: tok.end });
|
|
131
|
+
break;
|
|
132
|
+
case "interp":
|
|
133
|
+
top().body.push({ type: "interp", src: tok.src, start: tok.start, end: tok.end });
|
|
134
|
+
break;
|
|
135
|
+
case "schema":
|
|
136
|
+
top().body.push({ type: "schema", json: tok.json, raw: tok.raw, start: tok.start, end: tok.end });
|
|
137
|
+
break;
|
|
138
|
+
case "open": {
|
|
139
|
+
const node = { type: "element", name: tok.name, attrs: tok.attrs, selfClosing: tok.selfClosing, children: [], start: tok.start, end: tok.end, openEnd: tok.end, closeStart: tok.end };
|
|
140
|
+
top().body.push(node);
|
|
141
|
+
if (!tok.selfClosing && !VOID_ELEMENTS.has(tok.name.toLowerCase())) stack.push({ kind: "el", node, body: node.children });
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "close": {
|
|
145
|
+
const f = top();
|
|
146
|
+
if (f.kind !== "el" || f.node.name.toLowerCase() !== tok.name.toLowerCase()) fail(`</${tok.name}> does not close the open ${f.kind === "el" ? `<${f.node.name}>` : f.kind === "root" ? "nothing" : `{% ${f.kind} %}`}`, tok.start);
|
|
147
|
+
const el = f.node;
|
|
148
|
+
el.end = tok.end;
|
|
149
|
+
el.closeStart = tok.start;
|
|
150
|
+
stack.pop();
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "dir": {
|
|
154
|
+
const kw = keywordOf(tok.src);
|
|
155
|
+
if (kw === "if") {
|
|
156
|
+
const node = { type: "if", branches: [{ src: tok.src, body: [] }], endSrc: "", start: tok.start, end: tok.end };
|
|
157
|
+
top().body.push(node);
|
|
158
|
+
stack.push({ kind: "if", node, body: node.branches[0].body });
|
|
159
|
+
} else if (kw === "elsif" || kw === "else") {
|
|
160
|
+
const f = top();
|
|
161
|
+
if (f.kind !== "if") fail(`{% ${kw} %} outside {% if %}`, tok.start);
|
|
162
|
+
const n = f.node;
|
|
163
|
+
if (n.else) fail(`{% ${kw} %} after {% else %}`, tok.start);
|
|
164
|
+
if (kw === "elsif") {
|
|
165
|
+
const b = { src: tok.src, body: [] };
|
|
166
|
+
n.branches.push(b);
|
|
167
|
+
f.body = b.body;
|
|
168
|
+
} else {
|
|
169
|
+
n.else = { src: tok.src, body: [] };
|
|
170
|
+
f.body = n.else.body;
|
|
171
|
+
}
|
|
172
|
+
} else if (kw === "endif" || kw === "endfor") {
|
|
173
|
+
const f = top();
|
|
174
|
+
if (f.kind !== (kw === "endif" ? "if" : "for")) fail(`{% ${kw} %} has no matching opening tag`, tok.start);
|
|
175
|
+
const n = f.node;
|
|
176
|
+
n.endSrc = tok.src;
|
|
177
|
+
n.end = tok.end;
|
|
178
|
+
stack.pop();
|
|
179
|
+
} else if (kw === "for") {
|
|
180
|
+
const node = { type: "for", src: tok.src, body: [], endSrc: "", start: tok.start, end: tok.end };
|
|
181
|
+
top().body.push(node);
|
|
182
|
+
stack.push({ kind: "for", node, body: node.body });
|
|
183
|
+
} else {
|
|
184
|
+
top().body.push({ type: "directive", kw, src: tok.src, start: tok.start, end: tok.end });
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const open = top();
|
|
191
|
+
if (open.kind !== "root") fail(`${open.kind === "el" ? `<${open.node.name}>` : `{% ${open.kind} %}`} is never closed`, open.node.start);
|
|
192
|
+
return root;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/print.ts
|
|
196
|
+
import { doc as doc2 } from "prettier";
|
|
197
|
+
import { freeBeside, innerContext, isBlockLevel, VOID_ELEMENTS as VOID_ELEMENTS2, parseExpr as parseExpr2 } from "@hekana/sneferu-compiler";
|
|
198
|
+
|
|
199
|
+
// src/expr.ts
|
|
200
|
+
import { parseExpr, parseFor, splitArgs } from "@hekana/sneferu-compiler";
|
|
201
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not"]);
|
|
202
|
+
function tokenize(src) {
|
|
203
|
+
const out = [];
|
|
204
|
+
let i = 0;
|
|
205
|
+
while (i < src.length) {
|
|
206
|
+
const c = src[i];
|
|
207
|
+
if (/\s/.test(c)) {
|
|
208
|
+
i++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (c === "'" || c === '"') {
|
|
212
|
+
const end = src.indexOf(c, i + 1);
|
|
213
|
+
if (end === -1) return void 0;
|
|
214
|
+
out.push({ k: "str", v: src.slice(i + 1, end), q: c });
|
|
215
|
+
i = end + 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const num = /^-?\d+(\.\d+)?/.exec(src.slice(i));
|
|
219
|
+
if (num && !/[A-Za-z_]/.test(src[i + num[0].length] ?? "")) {
|
|
220
|
+
out.push({ k: "num", v: num[0] });
|
|
221
|
+
i += num[0].length;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const id = /^[A-Za-z_][A-Za-z0-9_-]*/.exec(src.slice(i));
|
|
225
|
+
if (id) {
|
|
226
|
+
out.push({ k: "id", v: id[0] });
|
|
227
|
+
i += id[0].length;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const op = /^(==|!=|>=|<=|>|<)/.exec(src.slice(i));
|
|
231
|
+
if (op) {
|
|
232
|
+
out.push({ k: "op", v: op[0] });
|
|
233
|
+
i += op[0].length;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (".[]()|:,".includes(c)) {
|
|
237
|
+
out.push({ k: "punc", v: c });
|
|
238
|
+
i++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
var isP = (t, v) => !!t && t.k === "punc" && t.v === v;
|
|
246
|
+
function sep(p, c) {
|
|
247
|
+
if (c.k === "op" || p.k === "op" || isP(c, "|") || isP(p, "|")) return " ";
|
|
248
|
+
if (isP(c, ".") || isP(c, "]") || isP(c, ")") || isP(c, ",") || isP(c, ":") || isP(c, "[")) return "";
|
|
249
|
+
if (isP(p, ".") || isP(p, "[") || isP(p, "(")) return "";
|
|
250
|
+
if (isP(c, "(")) return p.k === "id" && !KEYWORDS.has(p.v) ? "" : " ";
|
|
251
|
+
return " ";
|
|
252
|
+
}
|
|
253
|
+
function printTokens(toks, quote) {
|
|
254
|
+
let out = "";
|
|
255
|
+
toks.forEach((t, i) => {
|
|
256
|
+
if (i > 0) out += sep(toks[i - 1], t);
|
|
257
|
+
if (t.k === "str") {
|
|
258
|
+
const q = t.v.includes(quote) ? t.q : quote;
|
|
259
|
+
out += q + t.v + q;
|
|
260
|
+
} else out += t.v;
|
|
261
|
+
});
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
265
|
+
function formatExpr(src, quote) {
|
|
266
|
+
const before = parseExpr(src);
|
|
267
|
+
if (!before.expr) return src;
|
|
268
|
+
const toks = tokenize(src);
|
|
269
|
+
if (!toks) return src;
|
|
270
|
+
const out = printTokens(toks, quote);
|
|
271
|
+
return same(parseExpr(out), before) ? out : src;
|
|
272
|
+
}
|
|
273
|
+
function formatDirective(src, kw, quote) {
|
|
274
|
+
const arg = src.slice(kw.length).trim();
|
|
275
|
+
if (kw === "if" || kw === "elsif") return arg ? `${kw} ${formatExpr(arg, quote)}` : src;
|
|
276
|
+
if (kw === "else" || kw === "endif" || kw === "endfor") return arg ? src : kw;
|
|
277
|
+
if (kw === "for") {
|
|
278
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)\s+in\s+(.+?)(?:\s+limit:\s*(\S+))?$/.exec(arg);
|
|
279
|
+
if (!m) return src;
|
|
280
|
+
const out = `for ${m[1]} in ${formatExpr(m[2], quote)}${m[3] !== void 0 ? ` limit: ${m[3]}` : ""}`;
|
|
281
|
+
return same(parseFor(out.slice(4)), parseFor(arg)) ? out : src;
|
|
282
|
+
}
|
|
283
|
+
if (kw === "render") {
|
|
284
|
+
const out = formatRender(arg, quote);
|
|
285
|
+
return out !== void 0 && same(renderMeaning(out.slice(0)), renderMeaning(arg)) ? `render ${out}` : src;
|
|
286
|
+
}
|
|
287
|
+
return src;
|
|
288
|
+
}
|
|
289
|
+
var RENDER = /^'([a-z0-9-]+)'\s*(?:,\s*(.*))?$/;
|
|
290
|
+
var PAIR = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)$/;
|
|
291
|
+
function renderMeaning(arg) {
|
|
292
|
+
const m = RENDER.exec(arg);
|
|
293
|
+
if (!m) return null;
|
|
294
|
+
return [m[1], splitArgs(m[2] ?? "").map((pair) => {
|
|
295
|
+
const pm = PAIR.exec(pair);
|
|
296
|
+
return pm ? [pm[1], parseExpr(pm[2])] : ["?", pair];
|
|
297
|
+
})];
|
|
298
|
+
}
|
|
299
|
+
function formatRender(arg, quote) {
|
|
300
|
+
const m = RENDER.exec(arg);
|
|
301
|
+
if (!m) return void 0;
|
|
302
|
+
const pairs = splitArgs(m[2] ?? "").map((pair) => {
|
|
303
|
+
const pm = PAIR.exec(pair);
|
|
304
|
+
return pm ? `${pm[1]}: ${formatExpr(pm[2], quote)}` : void 0;
|
|
305
|
+
});
|
|
306
|
+
if (pairs.some((p) => p === void 0)) return void 0;
|
|
307
|
+
return [`'${m[1]}'`, ...pairs].join(", ");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/json.ts
|
|
311
|
+
import { doc } from "prettier";
|
|
312
|
+
var { group, indent, line, softline, hardline, join } = doc.builders;
|
|
313
|
+
function parseJson(src) {
|
|
314
|
+
let i = 0;
|
|
315
|
+
const ws = () => {
|
|
316
|
+
while (i < src.length && " \n\r".includes(src[i])) i++;
|
|
317
|
+
};
|
|
318
|
+
const str = () => {
|
|
319
|
+
const m = /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/.exec(src.slice(i));
|
|
320
|
+
if (!m) throw new Error("string");
|
|
321
|
+
i += m[0].length;
|
|
322
|
+
return m[0];
|
|
323
|
+
};
|
|
324
|
+
const value = () => {
|
|
325
|
+
ws();
|
|
326
|
+
const c = src[i];
|
|
327
|
+
if (c === "{") {
|
|
328
|
+
i++;
|
|
329
|
+
ws();
|
|
330
|
+
const props = [];
|
|
331
|
+
if (src[i] === "}") {
|
|
332
|
+
i++;
|
|
333
|
+
return { k: "obj", props };
|
|
334
|
+
}
|
|
335
|
+
for (; ; ) {
|
|
336
|
+
ws();
|
|
337
|
+
const key = str();
|
|
338
|
+
ws();
|
|
339
|
+
if (src[i++] !== ":") throw new Error(":");
|
|
340
|
+
props.push([key, value()]);
|
|
341
|
+
ws();
|
|
342
|
+
const d = src[i++];
|
|
343
|
+
if (d === "}") return { k: "obj", props };
|
|
344
|
+
if (d !== ",") throw new Error(",");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (c === "[") {
|
|
348
|
+
i++;
|
|
349
|
+
ws();
|
|
350
|
+
const items = [];
|
|
351
|
+
if (src[i] === "]") {
|
|
352
|
+
i++;
|
|
353
|
+
return { k: "arr", items };
|
|
354
|
+
}
|
|
355
|
+
for (; ; ) {
|
|
356
|
+
items.push(value());
|
|
357
|
+
ws();
|
|
358
|
+
const d = src[i++];
|
|
359
|
+
if (d === "]") return { k: "arr", items };
|
|
360
|
+
if (d !== ",") throw new Error(",");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (c === '"') return { k: "raw", v: str() };
|
|
364
|
+
const m = /^(?:-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/.exec(src.slice(i));
|
|
365
|
+
if (!m) throw new Error("value");
|
|
366
|
+
i += m[0].length;
|
|
367
|
+
return { k: "raw", v: m[0] };
|
|
368
|
+
};
|
|
369
|
+
try {
|
|
370
|
+
JSON.parse(src);
|
|
371
|
+
const v = value();
|
|
372
|
+
ws();
|
|
373
|
+
return i === src.length ? v : void 0;
|
|
374
|
+
} catch {
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function print(j) {
|
|
379
|
+
if (j.k === "raw") return j.v;
|
|
380
|
+
if (j.k === "obj") {
|
|
381
|
+
if (j.props.length === 0) return "{}";
|
|
382
|
+
return ["{", indent([hardline, join([",", hardline], j.props.map(([k, v]) => [k, ": ", print(v)]))]), hardline, "}"];
|
|
383
|
+
}
|
|
384
|
+
if (j.items.length === 0) return "[]";
|
|
385
|
+
const items = j.items.map(print);
|
|
386
|
+
if (j.items.every((x) => x.k === "raw")) return group(["[", indent([softline, join([",", line], items)]), softline, "]"]);
|
|
387
|
+
return ["[", indent([hardline, join([",", hardline], items)]), hardline, "]"];
|
|
388
|
+
}
|
|
389
|
+
function jsonDoc(src) {
|
|
390
|
+
const j = parseJson(src);
|
|
391
|
+
return j ? print(j) : void 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/print.ts
|
|
395
|
+
var { group: group2, indent: indent2, line: line2, softline: softline2, hardline: hardline2, fill, literalline } = doc2.builders;
|
|
396
|
+
var { replaceEndOfLine } = doc2.utils;
|
|
397
|
+
var WS_SPLIT = /([ \t\n\r\f]+)/;
|
|
398
|
+
var IGNORE = /^<!--\s*prettier-ignore\s*-->$/;
|
|
399
|
+
var isWord = (x) => !!x && "word" in x;
|
|
400
|
+
var neighbor = (x) => x === null ? null : "word" in x ? { other: true } : x.node.type === "element" ? { el: x.node.name } : { other: true };
|
|
401
|
+
var blockyNode = (n) => !!n && ["element", "if", "for", "directive", "schema"].includes(n.type);
|
|
402
|
+
var blocky = (x) => !!x && "node" in x && blockyNode(x.node);
|
|
403
|
+
var edgeBlocky = (x) => !!x && "node" in x && (x.node.type === "element" ? isBlockLevel(x.node.name) : blockyNode(x.node));
|
|
404
|
+
var newGap = () => ({ ws: false, hidden: [], runs: [{ ws: false, newlines: 0 }] });
|
|
405
|
+
function split(children) {
|
|
406
|
+
const items = [];
|
|
407
|
+
const gaps = [newGap()];
|
|
408
|
+
const gap = () => gaps[gaps.length - 1];
|
|
409
|
+
let ignoreNext = false;
|
|
410
|
+
const push = (it) => {
|
|
411
|
+
items.push(it);
|
|
412
|
+
gaps.push(newGap());
|
|
413
|
+
};
|
|
414
|
+
for (const n of children) {
|
|
415
|
+
if (n.type === "text") {
|
|
416
|
+
for (const part of n.value.split(WS_SPLIT)) {
|
|
417
|
+
if (part === "") continue;
|
|
418
|
+
if (/^[ \t\n\r\f]+$/.test(part)) {
|
|
419
|
+
const run = gap().runs[gap().runs.length - 1];
|
|
420
|
+
gap().ws = true;
|
|
421
|
+
run.ws = true;
|
|
422
|
+
run.newlines = Math.max(run.newlines, (part.match(/\n/g) ?? []).length);
|
|
423
|
+
} else {
|
|
424
|
+
push({ word: part });
|
|
425
|
+
ignoreNext = false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} else if (n.type === "comment" || n.type === "schema") {
|
|
429
|
+
gap().hidden.push(n);
|
|
430
|
+
gap().runs.push({ ws: false, newlines: 0 });
|
|
431
|
+
if (n.type === "comment" && IGNORE.test(n.value)) ignoreNext = true;
|
|
432
|
+
} else {
|
|
433
|
+
push({ node: n, ignore: ignoreNext });
|
|
434
|
+
ignoreNext = false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return { items, gaps };
|
|
438
|
+
}
|
|
439
|
+
function isFree(items, i, ctx, edges) {
|
|
440
|
+
const L = items[i - 1] ?? null;
|
|
441
|
+
const R = items[i] ?? null;
|
|
442
|
+
if (isWord(L) && isWord(R)) return false;
|
|
443
|
+
if (isWord(L)) return freeBeside(neighbor(R), ctx, edges);
|
|
444
|
+
if (isWord(R)) return freeBeside(neighbor(L), ctx, edges);
|
|
445
|
+
return freeBeside(neighbor(L), ctx, edges) || freeBeside(neighbor(R), ctx, edges);
|
|
446
|
+
}
|
|
447
|
+
var verbatim = (s) => replaceEndOfLine(s, literalline);
|
|
448
|
+
function printList(children, ctx, edges, c) {
|
|
449
|
+
const { items, gaps } = split(children);
|
|
450
|
+
const parts = [];
|
|
451
|
+
let keepSpace = false;
|
|
452
|
+
gaps.forEach((g, i) => {
|
|
453
|
+
const L = items[i - 1] ?? null;
|
|
454
|
+
const R = items[i] ?? null;
|
|
455
|
+
const free = isFree(items, i, ctx, edges);
|
|
456
|
+
if (!free && g.ws) keepSpace = true;
|
|
457
|
+
g.runs.forEach((run, k) => {
|
|
458
|
+
const edge = L === null || R === null;
|
|
459
|
+
const near = (x) => edge && edges ? edgeBlocky(x) : blocky(x);
|
|
460
|
+
const left = k === 0 ? near(L) : blockyNode(g.hidden[k - 1]);
|
|
461
|
+
const right = k === g.runs.length - 1 ? near(R) : blockyNode(g.hidden[k]);
|
|
462
|
+
const inner = (k > 0 || L !== null) && (k < g.runs.length - 1 || R !== null);
|
|
463
|
+
const hard = inner && run.newlines >= 2 ? BLANK : hardline2;
|
|
464
|
+
let sep2;
|
|
465
|
+
if (free) sep2 = g.hidden.length || left || right ? hard : softline2;
|
|
466
|
+
else if (run.ws) sep2 = run.newlines > 0 && (left || right) ? hard : line2;
|
|
467
|
+
else sep2 = "";
|
|
468
|
+
parts.push(sep2);
|
|
469
|
+
if (k < g.hidden.length) parts.push(printNode(g.hidden[k], ctx, c));
|
|
470
|
+
});
|
|
471
|
+
if (R) parts.push(printItem(R, ctx, c));
|
|
472
|
+
});
|
|
473
|
+
return { parts, keepSpace };
|
|
474
|
+
}
|
|
475
|
+
var BLANK = [hardline2, hardline2];
|
|
476
|
+
var isHard = (d) => d === hardline2 || d === BLANK;
|
|
477
|
+
function layout(body) {
|
|
478
|
+
const out = [];
|
|
479
|
+
let run = [];
|
|
480
|
+
for (let i = 0; i < body.length; i++) {
|
|
481
|
+
if (i % 2 === 1 && isHard(body[i])) {
|
|
482
|
+
out.push(fill(run), body[i]);
|
|
483
|
+
run = [];
|
|
484
|
+
} else run.push(body[i]);
|
|
485
|
+
}
|
|
486
|
+
out.push(fill(run));
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
function wrapList({ parts, keepSpace }) {
|
|
490
|
+
if (parts.length === 1) return keepSpace ? " " : "";
|
|
491
|
+
return [indent2([parts[0], layout(parts.slice(1, -1))]), parts[parts.length - 1]];
|
|
492
|
+
}
|
|
493
|
+
function printItem(it, ctx, c) {
|
|
494
|
+
if ("word" in it) return it.word;
|
|
495
|
+
if (it.ignore) return verbatim(c.src.slice(it.node.start, it.node.end));
|
|
496
|
+
return printNode(it.node, ctx, c);
|
|
497
|
+
}
|
|
498
|
+
function printNode(n, ctx, c) {
|
|
499
|
+
switch (n.type) {
|
|
500
|
+
case "text":
|
|
501
|
+
return n.value;
|
|
502
|
+
case "comment":
|
|
503
|
+
return verbatim(n.value);
|
|
504
|
+
case "interp":
|
|
505
|
+
return `{{ ${formatExpr(n.src, c.quote)} }}`;
|
|
506
|
+
case "directive":
|
|
507
|
+
return `{% ${formatDirective(n.src, n.kw, c.quote)} %}`;
|
|
508
|
+
case "schema": {
|
|
509
|
+
const j = jsonDoc(n.json);
|
|
510
|
+
if (!j) return verbatim(`<hk-schema>${n.raw}</hk-schema>`);
|
|
511
|
+
return c.opts.indentSchema ? ["<hk-schema>", indent2([hardline2, j]), hardline2, "</hk-schema>"] : ["<hk-schema>", hardline2, j, hardline2, "</hk-schema>"];
|
|
512
|
+
}
|
|
513
|
+
case "if": {
|
|
514
|
+
const parts = [];
|
|
515
|
+
for (const b of n.branches) parts.push(`{% ${formatDirective(b.src, keywordOf(b.src), c.quote)} %}`, wrapList(printList(b.body, ctx, false, c)));
|
|
516
|
+
if (n.else) parts.push(`{% ${formatDirective(n.else.src, keywordOf(n.else.src), c.quote)} %}`, wrapList(printList(n.else.body, ctx, false, c)));
|
|
517
|
+
parts.push(`{% ${formatDirective(n.endSrc, keywordOf(n.endSrc), c.quote)} %}`);
|
|
518
|
+
return group2(parts);
|
|
519
|
+
}
|
|
520
|
+
case "for":
|
|
521
|
+
return group2([
|
|
522
|
+
`{% ${formatDirective(n.src, "for", c.quote)} %}`,
|
|
523
|
+
wrapList(printList(n.body, ctx, false, c)),
|
|
524
|
+
`{% ${formatDirective(n.endSrc, keywordOf(n.endSrc), c.quote)} %}`
|
|
525
|
+
]);
|
|
526
|
+
case "element": {
|
|
527
|
+
const open = printOpenTag(n, c);
|
|
528
|
+
if (n.selfClosing || VOID_ELEMENTS2.has(n.name.toLowerCase())) return open;
|
|
529
|
+
const close = `</${n.name}>`;
|
|
530
|
+
const inner = innerContext(n.name, ctx);
|
|
531
|
+
if (inner === "raw") return [open, verbatim(c.src.slice(n.openEnd, n.closeStart)), close];
|
|
532
|
+
return group2([open, wrapList(printList(n.children, inner, true, c)), close]);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function printOpenTag(n, c) {
|
|
537
|
+
const attrs = n.attrs.map((a) => printAttr(a, c));
|
|
538
|
+
if (attrs.length === 0) return n.selfClosing ? `<${n.name} />` : `<${n.name}>`;
|
|
539
|
+
return group2([`<${n.name}`, indent2([line2, join2(line2, attrs)]), n.selfClosing ? [line2, "/>"] : [softline2, ">"]]);
|
|
540
|
+
}
|
|
541
|
+
var join2 = doc2.builders.join;
|
|
542
|
+
var same2 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
543
|
+
var INTERP = /\{\{([\s\S]*?)\}\}/g;
|
|
544
|
+
function attrMeaning(v) {
|
|
545
|
+
const parts = [];
|
|
546
|
+
let last = 0;
|
|
547
|
+
for (const m of v.matchAll(INTERP)) {
|
|
548
|
+
if (m.index > last) parts.push(v.slice(last, m.index));
|
|
549
|
+
parts.push(parseExpr2(m[1].trim()));
|
|
550
|
+
last = m.index + m[0].length;
|
|
551
|
+
}
|
|
552
|
+
if (last < v.length) parts.push(v.slice(last));
|
|
553
|
+
return parts;
|
|
554
|
+
}
|
|
555
|
+
function printAttr(a, c) {
|
|
556
|
+
if (a.value === null) return a.name;
|
|
557
|
+
const v = a.value;
|
|
558
|
+
const pref = c.opts.singleQuote ? "'" : '"';
|
|
559
|
+
const other = pref === '"' ? "'" : '"';
|
|
560
|
+
for (const q of [pref, other]) {
|
|
561
|
+
const nv = v.includes("{{") ? v.replace(INTERP, (_m, e) => `{{ ${formatExpr(e.trim(), q === '"' ? "'" : '"')} }}`) : v;
|
|
562
|
+
if (!nv.includes(q) && same2(attrMeaning(nv), attrMeaning(v))) return `${a.name}=${q}${nv}${q}`;
|
|
563
|
+
}
|
|
564
|
+
return `${a.name}=${a.quote}${v}${a.quote}`;
|
|
565
|
+
}
|
|
566
|
+
function print2(path, options2) {
|
|
567
|
+
const root = path.node;
|
|
568
|
+
const c = { src: root.source, opts: options2, quote: options2.sneferuSingleQuote ? "'" : '"' };
|
|
569
|
+
const { parts } = printList(root.children, "block", true, c);
|
|
570
|
+
if (parts.length === 1) return "";
|
|
571
|
+
return [layout(parts.slice(1, -1)), hardline2];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/index.ts
|
|
575
|
+
var languages = [
|
|
576
|
+
{ name: "Sneferu", parsers: ["sneferu"], extensions: [".sneferu"], vscodeLanguageIds: ["sneferu"] }
|
|
577
|
+
];
|
|
578
|
+
var parsers = {
|
|
579
|
+
sneferu: {
|
|
580
|
+
parse: (text) => parse(text),
|
|
581
|
+
astFormat: "sneferu-ast",
|
|
582
|
+
locStart: (n) => n.start,
|
|
583
|
+
locEnd: (n) => n.end
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
var printers = {
|
|
587
|
+
"sneferu-ast": { print: print2 }
|
|
588
|
+
};
|
|
589
|
+
var options = {
|
|
590
|
+
sneferuSingleQuote: {
|
|
591
|
+
type: "boolean",
|
|
592
|
+
category: "Sneferu",
|
|
593
|
+
default: true,
|
|
594
|
+
description: "Use single quotes for strings inside {{ }} and {% %} (a string that contains a single quote keeps double quotes)."
|
|
595
|
+
},
|
|
596
|
+
indentSchema: {
|
|
597
|
+
type: "boolean",
|
|
598
|
+
category: "Sneferu",
|
|
599
|
+
default: true,
|
|
600
|
+
description: "Indent the JSON inside <hk-schema> one level."
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
var defaultOptions = { printWidth: 120, tabWidth: 2 };
|
|
604
|
+
var plugin = { languages, parsers, printers, options, defaultOptions };
|
|
605
|
+
var index_default = plugin;
|
|
606
|
+
export {
|
|
607
|
+
index_default as default,
|
|
608
|
+
defaultOptions,
|
|
609
|
+
languages,
|
|
610
|
+
options,
|
|
611
|
+
parse,
|
|
612
|
+
parsers,
|
|
613
|
+
printers
|
|
614
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hekana/prettier-plugin-sneferu",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Prettier plugin for Sneferu theme files (.sneferu)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/index.d.cts",
|
|
18
|
+
"default": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"prettier",
|
|
27
|
+
"prettier-plugin",
|
|
28
|
+
"sneferu",
|
|
29
|
+
"hekana",
|
|
30
|
+
"theme"
|
|
31
|
+
],
|
|
32
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@hekana/sneferu-compiler": "^0.2.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"prettier": "^3.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"prettier": "^3.6.2",
|
|
41
|
+
"@hekana/sneferu-components": "^0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://hekana.dev/guide/format-your-theme",
|
|
47
|
+
"author": "Hekana",
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
6
52
|
}
|