A lightweight text templating library written in C# which can be a drop-in replacement for string.Format
Parser
(#246)A single escape character (\
) at the end of the input string will now throw an ArgumentException
with a comprehensive error message.
KeyValuePairSource
(#244)KeyValuePairSource
isas a simple, cheap and performant way to create named placeholders.
Example:
Smart.Format("{placeholder}", new KeyValuePair<string, object?>("placeholder", "some value")
// Result: "some value"
IsMatchFormatter
(#245)The IsMatchFormatter
is a formatter with evaluation of regular expressions.
New: The formatter can output matching group values of a RegEx
.
We'll evalute this argument with IsMatchFormatter
:
KeyValuePair<string, object> arg = new("theValue", "Some123Content");
This behavior is unchanged.
_ = Smart.Format("{theValue:ismatch(^.+123.+$):Okay - {}|No match content}", arg);
// Result: "Okay - Some123Content"
_ = Smart.Format("{theValue:ismatch(^.+999.+$):Okay - {}|No match content}", arg);
// Result: "No match content"
// List the matching RegEx group values
_ = Smart.Format("{theValue:ismatch(^.+\\(1\\)\\(2\\)\\(3\\).+$):Matches for '{}'\\: {m:list:| - }|No match}", arg);
// Result: "Matches for 'Some123Content': Some123Content - 1 - 2 - 3"
// Show specific matching RegEx group values with their index in the list
_ = Smart.Format("{theValue:ismatch(^.+\\(1\\)\\(2\\)\\(3\\).+$):First 2 matches in '{}'\\: {m[1]} and {m[2]}|No match}", arg);
// Result: "First 2 matches in 'Some123Content': 1 and 2"
The placeholder m
is for the collection of matching RegEx group values generated by IsMatchFormatter
. The collection has at least one entry for a successful match. See more details in the Microsoft docs for the GroupCollection
class.
The name of the placeholder can be set with IsMatchFormatter.PlaceholderNameForMatches
. "m" is the default.
Remainders from the v2.x API: All obsolete element usage creates a compile time error
SmartFormat has the following NuGet packages:
a) SmartFormat.NET
This is package which references all other packages below.
b) SmartFormat
SmartFormat is the core package. It comes with the most frequently used extensions built-in:
GlobalVariablesSource
PersistentVariablesSource
StringSource
✔️ListFormatter
(implementing ISource
) ✔️DictionarySource
✔️ValueTupleSource
✔️ReflectionSource
✔️DefaultSource
✔️ListFormatter
(implementing IFormatter
) ✔️PluralLocalizationFormatter
✔️ConditionalFormatter
✔️IsMatchFormatter
✔️NullFormatter
✔️LocalizationFormatter
TemplateFormatter
ChooseFormatter
✔️SubStringFormatter
✔️DefaultFormatter
✔️Breaking change:
Note that only extensions marked (✔️) are included when calling
Smart.CreateDefaultFormatter(...)
. These default extensions differ from previous versions.
Some extensions (like PersistentVariablesSource
and TemplateFormatter
) require configuration to be useful.
c) SmartFormat.Extensions.System.Text.Json
This package is a SmartFormat extension for formatting System.Text.Json
types as a source.
d) SmartFormat.Extensions.Newtonsoft.Json
This package is a SmartFormat extension for formatting Newtonsoft.Json
types as a source.
e) SmartFormat.Extensions.Xml
This package is a SmartFormat extension for reading and formatting System.Xml.Linq.XElement
s.
f) SmartFormat.Extensions.Time
This package is a SmartFormat extension for formatting System.DateTime
, System.DateTimeOffset
and System.TimeSpan
types.
SmartFormatter
a) The easy way
Call Smart.CreateDefaultFormatter(...)
and get a ready-to-use SmartFormatter
. The same happens under the hood when calling one of the Smart.Format(...)
methods.
b) The tailor-made alternative
When it comes to performance, it is advisable to add only those specific extensions that are needed. Just like this:
var formatter = new SmartFormatter()
.AddExtensions(new ReflectionSource())
.AddExtensions(new PluralLocalizationFormatter(), new DefaultFormatter());
Breaking change:
In v3.0 all
WellKnownExtensionTypes.Sources
andWellKnownExtensionTypes.Formatters
are automatically inserted to the extension list at the place where they usually should be.
Any extension can, however, be inserted to the desired position in the extension list:
SmartFormatter.InsertExtension(int position, IFormatter sourceExtension)
SmartFormatter.InsertExtension(int position, IFormatter formatterExtension)
This can be useful especially when adding your custom extensions. You should call SmartFormatter.InsertExtension(...)
after SmartFormatter.AddExtensions(...)
:
var formatter = new SmartFormatter()
.AddExtensions(new ReflectionSource())
.AddExtensions(new PluralLocalizationFormatter(), new DefaultFormatter())
.InsertExtension(0, new MyCustomFormatter());
ConditionalFormatter
processes unsigned numbers in arguments correctly.JsonSource
: Corrected handling of null
values in Newtonsoft.Json
objects.After implementing Object Pools for all classes which are frequently instantiated, GC and memory allocation again went down significantly.
In order to return "smart" objects back to the object pool, its important to use one of the following patterns.
Examples:
a) Single thread context (no need to care about object pooling)
var resultString = Smart.Format("format string", args);
b) Recommended: Auto-dispose Format
(e.g.: caching, multi treading context)
var smart = Smart.CreateDefaultSmartFormat();
// Note "using" for auto-disposing the parsedFormat
using var parsedFormat = new Parser().ParseFormat("format string", args);
var resultString = smart.Format(parsedFormat);
c) Call Format.Dispose()
(e.g.: caching, multi treading context)
var smart = Smart.CreateDefaultSmartFormat();
var parsedFormat = new Parser().ParseFormat("format string", args);
var resultString = smart.Format(parsedFormat);
// Don't use (or reference) "parsedFormat" after disposing
parsedFormat.Dispose();
SmartFormat makes heavy use of caching and object pooling for expensive operations, which both require static
containers.
a) Instantiating SmartFormatter
s from different threads:
`SmartSettings.IsThreadSafeMode=true` **must** be set, so that thread safe containers are used. This brings an inherent performance penalty.
**Note:** The simplified `Smart.Format(...)` API overloads use a static `SmartFormatter` instance which is **not** thread safe. Call `Smart.CreateDefaultSmartFormat()` to create a default `Formatter`.
a) Instantiating SmartFormatter
s from a single thread:
`SmartSettings.IsThreadSafeMode=false` **should** be set for avoiding the multithreading overhead and thus for best performance.
The simplified `Smart.Format(...)` API overloads are allowed here.
Changes on top of v3.0.0-alpha.3:
LocalizationFormatter
(#176)LocalizationFormatter
to localize literals and placeholdersILocalizationProvider
and a standard implemention as LocalizationProvider
, which handles resx
resource files. A fallback culture can be set. It will be used, in case no item for a certain culture could be found in any of the resources. LocalizationProvider
can search an unlimited number of defined resoures.SmartSettings
were exended with category Localization
. That way, custom IFormatter
s can also make use of localization, if needed.LocalizationFormattingException
, which is derived from FormattingException
to easily identify this kind of issuesCulture-specific results shown here are included in embedded resource files, which are omitted for brevity.
a) Localize pure literals into Spanish:
// culture supplied as a format option
_ = Smart.Format(culture, "{:L(en):WeTranslateText}");
// culture supplied as an argument to the formatter
var culture = CultureInfo.GetCultureInfo("es");
_ = Smart.Format(culture, "{:L:WeTranslateText}");
// result for both: "Traducimos el texto"
b) Localized strings may contain placeholders
_ = Smart.Format("{0} {1:L(es):has {:#,#} inhabitants}", "X-City", 8900000);
// result: "X-City tiene 8.900.000 habitantes"
_ = Smart.Format("{0} {1:L(es):has {:#,#} inhabitants}", "X-City", 8900000);
// result: "X-City has 8,900,000 inhabitants"
c) Localization can be used together with other formatters
_ = Smart.Format("{0:plural:{:L(en):{} item}|{:L(en):{} items}}", 0;
// result for English: 0 items
_ = Smart.Format("{0:plural:{:L(fr):{} item}|{:L(fr):{} items}}", 0;
// result for French: 0 élément
_ = Smart.Format("{0:plural:{:L(fr):{} item}|{:L(fr):{} items}}", 200;
// result for French: 200 éléments
PluralLocalizationFormatter
(#209)DefaultTwoLetterISOLanguageName
is obsolete.LocalizationFormatter
):FormattingInfo.FormatterOptions
.IFormatProvider
argument (which may be a CultureInfo
) to SmartFormatter.Format(IFormatProvider, string, object?[])
CultureInfo.CurrentUICulture
TimeFormatter
(#220, #221)DefaultTwoLetterISOLanguageName
is obsolete.LocalizationFormatter
and PluralLocalizationFormatter
):FormattingInfo.FormatterOptions
.IFormatProvider
argument (which may be a CultureInfo
) to SmartFormatter.Format(IFormatProvider, string, object?[])
CultureInfo.CurrentUICulture
CommonLanguagesTimeTextInfo
, TimeFormatter
includes French, Spanish, Portuguese, Italian and German as new languages besides English out-of-the-box.TimeFormatter.FallbackLanguage = "en";
, this fallback language will be used, if no supported language could be found.CommonLanguagesTimeTextInfo
. Custom languages override built-in definitions.
var language = "nl"; // dummy - it's English, not Dutch ;-)
TimeTextInfo custom = new(
pluralRule: PluralRules.GetPluralRule(language),
week: new[] { "{0} week", "{0} weeks" },
day: new[] { "{0} day", "{0} days" },
hour: new[] { "{0} hour", "{0} hours" },
minute: new[] { "{0} minute", "{0} minutes" },
second: new[] { "{0} second", "{0} seconds" },
millisecond: new[] { "{0} millisecond", "{0} milliseconds" },
w: new[] { "{0}w" },
d: new[] { "{0}d" },
h: new[] { "{0}h" },
m: new[] { "{0}m" },
s: new[] { "{0}s" },
ms: new[] { "{0}ms" },
lessThan: "less than {0}");
CommonLanguagesTimeTextInfo.AddLanguage(language, custom)
var formatDepreciated = "{0:time(abbr hours noless)}";
b) This format string is recommended for Smart.Format v3 and later. It allows for including the language as an option to the TimeFormatter
:
// Without language option:
var formatRecommended = "{0:time:abbr hours noless:}";
// With language option:
var formatRecommended = "{0:time(en):abbr hours noless:}";
var timeSpan = new TimeSpan(1,1,1,1,1)
Smart.Format("{0:time(en):hours minutes}", timeSpan);
// result: "25 hours 1 minute"
Smart.Format("{0:time(fr):hours minutes}", timeSpan);
// result: "25 heures 1 minute"
Changes since v3.0.0-alpha.1:
1. All Format()
methods accept nullable args
string.Format
null(able) arguments are allowed.Smart
and SmartFormatter
2. ListFormatter will be initialized only once
Changes on top of v3.0.0-alpha.2:
1. NullFormatter
processes complex formats:
ChooseFormatter
2. NewtonsoftJsonSource
: fix evaluating null values
3. Add missing formatters to Smart.CreatDefaultFormatter
4. Improved parsing of HTML input
Introduced experimental bool ParserSettings.ParseInputAsHtml
.
The default is false
.
If true
, theParser
will parse all content inside <script> and <style> tags as LiteralText
. All other places may still contain Placeholder
s.
This is because <script> and <style> tags may contain curly or square braces, that interfere with the SmartFormat {Placeholder
}.
Best results can only be expected with clean HTML: balanced opening and closing tags, single and double quotes. Also, do not use angle brackets, single and double quotes in script or style comments.
SmartFormat is not a fully-fledged HTML parser. If this is required, use AngleSharp or HtmlAgilityPack.
Significant improvements of performance:
BenchmarkDotNet performance tests for formatters and ISource
s now show (depending on different input format strings) the following improvements compared to v2.7.0:
Formatting measured with a cached parsed Format
, and including the result string
returned to the caller. Parser
was already optimized with PR #187.
"this is {uncomplete"
(missing closing brace). Before v2.7.0 the parser handled {uncomplete
as a TextLiteral
, not as an erroneous Placeholder
.Parser
encountered a ParsingError.TooManyClosingBraces
, this closing brace was simply "swallowed-up". This way, the result with Parser.ErrorAction.MaintainTokens
differs from the original format string. From v2.7.0, the redundant closing brace is handled as a TextLiteral
.