Skip to content

Commit 741913c

Browse files
Add CSV include directive (#1742)
* Add CSV file directive * Change to Sep and defer rendering * Use only Sep * Add streaming and max size * Update docs/syntax/csv-file.md Co-authored-by: shainaraskas <[email protected]> * Refactor and other stuff * Remove preview mode * Increase rows limit * Formatting * Reinstate old unsupported statement * Add also to dict * Change columns default --------- Co-authored-by: shainaraskas <[email protected]>
1 parent 4e9488d commit 741913c

File tree

13 files changed

+510
-2
lines changed

13 files changed

+510
-2
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<PackageVersion Include="Proc" Version="0.9.1" />
5757
<PackageVersion Include="RazorSlices" Version="0.9.4" />
5858
<PackageVersion Include="Samboy063.Tomlet" Version="6.0.0" />
59+
<PackageVersion Include="Sep" Version="0.11.0" />
5960
<PackageVersion Include="Slugify.Core" Version="4.0.1" />
6061
<PackageVersion Include="SoftCircuits.IniFileParser" Version="2.7.0" />
6162
<PackageVersion Include="System.IO.Abstractions" Version="22.0.15" />

docs/_docset.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ toc:
9696
- file: code.md
9797
- file: comments.md
9898
- file: conditionals.md
99+
- file: csv-include.md
99100
- hidden: diagrams.md
100101
- file: dropdowns.md
101102
- file: definition-lists.md

docs/_snippets/sample-data.csv

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Name,Age,City,Occupation
2+
John Doe,30,New York,Software Engineer
3+
Jane Smith,25,Los Angeles,Product Manager
4+
Bob Johnson,35,Chicago,Data Scientist
5+
Alice Brown,28,San Francisco,UX Designer
6+
Charlie Wilson,32,Boston,DevOps Engineer

docs/syntax/csv-include.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# CSV files
2+
3+
The `{csv-include}` directive allows you to include and render CSV files as formatted tables in your documentation. The directive automatically parses CSV content and renders it using the standard table styles defined in `table.css`.
4+
5+
## Usage
6+
7+
:::::{tab-set}
8+
9+
::::{tab-item} Output
10+
11+
:::{csv-include} ../_snippets/sample-data.csv
12+
:caption: Sample user data from the database
13+
:::
14+
15+
::::
16+
17+
::::{tab-item} Markdown
18+
19+
```markdown
20+
:::{csv-include} _snippets/sample-data.csv
21+
:::
22+
```
23+
24+
::::
25+
26+
:::::
27+
28+
## Options
29+
30+
The CSV file directive supports several options to customize the table rendering:
31+
32+
### Caption
33+
34+
Add a descriptive caption above the table:
35+
36+
```markdown
37+
:::{csv-include} _snippets/sample-data.csv
38+
:caption: Sample user data from the database
39+
:::
40+
```
41+
42+
### Custom separator
43+
44+
Specify a custom field separator (default is comma):
45+
46+
```markdown
47+
:::{csv-include} _snippets/sample-data.csv
48+
:separator: ;
49+
:::
50+
```
51+
52+
### Performance limits
53+
54+
The directive includes built-in performance limits to handle large files efficiently:
55+
56+
- **Row limit**: Maximum of 25,000 rows will be displayed
57+
- **Column limit**: Maximum of 10 columns will be displayed
58+
- **File size limit**: Maximum file size of 10MB
59+
60+
## Performance considerations
61+
62+
The CSV directive is optimized for large files:
63+
64+
- Files are processed using streaming to avoid loading everything into memory
65+
- Built-in size validation prevents processing of files that exceed 10MB
66+
- Row and column limits protect against accidentally rendering massive tables
67+
- Warning messages are displayed when limits are exceeded
68+
69+
For optimal performance with large CSV files, consider:
70+
- Breaking very large files into smaller, more manageable chunks

docs/syntax/index.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ V3 fully supports [CommonMark](https://commonmark.org/), a strongly defined, sta
1212
* [Directives](#directives)
1313
* [GitHub-flavored markdown](#github-flavored-markdown)
1414

15+
## Available directives
16+
17+
The following directives are available in Elastic Docs V3:
18+
19+
* [Admonitions](admonitions.md) - Callouts and warnings
20+
* [Code blocks](code.md) - Syntax-highlighted code
21+
* [CSV include](csv-include.md) - Render CSV files as tables
22+
* [Diagrams](diagrams.md) - Visual diagrams and charts
23+
* [Dropdowns](dropdowns.md) - Collapsible content
24+
* [Images](images.md) - Enhanced image handling
25+
* [Include](file_inclusion.md) - Include content from other files
26+
* [Settings](automated_settings.md) - Configuration blocks
27+
* [Stepper](stepper.md) - Step-by-step content
28+
* [Tabs](tabs.md) - Tabbed content organization
29+
* [Tables](tables.md) - Data tables
30+
* [Version blocks](version-variables.md) - API version information
31+
1532
## Directives
1633

1734
Directives extend CommonMark functionality. Directives have the following syntax:

src/Elastic.Markdown/Elastic.Markdown.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PackageReference Include="Markdig" />
2121
<PackageReference Include="Microsoft.Extensions.Logging" />
2222
<PackageReference Include="RazorSlices" />
23+
<PackageReference Include="Sep" />
2324
<PackageReference Include="Slugify.Core" />
2425
<PackageReference Include="Utf8StreamReader" />
2526
<PackageReference Include="Vecc.YamlDotNet.Analyzers.StaticGenerator" />
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Globalization;
6+
using System.IO.Abstractions;
7+
using Elastic.Markdown.Diagnostics;
8+
9+
namespace Elastic.Markdown.Myst.Directives.CsvInclude;
10+
11+
public class CsvIncludeBlock(DirectiveBlockParser parser, ParserContext context) : DirectiveBlock(parser, context)
12+
{
13+
public override string Directive => "csv-include";
14+
15+
public string? CsvFilePath { get; private set; }
16+
public string? CsvFilePathRelativeToSource { get; private set; }
17+
public bool Found { get; private set; }
18+
public string? Caption { get; private set; }
19+
public string Separator { get; private set; } = ",";
20+
public int MaxRows { get; private set; } = 25000;
21+
public long MaxFileSizeBytes { get; private set; } = 10 * 1024 * 1024; // 10MB
22+
public int MaxColumns { get; private set; } = 10;
23+
24+
public override void FinalizeAndValidate(ParserContext context)
25+
{
26+
Caption = Prop("caption");
27+
28+
var separator = Prop("separator", "delimiter");
29+
if (!string.IsNullOrEmpty(separator))
30+
Separator = separator;
31+
32+
33+
34+
ExtractCsvPath(context);
35+
}
36+
37+
private void ExtractCsvPath(ParserContext context)
38+
{
39+
var csvPath = Arguments;
40+
if (string.IsNullOrWhiteSpace(csvPath))
41+
{
42+
this.EmitError("csv-include requires an argument specifying the path to the CSV file.");
43+
return;
44+
}
45+
46+
var csvFrom = context.MarkdownSourcePath.Directory!.FullName;
47+
if (csvPath.StartsWith('/'))
48+
csvFrom = Build.DocumentationSourceDirectory.FullName;
49+
50+
CsvFilePath = Path.Combine(csvFrom, csvPath.TrimStart('/'));
51+
CsvFilePathRelativeToSource = Path.GetRelativePath(Build.DocumentationSourceDirectory.FullName, CsvFilePath);
52+
53+
if (Build.ReadFileSystem.File.Exists(CsvFilePath))
54+
{
55+
ValidateFileSize();
56+
Found = true;
57+
}
58+
else
59+
this.EmitError($"CSV file `{CsvFilePath}` does not exist.");
60+
}
61+
62+
private void ValidateFileSize()
63+
{
64+
if (CsvFilePath == null)
65+
return;
66+
67+
try
68+
{
69+
var fileInfo = Build.ReadFileSystem.FileInfo.New(CsvFilePath);
70+
if (fileInfo.Length > MaxFileSizeBytes)
71+
{
72+
var sizeMB = fileInfo.Length / (1024.0 * 1024.0);
73+
var maxSizeMB = MaxFileSizeBytes / (1024.0 * 1024.0);
74+
this.EmitError($"CSV file `{CsvFilePath}` is {sizeMB:F1}MB, which exceeds the maximum allowed size of {maxSizeMB:F1}MB.");
75+
Found = false;
76+
}
77+
}
78+
catch (Exception ex)
79+
{
80+
this.EmitError($"Could not validate CSV file size: {ex.Message}");
81+
Found = false;
82+
}
83+
}
84+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@using Elastic.Markdown.Myst.Directives.CsvInclude
2+
@inherits RazorSlice<CsvIncludeViewModel>
3+
4+
@{
5+
var csvBlock = (CsvIncludeBlock)Model.DirectiveBlock;
6+
if (!csvBlock.Found)
7+
{
8+
return;
9+
}
10+
11+
var csvRows = Model.GetCsvRows().ToList();
12+
if (!csvRows.Any())
13+
{
14+
return;
15+
}
16+
}
17+
18+
<div class="table-wrapper">
19+
@if (!string.IsNullOrEmpty(csvBlock.Caption))
20+
{
21+
<caption>@csvBlock.Caption</caption>
22+
}
23+
24+
<table>
25+
<thead>
26+
<tr>
27+
@for (var i = 0; i < csvRows[0].Length; i++)
28+
{
29+
<th>@csvRows[0][i]</th>
30+
}
31+
</tr>
32+
</thead>
33+
34+
<tbody>
35+
@for (var rowIndex = 1; rowIndex < csvRows.Count; rowIndex++)
36+
{
37+
<tr>
38+
@for (var i = 0; i < csvRows[rowIndex].Length; i++)
39+
{
40+
<td>@csvRows[rowIndex][i]</td>
41+
}
42+
</tr>
43+
}
44+
</tbody>
45+
</table>
46+
</div>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Diagnostics;
6+
7+
namespace Elastic.Markdown.Myst.Directives.CsvInclude;
8+
9+
public class CsvIncludeViewModel : DirectiveViewModel
10+
{
11+
public IEnumerable<string[]> GetCsvRows()
12+
{
13+
if (DirectiveBlock is not CsvIncludeBlock csvBlock || !csvBlock.Found || string.IsNullOrEmpty(csvBlock.CsvFilePath))
14+
return [];
15+
16+
var csvData = CsvReader.ReadCsvFile(csvBlock.CsvFilePath, csvBlock.Separator, csvBlock.Build.ReadFileSystem);
17+
var rowCount = 0;
18+
var columnCountExceeded = false;
19+
20+
return csvData.TakeWhile(row =>
21+
{
22+
if (rowCount >= csvBlock.MaxRows)
23+
{
24+
csvBlock.EmitWarning($"CSV file contains more than {csvBlock.MaxRows} rows. Only the first {csvBlock.MaxRows} rows will be displayed.");
25+
return false;
26+
}
27+
28+
if (row.Length > csvBlock.MaxColumns)
29+
{
30+
if (!columnCountExceeded)
31+
{
32+
csvBlock.EmitWarning($"CSV file contains more than {csvBlock.MaxColumns} columns. Only the first {csvBlock.MaxColumns} columns will be displayed.");
33+
columnCountExceeded = true;
34+
}
35+
}
36+
37+
rowCount++;
38+
return true;
39+
}).Select(row =>
40+
{
41+
if (row.Length > csvBlock.MaxColumns)
42+
{
43+
var trimmedRow = new string[csvBlock.MaxColumns];
44+
Array.Copy(row, trimmedRow, csvBlock.MaxColumns);
45+
return trimmedRow;
46+
}
47+
return row;
48+
});
49+
}
50+
51+
public static CsvIncludeViewModel Create(CsvIncludeBlock csvBlock) =>
52+
new()
53+
{
54+
DirectiveBlock = csvBlock
55+
};
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.IO.Abstractions;
6+
using nietras.SeparatedValues;
7+
8+
namespace Elastic.Markdown.Myst.Directives.CsvInclude;
9+
10+
public static class CsvReader
11+
{
12+
public static IEnumerable<string[]> ReadCsvFile(string filePath, string separator, IFileSystem? fileSystem = null)
13+
{
14+
var fs = fileSystem ?? new FileSystem();
15+
return ReadWithSep(filePath, separator, fs);
16+
}
17+
18+
private static IEnumerable<string[]> ReadWithSep(string filePath, string separator, IFileSystem fileSystem)
19+
{
20+
var separatorChar = separator == "," ? ',' : separator[0];
21+
var spec = Sep.New(separatorChar);
22+
23+
// Sep works with actual file paths, not virtual file systems
24+
// For testing with MockFileSystem, we'll read content first
25+
if (fileSystem.GetType().Name == "MockFileSystem")
26+
{
27+
var content = fileSystem.File.ReadAllText(filePath);
28+
using var reader = spec.Reader(o => o with { HasHeader = false, Unescape = true }).FromText(content);
29+
30+
foreach (var row in reader)
31+
{
32+
var rowData = new string[row.ColCount];
33+
for (var i = 0; i < row.ColCount; i++)
34+
{
35+
rowData[i] = row[i].ToString();
36+
}
37+
yield return rowData;
38+
}
39+
}
40+
else
41+
{
42+
using var reader = spec.Reader(o => o with { HasHeader = false, Unescape = true }).FromFile(filePath);
43+
44+
foreach (var row in reader)
45+
{
46+
var rowData = new string[row.ColCount];
47+
for (var i = 0; i < row.ColCount; i++)
48+
{
49+
rowData[i] = row[i].ToString();
50+
}
51+
yield return rowData;
52+
}
53+
}
54+
}
55+
56+
}

0 commit comments

Comments
 (0)