From 5b7d3c8c7766e25c4ef0d35820b929964b9847e9 Mon Sep 17 00:00:00 2001 From: Shawn Inder Date: Sun, 22 Dec 2024 12:51:41 -0500 Subject: [PATCH 1/6] POC: support regions in @includeCode --- src/lib/converter/plugins/IncludePlugin.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib/converter/plugins/IncludePlugin.ts b/src/lib/converter/plugins/IncludePlugin.ts index 8e07c6aa3..1dd4d13fd 100644 --- a/src/lib/converter/plugins/IncludePlugin.ts +++ b/src/lib/converter/plugins/IncludePlugin.ts @@ -62,7 +62,8 @@ export class IncludePlugin extends ConverterComponent { continue; } - const file = path.resolve(relative, part.text.trim()); + const [filename, target] = part.text.trim().split("#"); + const file = path.resolve(relative, filename); if (included.includes(file) && part.tag === "@include") { this.logger.error( this.logger.i18n.include_0_in_1_specified_2_circular_include_3( @@ -88,11 +89,19 @@ export class IncludePlugin extends ConverterComponent { ); parts.splice(i, 1, ...content); } else { + const regionStart = `// #region ${target}`; + const regionEnd = `// #endregion ${target}`; parts[i] = { kind: "code", text: makeCodeBlock( path.extname(file).substring(1), - text, + target + ? text.substring( + text.indexOf(regionStart) + + regionStart.length, + text.indexOf(regionEnd), + ) + : text, ), }; } From 43fceb778e530043bb581546a0efe242e6c1b710 Mon Sep 17 00:00:00 2001 From: Shawn Inder Date: Thu, 2 Jan 2025 18:44:27 -0500 Subject: [PATCH 2/6] Add language-dependant regions and line number syntax + docs & logs --- CHANGELOG.md | 5 + example/src/documents/include-code.md | 67 +++++++ example/src/enums.ts | 2 + example/src/index.ts | 1 + site/tags/include.md | 40 ++++ src/lib/converter/plugins/IncludePlugin.ts | 199 ++++++++++++++++++- src/lib/converter/utils/regionTagREsByExt.ts | 75 +++++++ src/lib/internationalization/locales/en.cts | 8 + 8 files changed, 388 insertions(+), 9 deletions(-) create mode 100644 example/src/documents/include-code.md create mode 100644 src/lib/converter/utils/regionTagREsByExt.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 300e04535..3b1d38fc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/src/documents/include-code.md b/example/src/documents/include-code.md new file mode 100644 index 000000000..03c0c9e66 --- /dev/null +++ b/example/src/documents/include-code.md @@ -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 `// regionName` | `//#endregion regionName` or `// regionName` +Markdown | `` | `` +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. diff --git a/example/src/enums.ts b/example/src/enums.ts index 65451c0c1..c616dd1bf 100644 --- a/example/src/enums.ts +++ b/example/src/enums.ts @@ -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, @@ -9,6 +10,7 @@ export enum SimpleEnum { /** The order has been delivered. */ Complete = "COMPLETE", } +// #endregion simpleEnum /** * [A crazy enum from the TypeScript diff --git a/example/src/index.ts b/example/src/index.ts index 1347b8fff..01d46336f 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -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"; diff --git a/site/tags/include.md b/site/tags/include.md index 26571c85a..c9ba9904b 100644 --- a/site/tags/include.md +++ b/site/tags/include.md @@ -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 `// regionName` | `//#endregion regionName` or `// regionName` +Markdown | `` | `` +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. diff --git a/src/lib/converter/plugins/IncludePlugin.ts b/src/lib/converter/plugins/IncludePlugin.ts index 1dd4d13fd..7497a7fe7 100644 --- a/src/lib/converter/plugins/IncludePlugin.ts +++ b/src/lib/converter/plugins/IncludePlugin.ts @@ -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. @@ -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( @@ -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, ), }; } @@ -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]; +} diff --git a/src/lib/converter/utils/regionTagREsByExt.ts b/src/lib/converter/utils/regionTagREsByExt.ts new file mode 100644 index 000000000..dbfb27a03 --- /dev/null +++ b/src/lib/converter/utils/regionTagREsByExt.ts @@ -0,0 +1,75 @@ +type RegionTagRETuple = [ + (regionName: string) => RegExp, + (regionName: string) => RegExp, +]; +const regionTagREsByExt: Record = { + bat: [ + [ + (regionName) => new RegExp(`:: *#region *${regionName}`, "g"), + (regionName) => new RegExp(`:: *#endregion *${regionName}`, "g"), + ], + [ + (regionName) => new RegExp(`REM *#region *${regionName}`, "g"), + (regionName) => new RegExp(`REM *#endregion *${regionName}`, "g"), + ], + ], + cs: [ + [ + (regionName) => new RegExp(`#region *${regionName}`, "g"), + (regionName) => new RegExp(`#endregion *${regionName}`, "g"), + ], + ], + c: [ + [ + (regionName) => new RegExp(`#pragma *region *${regionName}`, "g"), + (regionName) => + new RegExp(`#pragma *endregion *${regionName}`, "g"), + ], + ], + css: [ + [ + (regionName) => + new RegExp(`/\\* *#region *\\*/ *${regionName}`, "g"), + (regionName) => + new RegExp(`/\\* *#endregion *\\*/ *${regionName}`, "g"), + ], + ], + md: [ + [ + (regionName) => + new RegExp(` *${regionName}`, "g"), + (regionName) => + new RegExp(` *${regionName}`, "g"), + ], + ], + ts: [ + [ + (regionName) => new RegExp(`// *#region *${regionName}`, "g"), + (regionName) => new RegExp(`// *#endregion *${regionName}`, "g"), + ], + ], + vb: [ + [ + (regionName) => new RegExp(`#Region *${regionName}`, "g"), + (regionName) => new RegExp(`#End Region *${regionName}`, "g"), + ], + ], +}; +regionTagREsByExt["fs"] = regionTagREsByExt["ts"].concat([ + (regionName) => new RegExp(`(#_region) *${regionName}`, "g"), + (regionName) => new RegExp(`(#_endregion) *${regionName}`, "g"), +]); +regionTagREsByExt["java"] = regionTagREsByExt["ts"].concat([ + (regionName) => new RegExp(`// * *${regionName}`, "g"), + (regionName) => new RegExp(`// * *${regionName}`, "g"), +]); +regionTagREsByExt["cpp"] = regionTagREsByExt["c"]; +regionTagREsByExt["less"] = regionTagREsByExt["css"]; +regionTagREsByExt["scss"] = regionTagREsByExt["css"]; +regionTagREsByExt["coffee"] = regionTagREsByExt["cs"]; +regionTagREsByExt["php"] = regionTagREsByExt["cs"]; +regionTagREsByExt["ps1"] = regionTagREsByExt["cs"]; +regionTagREsByExt["py"] = regionTagREsByExt["cs"]; +regionTagREsByExt["js"] = regionTagREsByExt["ts"]; + +export default regionTagREsByExt; diff --git a/src/lib/internationalization/locales/en.cts b/src/lib/internationalization/locales/en.cts index 48c98b3d9..60c9ea7e2 100644 --- a/src/lib/internationalization/locales/en.cts +++ b/src/lib/internationalization/locales/en.cts @@ -119,6 +119,14 @@ export = { include_0_in_1_specified_2_resolved_to_3_does_not_exist: `{0} tag in comment for {1} specified "{2}" to include, which was resolved to "{3}" and does not exist or is not a file.`, include_0_in_1_specified_2_circular_include_3: `{0} tag in comment for {1} specified "{2}" to include, which resulted in a circular include:\n\t{3}`, + includeCode_tag_in_0_specified_1_file_2_region_3_region_not_found: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled "{3}", but the region was not found in the file.`, + includeCode_tag_in_0_specified_1_file_2_region_3_region_close_not_found: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled "{3}", but the region closing comment was not found in the file.`, + includeCode_tag_in_0_specified_1_file_2_region_3_region_open_not_found: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled "{3}", but the region opening comment was not found in the file.`, + includeCode_tag_in_0_specified_1_file_2_region_3_region_close_found_multiple_times: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled {3}, but the region closing comment was found multiple times in the file.`, + includeCode_tag_in_0_specified_1_file_2_region_3_region_open_found_multiple_times: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled {3}, but the region opening comment was found multiple times in the file.`, + includeCode_tag_in_0_specified_1_file_2_region_3_region_found_multiple_times: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled {3}, but the region was found multiple times in the file.`, + includeCode_tag_in_0_specified_1_file_2_lines_3_invalid_range: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the lines {3}, but an invalid range was specified.`, + includeCode_tag_in_0_specified_1_file_2_lines_3_but_only_4_lines: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the lines {3}, but the file only has {4} lines.`, // output plugins custom_css_file_0_does_not_exist: `Custom CSS file at {0} does not exist`, From 08dea69b3074e6a7fc79a1a4ada76b4ac08691a1 Mon Sep 17 00:00:00 2001 From: Shawn Inder Date: Thu, 2 Jan 2025 23:18:58 -0500 Subject: [PATCH 3/6] lint --- example/src/documents/include-code.md | 36 +++++++++++++++------------ site/tags/include.md | 35 ++++++++++++++------------ 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/example/src/documents/include-code.md b/example/src/documents/include-code.md index 03c0c9e66..3f80f3300 100644 --- a/example/src/documents/include-code.md +++ b/example/src/documents/include-code.md @@ -4,12 +4,14 @@ 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 @@ -22,24 +24,25 @@ The `@includeCode` tag can be placed in an md file to insert a code snippet at t ### 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 `// regionName` | `//#endregion regionName` or `// regionName` -Markdown | `` | `` -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` +| 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 `// regionName` | `//#endregion regionName` or `// regionName` | +| Markdown | `` | `` | +| 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: @@ -52,6 +55,7 @@ For example: {@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: diff --git a/site/tags/include.md b/site/tags/include.md index c9ba9904b..737e2f9b4 100644 --- a/site/tags/include.md +++ b/site/tags/include.md @@ -33,24 +33,25 @@ 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 `// regionName` | `//#endregion regionName` or `// regionName` -Markdown | `` | `` -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` +| 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 `// regionName` | `//#endregion regionName` or `// regionName` | +| Markdown | `` | `` | +| 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 @@ -60,6 +61,7 @@ Here is a simple enum: ``` #### 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 @@ -68,6 +70,7 @@ For cases where you can't modify the source file or where comments are not allow 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 From f3ddf957eb1209b850c0e7e157f892af27f9b2b1 Mon Sep 17 00:00:00 2001 From: Shawn Inder Date: Sun, 5 Jan 2025 12:21:38 -0500 Subject: [PATCH 4/6] Support regions for `@include` + warn on empty regions + cleanup --- src/lib/converter/plugins/IncludePlugin.ts | 114 ++++++++++++++++++- src/lib/converter/utils/regionTagREsByExt.ts | 75 ------------ src/lib/internationalization/locales/en.cts | 1 + 3 files changed, 112 insertions(+), 78 deletions(-) delete mode 100644 src/lib/converter/utils/regionTagREsByExt.ts diff --git a/src/lib/converter/plugins/IncludePlugin.ts b/src/lib/converter/plugins/IncludePlugin.ts index 7497a7fe7..aba8e7fee 100644 --- a/src/lib/converter/plugins/IncludePlugin.ts +++ b/src/lib/converter/plugins/IncludePlugin.ts @@ -7,7 +7,6 @@ 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. @@ -79,8 +78,29 @@ export class IncludePlugin extends ConverterComponent { ); } else if (isFile(file)) { const text = fs.readFileSync(file, "utf-8"); + const ext = path.extname(file).substring(1); if (part.tag === "@include") { - const sf = new MinimalSourceFile(text, file); + const sf = new MinimalSourceFile( + target + ? this.getRegion( + refl, + file, + ext, + part.text, + text, + target, + ) + : requestedLines + ? this.getLines( + refl, + file, + part.text, + text, + requestedLines, + ) + : text, + file, + ); const { content } = this.owner.parseRawComment( sf, refl.project.files, @@ -93,7 +113,6 @@ export class IncludePlugin extends ConverterComponent { ); parts.splice(i, 1, ...content); } else { - const ext = path.extname(file).substring(1); parts[i] = { kind: "code", text: makeCodeBlock( @@ -221,6 +240,16 @@ export class IncludePlugin extends ConverterComponent { ); return ""; } + if (found.trim() === "") { + this.logger.warn( + this.logger.i18n.includeCode_tag_in_0_specified_1_file_2_region_3_region_empty( + refl.getFriendlyFullName(), + textPart, + file, + target, + ), + ); + } return found; } @@ -303,3 +332,82 @@ function parseIncludeCodeTextPart( } return [filename, target, requestedLines]; } + +type RegionTagRETuple = [ + (regionName: string) => RegExp, + (regionName: string) => RegExp, +]; +const regionTagREsByExt: Record = { + bat: [ + [ + (regionName) => new RegExp(`:: *#region *${regionName} *\n`, "g"), + (regionName) => + new RegExp(`:: *#endregion *${regionName} *\n`, "g"), + ], + [ + (regionName) => + new RegExp(`REM *#region *${regionName} *\n`, "g"), + (regionName) => + new RegExp(`REM *#endregion *${regionName} *\n`, "g"), + ], + ], + cs: [ + [ + (regionName) => new RegExp(`#region *${regionName} *\n`, "g"), + (regionName) => new RegExp(`#endregion *${regionName} *\n`, "g"), + ], + ], + c: [ + [ + (regionName) => + new RegExp(`#pragma *region *${regionName} *\n`, "g"), + (regionName) => + new RegExp(`#pragma *endregion *${regionName} *\n`, "g"), + ], + ], + css: [ + [ + (regionName) => + new RegExp(`/\\* *#region *\\*/ *${regionName} *\n`, "g"), + (regionName) => + new RegExp(`/\\* *#endregion *\\*/ *${regionName} *\n`, "g"), + ], + ], + md: [ + [ + (regionName) => + new RegExp(` *\n`, "g"), + (regionName) => + new RegExp(` *\n`, "g"), + ], + ], + ts: [ + [ + (regionName) => new RegExp(`// *#region *${regionName} *\n`, "g"), + (regionName) => + new RegExp(`// *#endregion *${regionName} *\n`, "g"), + ], + ], + vb: [ + [ + (regionName) => new RegExp(`#Region *${regionName} *\n`, "g"), + (regionName) => new RegExp(`#End Region *${regionName} *\n`, "g"), + ], + ], +}; +regionTagREsByExt["fs"] = regionTagREsByExt["ts"].concat([ + (regionName) => new RegExp(`(#_region) *${regionName} *\n`, "g"), + (regionName) => new RegExp(`(#_endregion) *${regionName} *\n`, "g"), +]); +regionTagREsByExt["java"] = regionTagREsByExt["ts"].concat([ + (regionName) => new RegExp(`// * *${regionName} *\n`, "g"), + (regionName) => new RegExp(`// * *${regionName} *\n`, "g"), +]); +regionTagREsByExt["cpp"] = regionTagREsByExt["c"]; +regionTagREsByExt["less"] = regionTagREsByExt["css"]; +regionTagREsByExt["scss"] = regionTagREsByExt["css"]; +regionTagREsByExt["coffee"] = regionTagREsByExt["cs"]; +regionTagREsByExt["php"] = regionTagREsByExt["cs"]; +regionTagREsByExt["ps1"] = regionTagREsByExt["cs"]; +regionTagREsByExt["py"] = regionTagREsByExt["cs"]; +regionTagREsByExt["js"] = regionTagREsByExt["ts"]; diff --git a/src/lib/converter/utils/regionTagREsByExt.ts b/src/lib/converter/utils/regionTagREsByExt.ts deleted file mode 100644 index dbfb27a03..000000000 --- a/src/lib/converter/utils/regionTagREsByExt.ts +++ /dev/null @@ -1,75 +0,0 @@ -type RegionTagRETuple = [ - (regionName: string) => RegExp, - (regionName: string) => RegExp, -]; -const regionTagREsByExt: Record = { - bat: [ - [ - (regionName) => new RegExp(`:: *#region *${regionName}`, "g"), - (regionName) => new RegExp(`:: *#endregion *${regionName}`, "g"), - ], - [ - (regionName) => new RegExp(`REM *#region *${regionName}`, "g"), - (regionName) => new RegExp(`REM *#endregion *${regionName}`, "g"), - ], - ], - cs: [ - [ - (regionName) => new RegExp(`#region *${regionName}`, "g"), - (regionName) => new RegExp(`#endregion *${regionName}`, "g"), - ], - ], - c: [ - [ - (regionName) => new RegExp(`#pragma *region *${regionName}`, "g"), - (regionName) => - new RegExp(`#pragma *endregion *${regionName}`, "g"), - ], - ], - css: [ - [ - (regionName) => - new RegExp(`/\\* *#region *\\*/ *${regionName}`, "g"), - (regionName) => - new RegExp(`/\\* *#endregion *\\*/ *${regionName}`, "g"), - ], - ], - md: [ - [ - (regionName) => - new RegExp(` *${regionName}`, "g"), - (regionName) => - new RegExp(` *${regionName}`, "g"), - ], - ], - ts: [ - [ - (regionName) => new RegExp(`// *#region *${regionName}`, "g"), - (regionName) => new RegExp(`// *#endregion *${regionName}`, "g"), - ], - ], - vb: [ - [ - (regionName) => new RegExp(`#Region *${regionName}`, "g"), - (regionName) => new RegExp(`#End Region *${regionName}`, "g"), - ], - ], -}; -regionTagREsByExt["fs"] = regionTagREsByExt["ts"].concat([ - (regionName) => new RegExp(`(#_region) *${regionName}`, "g"), - (regionName) => new RegExp(`(#_endregion) *${regionName}`, "g"), -]); -regionTagREsByExt["java"] = regionTagREsByExt["ts"].concat([ - (regionName) => new RegExp(`// * *${regionName}`, "g"), - (regionName) => new RegExp(`// * *${regionName}`, "g"), -]); -regionTagREsByExt["cpp"] = regionTagREsByExt["c"]; -regionTagREsByExt["less"] = regionTagREsByExt["css"]; -regionTagREsByExt["scss"] = regionTagREsByExt["css"]; -regionTagREsByExt["coffee"] = regionTagREsByExt["cs"]; -regionTagREsByExt["php"] = regionTagREsByExt["cs"]; -regionTagREsByExt["ps1"] = regionTagREsByExt["cs"]; -regionTagREsByExt["py"] = regionTagREsByExt["cs"]; -regionTagREsByExt["js"] = regionTagREsByExt["ts"]; - -export default regionTagREsByExt; diff --git a/src/lib/internationalization/locales/en.cts b/src/lib/internationalization/locales/en.cts index 60c9ea7e2..4cd19b349 100644 --- a/src/lib/internationalization/locales/en.cts +++ b/src/lib/internationalization/locales/en.cts @@ -125,6 +125,7 @@ export = { includeCode_tag_in_0_specified_1_file_2_region_3_region_close_found_multiple_times: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled {3}, but the region closing comment was found multiple times in the file.`, includeCode_tag_in_0_specified_1_file_2_region_3_region_open_found_multiple_times: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled {3}, but the region opening comment was found multiple times in the file.`, includeCode_tag_in_0_specified_1_file_2_region_3_region_found_multiple_times: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled {3}, but the region was found multiple times in the file.`, + includeCode_tag_in_0_specified_1_file_2_region_3_region_empty: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the region labeled {3}. The region was found but it is empty or contains only whitespace.`, includeCode_tag_in_0_specified_1_file_2_lines_3_invalid_range: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the lines {3}, but an invalid range was specified.`, includeCode_tag_in_0_specified_1_file_2_lines_3_but_only_4_lines: `@includeCode tag in {0} specified "{1}" to include from file "{2}" the lines {3}, but the file only has {4} lines.`, From 8b6b7ebacbf8d40f45863ede134fd46ad5b3662e Mon Sep 17 00:00:00 2001 From: Shawn Inder Date: Sun, 5 Jan 2025 12:28:38 -0500 Subject: [PATCH 5/6] Improve docs --- example/src/documents/include-code.md | 64 ++------------------------- example/src/documents/include.md | 19 ++++++++ example/src/enums.ts | 3 ++ example/src/index.ts | 2 +- site/tags/include.md | 48 ++++++++++++++------ 5 files changed, 60 insertions(+), 76 deletions(-) create mode 100644 example/src/documents/include.md diff --git a/example/src/documents/include-code.md b/example/src/documents/include-code.md index 3f80f3300..3a35e4118 100644 --- a/example/src/documents/include-code.md +++ b/example/src/documents/include-code.md @@ -1,18 +1,7 @@ ---- -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: +For convenience, an `@includeCode` inline tag is also recognized, which will include the referenced file within a code block, using the file extension for selecting the syntax highlighting language. +As an example, this file is inserting the code block below using: ```md {@includeCode ../reexports.ts} @@ -21,51 +10,4 @@ The `@includeCode` tag can be placed in an md file to insert a code snippet at t **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 `// regionName` | `//#endregion regionName` or `// regionName` | -| Markdown | `` | `` | -| 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. +{@include ../../../site/tags/include.md#includePartsOfFiles} diff --git a/example/src/documents/include.md b/example/src/documents/include.md new file mode 100644 index 000000000..53b93e5de --- /dev/null +++ b/example/src/documents/include.md @@ -0,0 +1,19 @@ +--- +title: Include and Include Code +category: Documents +--- + +# Including other files + +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 other files using the `@include` tag. + +For example, the rest of this page is imported from `include-code.md` using: + +```md +{@include ./include-code.md} +``` + +{@include ./include-code.md} diff --git a/example/src/enums.ts b/example/src/enums.ts index c616dd1bf..d1c6885d3 100644 --- a/example/src/enums.ts +++ b/example/src/enums.ts @@ -1,4 +1,6 @@ /** Describes the status of a delivery order. */ + +// #region simpleEnumRegion // #region simpleEnum export enum SimpleEnum { /** This order has just been placed and is yet to be processed. */ @@ -11,6 +13,7 @@ export enum SimpleEnum { Complete = "COMPLETE", } // #endregion simpleEnum +// #endregion simpleEnumRegion /** * [A crazy enum from the TypeScript diff --git a/example/src/index.ts b/example/src/index.ts index 01d46336f..2f3d46b4d 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -7,7 +7,7 @@ * @document documents/external-markdown.md * @document documents/markdown.md * @document documents/syntax-highlighting.md - * @document documents/include-code.md + * @document documents/include.md */ export * from "./functions"; export * from "./variables"; diff --git a/site/tags/include.md b/site/tags/include.md index 737e2f9b4..4762b5675 100644 --- a/site/tags/include.md +++ b/site/tags/include.md @@ -16,7 +16,7 @@ selecting the syntax highlighting language. ## Example -```js +```ts /** * {@include ./doSomething_docs.md} * @@ -30,11 +30,31 @@ selecting the syntax highlighting language. function doSomething() {} ``` + + ### Include parts of files -#### Using regions +#### Using regions (preferred) + +The `@include` and `@includeCode` tags can also include only parts of a file by referring to a specific named region. + +For example: + +```md +{@includeCode ../../example/src/enums.ts#simpleEnum} +``` + +Regions are specified in the files themselves via comments. + +In Typescript for example, the following would be a valid region: + +{@includeCode ../../example/src/enums.ts#simpleEnumRegion} -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: +**Result:** + +{@includeCode ../../example/src/enums.ts#simpleEnum} + +Language-dependent region syntax is meant to be compatible with VS Code [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding). Here is a reproduction of their table, but with `regionName` everywhere because we want to insist on named regions. | Language | Start region | End region | | --------------------- | ------------------------------------------------------ | ---------------------------------------------------------- | @@ -53,25 +73,25 @@ The `@include` and `@includeCode` tags can also include only parts of a file usi | TypeScript/JavaScript | `//#region regionName` | `//#endregion regionName` | | Visual Basic | `#Region regionName` | `#End Region regionName` | -##### Example +#### Using line numbers (risky) + +When you can't add comments to define regions (in JSON files, for example) you can use line numbers instead to include a specific region of a file. + +> **Warning:** Referencing line numbers should be avoided since the reference will likely break when every time the file changes. ```md -Here is a simple enum: -{@includeCode ../enums.js#simpleEnum} +{@includeCode ../../package.json:2,6-7} ``` -#### Using line numbers +**Result:** -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. +{@includeCode ../../package.json:2,6-7} -##### Example +A colon (`:`) separates the file path from the line numbers: a comma-separated list of numbers or ranges of the form `-` (`6-7` in the example above). -```md -In package.json, notice the following information: -{@includeCode ../../package.json:2,6-7} -``` +> **Note:** The first line in the file Line 1, not Line 0, just like you would see in most code editors. -> **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 From 2e5c830a4c4aa812fb84ea0967e11962268bbb0c Mon Sep 17 00:00:00 2001 From: Shawn Inder Date: Mon, 13 Jan 2025 17:51:33 -0500 Subject: [PATCH 6/6] regexp-escape the `target` before using it in regular expressions --- src/lib/converter/plugins/IncludePlugin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/converter/plugins/IncludePlugin.ts b/src/lib/converter/plugins/IncludePlugin.ts index aba8e7fee..7d03e96e2 100644 --- a/src/lib/converter/plugins/IncludePlugin.ts +++ b/src/lib/converter/plugins/IncludePlugin.ts @@ -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 { escapeRegExp } from "../../utils/general.js"; /** * Handles `@include` and `@includeCode` within comments/documents. @@ -162,8 +163,9 @@ export class IncludePlugin extends ConverterComponent { 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 safeTarget = escapeRegExp(target); + const start = text.match(startTag(safeTarget)); + const end = text.match(endTag(safeTarget)); const foundStart = start && start.length > 0; const foundEnd = end && end.length > 0;