@adecore/editor-react 0.31.12 → 0.32.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/dist/AnchoredPopup.js +73 -19
- package/dist/AttributionCard.js +162 -4
- package/dist/BaselineStatus.js +62 -8
- package/dist/CallHierarchyPanel.js +977 -60
- package/dist/ChangeReview.js +76 -25
- package/dist/CodeAuthorsCard.js +314 -14
- package/dist/CompletionPopup.js +490 -83
- package/dist/DiffView.js +548 -181
- package/dist/EditorContextMenu.js +352 -34
- package/dist/EditorView.js +276 -99
- package/dist/FindReplace.js +525 -67
- package/dist/HoverCard.js +378 -38
- package/dist/HoverSections.js +367 -56
- package/dist/LanguagePopups.js +95 -34
- package/dist/Markdown.js +41 -6
- package/dist/PanelResizeHandle.js +199 -66
- package/dist/PathText.js +49 -7
- package/dist/PeekPanel.js +675 -72
- package/dist/PickPopup.js +304 -34
- package/dist/RenameCard.js +679 -85
- package/dist/RenderedDocs.js +329 -58
- package/dist/SignatureCard.js +91 -7
- package/dist/SplitDiffView.js +344 -254
- package/dist/SymbolPicker.js +440 -85
- package/dist/call-hierarchy-model.js +102 -68
- package/dist/call-hierarchy.js +458 -359
- package/dist/code-actions-model.js +88 -76
- package/dist/code-actions.js +372 -283
- package/dist/code-authors.js +64 -55
- package/dist/code-vision-model.js +134 -127
- package/dist/code-vision.js +224 -206
- package/dist/completion-call.js +94 -67
- package/dist/completion-model.js +334 -244
- package/dist/completion.js +840 -649
- package/dist/conflict-model.js +35 -29
- package/dist/context-menu-rows.js +42 -17
- package/dist/context-menu.js +64 -46
- package/dist/definition-link.js +88 -85
- package/dist/diagnostics-model.js +74 -55
- package/dist/diagnostics.js +121 -100
- package/dist/diff-expansion.js +38 -35
- package/dist/diff-overlays.js +144 -22
- package/dist/diff-syntax.js +65 -42
- package/dist/diff-wordmarks.js +26 -23
- package/dist/doc-comment-model.js +265 -216
- package/dist/editor-language.js +177 -152
- package/dist/error-message.js +2 -2
- package/dist/find-query.js +49 -36
- package/dist/folding.js +81 -62
- package/dist/format-on-type.js +86 -67
- package/dist/highlight-layers.js +19 -20
- package/dist/highlights.js +105 -83
- package/dist/history.js +44 -38
- package/dist/holder.js +18 -18
- package/dist/host-types.js +1 -1
- package/dist/hover-content.js +167 -129
- package/dist/hover.js +285 -245
- package/dist/index.js +10 -10
- package/dist/inlay-hints.js +369 -324
- package/dist/key-priority.js +34 -31
- package/dist/language-ids.js +45 -42
- package/dist/line-edits.js +74 -28
- package/dist/line-text.js +18 -3
- package/dist/locales/en.json +38 -38
- package/dist/locales/nl.json +41 -41
- package/dist/models.js +1 -1
- package/dist/navigation-history.js +40 -41
- package/dist/navigation.js +141 -123
- package/dist/panel-size.js +8 -8
- package/dist/patch-decorations.js +236 -160
- package/dist/patch-layout.js +331 -262
- package/dist/paths.js +3 -3
- package/dist/peek-model.js +86 -69
- package/dist/peek.js +254 -192
- package/dist/pick.js +164 -157
- package/dist/popup-focus.js +5 -5
- package/dist/popup-placement.js +36 -21
- package/dist/popups.js +13 -13
- package/dist/project-language.js +228 -211
- package/dist/project-problems.js +81 -69
- package/dist/proposal-model.js +163 -126
- package/dist/recent-choices.js +25 -25
- package/dist/refresher.js +44 -49
- package/dist/rename-model.js +113 -113
- package/dist/rename.js +321 -233
- package/dist/rendered-docs.d.ts +4 -2
- package/dist/rendered-docs.js +384 -318
- package/dist/rendering-context.js +5 -3
- package/dist/rendering.js +22 -4
- package/dist/review-group.js +42 -39
- package/dist/row-host.d.ts +4 -2
- package/dist/row-host.js +90 -78
- package/dist/selection-ranges.js +21 -17
- package/dist/semantic-delta.js +22 -19
- package/dist/semantic-model.js +86 -73
- package/dist/semantic-tokens.js +201 -162
- package/dist/shortcut-keys.js +16 -10
- package/dist/signature-model.js +34 -28
- package/dist/signature.js +177 -129
- package/dist/snippet-field-transforms.js +118 -106
- package/dist/snippet-indent.js +48 -43
- package/dist/snippet-range-order.js +11 -11
- package/dist/snippet-session.js +703 -667
- package/dist/snippet-transform-client.js +59 -59
- package/dist/snippet-transform-format.js +285 -263
- package/dist/snippet-transform-protocol.js +1 -1
- package/dist/snippet-transform-queue.js +163 -153
- package/dist/snippet-transform-worker.js +42 -35
- package/dist/snippet-variable-names.js +1 -36
- package/dist/snippet-variables.js +53 -63
- package/dist/snippet.js +477 -405
- package/dist/split-alignment.js +47 -35
- package/dist/split-scroll.js +42 -40
- package/dist/symbol-links.js +33 -25
- package/dist/symbol-picker-model.js +126 -116
- package/dist/symbol-picker.js +98 -59
- package/dist/symbols.js +117 -88
- package/dist/testing.js +248 -165
- package/dist/timers.js +35 -30
- package/dist/use-panel-body.js +49 -8
- package/dist/use-problem-counts.js +29 -15
- package/dist/view-state.js +54 -33
- package/dist/word-diff.js +18 -18
- package/dist/workspace-edit.js +300 -249
- package/package.json +6 -6
- package/src/CallHierarchyPanel.tsx +2 -2
- package/src/DiffView.tsx +3 -3
- package/src/FindReplace.tsx +2 -2
- package/src/LanguagePopups.tsx +2 -0
- package/src/PeekPanel.tsx +3 -3
- package/src/RenderedDocs.tsx +9 -10
- package/src/SymbolPicker.tsx +2 -2
- package/src/diff-overlays.tsx +6 -4
- package/src/locales/en.json +38 -38
- package/src/locales/nl.json +41 -41
- package/src/rendered-docs.ts +14 -3
- package/src/row-host.ts +14 -3
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
/* The prefix of an address that stands for a name a `{@link}` points at, which the view resolves instead of following. */
|
|
7
7
|
export const DOC_LINK_PREFIX = '#doc-link:';
|
|
8
8
|
const ALIASES = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
returns: 'return',
|
|
10
|
+
exception: 'throws',
|
|
11
|
+
arg: 'param',
|
|
12
|
+
argument: 'param',
|
|
13
|
+
prop: 'property',
|
|
14
|
+
yields: 'yield',
|
|
15
|
+
link: 'see'
|
|
16
16
|
};
|
|
17
17
|
/* Tags that carry a type before their subject. */
|
|
18
18
|
const TYPED = new Set(['param', 'property', 'return', 'throws', 'var', 'type', 'yield', 'template', 'this']);
|
|
@@ -25,130 +25,167 @@ const DANGEROUS = /<(script|style|iframe|object|embed|svg|math|form)\b[\s\S]*?<\
|
|
|
25
25
|
const SAFE_SCHEME = /^(https?:|mailto:)/i;
|
|
26
26
|
/* Reads a type that starts the text: in braces as JSDoc writes it, or as one word with its brackets as PHPDoc does. */
|
|
27
27
|
function takeType(text, braces) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let depth = 0;
|
|
31
|
-
for (let i = 0; i < trimmed.length; i++) {
|
|
32
|
-
depth += trimmed[i] === '{' ? 1 : trimmed[i] === '}' ? -1 : 0;
|
|
33
|
-
if (depth === 0) {
|
|
34
|
-
return { type: trimmed.slice(1, i).trim(), rest: trimmed.slice(i + 1).trimStart() };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return { type: null, rest: trimmed };
|
|
38
|
-
}
|
|
39
|
-
if (braces || /^[$&.]/.test(trimmed)) {
|
|
40
|
-
return { type: null, rest: trimmed };
|
|
41
|
-
}
|
|
28
|
+
const trimmed = text.trimStart();
|
|
29
|
+
if (trimmed.startsWith('{')) {
|
|
42
30
|
let depth = 0;
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
31
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
32
|
+
depth += trimmed[i] === '{' ? 1 : trimmed[i] === '}' ? -1 : 0;
|
|
33
|
+
if (depth === 0) {
|
|
34
|
+
return {
|
|
35
|
+
type: trimmed.slice(1, i).trim(),
|
|
36
|
+
rest: trimmed.slice(i + 1).trimStart()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
type: null,
|
|
42
|
+
rest: trimmed
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (braces || /^[$&.]/.test(trimmed)) {
|
|
46
|
+
return {
|
|
47
|
+
type: null,
|
|
48
|
+
rest: trimmed
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let depth = 0;
|
|
52
|
+
let end = 0;
|
|
53
|
+
for (; end < trimmed.length; end++) {
|
|
54
|
+
const character = trimmed[end];
|
|
55
|
+
if ('<([{'.includes(character)) {
|
|
56
|
+
depth++;
|
|
57
|
+
} else if ('>)]}'.includes(character)) {
|
|
58
|
+
depth--;
|
|
59
|
+
} else if (/\s/.test(character) && depth <= 0) {
|
|
60
|
+
break;
|
|
55
61
|
}
|
|
56
|
-
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
type: end === 0 ? null : trimmed.slice(0, end),
|
|
65
|
+
rest: trimmed.slice(end).trimStart()
|
|
66
|
+
};
|
|
57
67
|
}
|
|
58
68
|
/* The first word of a tag's text: a name with its sigils and the `[name=default]` form of JSDoc. */
|
|
59
69
|
function takeSubject(text) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
const optional = /^\[([^\]]+)\]\s*/.exec(text);
|
|
71
|
+
if (optional !== null) {
|
|
72
|
+
return {
|
|
73
|
+
subject: optional[1],
|
|
74
|
+
rest: text.slice(optional[0].length)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const word = /^(?:\.\.\.|&)?[$\w.\\:-]+/.exec(text);
|
|
78
|
+
if (word === null) {
|
|
79
|
+
return {
|
|
80
|
+
subject: null,
|
|
81
|
+
rest: text
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
subject: word[0],
|
|
86
|
+
rest: text.slice(word[0].length).trimStart()
|
|
87
|
+
};
|
|
69
88
|
}
|
|
70
89
|
/* A comment's lines cut where a tag starts at the beginning of a line, outside a fenced block. */
|
|
71
90
|
function splitTags(body) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
const description = [];
|
|
92
|
+
const raw = [];
|
|
93
|
+
let fenced = false;
|
|
94
|
+
for (const line of body.split('\n')) {
|
|
95
|
+
if (FENCE.test(line)) {
|
|
96
|
+
fenced = !fenced;
|
|
97
|
+
}
|
|
98
|
+
const start = fenced ? null : TAG_START.exec(line.trimStart());
|
|
99
|
+
if (start !== null) {
|
|
100
|
+
raw.push({
|
|
101
|
+
name: start[1].toLowerCase(),
|
|
102
|
+
text: start[2]
|
|
103
|
+
});
|
|
104
|
+
} else if (raw.length > 0) {
|
|
105
|
+
raw[raw.length - 1].text += `\n${line}`;
|
|
106
|
+
} else {
|
|
107
|
+
description.push(line);
|
|
89
108
|
}
|
|
90
|
-
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
description: description.join('\n').trim(),
|
|
112
|
+
raw
|
|
113
|
+
};
|
|
91
114
|
}
|
|
92
115
|
function entryOf(name, text, php) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const target = /^\S+/.exec(rest);
|
|
109
|
-
subject = target === null ? null : target[0];
|
|
110
|
-
rest = target === null ? rest : rest.slice(target[0].length).trimStart();
|
|
111
|
-
}
|
|
112
|
-
if (name === 'throws' && type === null && subject !== null) {
|
|
113
|
-
type = subject;
|
|
114
|
-
subject = null;
|
|
116
|
+
let rest = text.trim();
|
|
117
|
+
let type = null;
|
|
118
|
+
let subject = null;
|
|
119
|
+
if (TYPED.has(name)) {
|
|
120
|
+
const taken = name === 'return' && rest.startsWith('$this') ? {
|
|
121
|
+
type: '$this',
|
|
122
|
+
rest: rest.slice(5).trimStart()
|
|
123
|
+
} : takeType(rest, !php);
|
|
124
|
+
type = taken.type;
|
|
125
|
+
rest = taken.rest;
|
|
126
|
+
// A type that stands alone is the whole text of a tag that has no subject.
|
|
127
|
+
if (SUBJECT_FIRST.has(name) || name === 'throws' && type === null) {
|
|
128
|
+
const word = takeSubject(rest);
|
|
129
|
+
subject = word.subject;
|
|
130
|
+
rest = word.rest;
|
|
115
131
|
}
|
|
116
|
-
|
|
132
|
+
} else if (name === 'see' && !rest.startsWith('{')) {
|
|
133
|
+
const target = /^\S+/.exec(rest);
|
|
134
|
+
subject = target === null ? null : target[0];
|
|
135
|
+
rest = target === null ? rest : rest.slice(target[0].length).trimStart();
|
|
136
|
+
}
|
|
137
|
+
if (name === 'throws' && type === null && subject !== null) {
|
|
138
|
+
type = subject;
|
|
139
|
+
subject = null;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
type,
|
|
144
|
+
subject,
|
|
145
|
+
text: rest.replace(DASH, '').trim()
|
|
146
|
+
};
|
|
117
147
|
}
|
|
118
148
|
/* Text outside backtick code, which is the only text HTML and inline tags are read in. */
|
|
119
149
|
function outsideCode(markdown, convert) {
|
|
120
|
-
|
|
121
|
-
.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/)
|
|
122
|
-
.map((part, index) => (index % 2 === 1 ? part : convert(part)))
|
|
123
|
-
.join('');
|
|
150
|
+
return markdown.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/).map((part, index) => index % 2 === 1 ? part : convert(part)).join('');
|
|
124
151
|
}
|
|
125
|
-
const ENTITIES = {
|
|
152
|
+
const ENTITIES = {
|
|
153
|
+
lt: '<',
|
|
154
|
+
gt: '>',
|
|
155
|
+
amp: '&',
|
|
156
|
+
quot: '"',
|
|
157
|
+
apos: "'",
|
|
158
|
+
nbsp: ' ',
|
|
159
|
+
ndash: '-',
|
|
160
|
+
mdash: '-',
|
|
161
|
+
hellip: '...'
|
|
162
|
+
};
|
|
126
163
|
/* Named entities of the small set docblocks use, and numeric ones; anything else stays as written. */
|
|
127
164
|
export function decodeEntities(text) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
165
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, code) => {
|
|
166
|
+
if (code[0] === '#') {
|
|
167
|
+
const point = code[1].toLowerCase() === 'x' ? Number.parseInt(code.slice(2), 16) : Number.parseInt(code.slice(1), 10);
|
|
168
|
+
return Number.isFinite(point) && point > 0 && point <= 0x10ffff ? String.fromCodePoint(point) : match;
|
|
169
|
+
}
|
|
170
|
+
return ENTITIES[code.toLowerCase()] ?? match;
|
|
171
|
+
});
|
|
135
172
|
}
|
|
136
173
|
function dedent(text) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
const lines = text.replace(/^\n+|\s+$/g, '').split('\n');
|
|
175
|
+
const indents = lines.filter(line => line.trim() !== '').map(line => line.length - line.trimStart().length);
|
|
176
|
+
const common = indents.length === 0 ? 0 : Math.min(...indents);
|
|
177
|
+
return lines.map(line => line.slice(common)).join('\n');
|
|
141
178
|
}
|
|
142
179
|
function fenced(body) {
|
|
143
|
-
|
|
180
|
+
return `\n\n\`\`\`\n${dedent(decodeEntities(stripTags(body)))}\n\`\`\`\n\n`;
|
|
144
181
|
}
|
|
145
182
|
function stripTags(html) {
|
|
146
|
-
|
|
183
|
+
return html.replace(/<\/?[a-zA-Z][^>]*>/g, '');
|
|
147
184
|
}
|
|
148
185
|
/* The address of a link when it is one a reader may follow; null for everything else, such as a script address. */
|
|
149
186
|
export function safeAddress(address) {
|
|
150
|
-
|
|
151
|
-
|
|
187
|
+
const trimmed = decodeEntities(address.trim());
|
|
188
|
+
return SAFE_SCHEME.test(trimmed) || trimmed.startsWith(DOC_LINK_PREFIX) ? trimmed.replace(/\s/g, '%20').replace(/\(/g, '%28').replace(/\)/g, '%29') : null;
|
|
152
189
|
}
|
|
153
190
|
/*
|
|
154
191
|
* The HTML subset docblocks use, as Markdown: paragraphs and breaks, bold, italic, code, preformatted
|
|
@@ -157,135 +194,147 @@ export function safeAddress(address) {
|
|
|
157
194
|
* as the text it was, which the view draws escaped.
|
|
158
195
|
*/
|
|
159
196
|
export function htmlToMarkdown(source) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const depth = Math.max(0, lists.length - 1);
|
|
220
|
-
const marker = lists.at(-1) === 'ol' ? `${(counters[depth] = (counters[depth] ?? 0) + 1)}.` : '-';
|
|
221
|
-
return `\n${' '.repeat(depth)}${marker} `;
|
|
222
|
-
}
|
|
223
|
-
case 'blockquote':
|
|
224
|
-
return closing ? '\n\n' : '\n\n> ';
|
|
225
|
-
case 'dt':
|
|
226
|
-
return closing ? '**\n\n' : '\n\n**';
|
|
227
|
-
case 'dd':
|
|
228
|
-
return '\n\n';
|
|
229
|
-
default:
|
|
230
|
-
if (/^h[1-6]$/i.test(tag)) {
|
|
231
|
-
return closing ? '\n\n' : `\n\n${'#'.repeat(Number(tag[1]))} `;
|
|
232
|
-
}
|
|
233
|
-
return '';
|
|
197
|
+
if (!source.includes('<') && !source.includes('&')) {
|
|
198
|
+
return source;
|
|
199
|
+
}
|
|
200
|
+
const lists = [];
|
|
201
|
+
const counters = [];
|
|
202
|
+
let anchor = null;
|
|
203
|
+
const blocks = source.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre\s*>/gi, (_match, body) => fenced(body)).replace(/<code>([^<]*\n[^<]*)<\/code>/gi, (_match, body) => fenced(body));
|
|
204
|
+
const converted = outsideCode(blocks, text => text.replace(DANGEROUS, '').replace(/<(\/?)(p|br|b|strong|i|em|u|code|tt|kbd|samp|var|a|ul|ol|li|h[1-6]|blockquote|hr|div|span|dl|dt|dd|sup|sub|small|center)\b([^>]*?)(\/?)>/gi, (_match, close, tag, attributes) => {
|
|
205
|
+
const closing = close === '/';
|
|
206
|
+
switch (tag.toLowerCase()) {
|
|
207
|
+
case 'p':
|
|
208
|
+
case 'div':
|
|
209
|
+
case 'dl':
|
|
210
|
+
return '\n\n';
|
|
211
|
+
case 'br':
|
|
212
|
+
return '\n';
|
|
213
|
+
case 'hr':
|
|
214
|
+
return '\n\n---\n\n';
|
|
215
|
+
case 'b':
|
|
216
|
+
case 'strong':
|
|
217
|
+
return '**';
|
|
218
|
+
case 'i':
|
|
219
|
+
case 'em':
|
|
220
|
+
case 'var':
|
|
221
|
+
return '*';
|
|
222
|
+
case 'code':
|
|
223
|
+
case 'tt':
|
|
224
|
+
case 'kbd':
|
|
225
|
+
case 'samp':
|
|
226
|
+
return '`';
|
|
227
|
+
case 'a':
|
|
228
|
+
{
|
|
229
|
+
if (closing) {
|
|
230
|
+
const target = anchor;
|
|
231
|
+
anchor = null;
|
|
232
|
+
return target === null ? '' : `](${target})`;
|
|
233
|
+
}
|
|
234
|
+
const href = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(attributes);
|
|
235
|
+
anchor = href === null ? null : safeAddress(href[1] ?? href[2] ?? href[3] ?? '');
|
|
236
|
+
return anchor === null ? '' : '[';
|
|
237
|
+
}
|
|
238
|
+
case 'ul':
|
|
239
|
+
case 'ol':
|
|
240
|
+
if (closing) {
|
|
241
|
+
lists.pop();
|
|
242
|
+
counters.pop();
|
|
243
|
+
} else {
|
|
244
|
+
lists.push(tag.toLowerCase());
|
|
245
|
+
counters.push(0);
|
|
246
|
+
}
|
|
247
|
+
return '\n';
|
|
248
|
+
case 'li':
|
|
249
|
+
{
|
|
250
|
+
if (closing) {
|
|
251
|
+
return '';
|
|
252
|
+
}
|
|
253
|
+
const depth = Math.max(0, lists.length - 1);
|
|
254
|
+
const marker = lists.at(-1) === 'ol' ? `${counters[depth] = (counters[depth] ?? 0) + 1}.` : '-';
|
|
255
|
+
return `\n${' '.repeat(depth)}${marker} `;
|
|
234
256
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
257
|
+
case 'blockquote':
|
|
258
|
+
return closing ? '\n\n' : '\n\n> ';
|
|
259
|
+
case 'dt':
|
|
260
|
+
return closing ? '**\n\n' : '\n\n**';
|
|
261
|
+
case 'dd':
|
|
262
|
+
return '\n\n';
|
|
263
|
+
default:
|
|
264
|
+
if (/^h[1-6]$/i.test(tag)) {
|
|
265
|
+
return closing ? '\n\n' : `\n\n${'#'.repeat(Number(tag[1]))} `;
|
|
266
|
+
}
|
|
267
|
+
return '';
|
|
268
|
+
}
|
|
269
|
+
}));
|
|
270
|
+
// Entities last, so `<b>` stays the text it spells and is not read as a tag above.
|
|
271
|
+
return outsideCode(converted, decodeEntities).replace(/\n{3,}/g, '\n\n');
|
|
238
272
|
}
|
|
239
273
|
/*
|
|
240
274
|
* Inline `{@link Name}`, `{@link Name label}`, `{@link Name|label}`, the `linkcode` and `linkplain` forms, `{@see Name}`
|
|
241
275
|
* and `{@code text}`. A name becomes a link the view resolves; an address becomes a link to it.
|
|
242
276
|
*/
|
|
243
277
|
export function inlineTagsToMarkdown(markdown) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
})
|
|
250
|
-
.replace(/\{@code\s+([^}]*)\}/g, (_match, code) => `\`${code.trim()}\``)
|
|
251
|
-
.replace(/\{@inheritDoc\}/g, ''));
|
|
278
|
+
return outsideCode(markdown, text => text.replace(/\{@(?:link|linkcode|linkplain|see)\s+([^}\s|]+)(?:\s*[|\s]\s*([^}]+?))?\s*\}/g, (_match, target, label) => {
|
|
279
|
+
const address = /^https?:\/\//i.test(target) ? safeAddress(target) : `${DOC_LINK_PREFIX}${encodeURIComponent(target)}`;
|
|
280
|
+
const shown = label ?? target;
|
|
281
|
+
return address === null ? `\`${shown}\`` : `[${label === undefined ? `\`${shown}\`` : shown}](${address})`;
|
|
282
|
+
}).replace(/\{@code\s+([^}]*)\}/g, (_match, code) => `\`${code.trim()}\``).replace(/\{@inheritDoc\}/g, ''));
|
|
252
283
|
}
|
|
253
284
|
/* An example's lines without the caption, and without the indentation they share; the text after the tag on its own line is not indented like the rest. */
|
|
254
285
|
function exampleOf(text) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
286
|
+
const code = text.replace(/^\s*<caption>[\s\S]*?<\/caption>[ \t]*\n?/i, '');
|
|
287
|
+
const [head = '', ...tail] = code.split('\n');
|
|
288
|
+
return head.trim() === '' ? dedent(code) : [head.trim(), dedent(tail.join('\n'))].join('\n').trim();
|
|
258
289
|
}
|
|
259
290
|
function proseOf(text) {
|
|
260
|
-
|
|
291
|
+
return inlineTagsToMarkdown(htmlToMarkdown(text)).trim();
|
|
261
292
|
}
|
|
262
293
|
/* Reads the body of a documentation comment, as `findDocComments` returns it. `php` lets a type stand without braces. */
|
|
263
294
|
export function parseDocComment(body, options = {}) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
295
|
+
const php = options.php === true;
|
|
296
|
+
const {
|
|
297
|
+
description,
|
|
298
|
+
raw
|
|
299
|
+
} = splitTags(body);
|
|
300
|
+
const tags = raw.map(({
|
|
301
|
+
name,
|
|
302
|
+
text
|
|
303
|
+
}) => {
|
|
304
|
+
const bare = name.replace(/^(?:psalm|phpstan)-/, '');
|
|
305
|
+
const canonical = ALIASES[bare] ?? bare;
|
|
306
|
+
const entry = entryOf(canonical, text, php);
|
|
307
|
+
// An example is code: it keeps its lines and gets no prose treatment, only its caption removed.
|
|
308
|
+
if (canonical === 'example') {
|
|
309
|
+
return {
|
|
310
|
+
...entry,
|
|
311
|
+
text: exampleOf(text)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
...entry,
|
|
316
|
+
text: proseOf(entry.text)
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
description: proseOf(description),
|
|
321
|
+
tags
|
|
322
|
+
};
|
|
277
323
|
}
|
|
278
324
|
/* Tags in the order a reader looks for them; every other tag follows in the order it was written. */
|
|
279
325
|
const ORDER = ['deprecated', 'template', 'param', 'property', 'type', 'var', 'return', 'yield', 'throws', 'see', 'example', 'since', 'version', 'author'];
|
|
280
326
|
/* The tags gathered by name, so a list of parameters is one section, in reading order. */
|
|
281
327
|
export function sectionsOf(tags) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
328
|
+
const byName = new Map();
|
|
329
|
+
for (const tag of tags) {
|
|
330
|
+
byName.set(tag.name, [...(byName.get(tag.name) ?? []), tag]);
|
|
331
|
+
}
|
|
332
|
+
const rank = name => {
|
|
333
|
+
const index = ORDER.indexOf(name);
|
|
334
|
+
return index === -1 ? ORDER.length : index;
|
|
335
|
+
};
|
|
336
|
+
return [...byName].map(([name, entries]) => ({
|
|
337
|
+
name,
|
|
338
|
+
tags: entries
|
|
339
|
+
})).sort((left, right) => rank(left.name) - rank(right.name));
|
|
340
|
+
}
|