mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-30 12:53:45 -04:00
QS updates:
- fixed documentation - regexp-checks running with timeout and culture-invariant matching - changed check-building in ConditionalTagCollection to use NonNull parameters. So no warnings occure. - add tests for <!is ...> and escaped chars
This commit is contained in:
@@ -83,7 +83,7 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, ValueProvider<TClass> valueProvider, ConditionEvaluator conditionEvaluator)
|
||||
: base(templateTag, Expression.Constant(false))
|
||||
{
|
||||
// <property> needs to match on at least one character which is not a space
|
||||
// <property> needs to match on at least one character, which is not a space
|
||||
NameMatcher = new Regex($"""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
^<(?<not>!)? # tags start with a '<'. Condtionals allow an optional ! captured in <not> to negate the condition
|
||||
@@ -103,8 +103,8 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, ValueProvider<TClass> valueProvider)
|
||||
: base(templateTag, Expression.Constant(false))
|
||||
{
|
||||
// <property> needs to match on at least one character which is not a space
|
||||
// though we will capture check enclosed in [] at the end of the tag the property itself migth also have a [] part for formatting purposes
|
||||
// <property> needs to match on at least one character, which is not a space.
|
||||
// though we will capture the group named `check` enclosed in [] at the end of the tag, the property itself might also have a [] part for formatting purposes
|
||||
NameMatcher = new Regex($"""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
^<(?<not>!)? # tags start with a '<'. Condtionals allow an optional ! captured in <not> to negate the condition
|
||||
@@ -155,28 +155,33 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
private static ConditionEvaluator GetPredicate(string? checkString)
|
||||
{
|
||||
if (checkString == null)
|
||||
return DefaultPredicate;
|
||||
return (v, _) => v switch
|
||||
{
|
||||
null => false,
|
||||
IEnumerable<object> e => e.Any(),
|
||||
_ => !string.IsNullOrWhiteSpace(v.ToString())
|
||||
};
|
||||
|
||||
var match = CheckRegex().Match(checkString);
|
||||
|
||||
var valStr = match.Groups["val"].Value;
|
||||
var ival = -1;
|
||||
var isNumop = match.Groups["numop"].Success && int.TryParse(valStr, out ival);
|
||||
var iVal = -1;
|
||||
var isNumericalOperator = match.Groups["num_op"].Success && int.TryParse(valStr, out iVal);
|
||||
|
||||
var checkItem = match.Groups["op"].ValueSpan switch
|
||||
Func<object, CultureInfo?, bool> checkItem = match.Groups["op"].ValueSpan switch
|
||||
{
|
||||
"=" or "" => (v, culture) => VComparedToStr(v, culture, valStr) == 0,
|
||||
"!=" or "!" => (v, culture) => VComparedToStr(v, culture, valStr) != 0,
|
||||
"~" => GetRegExpCheck(valStr),
|
||||
"#=" => (v, _) => VAsInt(v) == ival,
|
||||
"#!=" => (v, _) => VAsInt(v) != ival,
|
||||
"#>=" or ">=" => (v, _) => VAsInt(v) >= ival,
|
||||
"#>" or ">" => (v, _) => VAsInt(v) > ival,
|
||||
"#<=" or "<=" => (v, _) => VAsInt(v) <= ival,
|
||||
"#<" or "<" => (v, _) => VAsInt(v) < ival,
|
||||
_ => DefaultPredicate,
|
||||
"#=" => (v, _) => VAsInt(v) == iVal,
|
||||
"#!=" => (v, _) => VAsInt(v) != iVal,
|
||||
"#>=" or ">=" => (v, _) => VAsInt(v) >= iVal,
|
||||
"#>" or ">" => (v, _) => VAsInt(v) > iVal,
|
||||
"#<=" or "<=" => (v, _) => VAsInt(v) <= iVal,
|
||||
"#<" or "<" => (v, _) => VAsInt(v) < iVal,
|
||||
_ => (v, _) => !string.IsNullOrWhiteSpace(v.ToString())
|
||||
};
|
||||
return isNumop
|
||||
return isNumericalOperator
|
||||
? (v, culture) => v switch
|
||||
{
|
||||
null => false,
|
||||
@@ -201,34 +206,54 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// build a regular expression check which take the <see cref="CultureInfo"/> into account.
|
||||
/// Build a regular expression check. Uses culture-invariant matching for thread-safety and consistency.
|
||||
/// Applies a timeout to prevent regex patterns from causing excessive backtracking and blocking.
|
||||
/// </summary>
|
||||
/// <param name="valStr"></param>
|
||||
/// <param name="valStr">The regex pattern to match</param>
|
||||
/// <returns>check function to validate an object</returns>
|
||||
private static ConditionEvaluator GetRegExpCheck(string valStr)
|
||||
private static Func<object, CultureInfo?, bool> GetRegExpCheck(string valStr)
|
||||
{
|
||||
return (v, culture) =>
|
||||
try
|
||||
{
|
||||
var old = CultureInfo.CurrentCulture;
|
||||
try
|
||||
// Compile regex with timeout to prevent catastrophic backtracking
|
||||
var regex = new Regex(valStr,
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled,
|
||||
TimeSpan.FromMilliseconds(100));
|
||||
|
||||
return (v, _) =>
|
||||
{
|
||||
CultureInfo.CurrentCulture = culture ?? CultureInfo.CurrentCulture;
|
||||
return Regex.IsMatch(v?.ToString().Trim() ?? "", valStr, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = old;
|
||||
}
|
||||
};
|
||||
try
|
||||
{
|
||||
// CultureInfo parameter is intentionally ignored (discarded with _).
|
||||
// RegexOptions.CultureInvariant ensures culture-independent matching for predictable behavior.
|
||||
// This is preferred for template conditions because:
|
||||
// 1. Thread-safety: Regex operations are isolated and don't depend on thread-local culture
|
||||
// 2. Consistency: Template matches produce identical results regardless of system locale
|
||||
// 3. Predictability: Rules don't unexpectedly change based on user's OS settings
|
||||
//
|
||||
// Culture-sensitive matching would be problematic in cases like:
|
||||
// - Turkish locale: 'I' has different case folding (I ↔ ı vs. I ↔ i). Pattern "[i-z]" might match Turkish 'ı'.
|
||||
// - German locale: ß might be treated as equivalent to 'ss' during case-insensitive matching.
|
||||
// - Lithuanian locale: 'i' after 'ž' has an accent that affects sorting/matching.
|
||||
//
|
||||
// For naming templates, culture-invariant is the safer default.
|
||||
return regex.IsMatch(v.ToString()?.Trim() ?? "");
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
// Return false if regex evaluation times out
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If regex compilation fails, return a predicate that always returns false
|
||||
return (_, _) => false;
|
||||
}
|
||||
}
|
||||
|
||||
// without any special check only the existance of the property is checked. Strings need to be non empty.
|
||||
private static readonly ConditionEvaluator DefaultPredicate = (v, _) => v switch
|
||||
{
|
||||
null => false,
|
||||
IEnumerable<object> e => e.Any(),
|
||||
_ => !string.IsNullOrWhiteSpace(v.ToString())
|
||||
};
|
||||
// without any special check, only the existence of the property is checked. Strings need to be non-empty.
|
||||
|
||||
public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)
|
||||
{
|
||||
@@ -256,11 +281,11 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
[GeneratedRegex("""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
^\s* # anchor at start of line trimming leading whitespace
|
||||
(?<op> # capture operator in <op> and <numop>
|
||||
(?<numop>\#=|\#!=|\#?>=|\#?>|\#?<=|\#?<) # - numerical operators start with a # and might be omitted if unique
|
||||
(?<op> # capture operator in <op> and <num_op>
|
||||
(?<num_op>\#=|\#!=|\#?>=|\#?>|\#?<=|\#?<) # - numerical operators start with a # and might be omitted if unique
|
||||
| ~|!=?|=? # - string comparison operators including ~ for regexp. No operator is like =
|
||||
) \s* # ignore space between operator and value
|
||||
(?<val>(?(numop) # capture value in <val>
|
||||
(?<val>(?(num_op) # capture value in <val>
|
||||
\d+ # - numerical operators have to be followed by a number
|
||||
| .*? ) # - string for comparison. May be empty. Non-greedy capture resulting in no whitespace at the end
|
||||
)\s*$ # trimming up to the end
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace TemplatesTests
|
||||
Language = "English",
|
||||
Subtitle = "An Audible Original Drama",
|
||||
TitleWithSubtitle = "A Study in Scarlet: An Audible Original Drama",
|
||||
Codec = "AAC-LC",
|
||||
Codec = @"AAC[LC]\MP3", // special chars added
|
||||
FileVersion = null, // explicitly null
|
||||
LibationVersion = "", // explicitly empty string
|
||||
LengthInMinutes = 100,
|
||||
@@ -167,7 +167,7 @@ namespace TemplatesTests
|
||||
[DataRow("<bitrate[2]>Kbps <titleshort[u]>", "128Kbps A STUDY IN SCARLET")]
|
||||
[DataRow("<bitrate[3]>Kbps <titleshort[t]>", "128Kbps A Study In Scarlet")]
|
||||
[DataRow("<bitrate[4]>Kbps <titleshort[l]>", "0128Kbps a study in scarlet")]
|
||||
[DataRow("<codec[t]> <samplerate[6]>Hz", "Aac-Lc 044100Hz")]
|
||||
[DataRow("<codec[t]> <samplerate[6]>Hz", "Aac[Lc]Mp3 044100Hz")]
|
||||
[DataRow("<codec[3T]> <titleshort[ 5 U ]>", "AAC A STU")]
|
||||
[DataRow("<bitrate [ 4 ] >Kbps <samplerate [ 6 ] >Hz", "0128Kbps 044100Hz")]
|
||||
public void FormatTags(string template, string expected)
|
||||
@@ -433,27 +433,36 @@ namespace TemplatesTests
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<has libation version->empty-string<-has>", "")]
|
||||
[DataRow("<!has libation version->empty-string<-has>", "empty-string")]
|
||||
[DataRow("<is libation version[=foobar]->empty-string<-has>", "")]
|
||||
[DataRow("<!is libation version[=foobar]->empty-string<-has>", "empty-string")]
|
||||
[DataRow("<is libation version[=]->empty-string<-has>", "empty-string")]
|
||||
[DataRow("<is libation version[#=0]->empty-string<-has>", "empty-string")]
|
||||
[DataRow("<is libation version[]->empty-string<-has>", "empty-string")]
|
||||
[DataRow("<has file version->null-string<-has>", "")]
|
||||
[DataRow("<has file version[=foobar]->null-string<-has>", "")]
|
||||
[DataRow("<has file version[=]->null-string<-has>", "")]
|
||||
[DataRow("<!has file version->null-string<-has>", "null-string")]
|
||||
[DataRow("<is file version[=foobar]->null-string<-has>", "")]
|
||||
[DataRow("<is file version[=]->null-string<-has>", "")]
|
||||
[DataRow("<!is file version[=]->null-string<-has>", "null-string")]
|
||||
[DataRow("<is file version[#=0]->null-string<-has>", "")]
|
||||
[DataRow("<has file version[]->null-string<-has>", "")]
|
||||
[DataRow("<is file version[]->null-string<-has>", "")]
|
||||
[DataRow("<has year->null-int<-has>", "")]
|
||||
[DataRow("<is year[=]->null-int<-has>", "")]
|
||||
[DataRow("<is year[#=0]->null-int<-has>", "")]
|
||||
[DataRow("<is year[0]->null-int<-has>", "")]
|
||||
[DataRow("<!is year[0]->null-int<-has>", "null-int")]
|
||||
[DataRow("<is year[]->null-int<-has>", "")]
|
||||
[DataRow("<has FAKE->unknown-tag<-has>", "")]
|
||||
[DataRow("<is FAKE[=]->unknown-tag<-has>", "")]
|
||||
[DataRow("<!is FAKE[=]->unknown-tag<-has>", "unknown-tag")]
|
||||
[DataRow("<is FAKE[=foobar]->unknown-tag<-has>", "")]
|
||||
[DataRow("<is FAKE[#=0]->unknown-tag<-has>", "")]
|
||||
[DataRow("<is FAKE[]->unknown-tag<-has>", "")]
|
||||
[DataRow("<has narrator->empty-list<-has>", "")]
|
||||
[DataRow("<is narrator[=foobar]->empty-list<-has>", "")]
|
||||
[DataRow("<!is narrator[=foobar]->empty-list<-has>", "empty-list")]
|
||||
[DataRow("<is narrator[!=foobar]->empty-list<-has>", "")]
|
||||
[DataRow("<!is narrator[!=foobar]->empty-list<-has>", "empty-list")]
|
||||
[DataRow("<is narrator[=]->empty-list<-has>", "")]
|
||||
[DataRow("<is narrator[~.*]->empty-list<-has>", "")]
|
||||
[DataRow("<is narrator[<1]->empty-list<-has>", "empty-list")]
|
||||
@@ -484,11 +493,13 @@ namespace TemplatesTests
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<has id->true<-has>", "true")]
|
||||
[DataRow("<!has id->false<-has>", "")]
|
||||
[DataRow("<has title->true<-has>", "true")]
|
||||
[DataRow("<has title short->true<-has>", "true")]
|
||||
[DataRow("<has audible title->true<-has>", "true")]
|
||||
[DataRow("<has audible subtitle->true<-has>", "true")]
|
||||
[DataRow("<has author->true<-has>", "true")]
|
||||
[DataRow("<!has author->false<-has>", "")]
|
||||
[DataRow("<has first author->true<-has>", "true")]
|
||||
[DataRow("<has series->true<-has>", "true")]
|
||||
[DataRow("<has first series->true<-has>", "true")]
|
||||
@@ -497,6 +508,8 @@ namespace TemplatesTests
|
||||
[DataRow("<has samplerate->true<-has>", "true")]
|
||||
[DataRow("<has channels->true<-has>", "true")]
|
||||
[DataRow("<has codec->true<-has>", "true")]
|
||||
[DataRow(@"<is codec[=aac\[lc\]\\mp3]->true<-has>", "true")]
|
||||
[DataRow(@"<is codec[=aac\[lc\]\\mp4]->true<-has>", "")]
|
||||
[DataRow("<has account->true<-has>", "true")]
|
||||
[DataRow("<has account nickname->true<-has>", "true")]
|
||||
[DataRow("<has locale->true<-has>", "true")]
|
||||
@@ -510,9 +523,11 @@ namespace TemplatesTests
|
||||
[DataRow("<has ch#->true<-has>", "true")]
|
||||
[DataRow("<has ch# 0->true<-has>", "true")]
|
||||
[DataRow("<is title[=A Study in Scarlet: An Audible Original Drama]->true<-has>", "true")]
|
||||
[DataRow("<!is title[=A Study in Scarlet: An Audible Original Drama]->false<-has>", "")]
|
||||
[DataRow("<is title[U][=A STUDY IN SCARLET: AN AUDIBLE ORIGINAL DRAMA]->true<-has>", "true")]
|
||||
[DataRow("<is title[#=45]->true<-has>", "true")]
|
||||
[DataRow("<is title[!=foo]->true<-has>", "true")]
|
||||
[DataRow("<!is title[!=foo]->false<-has>", "")]
|
||||
[DataRow("<is title[~A Study.*]->true<-has>", "true")]
|
||||
[DataRow("<is title[foo]->true<-has>", "")]
|
||||
[DataRow("<is ch count[>=1]->true<-has>", "true")]
|
||||
@@ -524,15 +539,21 @@ namespace TemplatesTests
|
||||
[DataRow("<is author[#=2]->true<-has>", "true")]
|
||||
[DataRow("<is author[=Arthur Conan Doyle]->true<-has>", "true")]
|
||||
[DataRow("<is author[format({L})][=Doyle]->true<-has>", "true")]
|
||||
[DataRow("<!is author[format({L})][=Doyle]->false<-has>", "")]
|
||||
[DataRow("<is author[format({L})][!=Doyle]->true<-has>", "true")]
|
||||
[DataRow("<!is author[format({L})][!=Doyle]->false<-has>", "")]
|
||||
[DataRow("<is author[format({L})separator(:)][=Doyle:Fry]->true<-has>", "true")]
|
||||
[DataRow("<is author[>=3]->true<-has>", "")]
|
||||
[DataRow("<is author[slice(99)][~.*]->true<-has>", "")]
|
||||
[DataRow(@"<is author[slice(99)][~.\*]->true<-has>", "")]
|
||||
[DataRow("<is author[slice(99)separator(:)][~.*]->true<-has>", "")]
|
||||
[DataRow("<is author[slice(-9)separator(:)][~.*]->true<-has>", "")]
|
||||
[DataRow("<is author[slice(2..1)separator(:)][~.*]->true<-has>", "")]
|
||||
[DataRow("<is author[slice(-1..1)separator(:)][~.*]->true<-has>", "")]
|
||||
[DataRow("<is author[slice(-1..-2)separator(:)][~.*]->true<-has>", "")]
|
||||
[DataRow("<is author[=Sherlock]->true<-has>", "")]
|
||||
[DataRow("<!is author[=Sherlock]->false<-has>", "false")]
|
||||
[DataRow("<is author[!=Sherlock]->true<-has>", "true")]
|
||||
[DataRow("<!is author[!=Sherlock]->false<-has>", "")]
|
||||
[DataRow("<is tag[=Tag1]->true<-has>", "true")]
|
||||
[DataRow("<is tag[separator(:)slice(-2..)][=Tag2:Tag3]->true<-has>", "true")]
|
||||
public void HasValue_test(string template, string expected)
|
||||
|
||||
@@ -123,10 +123,10 @@ Text formatting can change length and case of the text. Use <#>, <#><case> or <c
|
||||
### Text List Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | -------------------------------------------- |
|
||||
| separator() | Speficy the text used to join<br>multiple entries.<br><br>Default is ", " | `<tag[separator(_)]>` | Tag1_Tag2_Tag3_Tag4_Tag5 |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |--------------------------------------------- | ---------------------------------------------|
|
||||
| separator() | Specify the text used to join<br>multiple entries.<br><br>Default is ", " | `<tag[separator(_)]>` | Tag1_Tag2_Tag3_Tag4_Tag5 |
|
||||
| format(\{S\}) | Formats the entries by placing their values into the specified template.<br>Use {S:[Text_Formatter](#text-formatters)} to place the entry and optionally apply a format. | `<tag[format(Tag={S:l})`<br>`separator(;)]>` | Tag=tag1;Tag=tag2;Tag=tag3;Tag=tag4;Tag=tag5 |
|
||||
| sort(S) | Sorts the elements by their value.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<tag[sort(s)`<br>`separator(; )]>` | Tag5;Tag4;Tag4;Tag2;Tag1 |
|
||||
| sort(S) | Sorts the elements by their value.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<tag[sort(s)`<br>`separator(;)]>` | Tag5;Tag4;Tag3;Tag2;Tag1 |
|
||||
| max(#) | Only use the first # of entries | `<tag[max(1)]>` | Tag1 |
|
||||
| slice(#) | Only use the nth entry of the list | `<tag[slice(2)]>` | Tag2 |
|
||||
| slice(#..) | Only use entries of the list starting from # | `<tag[slice(2..)]>` | Tag2, Tag3, Tag4, Tag5 |
|
||||
@@ -136,15 +136,15 @@ Text formatting can change length and case of the text. Use <#>, <#><case> or <c
|
||||
|
||||
### Series Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| \{N \| # \| ID\} | Formats the series using<br>the series part tags.<br>\{N:[Text_Formatter](#text-formatters)\} = Series Name<br>\{#:[Number_Formatter](#number-formatters)\} = Number order in series<br>\{ID:[Text_Formatter](#text-formatters)\} = Audible Series ID<br><br>Formatter parts are optional and introduced by the colon. If specified the string will be used to format the part using the correspoing formatter.<br><br>Default is \{N\} | `<first series>`<hr>`<first series[{N:l}]>`<hr>`<first series[{N}, {#}, {ID}]>`<hr>`<first series[{N:10U}, {ID}, {#:00.0}]>` | Sherlock Holmes<hr>sherlock holmes<hr>Sherlock Holmes, 1-6, B08376S3R2<hr>SHERLOCK H, B08376S3R2, 01.0-06.0 |
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| \{N \| # \| ID\} | Formats the series using<br>the series part tags.<br>\{N:[Text_Formatter](#text-formatters)\} = Series Name<br>\{#:[Number_Formatter](#number-formatters)\} = Number order in series<br>\{ID:[Text_Formatter](#text-formatters)\} = Audible Series ID<br><br>Formatter parts are optional and introduced by the colon. If specified the string will be used to format the part using the corresponding formatter.<br><br>Default is \{N\} | `<first series>`<hr>`<first series[{N:l}]>`<hr>`<first series[{N}, {#}, {ID}]>`<hr>`<first series[{N:10U}, {ID}, {#:00.0}]>` | Sherlock Holmes<hr>sherlock holmes<hr>Sherlock Holmes, 1-6, B08376S3R2<hr>SHERLOCK H, B08376S3R2, 01.0-06.0 |
|
||||
|
||||
### Series List Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| separator() | Speficy the text used to join<br>multiple series names.<br><br>Default is ", " | `<series[separator(; )]>` | Sherlock Holmes; Some Other Series |
|
||||
| separator() | Specify the text used to join<br>multiple series names.<br><br>Default is ", " | `<series[separator(; )]>` | Sherlock Holmes; Some Other Series |
|
||||
| format(\{N \| # \| ID\}) | Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above. | `<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<series[format({ID}-{N}, {#:00.0})]>` | Sherlock Holmes, 1-6; Book Collection, 1<hr>B08376S3R2-Sherlock Holmes, 01.0-06.0, B000000000-Book Collection, 01.0 |
|
||||
| sort(N \| # \| ID) | Sorts the series by name, number or ID.<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multi‑level sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<series[sort(N)`<br>`separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 |
|
||||
| max(#) | Only use the first # of series | `<series[max(1)]>` | Sherlock Holmes |
|
||||
@@ -160,7 +160,7 @@ Text formatting can change length and case of the text. Use <#>, <#><case> or <c
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| separator() | Speficy the text used to join<br>multiple people's names.<br><br>Default is ", " | `<author[separator(; )]>` | Arthur Conan Doyle; Stephen Fry |
|
||||
| separator() | Specify the text used to join<br>multiple people's names.<br><br>Default is ", " | `<author[separator(; )]>` | Arthur Conan Doyle; Stephen Fry |
|
||||
| format(\{T \| F \| M \| L \| S \| ID\}) | Formats the human name using<br>the name part tags.<br>See [Name Formatter Usage](#name-formatters) above. | `<author[format({L:u}, {F})`<br>`separator(; )]>`<hr>`<author[format({L}, {F:1}.`<br>`_{ID}_) separator(; )]>` | DOYLE, Arthur; FRY, Stephen<hr>Doyle, A. \_B000AQ43GQ\_;<br>Fry, S. \_B000APAGVS\_ |
|
||||
| sort(T \| F \| M \| L \| S \| ID) | Sorts the names by title,<br> first, middle, or last name,<br>suffix or Audible Contributor ID<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multi‑level sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<author[sort(M)]>`<hr>`<author[sort(Fl)]>`<hr><author[sort(L FM ID)]> | Stephen Fry, Arthur Conan Doyle<hr>Stephen King, Stephen Fry<hr>John P. Smith \_B000TTTBBB\_, John P. Smith \_B000TTTCCC\_, John S. Smith \_B000HHHVVV\_ |
|
||||
| max(#) | Only use the first # of names<br><br>Default is all names | `<author[max(1)]>` | Arthur Conan Doyle |
|
||||
@@ -168,9 +168,9 @@ Text formatting can change length and case of the text. Use <#>, <#><case> or <c
|
||||
|
||||
### Minutes Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------- |
|
||||
| {M \| H \| D} | Format the minutes value in terms of minutes, hours and days.<br>{D:[Number_Formatter](#number-formatter) = Number of full days<br>{H:[Number_Formatter](#number-formatter) = Number of full (remaining) hours<br>{M:[Number_Formatter](#number-formatter) = Number of (remaining) minutes<br><br>Default is {M} | `<minutes[{M:4}minutes]>`<hr>`<minutes[{D:2}d {M:2}m]>`<hr>`<minutes[{D}-{H}-{M}]>` | 03000minutes<hr>02d 120m<hr>2-2-0 |
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------- |
|
||||
| {M \| H \| D} | Format the minutes value in terms of minutes, hours and days.<br>{D:[Number_Formatter](#number-formatters) = Number of full days<br>{H:[Number_Formatter](#number-formatters) = Number of full (remaining) hours<br>{M:[Number_Formatter](#number-formatters) = Number of (remaining) minutes<br><br>Default is {M} | `<minutes[{M:4}minutes]>`<hr>`<minutes[{D:2}d {M:2}m]>`<hr>`<minutes[{D}-{H}-{M}]>` | 03000minutes<hr>02d 120m<hr>2-2-0 |
|
||||
|
||||
### Number Formatters
|
||||
|
||||
@@ -183,7 +183,7 @@ For more custom formatters and examples, [see this guide from Microsoft](https:/
|
||||
|
||||
### Date Formatters
|
||||
|
||||
Form more standard formatters, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings).
|
||||
For more standard formatters, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings).
|
||||
|
||||
#### Standard DateTime Formatters
|
||||
|
||||
|
||||
Reference in New Issue
Block a user