UNPKG

16.7 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.formatPreamble = exports.parseStructuredHeaderDl = exports.parseStructuredHeaderH1 = void 0;
4const utils_1 = require("./utils");
5function parseStructuredHeaderH1(spec, header) {
6 // parsing is intentionally permissive; the linter can do stricter checks
7 // TODO have the linter do checks
8 var _a, _b, _c, _d;
9 let wrapper = null;
10 let headerText = header.innerHTML;
11 let beforeContents = 0;
12 const headerWrapperMatch = headerText.match(/^(?<beforeContents>\s*<(?<tag>ins|del|mark)>)(?<contents>.*)<\/\k<tag>>\s*$/is);
13 if (headerWrapperMatch != null) {
14 wrapper = headerWrapperMatch.groups.tag;
15 headerText = headerWrapperMatch.groups.contents;
16 beforeContents = headerWrapperMatch.groups.beforeContents.length;
17 }
18 const prefix = headerText.match(/^\s*(Static|Runtime) Semantics:\s*/);
19 if (prefix != null) {
20 headerText = headerText.substring(prefix[0].length);
21 }
22 const parsed = headerText.match(/^(?<beforeParams>\s*(?<name>[^(\s]+)\s*)(?:\((?<params>[^)]*)\)(?:\s*:(?<returnType>.*))?\s*)?$/s);
23 if (parsed == null) {
24 spec.warn({
25 type: 'contents',
26 ruleId: 'header-format',
27 message: `failed to parse header`,
28 node: header,
29 nodeRelativeColumn: 1,
30 nodeRelativeLine: 1,
31 });
32 return { name: null, formattedHeader: null, formattedParams: null, formattedReturnType: null };
33 }
34 const name = parsed.groups.name;
35 let paramText = (_a = parsed.groups.params) !== null && _a !== void 0 ? _a : '';
36 const returnType = (_c = (_b = parsed.groups.returnType) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : null;
37 if (returnType === '') {
38 const { column, line } = utils_1.offsetToLineAndColumn(header.innerHTML, beforeContents +
39 ((_d = prefix === null || prefix === void 0 ? void 0 : prefix[0].length) !== null && _d !== void 0 ? _d : 0) +
40 (headerText.length - headerText.match(/\s*$/)[0].length) -
41 1);
42 spec.warn({
43 type: 'contents',
44 ruleId: 'header-format',
45 message: `if a return type is given, it must not be empty`,
46 node: header,
47 nodeRelativeColumn: column,
48 nodeRelativeLine: line,
49 });
50 }
51 const params = [];
52 const optionalParams = [];
53 let formattedHeader = null;
54 if (/\(\s*\n/.test(headerText)) {
55 // if it's multiline, parse it for types
56 const paramLines = paramText.split('\n');
57 let index = 0;
58 let offset = 0;
59 for (const line of paramLines) {
60 offset += line.length;
61 let chunk = line.trim();
62 if (chunk === '') {
63 continue;
64 }
65 const wrapperMatch = chunk.match(/^<(ins|del|mark)>(.*)<\/\1>$/i);
66 let paramWrapper = null;
67 if (wrapperMatch != null) {
68 paramWrapper = wrapperMatch[1];
69 chunk = wrapperMatch[2];
70 }
71 ++index;
72 function getParameterOffset() {
73 var _a;
74 return (beforeContents +
75 ((_a = prefix === null || prefix === void 0 ? void 0 : prefix[0].length) !== null && _a !== void 0 ? _a : 0) +
76 parsed.groups.beforeParams.length +
77 1 + // `beforeParams` does not include the leading `(`
78 (offset - line.length) + // we've already updated offset to include line.length at this point
79 index + // to account for the `\n`s eaten by the .split
80 line.match(/^\s*/)[0].length);
81 }
82 const parsedChunk = chunk.match(/^(optional\s+)?([A-Za-z0-9_]+)\s*:\s*(\S.*\S)/);
83 if (parsedChunk == null) {
84 const { line: nodeRelativeLine, column: nodeRelativeColumn } = utils_1.offsetToLineAndColumn(header.innerHTML, getParameterOffset());
85 spec.warn({
86 type: 'contents',
87 ruleId: 'header-format',
88 message: `failed to parse parameter ${index}`,
89 node: header,
90 nodeRelativeColumn,
91 nodeRelativeLine,
92 });
93 continue;
94 }
95 const optional = parsedChunk[1] != null;
96 const paramName = parsedChunk[2];
97 let paramType = parsedChunk[3];
98 if (paramType.endsWith(',')) {
99 paramType = paramType.slice(0, -1);
100 }
101 if (!optional && optionalParams.length > 0) {
102 const { line: nodeRelativeLine, column: nodeRelativeColumn } = utils_1.offsetToLineAndColumn(header.innerHTML, getParameterOffset());
103 spec.warn({
104 type: 'contents',
105 ruleId: 'header-format',
106 message: `required parameters should not follow optional parameters`,
107 node: header,
108 nodeRelativeColumn,
109 nodeRelativeLine,
110 });
111 }
112 (optional ? optionalParams : params).push({
113 name: paramName,
114 type: paramType === 'unknown' ? null : paramType,
115 wrapper: paramWrapper,
116 });
117 }
118 const formattedPrefix = prefix == null ? '' : prefix[0].trim() + ' ';
119 // prettier-ignore
120 const printParam = (p) => ` ${p.wrapper == null ? '' : `<${p.wrapper}>`}${p.name}${p.wrapper == null ? '' : `</${p.wrapper}>`}`;
121 formattedHeader = `${formattedPrefix}${name} (${params.map(printParam).join(',')}`;
122 if (optionalParams.length > 0) {
123 formattedHeader +=
124 optionalParams
125 .map((p, i) => ' [ ' + (i > 0 || params.length > 0 ? ', ' : '') + p.name)
126 .join('') + optionalParams.map(() => ' ]').join('');
127 }
128 formattedHeader += ' )';
129 }
130 else {
131 let optional = false;
132 paramText = paramText.trim();
133 while (true) {
134 if (paramText.length == 0) {
135 break;
136 }
137 // eslint-disable-next-line @typescript-eslint/no-unused-vars
138 let { success, text, match } = eat(paramText, /^\s*\[(\s*,)?/);
139 if (success) {
140 optional = true;
141 paramText = text;
142 }
143 ({ success, text, match } = eat(paramText, /^\s*([A-Za-z0-9_]+)/));
144 if (!success) {
145 spec.warn({
146 type: 'contents',
147 ruleId: 'header-format',
148 message: `failed to parse header`,
149 node: header,
150 // we could be more precise, but it's probably not worth the effort
151 nodeRelativeLine: 1,
152 nodeRelativeColumn: 1,
153 });
154 break;
155 }
156 paramText = text;
157 (optional ? optionalParams : params).push({ name: match[1], type: null, wrapper: null });
158 ({ success, text } = eat(paramText, /^(\s*\])+|,/));
159 if (success) {
160 paramText = text;
161 }
162 }
163 }
164 // prettier-ignore
165 const printParamWithType = (p) => `${p.wrapper == null ? '' : `<${p.wrapper}>`}${p.name}${p.type == null ? '' : ` (${p.type})`}${p.wrapper == null ? '' : `</${p.wrapper}>`}`;
166 const paramsWithTypes = params.map(printParamWithType);
167 const optionalParamsWithTypes = optionalParams.map(printParamWithType);
168 let formattedParams = '';
169 if (params.length === 0 && optionalParams.length === 0) {
170 formattedParams = 'no arguments';
171 }
172 else {
173 if (params.length > 0) {
174 formattedParams =
175 (params.length === 1 ? 'argument' : 'arguments') + ' ' + formatEnglishList(paramsWithTypes);
176 if (optionalParams.length > 0) {
177 formattedParams += ' and ';
178 }
179 }
180 if (optionalParams.length > 0) {
181 formattedParams +=
182 'optional ' +
183 (optionalParams.length === 1 ? 'argument' : 'arguments') +
184 ' ' +
185 formatEnglishList(optionalParamsWithTypes);
186 }
187 }
188 if (formattedHeader != null && wrapper != null) {
189 formattedHeader = `<${wrapper}>${formattedHeader}</${wrapper}>`;
190 }
191 return { name, formattedHeader, formattedParams, formattedReturnType: returnType };
192}
193exports.parseStructuredHeaderH1 = parseStructuredHeaderH1;
194function parseStructuredHeaderDl(spec, type, dl) {
195 var _a;
196 let description = null;
197 let _for = null;
198 let effects = [];
199 for (let i = 0; i < dl.children.length; ++i) {
200 const dt = dl.children[i];
201 if (dt.tagName !== 'DT') {
202 spec.warn({
203 type: 'node',
204 ruleId: 'header-format',
205 message: `expecting header to have DT, but found ${dt.tagName}`,
206 node: dt,
207 });
208 break;
209 }
210 ++i;
211 const dd = dl.children[i];
212 if ((dd === null || dd === void 0 ? void 0 : dd.tagName) !== 'DD') {
213 spec.warn({
214 type: 'node',
215 ruleId: 'header-format',
216 message: `expecting header to have DD, but found ${dd.tagName}`,
217 node: dd,
218 });
219 break;
220 }
221 const dtype = (_a = dt.textContent) !== null && _a !== void 0 ? _a : '';
222 switch (dtype.trim().toLowerCase()) {
223 case 'description': {
224 if (description != null) {
225 spec.warn({
226 type: 'node',
227 ruleId: 'header-format',
228 message: `duplicate "description" attribute`,
229 node: dt,
230 });
231 }
232 description = dd;
233 break;
234 }
235 case 'for': {
236 if (_for != null) {
237 spec.warn({
238 type: 'node',
239 ruleId: 'header-format',
240 message: `duplicate "for" attribute`,
241 node: dt,
242 });
243 }
244 if (type === 'concrete method' || type === 'internal method') {
245 _for = dd;
246 }
247 else {
248 spec.warn({
249 type: 'node',
250 ruleId: 'header-format',
251 message: `"for" attributes only apply to concrete or internal methods`,
252 node: dt,
253 });
254 }
255 break;
256 }
257 case 'effects': {
258 // The dd contains a comma-separated list of effects.
259 if (dd.textContent !== null) {
260 effects = utils_1.validateEffects(spec, dd.textContent.split(',').map(c => c.trim()), dd);
261 }
262 break;
263 }
264 case '': {
265 spec.warn({
266 type: 'node',
267 ruleId: 'header-format',
268 message: `missing value for structured header attribute`,
269 node: dt,
270 });
271 break;
272 }
273 default: {
274 spec.warn({
275 type: 'node',
276 ruleId: 'header-format',
277 message: `unknown structured header entry type ${JSON.stringify(dtype)}`,
278 node: dt,
279 });
280 break;
281 }
282 }
283 }
284 return { description, for: _for, effects };
285}
286exports.parseStructuredHeaderDl = parseStructuredHeaderDl;
287function formatPreamble(spec, clause, dl, type, name, formattedParams, formattedReturnType, _for, description) {
288 const para = spec.doc.createElement('p');
289 const paras = [para];
290 type = (type !== null && type !== void 0 ? type : '').toLowerCase();
291 switch (type) {
292 case 'numeric method':
293 case 'abstract operation': {
294 // TODO tests (for each type of parametered thing) which have HTML in the parameter type
295 para.innerHTML += `The abstract operation ${name}`;
296 break;
297 }
298 case 'host-defined abstract operation': {
299 para.innerHTML += `The host-defined abstract operation ${name}`;
300 break;
301 }
302 case 'implementation-defined abstract operation': {
303 para.innerHTML += `The implementation-defined abstract operation ${name}`;
304 break;
305 }
306 case 'sdo':
307 case 'syntax-directed operation': {
308 para.innerHTML += `The syntax-directed operation ${name}`;
309 break;
310 }
311 case 'internal method':
312 case 'concrete method': {
313 if (_for == null) {
314 spec.warn({
315 type: 'contents',
316 ruleId: 'header-format',
317 message: `expected ${type} to have a "for"`,
318 node: dl,
319 nodeRelativeLine: 1,
320 nodeRelativeColumn: 1,
321 });
322 _for = spec.doc.createElement('div');
323 }
324 para.append(`The ${name} ${type} of `, ..._for.childNodes);
325 break;
326 }
327 default: {
328 if (type) {
329 spec.warn({
330 type: 'attr-value',
331 ruleId: 'header-type',
332 message: `unknown clause type ${JSON.stringify(type)}`,
333 node: clause,
334 attr: 'type',
335 });
336 }
337 else {
338 spec.warn({
339 type: 'node',
340 ruleId: 'header-type',
341 message: `clauses with structured headers should have a type`,
342 node: clause,
343 });
344 }
345 }
346 }
347 para.innerHTML += ` takes ${formattedParams}`;
348 if (formattedReturnType != null) {
349 para.innerHTML += ` and returns ${formattedReturnType}`;
350 }
351 para.innerHTML += '.';
352 if (description != null) {
353 const isJustElements = [...description.childNodes].every(n => { var _a; return n.nodeType === 1 || (n.nodeType === 3 && ((_a = n.textContent) === null || _a === void 0 ? void 0 : _a.trim()) === ''); });
354 if (isJustElements) {
355 paras.push(...description.childNodes);
356 }
357 else {
358 para.append(' ', ...description.childNodes);
359 }
360 }
361 const isSdo = type === 'sdo' || type === 'syntax-directed operation';
362 const lastSentence = isSdo
363 ? 'It is defined piecewise over the following productions:'
364 : 'It performs the following steps when called:';
365 let next = dl.nextElementSibling;
366 while (next != null && next.tagName === 'EMU-NOTE') {
367 next = next.nextElementSibling;
368 }
369 if ((isSdo && (next === null || next === void 0 ? void 0 : next.tagName) === 'EMU-GRAMMAR') ||
370 (!isSdo && (next === null || next === void 0 ? void 0 : next.tagName) === 'EMU-ALG' && !next.hasAttribute('replaces-step'))) {
371 if (paras.length > 1 || next !== dl.nextElementSibling) {
372 const whitespace = next.previousSibling;
373 const after = spec.doc.createElement('p');
374 after.append(lastSentence);
375 next.parentElement.insertBefore(after, next);
376 // fix up the whitespace in the generated HTML
377 if ((whitespace === null || whitespace === void 0 ? void 0 : whitespace.nodeType) === 3 /* TEXT_NODE */ && /^\s+$/.test(whitespace.nodeValue)) {
378 next.parentElement.insertBefore(whitespace.cloneNode(), next);
379 }
380 }
381 else {
382 para.append(' ' + lastSentence);
383 }
384 }
385 return paras;
386}
387exports.formatPreamble = formatPreamble;
388function eat(text, regex) {
389 const match = text.match(regex);
390 if (match == null) {
391 return { success: false, match, text };
392 }
393 return { success: true, match, text: text.substring(match[0].length) };
394}
395function formatEnglishList(list) {
396 if (list.length === 0) {
397 throw new Error('formatEnglishList should not be called with an empty list');
398 }
399 if (list.length === 1) {
400 return list[0];
401 }
402 if (list.length === 2) {
403 return `${list[0]} and ${list[1]}`;
404 }
405 return `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`;
406}