@excom/quark-formatter 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 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 36.430044999999836
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 78.7496329999999
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "../../packages/quark-formatter": "../../packages/quark-formatter:E4tLvQjm6cSAByA9WMEqUOdPY60uUIyhS1JfSsY46sA=:"
3
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
3
+ "rigPackageName": "@excom/heft-rig",
4
+ "rigProfile": "default"
5
+ }
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { format, type FormatOptions } from "./src/printer";
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@excom/quark-formatter",
3
+ "version": "0.1.0",
4
+ "description": "Opinionated SCSS-style formatter for the Quark language, built on the quark-parser AST",
5
+ "license": "ISC",
6
+ "engines": {
7
+ "node": ">=24.13.0"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {},
11
+ "peerDependencies": {},
12
+ "devDependencies": {
13
+ "@excom/quark-parser": "^0.1.0",
14
+ "@excom/heft-rig": "^0.1.0"
15
+ },
16
+ "repository": {
17
+ "url": "excom-dev/nucleus",
18
+ "directory": "packages/quark-formatter"
19
+ },
20
+ "homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/quark-formatter/support/docs/README.md",
21
+ "bugs": "https://github.com/excom-dev/nucleus/issues",
22
+ "keywords": [
23
+ "quark-formatter",
24
+ "tool",
25
+ "quark",
26
+ "formatter",
27
+ "scss",
28
+ "prettier"
29
+ ],
30
+ "excom": {
31
+ "packageType": "tool"
32
+ },
33
+ "scripts": {
34
+ "build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
35
+ "build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
36
+ "format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
37
+ "test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
38
+ "coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
39
+ "build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs"
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ Caching has been disabled for this project's "apply-exports" command.
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
package/src/doc.ts ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Minimal Wadler-style document printer, modelled on prettier's.
3
+ *
4
+ * A `Doc` describes text plus the places it *may* break. `printDoc` lays
5
+ * it out against a print width: a `group` prints flat when its contents
6
+ * fit on the rest of the line and breaks otherwise; `line` is a space
7
+ * when flat and a newline when broken; `softline` is nothing when flat;
8
+ * `hardline` always breaks and forces every enclosing group to break;
9
+ * `fill` flows its parts like words in a paragraph, breaking only the
10
+ * separators that would overflow.
11
+ */
12
+
13
+ export type Doc = string | Doc[] | Group | Indent | IndentIfBreak | Line | Fill;
14
+
15
+ interface Group {
16
+ kind: "group";
17
+ contents: Doc;
18
+ /** Set when the group contains a hard break (see `propagateBreaks`). */
19
+ break: boolean;
20
+ /** Lets `indentIfBreak` refer back to how this group was printed. */
21
+ id?: symbol;
22
+ }
23
+
24
+ interface Indent {
25
+ kind: "indent";
26
+ contents: Doc;
27
+ }
28
+
29
+ /** Indents its contents only if the group with `groupId` was broken. */
30
+ interface IndentIfBreak {
31
+ kind: "indentIfBreak";
32
+ contents: Doc;
33
+ groupId: symbol;
34
+ }
35
+
36
+ interface Line {
37
+ kind: "line";
38
+ soft: boolean;
39
+ hard: boolean;
40
+ }
41
+
42
+ /** Parts alternate content / separator and end with content (odd length). */
43
+ interface Fill {
44
+ kind: "fill";
45
+ parts: Doc[];
46
+ }
47
+
48
+ export const group = (contents: Doc, id?: symbol): Group => ({
49
+ kind: "group",
50
+ contents,
51
+ break: false,
52
+ id,
53
+ });
54
+ export const indent = (contents: Doc): Indent => ({ kind: "indent", contents });
55
+ export const indentIfBreak = (
56
+ contents: Doc,
57
+ groupId: symbol
58
+ ): IndentIfBreak => ({
59
+ kind: "indentIfBreak",
60
+ contents,
61
+ groupId,
62
+ });
63
+ export const fill = (parts: Doc[]): Fill => ({ kind: "fill", parts });
64
+ export const line: Line = { kind: "line", soft: false, hard: false };
65
+ export const softline: Line = { kind: "line", soft: true, hard: false };
66
+ export const hardline: Line = { kind: "line", soft: false, hard: true };
67
+
68
+ /** `[a, sep, b, sep, c]` — the shape `fill` expects. */
69
+ export function join(separator: Doc, docs: Doc[]): Doc[] {
70
+ const out: Doc[] = [];
71
+ docs.forEach((doc, i) => {
72
+ if (i) out.push(separator);
73
+ out.push(doc);
74
+ });
75
+ return out;
76
+ }
77
+
78
+ export interface PrintDocOptions {
79
+ /** Columns available per line; `Infinity` prints everything flat. */
80
+ width: number;
81
+ indentUnit: string;
82
+ /** Indentation the first line starts with (and hard breaks return to). */
83
+ rootIndent: string;
84
+ }
85
+
86
+ const FLAT = 0;
87
+ const BREAK = 1;
88
+ type Mode = typeof FLAT | typeof BREAK;
89
+
90
+ interface Cmd {
91
+ ind: string;
92
+ mode: Mode;
93
+ doc: Doc;
94
+ }
95
+
96
+ /** Columns a tab occupies when measuring (VS Code's default `tabSize`). */
97
+ const TAB_WIDTH = 4;
98
+
99
+ export function printDoc(doc: Doc, options: PrintDocOptions): string {
100
+ const { width, indentUnit, rootIndent } = options;
101
+ propagateBreaks(doc);
102
+ const out: string[] = [rootIndent];
103
+ let pos = strWidth(rootIndent);
104
+ const cmds: Cmd[] = [{ ind: rootIndent, mode: BREAK, doc }];
105
+ /** How each id'd group was printed, for `indentIfBreak`. */
106
+ const groupModes = new Map<symbol, Mode>();
107
+
108
+ while (cmds.length) {
109
+ const { ind, mode, doc } = cmds.pop()!;
110
+ if (typeof doc === "string") {
111
+ out.push(doc);
112
+ pos += strWidth(doc);
113
+ continue;
114
+ }
115
+ if (Array.isArray(doc)) {
116
+ pushReversed(cmds, ind, mode, doc);
117
+ continue;
118
+ }
119
+ switch (doc.kind) {
120
+ case "indent":
121
+ cmds.push({ ind: ind + indentUnit, mode, doc: doc.contents });
122
+ break;
123
+ case "indentIfBreak":
124
+ cmds.push({
125
+ ind: groupModes.get(doc.groupId) === BREAK ? ind + indentUnit : ind,
126
+ mode,
127
+ doc: doc.contents,
128
+ });
129
+ break;
130
+ case "group": {
131
+ const flat: Cmd = { ind, mode: FLAT, doc: doc.contents };
132
+ const next: Cmd =
133
+ mode === FLAT
134
+ ? { ind, mode: doc.break ? BREAK : FLAT, doc: doc.contents }
135
+ : !doc.break && fits(flat, cmds, width - pos, false)
136
+ ? flat
137
+ : { ind, mode: BREAK, doc: doc.contents };
138
+ if (doc.id) groupModes.set(doc.id, next.mode);
139
+ cmds.push(next);
140
+ break;
141
+ }
142
+ case "fill": {
143
+ const rem = width - pos;
144
+ const { parts } = doc;
145
+ const [content, separator, second] = parts;
146
+ const contentFlat: Cmd = { ind, mode: FLAT, doc: content };
147
+ const contentBreak: Cmd = { ind, mode: BREAK, doc: content };
148
+ if (parts.length === 1) {
149
+ // Last word: what follows the fill (`;`, `)`) counts against it.
150
+ cmds.push(
151
+ fits(contentFlat, cmds, rem, true) ? contentFlat : contentBreak
152
+ );
153
+ break;
154
+ }
155
+ const contentFits = fits(contentFlat, [], rem, true);
156
+ const separatorFlat: Cmd = { ind, mode: FLAT, doc: separator };
157
+ const separatorBreak: Cmd = { ind, mode: BREAK, doc: separator };
158
+ const remaining: Cmd = { ind, mode, doc: fill(parts.slice(2)) };
159
+ const pairFlat: Cmd = {
160
+ ind,
161
+ mode: FLAT,
162
+ doc: [content, separator, second],
163
+ };
164
+ // The final pair also has to leave room for what follows the fill.
165
+ const pairFits = fits(
166
+ pairFlat,
167
+ parts.length === 3 ? cmds : [],
168
+ rem,
169
+ true
170
+ );
171
+ if (pairFits) {
172
+ cmds.push(remaining, separatorFlat, contentFlat);
173
+ } else if (contentFits) {
174
+ cmds.push(remaining, separatorBreak, contentFlat);
175
+ } else {
176
+ cmds.push(remaining, separatorBreak, contentBreak);
177
+ }
178
+ break;
179
+ }
180
+ case "line":
181
+ if (mode === FLAT && !doc.hard) {
182
+ if (!doc.soft) {
183
+ out.push(" ");
184
+ pos += 1;
185
+ }
186
+ break;
187
+ }
188
+ trimTrailing(out);
189
+ out.push("\n" + ind);
190
+ pos = strWidth(ind);
191
+ break;
192
+ }
193
+ }
194
+ return out.join("");
195
+ }
196
+
197
+ /**
198
+ * Whether `next` fits in `width` columns when printed in its mode. `rest`
199
+ * is the printer's pending stack (top last): once `next` is exhausted the
200
+ * text that follows it is measured too, up to the first break in a
201
+ * broken context. `mustBeFlat` rejects `next` if it holds a hard break.
202
+ */
203
+ function fits(
204
+ next: Cmd,
205
+ rest: Cmd[],
206
+ width: number,
207
+ mustBeFlat: boolean
208
+ ): boolean {
209
+ let restIdx = rest.length;
210
+ const cmds: Cmd[] = [next];
211
+ while (width >= 0) {
212
+ if (!cmds.length) {
213
+ if (restIdx === 0) return true;
214
+ cmds.push(rest[--restIdx]);
215
+ mustBeFlat = false;
216
+ continue;
217
+ }
218
+ const { ind, mode, doc } = cmds.pop()!;
219
+ if (typeof doc === "string") {
220
+ width -= strWidth(doc);
221
+ continue;
222
+ }
223
+ if (Array.isArray(doc)) {
224
+ pushReversed(cmds, ind, mode, doc);
225
+ continue;
226
+ }
227
+ switch (doc.kind) {
228
+ case "indent":
229
+ case "indentIfBreak":
230
+ // Indentation only matters after a break, which ends the measure.
231
+ cmds.push({ ind, mode, doc: doc.contents });
232
+ break;
233
+ case "fill":
234
+ pushReversed(cmds, ind, mode, doc.parts);
235
+ break;
236
+ case "group":
237
+ if (mustBeFlat && doc.break) return false;
238
+ cmds.push({ ind, mode: doc.break ? BREAK : mode, doc: doc.contents });
239
+ break;
240
+ case "line":
241
+ if (mode === BREAK || doc.hard) return true;
242
+ if (!doc.soft) width -= 1;
243
+ break;
244
+ }
245
+ }
246
+ return false;
247
+ }
248
+
249
+ /** Mark every group that contains a hard break (or a broken group) as broken. */
250
+ function propagateBreaks(doc: Doc): boolean {
251
+ if (typeof doc === "string") return false;
252
+ if (Array.isArray(doc)) return containsBreak(doc);
253
+ switch (doc.kind) {
254
+ case "line":
255
+ return doc.hard;
256
+ case "indent":
257
+ case "indentIfBreak":
258
+ return propagateBreaks(doc.contents);
259
+ case "fill":
260
+ return containsBreak(doc.parts);
261
+ case "group": {
262
+ if (propagateBreaks(doc.contents)) doc.break = true;
263
+ return doc.break;
264
+ }
265
+ }
266
+ }
267
+
268
+ function containsBreak(docs: Doc[]): boolean {
269
+ // Visit every child: propagation must reach all nested groups.
270
+ let found = false;
271
+ for (const doc of docs) if (propagateBreaks(doc)) found = true;
272
+ return found;
273
+ }
274
+
275
+ function pushReversed(cmds: Cmd[], ind: string, mode: Mode, docs: Doc[]): void {
276
+ for (let i = docs.length - 1; i >= 0; i--) {
277
+ cmds.push({ ind, mode, doc: docs[i] });
278
+ }
279
+ }
280
+
281
+ /** Drop spaces / tabs before a line break (`prop:` + hard break, indent-only lines). */
282
+ function trimTrailing(out: string[]): void {
283
+ while (out.length) {
284
+ const trimmed = out[out.length - 1].replace(/[ \t]+$/, "");
285
+ if (trimmed) {
286
+ out[out.length - 1] = trimmed;
287
+ return;
288
+ }
289
+ out.pop();
290
+ }
291
+ }
292
+
293
+ function strWidth(text: string): number {
294
+ let width = 0;
295
+ for (const ch of text) width += ch === "\t" ? TAB_WIDTH : 1;
296
+ return width;
297
+ }