Skip to content

Commit 0f169be

Browse files
committed
Add CSV file directive
1 parent 99f92e5 commit 0f169be

File tree

9 files changed

+404
-2
lines changed

9 files changed

+404
-2
lines changed

docs/_docset.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ toc:
9090
- file: code.md
9191
- file: comments.md
9292
- file: conditionals.md
93+
- file: csv-file.md
9394
- hidden: diagrams.md
9495
- file: dropdowns.md
9596
- 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-file.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# CSV file directive
2+
3+
The `{csv-file}` 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-file} ../_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-file} _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-file} _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-file} _snippets/sample-data.csv
48+
:separator: ;
49+
:::
50+
```

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 file](csv-file.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:
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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.CsvFile;
10+
11+
public class CsvFileBlock(DirectiveBlockParser parser, ParserContext context) : DirectiveBlock(parser, context)
12+
{
13+
public override string Directive => "csv-file";
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 List<string[]> CsvData { get; private set; } = [];
21+
22+
public override void FinalizeAndValidate(ParserContext context)
23+
{
24+
Caption = Prop("caption");
25+
26+
var separator = Prop("separator", "delimiter");
27+
if (!string.IsNullOrEmpty(separator))
28+
Separator = separator;
29+
30+
ExtractCsvPath(context);
31+
if (Found)
32+
ParseCsvFile();
33+
}
34+
35+
private void ExtractCsvPath(ParserContext context)
36+
{
37+
var csvPath = Arguments;
38+
if (string.IsNullOrWhiteSpace(csvPath))
39+
{
40+
this.EmitError("csv-file requires an argument specifying the path to the CSV file.");
41+
return;
42+
}
43+
44+
var csvFrom = context.MarkdownSourcePath.Directory!.FullName;
45+
if (csvPath.StartsWith('/'))
46+
csvFrom = Build.DocumentationSourceDirectory.FullName;
47+
48+
CsvFilePath = Path.Combine(csvFrom, csvPath.TrimStart('/'));
49+
CsvFilePathRelativeToSource = Path.GetRelativePath(Build.DocumentationSourceDirectory.FullName, CsvFilePath);
50+
51+
if (Build.ReadFileSystem.File.Exists(CsvFilePath))
52+
Found = true;
53+
else
54+
this.EmitError($"CSV file `{CsvFilePath}` does not exist.");
55+
}
56+
57+
private void ParseCsvFile()
58+
{
59+
try
60+
{
61+
var file = Build.ReadFileSystem.FileInfo.New(CsvFilePath!);
62+
var content = file.FileSystem.File.ReadAllText(file.FullName);
63+
64+
// Split into lines and parse each line
65+
var lines = content.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
66+
67+
foreach (var line in lines)
68+
{
69+
if (string.IsNullOrWhiteSpace(line.Trim()))
70+
continue;
71+
72+
var fields = ParseCsvLine(line);
73+
CsvData.Add(fields);
74+
}
75+
}
76+
catch (Exception e)
77+
{
78+
this.EmitError($"Failed to parse CSV file: {e.Message}");
79+
}
80+
}
81+
82+
private string[] ParseCsvLine(string line)
83+
{
84+
var fields = new List<string>();
85+
var currentField = "";
86+
var inQuotes = false;
87+
var i = 0;
88+
89+
while (i < line.Length)
90+
{
91+
var c = line[i];
92+
93+
if (c == '"')
94+
{
95+
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
96+
{
97+
// Escaped quote
98+
currentField += '"';
99+
i += 2;
100+
}
101+
else
102+
{
103+
// Toggle quote state
104+
inQuotes = !inQuotes;
105+
i++;
106+
}
107+
}
108+
else if (c.ToString() == Separator && !inQuotes)
109+
{
110+
// End of field
111+
fields.Add(currentField.Trim());
112+
currentField = "";
113+
i++;
114+
}
115+
else
116+
{
117+
currentField += c;
118+
i++;
119+
}
120+
}
121+
122+
// Add the last field
123+
fields.Add(currentField.Trim());
124+
125+
return fields.ToArray();
126+
}
127+
}

src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Collections.Frozen;
66
using Elastic.Markdown.Myst.Directives.Admonition;
7+
using Elastic.Markdown.Myst.Directives.CsvFile;
78
using Elastic.Markdown.Myst.Directives.Diagram;
89
using Elastic.Markdown.Myst.Directives.Image;
910
using Elastic.Markdown.Myst.Directives.Include;
@@ -44,7 +45,6 @@ public DirectiveBlockParser()
4445
{
4546
{ "bibliography", 5 },
4647
{ "blockquote", 6 },
47-
{ "csv-table", 9 },
4848
{ "iframe", 14 },
4949
{ "list-table", 17 },
5050
{ "myst", 22 },
@@ -122,6 +122,9 @@ protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor)
122122
if (info.IndexOf("{settings}") > 0)
123123
return new SettingsBlock(this, context);
124124

125+
if (info.IndexOf("{csv-file}") > 0)
126+
return new CsvFileBlock(this, context);
127+
125128
foreach (var admonition in _admonitions)
126129
{
127130
if (info.IndexOf($"{{{admonition}}}") > 0)

src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Elastic.Markdown.Diagnostics;
77
using Elastic.Markdown.Myst.CodeBlocks;
88
using Elastic.Markdown.Myst.Directives.Admonition;
9+
using Elastic.Markdown.Myst.Directives.CsvFile;
910
using Elastic.Markdown.Myst.Directives.Diagram;
1011
using Elastic.Markdown.Myst.Directives.Dropdown;
1112
using Elastic.Markdown.Myst.Directives.Image;
@@ -81,6 +82,9 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo
8182
case SettingsBlock settingsBlock:
8283
WriteSettingsBlock(renderer, settingsBlock);
8384
return;
85+
case CsvFileBlock csvFileBlock:
86+
WriteCsvFileBlock(renderer, csvFileBlock);
87+
return;
8488
case StepperBlock stepperBlock:
8589
WriteStepperBlock(renderer, stepperBlock);
8690
return;
@@ -407,4 +411,48 @@ void Render(Block o)
407411
_ = renderer.Write($"(Block: {o.GetType().Name}");
408412
}
409413
}
414+
415+
private static void WriteCsvFileBlock(HtmlRenderer renderer, CsvFileBlock block)
416+
{
417+
if (!block.Found || block.CsvData.Count == 0)
418+
return;
419+
420+
// Start table wrapper div with the table-wrapper class from table.css
421+
_ = renderer.Write("<div class=\"table-wrapper\">");
422+
423+
// Write caption if provided
424+
if (!string.IsNullOrEmpty(block.Caption))
425+
{
426+
_ = renderer.Write($"<caption>{block.Caption}</caption>");
427+
}
428+
429+
_ = renderer.Write("<table>");
430+
431+
// Always write header row (first row)
432+
if (block.CsvData.Count > 0)
433+
{
434+
_ = renderer.Write("<thead><tr>");
435+
foreach (var header in block.CsvData[0])
436+
{
437+
_ = renderer.Write($"<th>{header}</th>");
438+
}
439+
_ = renderer.Write("</tr></thead>");
440+
}
441+
442+
// Write body rows (starting from second row)
443+
_ = renderer.Write("<tbody>");
444+
for (var i = 1; i < block.CsvData.Count; i++)
445+
{
446+
_ = renderer.Write("<tr>");
447+
foreach (var cell in block.CsvData[i])
448+
{
449+
_ = renderer.Write($"<td>{cell}</td>");
450+
}
451+
_ = renderer.Write("</tr>");
452+
}
453+
_ = renderer.Write("</tbody>");
454+
455+
_ = renderer.Write("</table>");
456+
_ = renderer.Write("</div>");
457+
}
410458
}

0 commit comments

Comments
 (0)