Skip to content

Commit aff795d

Browse files
committed
Support multiple regions in @include and @includeCode
Ref: #2816 (comment)
1 parent c372df3 commit aff795d

File tree

10 files changed

+225
-178
lines changed

10 files changed

+225
-178
lines changed

site/tags/include.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ For example:
4444
{@includeCode ../../example/src/enums.ts#simpleEnum}
4545
```
4646

47+
Multiple regions may be specified, separated by commas. If multiple regions are
48+
specified, TypeDoc will combine them into a single code block.
49+
50+
```md
51+
{@includeCode file.ts#region1,region2}
52+
```
53+
4754
Regions are specified in the files themselves via comments.
4855

4956
In TypeScript for example, the following would be a valid region:

src/lib/converter/plugins/IncludePlugin.ts

Lines changed: 121 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { CommentDisplayPart, Reflection } from "../../models/index.js";
77
import { MinimalSourceFile } from "../../utils/minimalSourceFile.js";
88
import type { Converter } from "../converter.js";
99
import { isFile } from "../../utils/fs.js";
10-
import { escapeRegExp } from "../../utils/general.js";
10+
import { dedent, escapeRegExp } from "../../utils/general.js";
1111

1212
/**
1313
* Handles `@include` and `@includeCode` within comments/documents.
@@ -63,9 +63,8 @@ export class IncludePlugin extends ConverterComponent {
6363
continue;
6464
}
6565

66-
const [filename, target, requestedLines] = parseIncludeCodeTextPart(
67-
part.text,
68-
);
66+
const { filename, regionTarget, requestedLines } =
67+
parseIncludeCodeTextPart(part.text);
6968

7069
const file = path.resolve(relative, filename);
7170
if (included.includes(file) && part.tag === "@include") {
@@ -80,30 +79,31 @@ export class IncludePlugin extends ConverterComponent {
8079
} else if (isFile(file)) {
8180
const text = fs.readFileSync(file, "utf-8");
8281
const ext = path.extname(file).substring(1);
82+
83+
const includedText = regionTarget
84+
? this.getRegions(
85+
refl,
86+
file,
87+
ext,
88+
part.text,
89+
text,
90+
regionTarget,
91+
part.tag,
92+
part.tag === "@includeCode",
93+
)
94+
: requestedLines
95+
? this.getLines(
96+
refl,
97+
file,
98+
part.text,
99+
text,
100+
requestedLines,
101+
part.tag,
102+
)
103+
: text;
104+
83105
if (part.tag === "@include") {
84-
const sf = new MinimalSourceFile(
85-
target
86-
? this.getRegion(
87-
refl,
88-
file,
89-
ext,
90-
part.text,
91-
text,
92-
target,
93-
part.tag,
94-
)
95-
: requestedLines
96-
? this.getLines(
97-
refl,
98-
file,
99-
part.text,
100-
text,
101-
requestedLines,
102-
part.tag,
103-
)
104-
: text,
105-
file,
106-
);
106+
const sf = new MinimalSourceFile(includedText, file);
107107
const { content } = this.owner.parseRawComment(
108108
sf,
109109
refl.project.files,
@@ -118,29 +118,7 @@ export class IncludePlugin extends ConverterComponent {
118118
} else {
119119
parts[i] = {
120120
kind: "code",
121-
text: makeCodeBlock(
122-
ext,
123-
target
124-
? this.getRegion(
125-
refl,
126-
file,
127-
ext,
128-
part.text,
129-
text,
130-
target,
131-
part.tag,
132-
)
133-
: requestedLines
134-
? this.getLines(
135-
refl,
136-
file,
137-
part.text,
138-
text,
139-
requestedLines,
140-
part.tag,
141-
)
142-
: text,
143-
),
121+
text: makeCodeBlock(ext, includedText),
144122
};
145123
}
146124
} else {
@@ -156,14 +134,15 @@ export class IncludePlugin extends ConverterComponent {
156134
}
157135
}
158136

159-
getRegion(
137+
getRegions(
160138
refl: Reflection,
161139
file: string,
162140
ext: string,
163141
textPart: string,
164142
text: string,
165-
target: string,
143+
regionTargets: string,
166144
tag: string,
145+
ignoreIndent: boolean,
167146
) {
168147
const regionTagsList = regionTagREsByExt[ext];
169148
if (!regionTagsList) {
@@ -177,42 +156,21 @@ export class IncludePlugin extends ConverterComponent {
177156
return "";
178157
}
179158

180-
let found: string | false = false;
181-
for (const [startTag, endTag] of regionTagsList) {
182-
const safeTarget = escapeRegExp(target);
183-
const start = text.match(startTag(safeTarget));
184-
const end = text.match(endTag(safeTarget));
159+
const targets = regionTargets.split(",").map((s) => s.trim());
160+
let content = "";
185161

186-
const foundStart = start && start.length > 0;
187-
const foundEnd = end && end.length > 0;
188-
if (foundStart && !foundEnd) {
189-
this.logger.error(
190-
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_close_not_found(
191-
tag,
192-
refl.getFriendlyFullName(),
193-
textPart,
194-
file,
195-
target,
196-
),
197-
);
198-
return "";
199-
}
200-
if (!foundStart && foundEnd) {
201-
this.logger.error(
202-
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_open_not_found(
203-
tag,
204-
refl.getFriendlyFullName(),
205-
textPart,
206-
file,
207-
target,
208-
),
209-
);
210-
return "";
211-
}
212-
if (foundStart && foundEnd) {
213-
if (start.length > 1) {
162+
for (const target of targets) {
163+
let found: string | false = false;
164+
for (const [startTag, endTag] of regionTagsList) {
165+
const safeTarget = escapeRegExp(target);
166+
const start = text.match(startTag(safeTarget));
167+
const end = text.match(endTag(safeTarget));
168+
169+
const foundStart = start && start.length > 0;
170+
const foundEnd = end && end.length > 0;
171+
if (foundStart && !foundEnd) {
214172
this.logger.error(
215-
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_open_found_multiple_times(
173+
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_close_not_found(
216174
tag,
217175
refl.getFriendlyFullName(),
218176
textPart,
@@ -222,9 +180,9 @@ export class IncludePlugin extends ConverterComponent {
222180
);
223181
return "";
224182
}
225-
if (end.length > 1) {
183+
if (!foundStart && foundEnd) {
226184
this.logger.error(
227-
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_close_found_multiple_times(
185+
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_open_not_found(
228186
tag,
229187
refl.getFriendlyFullName(),
230188
textPart,
@@ -234,48 +192,77 @@ export class IncludePlugin extends ConverterComponent {
234192
);
235193
return "";
236194
}
237-
if (found) {
238-
this.logger.error(
239-
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_found_multiple_times(
240-
tag,
241-
refl.getFriendlyFullName(),
242-
textPart,
243-
file,
244-
target,
245-
),
195+
if (foundStart && foundEnd) {
196+
if (start.length > 1) {
197+
this.logger.error(
198+
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_open_found_multiple_times(
199+
tag,
200+
refl.getFriendlyFullName(),
201+
textPart,
202+
file,
203+
target,
204+
),
205+
);
206+
return "";
207+
}
208+
if (end.length > 1) {
209+
this.logger.error(
210+
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_close_found_multiple_times(
211+
tag,
212+
refl.getFriendlyFullName(),
213+
textPart,
214+
file,
215+
target,
216+
),
217+
);
218+
return "";
219+
}
220+
if (found) {
221+
this.logger.error(
222+
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_found_multiple_times(
223+
tag,
224+
refl.getFriendlyFullName(),
225+
textPart,
226+
file,
227+
target,
228+
),
229+
);
230+
return "";
231+
}
232+
found = text.substring(
233+
text.indexOf(start[0]) + start[0].length,
234+
text.indexOf(end[0]),
246235
);
247-
return "";
248236
}
249-
found = text.substring(
250-
text.indexOf(start[0]) + start[0].length,
251-
text.indexOf(end[0]),
237+
}
238+
if (found === false) {
239+
this.logger.error(
240+
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_not_found(
241+
tag,
242+
refl.getFriendlyFullName(),
243+
textPart,
244+
file,
245+
target,
246+
),
252247
);
248+
return "";
253249
}
250+
if (found.trim() === "") {
251+
this.logger.warn(
252+
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_empty(
253+
tag,
254+
refl.getFriendlyFullName(),
255+
textPart,
256+
file,
257+
target,
258+
),
259+
);
260+
}
261+
262+
content += ignoreIndent ? dedent(found) + "\n" : found;
254263
}
255-
if (found === false) {
256-
this.logger.error(
257-
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_not_found(
258-
tag,
259-
refl.getFriendlyFullName(),
260-
textPart,
261-
file,
262-
target,
263-
),
264-
);
265-
return "";
266-
}
267-
if (found.trim() === "") {
268-
this.logger.warn(
269-
this.logger.i18n.include_0_tag_in_1_specified_2_file_3_region_4_region_empty(
270-
tag,
271-
refl.getFriendlyFullName(),
272-
textPart,
273-
file,
274-
target,
275-
),
276-
);
277-
}
278-
return found;
264+
265+
return content;
279266
}
280267

281268
getLines(
@@ -345,22 +332,24 @@ function makeCodeBlock(lang: string, code: string) {
345332
return "\n\n```" + lang + "\n" + escaped.trimEnd() + "\n```";
346333
}
347334

348-
function parseIncludeCodeTextPart(
349-
text: string,
350-
): [string, string | undefined, string | undefined] {
335+
function parseIncludeCodeTextPart(text: string): {
336+
filename: string;
337+
regionTarget: string | undefined;
338+
requestedLines: string | undefined;
339+
} {
351340
let filename = text.trim();
352-
let target;
353-
let requestedLines;
341+
let regionTarget: string | undefined;
342+
let requestedLines: string | undefined;
354343
if (filename.includes("#")) {
355344
const parsed = filename.split("#");
356345
filename = parsed[0];
357-
target = parsed[1];
346+
regionTarget = parsed[1];
358347
} else if (filename.includes(":")) {
359348
const parsed = filename.split(":");
360349
filename = parsed[0];
361350
requestedLines = parsed[1];
362351
}
363-
return [filename, target, requestedLines];
352+
return { filename, regionTarget, requestedLines };
364353
}
365354

366355
type RegionTagRETuple = [

src/lib/utils/general.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ export function editDistance(s: string, t: string): number {
9494
return v0[t.length];
9595
}
9696

97+
export function dedent(text: string) {
98+
const lines = text.split(/\r?\n/);
99+
while (lines.length && lines[0].search(/\S/) === -1) {
100+
lines.shift();
101+
}
102+
while (lines.length && lines[lines.length - 1].search(/\S/) === -1) {
103+
lines.pop();
104+
}
105+
106+
const minIndent = lines.reduce(
107+
(indent, line) =>
108+
line.length ? Math.min(indent, line.search(/\S/)) : indent,
109+
Infinity,
110+
);
111+
112+
return lines.map((line) => line.substring(minIndent)).join("\n");
113+
}
114+
97115
export function getSimilarValues(values: Iterable<string>, compareTo: string) {
98116
const results = new DefaultMap<number, string[]>(() => []);
99117
let lowest = Infinity;

0 commit comments

Comments
 (0)