reladraw 0.18.0 → 0.19.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.
@@ -0,0 +1,250 @@
1
+ import { ALL_ATTR_KEYS } from './ast.js';
2
+ import { nearMatch } from './checks.js';
3
+ import { SourceError } from './errors.js';
4
+ const SETTING_COUNT = /^[1-9]\d*$/;
5
+ /**
6
+ * A setting whose values are words, one of them the default. Anything else is
7
+ * refused with the list.
8
+ */
9
+ function wordSetting(name, about, values) {
10
+ return {
11
+ name,
12
+ about,
13
+ values,
14
+ written: values[0][0],
15
+ read: (value) => {
16
+ if (values.some(([word]) => word === value))
17
+ return value;
18
+ const near = nearMatch(value, values.map(([word]) => word));
19
+ throw `${name}: "${value}" is not one of its values${near ? ` — did you mean ${near}?` : ''} — it takes ${orList(values.map(([word]) => word))}`;
20
+ },
21
+ };
22
+ }
23
+ /**
24
+ * The kinds of clause `clause-order` can list: a statement's texts, its
25
+ * placements, and each key by its own name. `text` is also a style's `text:`
26
+ * key, which is a style's way of saying what a text says.
27
+ */
28
+ export const CLAUSE_KINDS = [...new Set(['text', 'placement', ...ALL_ATTR_KEYS])];
29
+ export const SETTINGS = [
30
+ {
31
+ name: 'wrap-at',
32
+ about: 'Past this many characters, a statement wraps onto more lines, as few as fit.',
33
+ values: [
34
+ ['off', 'never wrap'],
35
+ ['a number', 'wrap past that many characters, such as 80'],
36
+ ],
37
+ written: 'off',
38
+ read: (value) => {
39
+ if (value === 'off')
40
+ return value;
41
+ if (SETTING_COUNT.test(value))
42
+ return Number(value);
43
+ throw `wrap-at: "${value}" is not one of its values — it takes off, or a number of characters such as 80`;
44
+ },
45
+ },
46
+ {
47
+ name: 'clause-per-line-at',
48
+ about: 'Past this many characters, each clause goes on its own line instead.',
49
+ values: [
50
+ ['off', 'never'],
51
+ ['a number', 'past that many characters, such as 120'],
52
+ ['always', 'every statement, whatever its length'],
53
+ ],
54
+ written: 'off',
55
+ read: (value) => {
56
+ if (value === 'off' || value === 'always')
57
+ return value;
58
+ if (SETTING_COUNT.test(value))
59
+ return Number(value);
60
+ throw `clause-per-line-at: "${value}" is not one of its values — it takes off, always, or a number of characters such as 120`;
61
+ },
62
+ },
63
+ wordSetting('first-line', 'When a statement breaks onto more lines, where its first line ends.', [
64
+ ['text', 'after the text: keyword, name and text stay together'],
65
+ ['name', 'after the name'],
66
+ ]),
67
+ {
68
+ name: 'continued-lines-indent',
69
+ about: 'Where the lines after a statement\'s first one start.',
70
+ values: [
71
+ ['4', 'that many spaces in, such as 4'],
72
+ ['tab', 'one tab in'],
73
+ ['under-name', 'under the statement\'s name'],
74
+ ['under-name + 2', 'that many characters past the start of the name'],
75
+ ['under-text', 'under the statement\'s text, or what comes after the name'],
76
+ ],
77
+ written: '4',
78
+ read: (value) => {
79
+ if (SETTING_COUNT.test(value))
80
+ return Number(value);
81
+ if (value === 'tab' || value === 'under-name' || value === 'under-text')
82
+ return value;
83
+ const shifted = /^under-name \+ ([1-9]\d*)$/.exec(value);
84
+ if (shifted)
85
+ return { underName: Number(shifted[1]) };
86
+ throw `continued-lines-indent: "${value}" is not one of its values — it takes a number of spaces, tab, under-name, under-name + 2 or under-text`;
87
+ },
88
+ },
89
+ {
90
+ name: 'clause-order',
91
+ about: 'The order a statement\'s clauses are written in after its text, which always\ncomes straight after the name. A list names placement and keys such as style;\nwhat it leaves out follows, in the order written.',
92
+ values: [
93
+ ['as-written', 'the order the writer chose'],
94
+ ['a list', 'that order, such as: placement, style, fill'],
95
+ ],
96
+ written: 'as-written',
97
+ read: (value) => {
98
+ if (value === 'as-written')
99
+ return value;
100
+ const kinds = value.split(',').map((kind) => kind.trim());
101
+ const seen = new Set();
102
+ for (const kind of kinds) {
103
+ if (kind === '')
104
+ throw `clause-order: "${value}" has an empty place in its list — name a kind of clause between each comma`;
105
+ if (!CLAUSE_KINDS.includes(kind)) {
106
+ const near = nearMatch(kind, CLAUSE_KINDS);
107
+ throw `clause-order: "${kind}" is not a kind of clause${near ? ` — did you mean ${near}?` : ''} — it takes text, placement and keys such as style or fill`;
108
+ }
109
+ if (seen.has(kind))
110
+ throw `clause-order: ${kind} is listed twice — keep one`;
111
+ seen.add(kind);
112
+ }
113
+ return kinds;
114
+ },
115
+ },
116
+ {
117
+ name: 'clause-gap',
118
+ about: 'The least space between two clauses on a line.',
119
+ values: [
120
+ ['2', 'that many spaces, such as 2'],
121
+ ['tab', 'one tab, which only works with columns: off'],
122
+ ],
123
+ written: '2',
124
+ read: (value) => {
125
+ if (SETTING_COUNT.test(value))
126
+ return Number(value);
127
+ if (value === 'tab')
128
+ return 'tab';
129
+ throw `clause-gap: "${value}" is not one of its values — it takes a number of spaces, or tab`;
130
+ },
131
+ },
132
+ wordSetting('columns', 'Whether statements that belong together line their clauses up in columns.', [
133
+ ['aligned', 'each column as wide as its longest clause'],
134
+ ['off', 'each clause straight after the one before, a clause gap apart'],
135
+ ]),
136
+ wordSetting('columns-shared-by', 'Which statements line their clauses up in shared columns. A comment line\nbetween them never ends a set.', [
137
+ ['same-keyword-neighbors', 'adjacent, with the same keyword\n(a blank line or new keyword ends the set)'],
138
+ ['neighbors', 'adjacent, whatever their keyword'],
139
+ ['same-keyword', 'the same keyword, across blank lines'],
140
+ ['file', 'the whole file'],
141
+ ]),
142
+ wordSetting('comments-inside', 'What happens to a statement written over several lines, with comments\namong them.', [
143
+ ['keep-lines', 'it stays on its own lines, with the indent tidied'],
144
+ ['move-above', 'the comments move above it, and it is joined onto one line'],
145
+ ['refuse', 'the file is left alone, and the comment in the way is named'],
146
+ ]),
147
+ wordSetting('trailing-comments', 'Where a comment at the end of a statement\'s line goes.', [
148
+ ['shared-column', 'lined up with the other comments in its set'],
149
+ ['after-statement', 'a clause gap after its own statement'],
150
+ ]),
151
+ {
152
+ name: 'blank-lines',
153
+ about: 'What happens to runs of blank lines. A run is never removed, since a blank\nline starts a new set of columns.',
154
+ values: [
155
+ ['as-written', 'every blank line stays'],
156
+ ['exactly 2', 'every run becomes that many'],
157
+ ['at-most 3', 'longer runs shrink to that many; shorter ones stay'],
158
+ ],
159
+ written: 'as-written',
160
+ read: (value) => {
161
+ if (value === 'as-written')
162
+ return value;
163
+ const counted = /^(exactly|at-most) ([1-9]\d*)$/.exec(value);
164
+ if (counted)
165
+ return counted[1] === 'exactly' ? { exactly: Number(counted[2]) } : { atMost: Number(counted[2]) };
166
+ throw `blank-lines: "${value}" is not one of its values — it takes as-written, exactly and a number, or at-most and a number`;
167
+ },
168
+ },
169
+ wordSetting('edge-name-and-ends', 'How an edge\'s name and ends, such as one a -> b, line up.', [
170
+ ['one-column', 'together, as one column, the way a node\'s name is'],
171
+ ['own-columns', 'the name, each end and the arrow in a column each'],
172
+ ]),
173
+ ];
174
+ const SETTING_BY_NAME = new Map(SETTINGS.map((setting) => [setting.name, setting]));
175
+ export const DEFAULT_FORMAT = Object.fromEntries(SETTINGS.map((setting) => [setting.name, setting.read(setting.written)]));
176
+ /** The file a project's settings are written in. */
177
+ export const SETTINGS_FILE = '.reladraw-format';
178
+ /**
179
+ * Read a `.reladraw-format` file: one `key: value` setting a line, `//`
180
+ * comments, blank lines. An unknown setting, one written twice, or a value it
181
+ * doesn't take is refused by name, with the line.
182
+ */
183
+ export function readFormatSettings(text) {
184
+ const settings = { ...DEFAULT_FORMAT };
185
+ const seen = new Map();
186
+ text.split(/\r?\n/).forEach((raw, index) => {
187
+ const line = index + 1;
188
+ const written = raw.replace(/\/\/.*$/, '').trim();
189
+ if (written === '')
190
+ return;
191
+ const colon = written.indexOf(':');
192
+ if (colon === -1)
193
+ throw new SourceError(`"${written}" is not a setting — write one as name: value, such as columns: aligned`, line);
194
+ const name = written.slice(0, colon).trim();
195
+ const value = written.slice(colon + 1).trim().replace(/\s+/g, ' ');
196
+ const setting = SETTING_BY_NAME.get(name);
197
+ if (setting === undefined) {
198
+ const near = nearMatch(name, SETTINGS.map((one) => one.name));
199
+ throw new SourceError(`there is no setting "${name}"` + (near ? ` — did you mean ${near}?` : ` — the settings are ${orList(SETTINGS.map((one) => one.name))}`), line);
200
+ }
201
+ const before = seen.get(name);
202
+ if (before !== undefined)
203
+ throw new SourceError(`${name} is set twice, here and on line ${before} — keep one`, line);
204
+ seen.set(name, line);
205
+ if (value === '')
206
+ throw new SourceError(`${name} has no value — it takes ${orList(setting.values.map(([word]) => word))}`, line);
207
+ try {
208
+ settings[name] = setting.read(value);
209
+ }
210
+ catch (reason) {
211
+ if (typeof reason === 'string')
212
+ throw new SourceError(reason, line);
213
+ throw reason;
214
+ }
215
+ });
216
+ if (settings['clause-gap'] === 'tab' && settings.columns === 'aligned') {
217
+ throw new SourceError(`clause-gap: tab can't line columns up, since how wide a tab is depends on the editor — use a number of spaces, or columns: off`, seen.get('clause-gap') ?? seen.get('columns') ?? 1);
218
+ }
219
+ return settings;
220
+ }
221
+ /**
222
+ * The `.reladraw-format` that `reladraw format --init` writes: every setting
223
+ * at its default, each under a comment saying what it controls and what each
224
+ * of its values means.
225
+ */
226
+ export function settingsFileText() {
227
+ const blocks = SETTINGS.map((setting) => {
228
+ const width = Math.max(...setting.values.map(([word]) => word.length));
229
+ // A meaning written over two lines carries on under where it started.
230
+ const values = setting.values.flatMap(([word, meaning], index) => {
231
+ const [first, ...rest] = meaning.split('\n');
232
+ return [
233
+ `// ${word.padEnd(width)} ${first}${index === 0 ? ' (default)' : ''}`,
234
+ ...rest.map((more) => `// ${' '.repeat(width)} ${more}`),
235
+ ];
236
+ });
237
+ const about = setting.about.split('\n').map((line) => `// ${line}`);
238
+ return [...about, ...values, `${setting.name}: ${setting.written}`].join('\n');
239
+ });
240
+ const header = [
241
+ '// .reladraw-format: how `reladraw format` lays out the .reladraw files in this',
242
+ '// folder and every folder below it, unless a nearer one says otherwise.',
243
+ '// Every setting is written out at its default; change the ones you want, or',
244
+ '// delete a line to use the default.',
245
+ ].join('\n');
246
+ return `${header}\n\n${blocks.join('\n\n')}\n`;
247
+ }
248
+ function orList(words) {
249
+ return words.length < 2 ? words.join('') : `${words.slice(0, -1).join(', ')} or ${words[words.length - 1]}`;
250
+ }
@@ -0,0 +1,14 @@
1
+ import { type FormatSettings } from './format-settings.js';
2
+ /**
3
+ * Lay a source file out again: spacing between clauses, columns shared by
4
+ * statements that belong together, comments and blank lines, as `settings`
5
+ * say. Only whitespace changes, and the order of clauses when `clause-order`
6
+ * asks. Every token is copied out of the line it was
7
+ * written on, so no word is respelled, and the result is parsed again and
8
+ * compared with the original, so a mistake here can never change what a file
9
+ * draws: it throws instead.
10
+ *
11
+ * Throws the parser's error for a file that does not parse, so a broken file
12
+ * is never rewritten.
13
+ */
14
+ export declare function format(source: string, settings?: FormatSettings): string;