Skip to content

Commit

Permalink
Add language-dependant regions and line number syntax + docs & logs
Browse files Browse the repository at this point in the history
  • Loading branch information
shawninder committed Jan 3, 2025
1 parent 5b7d3c8 commit 43fceb7
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 9 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ title: Changelog

## Unreleased

### Features

- `@includeCode` can now inject parts of files using region names or line
numbers, #2816.

## v0.27.6 (2024-12-26)

### Features
Expand Down
67 changes: 67 additions & 0 deletions example/src/documents/include-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: Include Code
category: Documents
---

# Including Code
It can be convenient to write long-form guides/tutorials outside of doc comments.
To support this, TypeDoc supports including documents (like this page!) which exist
as standalone `.md` files in your repository.
These files can then import code from other files using the `@includeCode` tag.

## The `@includeCode` Tag
The `@includeCode` tag can be placed in an md file to insert a code snippet at that location. As an example, this file is inserting the code block below using:

```md
{@includeCode ../reexports.ts}
```

**Result:**
{@includeCode ../reexports.ts}

### Include parts of files

#### Using regions
The `@includeCode` tag can also include only parts of a file using language-dependent region syntax as defined in the VS Code documentation for [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding), reproduced here for convenience:

Language | Start region | End region
---------|--------------|------------
Bat | `::#region regionName` or `REM #region regionName` | `::#endregion regionName` or `REM #endregion regionName`
C# | `#region regionName` | `#endregion regionName`
C/C++ | `#pragma region regionName` | `#pragma endregion regionName`
CSS/Less/SCSS | `/*#region regionName*/` | `/*#endregion regionName*/`
Coffeescript | `#region regionName` | `#endregion regionName`
F# | `//#region regionName` or `(#_region) regionName` | `//#endregion regionName` or `(#_endregion) regionName`
Java | `//#region regionName` or `//<editor-fold> regionName` | `//#endregion regionName` or `//</editor-fold> regionName`
Markdown | `<!-- #region regionName -->` | `<!-- #endregion regionName -->`
Perl5 | `#region regionName` or `=pod regionName` | `#endregion regionName` or `=cut regionName`
PHP | `#region regionName` | `#endregion regionName`
PowerShell | `#region regionName` | `#endregion regionName`
Python | `#region regionName` or `# region regionName` | `#endregion regionName` or `# endregion regionName`
TypeScript/JavaScript | `//#region regionName` | `//#endregion regionName`
Visual Basic | `#Region regionName` | `#End Region regionName`

For example:

```md
{@includeCode ../enums.ts#simpleEnum}
```

**Result:**

{@includeCode ../enums.ts#simpleEnum}

#### Using line numbers
For cases where you can't modify the source file or where comments are not allowed (in JSON files, for example), you can use line numbers to include a specific region of a file.

For example:

```md
{@includeCode ../../package.json:2,6-7}
```

**Result:**

{@includeCode ../../package.json:2,6-7}

> **Warning:** This makes it difficult to maintain the file, as you may need to update the line numbers if you change the code.
2 changes: 2 additions & 0 deletions example/src/enums.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/** Describes the status of a delivery order. */
// #region simpleEnum
export enum SimpleEnum {
/** This order has just been placed and is yet to be processed. */
Pending,
Expand All @@ -9,6 +10,7 @@ export enum SimpleEnum {
/** The order has been delivered. */
Complete = "COMPLETE",
}
// #endregion simpleEnum

/**
* [A crazy enum from the TypeScript
Expand Down
1 change: 1 addition & 0 deletions example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @document documents/external-markdown.md
* @document documents/markdown.md
* @document documents/syntax-highlighting.md
* @document documents/include-code.md
*/
export * from "./functions";
export * from "./variables";
Expand Down
40 changes: 40 additions & 0 deletions site/tags/include.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,46 @@ selecting the syntax highlighting language.
function doSomething() {}
```

### Include parts of files

#### Using regions
The `@include` and `@includeCode` tags can also include only parts of a file using language-dependent region syntax as defined in the VS Code documentation for [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding), reproduced here for convenience:

Language | Start region | End region
---------|--------------|------------
Bat | `::#region regionName` or `REM #region regionName` | `::#endregion regionName` or `REM #endregion regionName`
C# | `#region regionName` | `#endregion regionName`
C/C++ | `#pragma region regionName` | `#pragma endregion regionName`
CSS/Less/SCSS | `/*#region regionName*/` | `/*#endregion regionName*/`
Coffeescript | `#region regionName` | `#endregion regionName`
F# | `//#region regionName` or `(#_region) regionName` | `//#endregion regionName` or `(#_endregion) regionName`
Java | `//#region regionName` or `//<editor-fold> regionName` | `//#endregion regionName` or `//</editor-fold> regionName`
Markdown | `<!-- #region regionName -->` | `<!-- #endregion regionName -->`
Perl5 | `#region regionName` or `=pod regionName` | `#endregion regionName` or `=cut regionName`
PHP | `#region regionName` | `#endregion regionName`
PowerShell | `#region regionName` | `#endregion regionName`
Python | `#region regionName` or `# region regionName` | `#endregion regionName` or `# endregion regionName`
TypeScript/JavaScript | `//#region regionName` | `//#endregion regionName`
Visual Basic | `#Region regionName` | `#End Region regionName`

##### Example

```md
Here is a simple enum:
{@includeCode ../enums.js#simpleEnum}
```

#### Using line numbers
For cases where you can't modify the source file or where comments are not allowed (in JSON files, for example), you can use line numbers to include a specific region of a file.

##### Example

```md
In package.json, notice the following information:
{@includeCode ../../package.json:2,6-7}
```
> **Warning:** This makes it difficult to maintain the file, as you may need to update the line numbers if you change the code.
## See Also

- The [jsdocCompatibility](../options/comments.md#jsdoccompatibility) option.
199 changes: 190 additions & 9 deletions src/lib/converter/plugins/IncludePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { CommentDisplayPart, Reflection } from "../../models/index.js";
import { MinimalSourceFile } from "../../utils/minimalSourceFile.js";
import type { Converter } from "../converter.js";
import { isFile } from "../../utils/fs.js";
import regionTagREsByExt from "../utils/regionTagREsByExt.js";

/**
* Handles `@include` and `@includeCode` within comments/documents.
Expand Down Expand Up @@ -62,7 +63,10 @@ export class IncludePlugin extends ConverterComponent {
continue;
}

const [filename, target] = part.text.trim().split("#");
const [filename, target, requestedLines] = parseIncludeCodeTextPart(
part.text,
);

const file = path.resolve(relative, filename);
if (included.includes(file) && part.tag === "@include") {
this.logger.error(
Expand All @@ -89,19 +93,29 @@ export class IncludePlugin extends ConverterComponent {
);
parts.splice(i, 1, ...content);
} else {
const regionStart = `// #region ${target}`;
const regionEnd = `// #endregion ${target}`;
const ext = path.extname(file).substring(1);
parts[i] = {
kind: "code",
text: makeCodeBlock(
path.extname(file).substring(1),
ext,
target
? text.substring(
text.indexOf(regionStart) +
regionStart.length,
text.indexOf(regionEnd),
? this.getRegion(
refl,
file,
ext,
part.text,
text,
target,
)
: text,
: requestedLines
? this.getLines(
refl,
file,
part.text,
text,
requestedLines,
)
: text,
),
};
}
Expand All @@ -117,8 +131,175 @@ export class IncludePlugin extends ConverterComponent {
}
}
}

getRegion(
refl: Reflection,
file: string,
ext: string,
textPart: string,
text: string,
target: string,
) {
const regionTagsList = regionTagREsByExt[ext];
let found: string | false = false;
for (const [startTag, endTag] of regionTagsList) {
const start = text.match(startTag(target));
const end = text.match(endTag(target));

const foundStart = start && start.length > 0;
const foundEnd = end && end.length > 0;
if (foundStart && !foundEnd) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_region_3_region_close_not_found(
refl.getFriendlyFullName(),
textPart,
file,
target,
),
);
return "";
}
if (!foundStart && foundEnd) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_region_3_region_open_not_found(
refl.getFriendlyFullName(),
textPart,
file,
target,
),
);
return "";
}
if (foundStart && foundEnd) {
if (start.length > 1) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_region_3_region_open_found_multiple_times(
refl.getFriendlyFullName(),
textPart,
file,
target,
),
);
return "";
}
if (end.length > 1) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_region_3_region_close_found_multiple_times(
refl.getFriendlyFullName(),
textPart,
file,
target,
),
);
return "";
}
if (found) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_region_3_region_found_multiple_times(
refl.getFriendlyFullName(),
textPart,
file,
target,
),
);
return "";
}
found = text.substring(
text.indexOf(start[0]) + start[0].length,
text.indexOf(end[0]),
);
}
}
if (!found) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_region_3_region_not_found(
refl.getFriendlyFullName(),
textPart,
file,
target,
),
);
return "";
}
return found;
}

getLines(
refl: Reflection,
file: string,
textPart: string,
text: string,
requestedLines: string,
) {
let output = "";
const lines = text.split(/\r\n|\r|\n/);
requestedLines.split(",").forEach((requestedLineString) => {
if (requestedLineString.includes("-")) {
const [start, end] = requestedLineString.split("-").map(Number);
if (start > end) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_lines_3_invalid_range(
refl.getFriendlyFullName(),
textPart,
file,
requestedLines,
),
);
return "";
}
if (start > lines.length || end > lines.length) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_lines_3_but_only_4_lines(
refl.getFriendlyFullName(),
textPart,
file,
requestedLines,
lines.length.toString(),
),
);
return "";
}
output += lines.slice(start - 1, end).join("\n") + "\n";
} else {
const requestedLine = Number(requestedLineString);
if (requestedLine > lines.length) {
this.logger.error(
this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_lines_3_but_only_4_lines(
refl.getFriendlyFullName(),
textPart,
file,
requestedLines,
lines.length.toString(),
),
);
return "";
}
output += lines[requestedLine - 1] + "\n";
}
});

return output;
}
}
function makeCodeBlock(lang: string, code: string) {
const escaped = code.replace(/`(?=`)/g, "`\u200B");
return "\n\n```" + lang + "\n" + escaped.trimEnd() + "\n```";
}

function parseIncludeCodeTextPart(
text: string,
): [string, string | undefined, string | undefined] {
let filename = text.trim();
let target;
let requestedLines;
if (filename.includes("#")) {
const parsed = filename.split("#");
filename = parsed[0];
target = parsed[1];
} else if (filename.includes(":")) {
const parsed = filename.split(":");
filename = parsed[0];
requestedLines = parsed[1];
}
return [filename, target, requestedLines];
}
Loading

0 comments on commit 43fceb7

Please sign in to comment.