44
55namespace PrompterOne . Core . Services . Editor ;
66
7- public sealed class TpsFrontMatterDocumentService
7+ public sealed partial class TpsFrontMatterDocumentService
88{
9- private static readonly Regex FrontMatterRegex = new (
10- @"\A---\s*\r?\n(?<front>.*?)\r?\n---\s*\r?\n?" ,
11- RegexOptions . Singleline | RegexOptions . Compiled ) ;
12-
139 private static readonly string [ ] PreferredMetadataOrder =
1410 [
1511 MetadataKeys . Title ,
16- MetadataKeys . Author ,
1712 MetadataKeys . Profile ,
13+ MetadataKeys . Duration ,
1814 MetadataKeys . BaseWpm ,
19- MetadataKeys . DisplayDuration ,
20- MetadataKeys . XslowOffset ,
21- MetadataKeys . SlowOffset ,
22- MetadataKeys . FastOffset ,
23- MetadataKeys . XfastOffset ,
24- MetadataKeys . Version ,
25- MetadataKeys . Created
15+ MetadataKeys . Author ,
16+ MetadataKeys . Created ,
17+ MetadataKeys . Version
2618 ] ;
2719
20+ [ GeneratedRegex ( @"\A---\s*\r?\n(?<front>.*?)\r?\n---\s*\r?\n?" , RegexOptions . Singleline ) ]
21+ private static partial Regex FrontMatterRegex ( ) ;
22+
2823 public TpsFrontMatterDocument Parse ( string ? text )
2924 {
3025 if ( string . IsNullOrWhiteSpace ( text ) )
3126 {
3227 return new TpsFrontMatterDocument ( new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) , string . Empty , 0 ) ;
3328 }
3429
35- var match = FrontMatterRegex . Match ( text ) ;
30+ var match = FrontMatterRegex ( ) . Match ( text ) ;
3631 if ( ! match . Success )
3732 {
3833 return new TpsFrontMatterDocument ( new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) , text , 0 ) ;
@@ -45,25 +40,23 @@ public TpsFrontMatterDocument Parse(string? text)
4540 public string Build ( IReadOnlyDictionary < string , string > metadata , string ? body )
4641 {
4742 var builder = new StringBuilder ( ) ;
48- builder . AppendLine ( MetadataTokens . Delimiter ) ;
43+ builder . AppendLine ( "---" ) ;
4944
5045 foreach ( var key in PreferredMetadataOrder . Where ( metadata . ContainsKey ) )
5146 {
52- builder . Append ( key ) ;
53- builder . Append ( MetadataTokens . Separator ) ;
54- builder . AppendLine ( FormatMetadataValue ( key , metadata [ key ] ) ) ;
47+ AppendMetadataEntry ( builder , key , metadata [ key ] , IsNumericKey ( key ) ) ;
5548 }
5649
50+ AppendSpeedOffsets ( builder , metadata ) ;
51+
5752 foreach ( var entry in metadata
58- . Where ( entry => ! PreferredMetadataOrder . Contains ( entry . Key , StringComparer . OrdinalIgnoreCase ) )
53+ . Where ( entry => ! PreferredMetadataOrder . Contains ( entry . Key , StringComparer . OrdinalIgnoreCase ) && ! IsSpeedOffsetKey ( entry . Key ) )
5954 . OrderBy ( entry => entry . Key , StringComparer . OrdinalIgnoreCase ) )
6055 {
61- builder . Append ( entry . Key ) ;
62- builder . Append ( MetadataTokens . Separator ) ;
63- builder . AppendLine ( FormatMetadataValue ( entry . Key , entry . Value ) ) ;
56+ AppendMetadataEntry ( builder , entry . Key , entry . Value , IsNumericKey ( entry . Key ) ) ;
6457 }
6558
66- builder . AppendLine ( MetadataTokens . Delimiter ) ;
59+ builder . AppendLine ( "---" ) ;
6760 builder . AppendLine ( ) ;
6861 builder . Append ( ( body ?? string . Empty ) . TrimStart ( ) ) ;
6962 return builder . ToString ( ) ;
@@ -95,104 +88,165 @@ public string Upsert(string? text, IReadOnlyDictionary<string, string?> updates)
9588
9689 public string ResolveTitle ( string ? text , string fallback )
9790 {
98- var document = Parse ( text ) ;
99- return document . Metadata . TryGetValue ( MetadataKeys . Title , out var title ) &&
100- ! string . IsNullOrWhiteSpace ( title )
101- ? title . Trim ( )
102- : fallback ;
91+ return ResolveValue ( text , MetadataKeys . Title , fallback ) ;
10392 }
10493
10594 public string ResolveValue ( string ? text , string key , string fallback )
10695 {
10796 var document = Parse ( text ) ;
108- return document . Metadata . TryGetValue ( key , out var value ) &&
109- ! string . IsNullOrWhiteSpace ( value )
97+ return document . Metadata . TryGetValue ( key , out var value ) && ! string . IsNullOrWhiteSpace ( value )
11098 ? value . Trim ( )
11199 : fallback ;
112100 }
113101
114- private static string FormatMetadataValue ( string key , string ? value )
115- {
116- value ??= string . Empty ;
117-
118- if ( IsNumericMetadataKey ( key ) && int . TryParse ( value , out _ ) )
119- {
120- return value ;
121- }
122-
123- return $ "{ MetadataTokens . Quote } { value . Replace ( MetadataTokens . Quote , MetadataTokens . EscapedQuote , StringComparison . Ordinal ) } { MetadataTokens . Quote } ";
124- }
125-
126102 public static class MetadataKeys
127103 {
128- public const string Author = "author" ;
129- public const string BaseWpm = "base_wpm" ;
130- public const string Created = "created" ;
131- public const string DisplayDuration = "display_duration" ;
132- public const string FastOffset = "fast_offset" ;
133- public const string Profile = "profile" ;
134- public const string SlowOffset = "slow_offset" ;
135- public const string Title = "title" ;
136- public const string Version = "version" ;
137- public const string XfastOffset = "xfast_offset" ;
138- public const string XslowOffset = "xslow_offset" ;
104+ public const string Author = TpsSpec . FrontMatterKeys . Author ;
105+ public const string BaseWpm = TpsSpec . FrontMatterKeys . BaseWpm ;
106+ public const string Created = TpsSpec . FrontMatterKeys . Created ;
107+ public const string Duration = TpsSpec . FrontMatterKeys . Duration ;
108+ public const string Profile = TpsSpec . FrontMatterKeys . Profile ;
109+ public const string SpeedOffsetFast = TpsSpec . FrontMatterKeys . SpeedOffsetsFast ;
110+ public const string SpeedOffsetSlow = TpsSpec . FrontMatterKeys . SpeedOffsetsSlow ;
111+ public const string SpeedOffsetXfast = TpsSpec . FrontMatterKeys . SpeedOffsetsXfast ;
112+ public const string SpeedOffsetXslow = TpsSpec . FrontMatterKeys . SpeedOffsetsXslow ;
113+ public const string Title = TpsSpec . FrontMatterKeys . Title ;
114+ public const string Version = TpsSpec . FrontMatterKeys . Version ;
139115 }
140116
141117 private static Dictionary < string , string > ParseMetadata ( string frontMatterText )
142118 {
143119 var metadata = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
144120 string ? currentSection = null ;
145121
146- foreach ( var rawLine in frontMatterText . Split ( '\n ' , StringSplitOptions . RemoveEmptyEntries ) )
122+ foreach ( var rawLine in frontMatterText . Split ( '\n ' ) )
147123 {
148- var line = rawLine . TrimEnd ( ) ;
149- var trimmedLine = line . Trim ( ) ;
150- if ( string . IsNullOrWhiteSpace ( trimmedLine ) )
124+ if ( string . IsNullOrWhiteSpace ( rawLine ) )
151125 {
152126 continue ;
153127 }
154128
155- var separatorIndex = trimmedLine . IndexOf ( ':' ) ;
129+ var indentationLength = rawLine . Length - rawLine . TrimStart ( ) . Length ;
130+ var line = rawLine . Trim ( ) ;
131+ var separatorIndex = line . IndexOf ( ':' ) ;
156132 if ( separatorIndex <= 0 )
157133 {
158134 continue ;
159135 }
160136
161- var key = trimmedLine [ ..separatorIndex ] . Trim ( ) ;
162- var value = trimmedLine [ ( separatorIndex + 1 ) ..] . Trim ( ) . Trim ( '"' , '\' ' ) ;
163- var isNestedLine = line . Length != line . TrimStart ( ) . Length ;
164-
165- if ( isNestedLine && ! string . IsNullOrWhiteSpace ( currentSection ) )
137+ var key = line [ ..separatorIndex ] . Trim ( ) ;
138+ var value = TrimValue ( line [ ( separatorIndex + 1 ) ..] ) ;
139+ if ( indentationLength > 0 && ! string . IsNullOrWhiteSpace ( currentSection ) )
166140 {
167- metadata [ $ "{ currentSection } .{ key } "] = value ;
141+ var compositeKey = $ "{ currentSection } .{ key } ";
142+ if ( ! IsLegacyKey ( compositeKey ) )
143+ {
144+ metadata [ compositeKey ] = value ;
145+ }
146+
168147 continue ;
169148 }
170149
150+ currentSection = string . IsNullOrWhiteSpace ( value ) ? key : null ;
171151 if ( string . IsNullOrWhiteSpace ( value ) )
172152 {
173- currentSection = key ;
174153 continue ;
175154 }
176155
177- currentSection = null ;
178- metadata [ key ] = value ;
156+ if ( ! IsLegacyKey ( key ) )
157+ {
158+ metadata [ key ] = value ;
159+ }
179160 }
180161
181- return TpsFrontMatterMetadataNormalizer . Normalize ( metadata ) ;
162+ return metadata ;
182163 }
183164
184- private static bool IsNumericMetadataKey ( string key ) =>
185- string . Equals ( key , MetadataKeys . BaseWpm , StringComparison . OrdinalIgnoreCase ) ||
186- string . Equals ( key , MetadataKeys . XslowOffset , StringComparison . OrdinalIgnoreCase ) ||
187- string . Equals ( key , MetadataKeys . SlowOffset , StringComparison . OrdinalIgnoreCase ) ||
188- string . Equals ( key , MetadataKeys . FastOffset , StringComparison . OrdinalIgnoreCase ) ||
189- string . Equals ( key , MetadataKeys . XfastOffset , StringComparison . OrdinalIgnoreCase ) ;
165+ private static void AppendSpeedOffsets ( StringBuilder builder , IReadOnlyDictionary < string , string > metadata )
166+ {
167+ var speedOffsetKeys = new [ ]
168+ {
169+ MetadataKeys . SpeedOffsetXslow ,
170+ MetadataKeys . SpeedOffsetSlow ,
171+ MetadataKeys . SpeedOffsetFast ,
172+ MetadataKeys . SpeedOffsetXfast
173+ } ;
174+
175+ if ( ! speedOffsetKeys . Any ( metadata . ContainsKey ) )
176+ {
177+ return ;
178+ }
179+
180+ builder . AppendLine ( "speed_offsets:" ) ;
181+ AppendNestedSpeedOffset ( builder , metadata , MetadataKeys . SpeedOffsetXslow , TpsSpec . Tags . Xslow ) ;
182+ AppendNestedSpeedOffset ( builder , metadata , MetadataKeys . SpeedOffsetSlow , TpsSpec . Tags . Slow ) ;
183+ AppendNestedSpeedOffset ( builder , metadata , MetadataKeys . SpeedOffsetFast , TpsSpec . Tags . Fast ) ;
184+ AppendNestedSpeedOffset ( builder , metadata , MetadataKeys . SpeedOffsetXfast , TpsSpec . Tags . Xfast ) ;
185+ }
186+
187+ private static void AppendNestedSpeedOffset ( StringBuilder builder , IReadOnlyDictionary < string , string > metadata , string key , string nestedKey )
188+ {
189+ if ( ! metadata . TryGetValue ( key , out var value ) )
190+ {
191+ return ;
192+ }
193+
194+ builder . Append ( " " ) ;
195+ builder . Append ( nestedKey ) ;
196+ builder . Append ( ": " ) ;
197+ builder . AppendLine ( FormatValue ( value , numeric : true ) ) ;
198+ }
199+
200+ private static void AppendMetadataEntry ( StringBuilder builder , string key , string value , bool numeric )
201+ {
202+ builder . Append ( key ) ;
203+ builder . Append ( ": " ) ;
204+ builder . AppendLine ( FormatValue ( value , numeric ) ) ;
205+ }
206+
207+ private static string TrimValue ( string value )
208+ {
209+ var trimmed = value . Trim ( ) ;
210+ if ( trimmed . Length >= 2 &&
211+ ( ( trimmed . StartsWith ( "\" " , StringComparison . Ordinal ) && trimmed . EndsWith ( "\" " , StringComparison . Ordinal ) ) ||
212+ ( trimmed . StartsWith ( "'" , StringComparison . Ordinal ) && trimmed . EndsWith ( "'" , StringComparison . Ordinal ) ) ) )
213+ {
214+ return trimmed [ 1 ..^ 1 ] ;
215+ }
216+
217+ return trimmed ;
218+ }
219+
220+ private static string FormatValue ( string value , bool numeric )
221+ {
222+ return numeric && int . TryParse ( value , out _ )
223+ ? value
224+ : $ "\" { value . Replace ( "\" " , "\\ \" " , StringComparison . Ordinal ) } \" ";
225+ }
226+
227+ private static bool IsNumericKey ( string key )
228+ {
229+ return key . Equals ( MetadataKeys . BaseWpm , StringComparison . OrdinalIgnoreCase ) || IsSpeedOffsetKey ( key ) ;
230+ }
231+
232+ private static bool IsSpeedOffsetKey ( string key )
233+ {
234+ return key . Equals ( MetadataKeys . SpeedOffsetXslow , StringComparison . OrdinalIgnoreCase ) ||
235+ key . Equals ( MetadataKeys . SpeedOffsetSlow , StringComparison . OrdinalIgnoreCase ) ||
236+ key . Equals ( MetadataKeys . SpeedOffsetFast , StringComparison . OrdinalIgnoreCase ) ||
237+ key . Equals ( MetadataKeys . SpeedOffsetXfast , StringComparison . OrdinalIgnoreCase ) ;
238+ }
190239
191- private static class MetadataTokens
240+ private static bool IsLegacyKey ( string key )
192241 {
193- public const string Delimiter = "---" ;
194- public const string EscapedQuote = "\\ \" " ;
195- public const string Quote = "\" " ;
196- public const string Separator = ": " ;
242+ return key . Equals ( TpsSpec . LegacyKeys . DisplayDuration , StringComparison . OrdinalIgnoreCase ) ||
243+ key . Equals ( TpsSpec . LegacyKeys . XslowOffset , StringComparison . OrdinalIgnoreCase ) ||
244+ key . Equals ( TpsSpec . LegacyKeys . SlowOffset , StringComparison . OrdinalIgnoreCase ) ||
245+ key . Equals ( TpsSpec . LegacyKeys . FastOffset , StringComparison . OrdinalIgnoreCase ) ||
246+ key . Equals ( TpsSpec . LegacyKeys . XfastOffset , StringComparison . OrdinalIgnoreCase ) ||
247+ key . Equals ( TpsSpec . LegacyKeys . PresetsXslow , StringComparison . OrdinalIgnoreCase ) ||
248+ key . Equals ( TpsSpec . LegacyKeys . PresetsSlow , StringComparison . OrdinalIgnoreCase ) ||
249+ key . Equals ( TpsSpec . LegacyKeys . PresetsFast , StringComparison . OrdinalIgnoreCase ) ||
250+ key . Equals ( TpsSpec . LegacyKeys . PresetsXfast , StringComparison . OrdinalIgnoreCase ) ;
197251 }
198252}
0 commit comments