Skip to content

Commit fe8b587

Browse files
committed
feat(ndc): support arbitrary input_encoding names and code pages (#15)
ResolveEncoding now accepts any encoding name (e.g. iso-8859-5) or numeric code page (e.g. 28595) in addition to the latin1/utf-8/ascii fast paths. Unknown or invalid values throw NotSupportedException with a clear message instead of silently falling back to Latin1. CodePagesEncodingProvider is auto-registered in a static constructor so non-Latin1 ISO/Windows code pages (Cyrillic via ISO-8859-5, etc.) are available without caller setup. The binary parse path coerces numeric input_encoding values via ToString, matching NdcOptions.FromDictionary. Closes #15
1 parent 0dd97e7 commit fe8b587

2 files changed

Lines changed: 118 additions & 15 deletions

File tree

src/FlexRender.Content.Ndc/NdcContentParser.cs

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ namespace FlexRender.Content.Ndc;
1313
/// </summary>
1414
public sealed class NdcContentParser : IContentParser, IBinaryContentParser
1515
{
16+
/// <summary>
17+
/// Registers the code-pages encoding provider exactly once so that non-Latin1
18+
/// ISO/Windows code pages (e.g. ISO-8859-5 / code page 28595) are available to
19+
/// <see cref="Encoding.GetEncoding(string)"/> and <see cref="Encoding.GetEncoding(int)"/>.
20+
/// Static initialization is thread-safe and runs before any member is accessed.
21+
/// </summary>
22+
static NdcContentParser()
23+
{
24+
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
25+
}
26+
1627
/// <inheritdoc />
1728
public string FormatName => "ndc";
1829

@@ -43,30 +54,53 @@ public IReadOnlyList<TemplateElement> Parse(ReadOnlyMemory<byte> data, ContentPa
4354
return [];
4455

4556
var encodingName = "latin1";
46-
if (options is not null && options.TryGetValue("input_encoding", out var enc) && enc is string encStr)
47-
encodingName = encStr;
57+
if (options is not null && options.TryGetValue("input_encoding", out var enc) && enc is not null)
58+
encodingName = enc.ToString() ?? "latin1";
4859

4960
var encoding = ResolveEncoding(encodingName);
5061
var textContent = encoding.GetString(data.Span);
5162
return Parse(textContent, context, options);
5263
}
5364

5465
/// <summary>
55-
/// Resolves a human-friendly encoding name to a <see cref="System.Text.Encoding"/> instance.
66+
/// Resolves an encoding identifier to a <see cref="System.Text.Encoding"/> instance.
5667
/// </summary>
5768
/// <param name="name">
58-
/// The encoding name. Supported values: <c>latin1</c>, <c>iso-8859-1</c>, <c>utf-8</c>,
59-
/// <c>utf8</c>, <c>ascii</c>. Unrecognized values default to Latin-1.
69+
/// The encoding identifier. All values are resolved through <see cref="Encoding.GetEncoding(string)"/>
70+
/// (encoding names such as <c>latin1</c>, <c>iso-8859-1</c>, <c>iso-8859-5</c>, <c>utf-8</c>, <c>ascii</c>)
71+
/// or <see cref="Encoding.GetEncoding(int)"/> (numeric code pages such as <c>28595</c>). Common names
72+
/// already return the corresponding framework singletons. The dashless <c>utf8</c> alias is mapped to
73+
/// <c>utf-8</c> for backward compatibility. Non-Latin1 ISO/Windows code pages are supported through the
74+
/// registered code-pages provider.
6075
/// </param>
6176
/// <returns>The resolved <see cref="System.Text.Encoding"/>.</returns>
62-
internal static Encoding ResolveEncoding(string name) =>
63-
name.ToLowerInvariant() switch
77+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is <see langword="null"/>.</exception>
78+
/// <exception cref="NotSupportedException">
79+
/// Thrown when <paramref name="name"/> does not correspond to a known encoding name or code page.
80+
/// </exception>
81+
internal static Encoding ResolveEncoding(string name)
82+
{
83+
ArgumentNullException.ThrowIfNull(name);
84+
85+
var trimmed = name.Trim();
86+
87+
// GetEncoding does not recognize the dashless "utf8" form; map it to the canonical "utf-8".
88+
if (string.Equals(trimmed, "utf8", StringComparison.OrdinalIgnoreCase))
89+
trimmed = "utf-8";
90+
91+
try
6492
{
65-
"latin1" or "iso-8859-1" => Encoding.Latin1,
66-
"utf-8" or "utf8" => Encoding.UTF8,
67-
"ascii" => Encoding.ASCII,
68-
_ => Encoding.Latin1
69-
};
93+
return int.TryParse(trimmed, System.Globalization.CultureInfo.InvariantCulture, out var codePage)
94+
? Encoding.GetEncoding(codePage)
95+
: Encoding.GetEncoding(trimmed);
96+
}
97+
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
98+
{
99+
throw new NotSupportedException(
100+
$"Unknown or unsupported input encoding: '{name}'. Use a known encoding name (e.g. 'iso-8859-5') or a numeric code page (e.g. '28595').",
101+
ex);
102+
}
103+
}
70104

71105
private static int CalculateMaxLineWidth(List<NdcToken> tokens, int tabWidth = 8)
72106
{

tests/FlexRender.Tests/Content/Ndc/NdcBinaryParserTests.cs

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,25 @@ public void WithNdc_RegistersBothStringAndBinaryParser()
9393
[InlineData("utf-8")]
9494
[InlineData("utf8")]
9595
[InlineData("ascii")]
96-
[InlineData("unknown")]
96+
[InlineData("iso-8859-5")]
97+
[InlineData("28595")]
9798
public void ResolveEncoding_ReturnsValidEncoding(string name)
9899
{
99100
var encoding = NdcContentParser.ResolveEncoding(name);
100101

101102
Assert.NotNull(encoding);
102103
}
103104

105+
[Theory]
106+
[InlineData("iso-8859-5")]
107+
[InlineData("28595")]
108+
public void ResolveEncoding_Iso88595_ByNameOrCodePage_ResolvesToCodePage28595(string name)
109+
{
110+
var encoding = NdcContentParser.ResolveEncoding(name);
111+
112+
Assert.Equal(28595, encoding.CodePage);
113+
}
114+
104115
[Fact]
105116
public void ResolveEncoding_Latin1_ReturnsLatin1()
106117
{
@@ -122,8 +133,66 @@ public void ResolveEncoding_Ascii_ReturnsAscii()
122133
}
123134

124135
[Fact]
125-
public void ResolveEncoding_Unknown_DefaultsToLatin1()
136+
public void ResolveEncoding_Unknown_Throws()
137+
{
138+
var ex = Assert.Throws<NotSupportedException>(
139+
() => NdcContentParser.ResolveEncoding("something-else"));
140+
141+
Assert.Contains("something-else", ex.Message, StringComparison.Ordinal);
142+
}
143+
144+
[Fact]
145+
public void ResolveEncoding_UnknownCodePage_Throws()
146+
{
147+
var ex = Assert.Throws<NotSupportedException>(
148+
() => NdcContentParser.ResolveEncoding("999999"));
149+
150+
Assert.Contains("999999", ex.Message, StringComparison.Ordinal);
151+
}
152+
153+
[Fact]
154+
public void ParseBytes_WithIso88595Encoding_DecodesCyrillic()
155+
{
156+
var parser = new NdcContentParser();
157+
// Cyrillic text encoded with ISO-8859-5 (code page 28595). The default
158+
// NDC charset uses "none" encoding, so the decoded Unicode survives unchanged.
159+
// GetEncoding(28595) succeeds here because the parser's static constructor
160+
// already registered the code-pages provider.
161+
var text = "Привет";
162+
var iso88595 = global::System.Text.Encoding.GetEncoding(28595);
163+
var data = iso88595.GetBytes(text);
164+
var options = new Dictionary<string, object>
165+
{
166+
["input_encoding"] = "iso-8859-5"
167+
};
168+
169+
var result = parser.Parse(data, EmptyContext, options);
170+
171+
var root = Assert.IsType<FlexElement>(Assert.Single(result));
172+
var row = Assert.IsType<FlexElement>(root.Children[0]);
173+
var textElement = Assert.IsType<TextElement>(row.Children[0]);
174+
Assert.Equal("Привет", textElement.Content);
175+
}
176+
177+
[Fact]
178+
public void ParseBytes_WithNumericInputEncoding_DecodesCyrillic()
126179
{
127-
Assert.Same(global::System.Text.Encoding.Latin1, NdcContentParser.ResolveEncoding("something-else"));
180+
var parser = new NdcContentParser();
181+
// input_encoding arrives as a boxed int (as YAML numeric scalars do), not a string.
182+
// The binary path must coerce it to its string form so the numeric code page resolves.
183+
var text = "Привет";
184+
var iso88595 = global::System.Text.Encoding.GetEncoding(28595);
185+
var data = iso88595.GetBytes(text);
186+
var options = new Dictionary<string, object>
187+
{
188+
["input_encoding"] = 28595
189+
};
190+
191+
var result = parser.Parse(data, EmptyContext, options);
192+
193+
var root = Assert.IsType<FlexElement>(Assert.Single(result));
194+
var row = Assert.IsType<FlexElement>(root.Children[0]);
195+
var textElement = Assert.IsType<TextElement>(row.Children[0]);
196+
Assert.Equal("Привет", textElement.Content);
128197
}
129198
}

0 commit comments

Comments
 (0)