33// See the LICENSE file in the project root for more information
44
55using System . Buffers ;
6+ using System . ComponentModel . DataAnnotations ;
67using System . Diagnostics ;
8+ using System . Globalization ;
9+ using System . Text ;
10+ using System . Text . Json ;
11+ using Elastic . Documentation ;
712using Elastic . Markdown . Diagnostics ;
813using Markdig . Helpers ;
914using Markdig . Parsers ;
1015using Markdig . Renderers ;
1116using Markdig . Renderers . Html ;
1217using Markdig . Syntax ;
1318using Markdig . Syntax . Inlines ;
19+ using NetEscapades . EnumGenerators ;
1420
1521namespace Elastic . Markdown . Myst . InlineParsers . Substitution ;
1622
1723[ DebuggerDisplay ( "{GetType().Name} Line: {Line}, Found: {Found}, Replacement: {Replacement}" ) ]
18- public class SubstitutionLeaf ( string content , bool found , string replacement ) : CodeInline ( content )
24+ public class SubstitutionLeaf ( string content , bool found , string replacement )
25+ : CodeInline ( content )
1926{
2027 public bool Found { get ; } = found ;
2128 public string Replacement { get ; } = replacement ;
29+ public IReadOnlyCollection < SubstitutionMutation > ? Mutations { get ; set ; }
30+ }
31+
32+ [ EnumExtensions ]
33+ public enum SubstitutionMutation
34+ {
35+ [ Display ( Name = "M" ) ] MajorComponent ,
36+ [ Display ( Name = "M.x" ) ] MajorX ,
37+ [ Display ( Name = "M.M" ) ] MajorMinor ,
38+ [ Display ( Name = "M+1" ) ] IncreaseMajor ,
39+ [ Display ( Name = "M.M+1" ) ] IncreaseMinor ,
40+ [ Display ( Name = "lc" ) ] LowerCase ,
41+ [ Display ( Name = "uc" ) ] UpperCase ,
42+ [ Display ( Name = "tc" ) ] TitleCase ,
43+ [ Display ( Name = "c" ) ] Capitalize ,
44+ [ Display ( Name = "kc" ) ] KebabCase ,
45+ [ Display ( Name = "sc" ) ] SnakeCase ,
46+ [ Display ( Name = "cc" ) ] CamelCase ,
47+ [ Display ( Name = "pc" ) ] PascalCase ,
48+ [ Display ( Name = "trim" ) ] Trim
2249}
2350
2451public class SubstitutionRenderer : HtmlObjectRenderer < SubstitutionLeaf >
2552{
26- protected override void Write ( HtmlRenderer renderer , SubstitutionLeaf obj ) =>
27- renderer . Write ( obj . Found ? obj . Replacement : obj . Content ) ;
53+ protected override void Write ( HtmlRenderer renderer , SubstitutionLeaf leaf )
54+ {
55+ if ( ! leaf . Found )
56+ {
57+ _ = renderer . Write ( leaf . Content ) ;
58+ return ;
59+ }
60+
61+ var replacement = leaf . Replacement ;
62+ if ( leaf . Mutations is null or { Count : 0 } )
63+ {
64+ _ = renderer . Write ( replacement ) ;
65+ return ;
66+ }
67+
68+ foreach ( var mutation in leaf . Mutations )
69+ {
70+ var ( success , update ) = mutation switch
71+ {
72+ SubstitutionMutation . MajorComponent => TryGetVersion ( replacement , v => $ "{ v . Major } ") ,
73+ SubstitutionMutation . MajorX => TryGetVersion ( replacement , v => $ "{ v . Major } .x") ,
74+ SubstitutionMutation . MajorMinor => TryGetVersion ( replacement , v => $ "{ v . Major } .{ v . Minor } ") ,
75+ SubstitutionMutation . IncreaseMajor => TryGetVersion ( replacement , v => $ "{ v . Major + 1 } .0.0") ,
76+ SubstitutionMutation . IncreaseMinor => TryGetVersion ( replacement , v => $ "{ v . Major } .{ v . Minor + 1 } .0") ,
77+ SubstitutionMutation . LowerCase => ( true , replacement . ToLowerInvariant ( ) ) ,
78+ SubstitutionMutation . UpperCase => ( true , replacement . ToUpperInvariant ( ) ) ,
79+ SubstitutionMutation . Capitalize => ( true , Capitalize ( replacement ) ) ,
80+ SubstitutionMutation . KebabCase => ( true , ToKebabCase ( replacement ) ) ,
81+ SubstitutionMutation . CamelCase => ( true , ToCamelCase ( replacement ) ) ,
82+ SubstitutionMutation . PascalCase => ( true , ToPascalCase ( replacement ) ) ,
83+ SubstitutionMutation . SnakeCase => ( true , ToSnakeCase ( replacement ) ) ,
84+ SubstitutionMutation . TitleCase => ( true , TitleCase ( replacement ) ) ,
85+ SubstitutionMutation . Trim => ( true , Trim ( replacement ) ) ,
86+ _ => throw new Exception ( $ "encountered an unknown mutation '{ mutation . ToStringFast ( true ) } '")
87+ } ;
88+ if ( ! success )
89+ {
90+ _ = renderer . Write ( leaf . Content ) ;
91+ return ;
92+ }
93+ replacement = update ;
94+ }
95+ _ = renderer . Write ( replacement ) ;
96+ }
97+
98+ private static string ToCamelCase ( string str ) => JsonNamingPolicy . CamelCase . ConvertName ( str . Replace ( " " , string . Empty ) ) ;
99+ private static string ToSnakeCase ( string str ) => JsonNamingPolicy . SnakeCaseLower . ConvertName ( str ) . Replace ( " " , string . Empty ) ;
100+ private static string ToKebabCase ( string str ) => JsonNamingPolicy . KebabCaseLower . ConvertName ( str ) . Replace ( " " , string . Empty ) ;
101+ private static string ToPascalCase ( string str ) => TitleCase ( str ) . Replace ( " " , string . Empty ) ;
102+
103+ private static string TitleCase ( string str ) => CultureInfo . InvariantCulture . TextInfo . ToTitleCase ( str ) ;
104+
105+ private static string Trim ( string str ) =>
106+ str . AsSpan ( ) . Trim ( [ '!' , ' ' , '\t ' , '\r ' , '\n ' , '.' , ',' , ')' , '(' , ':' , ';' , '<' , '>' , '[' , ']' ] ) . ToString ( ) ;
107+
108+ private static string Capitalize ( string input ) =>
109+ input switch
110+ {
111+ null => string . Empty ,
112+ "" => string . Empty ,
113+ _ => string . Concat ( input [ 0 ] . ToString ( ) . ToUpper ( ) , input . AsSpan ( 1 ) )
114+ } ;
115+
116+ private ( bool , string ) TryGetVersion ( string version , Func < SemVersion , string > mutate )
117+ {
118+ if ( ! SemVersion . TryParse ( version , out var v ) && ! SemVersion . TryParse ( version + ".0" , out v ) )
119+ return ( false , string . Empty ) ;
120+
121+ return ( true , mutate ( v ) ) ;
122+ }
28123}
29124
30125public class SubstitutionParser : InlineParser
31126{
32127 public SubstitutionParser ( ) => OpeningCharacters = [ '{' ] ;
33128
34- private readonly SearchValues < char > _values = SearchValues . Create ( [ '\r ' , '\n ' , ' ' , ' \t ', '}' ] ) ;
129+ private readonly SearchValues < char > _values = SearchValues . Create ( [ '\r ' , '\n ' , '\t ' , '}' ] ) ;
35130
36131 public override bool Match ( InlineProcessor processor , ref StringSlice slice )
37132 {
@@ -84,6 +179,10 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
84179 var key = content . ToString ( ) . Trim ( [ '{' , '}' ] ) . ToLowerInvariant ( ) ;
85180 var found = false ;
86181 var replacement = string . Empty ;
182+ var components = key . Split ( '|' ) ;
183+ if ( components . Length > 1 )
184+ key = components [ 0 ] . Trim ( ) ;
185+
87186 if ( context . Substitutions . TryGetValue ( key , out var value ) )
88187 {
89188 found = true ;
@@ -100,7 +199,6 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
100199 var start = processor . GetSourcePosition ( startPosition , out var line , out var column ) ;
101200 var end = processor . GetSourcePosition ( slice . Start ) ;
102201 var sourceSpan = new SourceSpan ( start , end ) ;
103-
104202 var substitutionLeaf = new SubstitutionLeaf ( content . ToString ( ) , found , replacement )
105203 {
106204 Delimiter = '{' ,
@@ -109,8 +207,31 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
109207 Column = column ,
110208 DelimiterCount = openSticks
111209 } ;
210+
112211 if ( ! found )
113212 processor . EmitError ( line + 1 , column + 3 , substitutionLeaf . Span . Length - 3 , $ "Substitution key {{{key}}} is undefined") ;
213+ else
214+ {
215+ List < SubstitutionMutation > ? mutations = null ;
216+ if ( components . Length >= 10 )
217+ processor . EmitError ( line + 1 , column + 3 , substitutionLeaf . Span . Length - 3 , $ "Substitution key {{{key}}} defines too many mutations, none will be applied") ;
218+ else if ( components . Length > 1 )
219+ {
220+ foreach ( var c in components [ 1 ..] )
221+ {
222+ if ( SubstitutionMutationExtensions . TryParse ( c . Trim ( ) , out var mutation , true , true ) )
223+ {
224+ mutations ??= [ ] ;
225+ mutations . Add ( mutation ) ;
226+ }
227+ else
228+ processor . EmitError ( line + 1 , column + 3 , substitutionLeaf . Span . Length - 3 , $ "Mutation '{ c } ' on {{{key}}} is undefined") ;
229+ }
230+ }
231+
232+ substitutionLeaf . Mutations = mutations ;
233+ }
234+
114235
115236 if ( processor . TrackTrivia )
116237 {
0 commit comments