This repository was archived by the owner on Jan 3, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 313
Expand file tree
/
Copy pathFSharpEmbedResourceText.fs
More file actions
511 lines (455 loc) · 24.5 KB
/
FSharpEmbedResourceText.fs
File metadata and controls
511 lines (455 loc) · 24.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
namespace Microsoft.FSharp.Build
open System.Collections
open System.IO
open Microsoft.Build.Framework
open Microsoft.Build.Utilities
type FSharpEmbedResourceText() =
let mutable _buildEngine : IBuildEngine = null
let mutable _hostObject : ITaskHost = null
let mutable _embeddedText : ITaskItem[] = [||]
let mutable _generatedSource : ITaskItem[] = [||]
let mutable _generatedResx : ITaskItem[] = [||]
let mutable _outputPath : string = ""
let PrintErr(filename, line, msg) =
printfn "%s(%d): error : %s" filename line msg
let Err(filename, line, msg) =
PrintErr(filename, line, msg)
printfn "Note that the syntax of each line is one of these three alternatives:"
printfn "# comment"
printfn "ident,\"string\""
printfn "errNum,ident,\"string\""
failwith (sprintf "there were errors in the file '%s'" filename)
let xmlBoilerPlateString = @"<?xml version=""1.0"" encoding=""utf-8""?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name=""resmimetype"">text/microsoft-resx</resheader>
<resheader name=""version"">2.0</resheader>
<resheader name=""reader"">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name=""writer"">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name=""Name1""><value>this is my long string</value><comment>this is a comment</comment></data>
<data name=""Color1"" type=""System.Drawing.Color, System.Drawing"">Blue</data>
<data name=""Bitmap1"" mimetype=""application/x-microsoft.net.object.binary.base64"">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name=""Icon1"" type=""System.Drawing.Icon, System.Drawing"" mimetype=""application/x-microsoft.net.object.bytearray.base64"">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of ""resheader"" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id=""root"" xmlns="""" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:msdata=""urn:schemas-microsoft-com:xml-msdata"">
<xsd:import namespace=""http://www.w3.org/XML/1998/namespace"" />
<xsd:element name=""root"" msdata:IsDataSet=""true"">
<xsd:complexType>
<xsd:choice maxOccurs=""unbounded"">
<xsd:element name=""metadata"">
<xsd:complexType>
<xsd:sequence>
<xsd:element name=""value"" type=""xsd:string"" minOccurs=""0"" />
</xsd:sequence>
<xsd:attribute name=""name"" use=""required"" type=""xsd:string"" />
<xsd:attribute name=""type"" type=""xsd:string"" />
<xsd:attribute name=""mimetype"" type=""xsd:string"" />
<xsd:attribute ref=""xml:space"" />
</xsd:complexType>
</xsd:element>
<xsd:element name=""assembly"">
<xsd:complexType>
<xsd:attribute name=""alias"" type=""xsd:string"" />
<xsd:attribute name=""name"" type=""xsd:string"" />
</xsd:complexType>
</xsd:element>
<xsd:element name=""data"">
<xsd:complexType>
<xsd:sequence>
<xsd:element name=""value"" type=""xsd:string"" minOccurs=""0"" msdata:Ordinal=""1"" />
<xsd:element name=""comment"" type=""xsd:string"" minOccurs=""0"" msdata:Ordinal=""2"" />
</xsd:sequence>
<xsd:attribute name=""name"" type=""xsd:string"" use=""required"" msdata:Ordinal=""1"" />
<xsd:attribute name=""type"" type=""xsd:string"" msdata:Ordinal=""3"" />
<xsd:attribute name=""mimetype"" type=""xsd:string"" msdata:Ordinal=""4"" />
<xsd:attribute ref=""xml:space"" />
</xsd:complexType>
</xsd:element>
<xsd:element name=""resheader"">
<xsd:complexType>
<xsd:sequence>
<xsd:element name=""value"" type=""xsd:string"" minOccurs=""0"" msdata:Ordinal=""1"" />
</xsd:sequence>
<xsd:attribute name=""name"" type=""xsd:string"" use=""required"" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name=""resmimetype"">
<value>text/microsoft-resx</value>
</resheader>
<resheader name=""version"">
<value>2.0</value>
</resheader>
<resheader name=""reader"">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name=""writer"">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>"
// The kinds of 'holes' we can do
let ComputeHoles filename lineNum (txt:string) : ResizeArray<string> * string =
// takes in a %d%s kind of string, returns array of string and {0}{1} kind of string
let mutable i = 0
let mutable holeNumber = 0
let mutable holes = ResizeArray() // order
let sb = new System.Text.StringBuilder()
let AddHole holeType =
sb.Append(sprintf "{%d}" holeNumber) |> ignore
holeNumber <- holeNumber + 1
holes.Add(holeType)
while i < txt.Length do
if txt.[i] = '%' then
if i+1 = txt.Length then
Err(filename, lineNum, "(at end of string) % must be followed by d, f, s, or %")
else
match txt.[i+1] with
| 'd' -> AddHole "System.Int32"
| 'f' -> AddHole "System.Double"
| 's' -> AddHole "System.String"
| '%' -> sb.Append('%') |> ignore
| c -> Err(filename, lineNum, sprintf "'%%%c' is not a valid sequence, only %%d %%f %%s or %%%%" c)
i <- i + 2
else
match txt.[i] with
| '{' -> sb.Append "{{" |> ignore
| '}' -> sb.Append "}}" |> ignore
| c -> sb.Append c |> ignore
i <- i + 1
//printfn "holes.Length = %d, lineNum = %d" holes.Length //lineNum txt
(holes, sb.ToString())
let Unquote (s : string) =
if s.StartsWith "\"" && s.EndsWith "\"" then s.Substring(1, s.Length - 2)
else failwith "error message string should be quoted"
let ParseLine filename lineNum (txt:string) =
let mutable errNum = None
let identB = new System.Text.StringBuilder()
let mutable i = 0
// parse optional error number
if i < txt.Length && System.Char.IsDigit txt.[i] then
let numB = new System.Text.StringBuilder()
while i < txt.Length && System.Char.IsDigit txt.[i] do
numB.Append txt.[i] |> ignore
i <- i + 1
errNum <- Some(int (numB.ToString()))
if i = txt.Length || txt.[i] <> ',' then
Err(filename, lineNum, sprintf "After the error number '%d' there should be a comma" errNum.Value)
// Skip the comma
i <- i + 1
// parse short identifier
if i < txt.Length && not(System.Char.IsLetter(txt.[i])) then
Err(filename, lineNum, sprintf "The first character in the short identifier should be a letter, but found '%c'" txt.[i])
while i < txt.Length && System.Char.IsLetterOrDigit txt.[i] do
identB.Append txt.[i] |> ignore
i <- i + 1
let ident = identB.ToString()
if ident.Length = 0 then
Err(filename, lineNum, "Did not find the short identifier")
else
if i = txt.Length || txt.[i] <> ',' then
Err(filename, lineNum, sprintf "After the identifier '%s' there should be a comma" ident)
else
// Skip the comma
i <- i + 1
if i = txt.Length then
Err(filename, lineNum, sprintf "After the identifier '%s' and comma, there should be the quoted string resource" ident)
else
let str =
try
System.String.Format(Unquote(txt.Substring i)) // Format turns e.g '\n' into that char, but also requires that we 'escape' curlies in the original .txt file, e.g. "{{"
with
e -> Err(filename, lineNum, sprintf "Error calling System.String.Format (note that curly braces must be escaped, and there cannot be trailing space on the line): >>>%s<<< -- %s" (txt.Substring i) e.Message)
let holes, netFormatString = ComputeHoles filename lineNum str
(lineNum, (errNum,ident), str, holes.ToArray(), netFormatString)
let stringBoilerPlatePrefix = @"
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators
open Microsoft.FSharp.Reflection
open System.Reflection
// (namespaces below for specific case of using the tool to compile FSharp.Core itself)
open Microsoft.FSharp.Core
open Microsoft.FSharp.Core.Operators
open Microsoft.FSharp.Text
open Microsoft.FSharp.Collections
open Printf
"
let StringBoilerPlate filename = @"
// BEGIN BOILERPLATE
static let getCurrentAssembly () =
#if FX_RESHAPED_REFLECTION
typeof<SR>.GetTypeInfo().Assembly
#else
System.Reflection.Assembly.GetExecutingAssembly()
#endif
static let getTypeInfo (t: System.Type) =
#if FX_RESHAPED_REFLECTION
t.GetTypeInfo()
#else
t
#endif
static let resources = lazy (new System.Resources.ResourceManager(""" + filename + @""", getCurrentAssembly()))
static let GetString(name:string) =
let s = resources.Value.GetString(name, System.Globalization.CultureInfo.CurrentUICulture)
#if DEBUG
if null = s then
System.Diagnostics.Debug.Assert(false, sprintf ""**RESOURCE ERROR**: Resource token %s does not exist!"" name)
#endif
s
static let mkFunctionValue (tys: System.Type[]) (impl:obj->obj) =
FSharpValue.MakeFunction(FSharpType.MakeFunctionType(tys.[0],tys.[1]), impl)
static let funTyC = typeof<(obj -> obj)>.GetGenericTypeDefinition()
static let isNamedType(ty:System.Type) = not (ty.IsArray || ty.IsByRef || ty.IsPointer)
static let isFunctionType (ty1:System.Type) =
isNamedType(ty1) && getTypeInfo(ty1).IsGenericType && (ty1.GetGenericTypeDefinition()).Equals(funTyC)
static let rec destFunTy (ty:System.Type) =
if isFunctionType ty then
ty, ty.GetGenericArguments()
else
match getTypeInfo(ty).BaseType with
| null -> failwith ""destFunTy: not a function type""
| b -> destFunTy b
static let buildFunctionForOneArgPat (ty: System.Type) impl =
let _,tys = destFunTy ty
let rty = tys.[1]
// PERF: this technique is a bit slow (e.g. in simple cases, like 'sprintf ""%x""')
mkFunctionValue tys (fun inp -> impl rty inp)
static let capture1 (fmt:string) i args ty (go : obj list -> System.Type -> int -> obj) : obj =
match fmt.[i] with
| '%' -> go args ty (i+1)
| 'd'
| 'f'
| 's' -> buildFunctionForOneArgPat ty (fun rty n -> go (n::args) rty (i+1))
| _ -> failwith ""bad format specifier""
// newlines and tabs get converted to strings when read from a resource file
// this will preserve their original intention
static let postProcessString (s : string) =
s.Replace(""\\n"",""\n"").Replace(""\\t"",""\t"").Replace(""\\r"",""\r"").Replace(""\\\"""", ""\"""")
static let createMessageString (messageString : string) (fmt : Printf.StringFormat<'T>) : 'T =
let fmt = fmt.Value // here, we use the actual error string, as opposed to the one stored as fmt
let len = fmt.Length
/// Function to capture the arguments and then run.
let rec capture args ty i =
if i >= len || (fmt.[i] = '%' && i+1 >= len) then
let b = new System.Text.StringBuilder()
b.AppendFormat(messageString, [| for x in List.rev args -> x |]) |> ignore
box(b.ToString())
// REVIEW: For these purposes, this should be a nop, but I'm leaving it
// in incase we ever decide to support labels for the error format string
// E.g., ""<name>%s<foo>%d""
elif System.Char.IsSurrogatePair(fmt,i) then
capture args ty (i+2)
else
match fmt.[i] with
| '%' ->
let i = i+1
capture1 fmt i args ty capture
| _ ->
capture args ty (i+1)
(unbox (capture [] (typeof<'T>) 0) : 'T)
static let mutable swallowResourceText = false
static let GetStringFunc((messageID : string),(fmt : Printf.StringFormat<'T>)) : 'T =
if swallowResourceText then
sprintf fmt
else
let mutable messageString = GetString(messageID)
messageString <- postProcessString messageString
createMessageString messageString fmt
/// If set to true, then all error messages will just return the filled 'holes' delimited by ',,,'s - this is for language-neutral testing (e.g. localization-invariant baselines).
static member SwallowResourceText with get () = swallowResourceText
and set (b) = swallowResourceText <- b
// END BOILERPLATE
"
let generateResxAndSource (filename:string) =
try
let printMessage message = printfn "FSharpEmbedResourceText: %s" message
let justfilename = Path.GetFileNameWithoutExtension(filename) // .txt
if justfilename |> Seq.exists (System.Char.IsLetterOrDigit >> not) then
Err(filename, 0, sprintf "The filename '%s' is not allowed; only letters and digits can be used, as the filename also becomes the namespace for the SR class" justfilename)
let outFilename = Path.Combine(_outputPath, justfilename + ".fs")
let outXmlFilename = Path.Combine(_outputPath, justfilename + ".resx")
let condition1 = File.Exists(outFilename)
let condition2 = condition1 && File.Exists(outXmlFilename)
let condition3 = condition2 && File.Exists(filename)
let condition4 = condition3 && (File.GetLastWriteTimeUtc(filename) <= File.GetLastWriteTimeUtc(outFilename))
let condition5 = condition4 && (File.GetLastWriteTimeUtc(filename) <= File.GetLastWriteTimeUtc(outXmlFilename) )
if condition5 then
printMessage (sprintf "Skipping generation of %s and %s from %s since up-to-date" outFilename outXmlFilename filename)
Some (filename, outFilename, outXmlFilename)
else
printMessage (sprintf "Generating %s and %s from %s, because condition %d is false, see FSharpEmbedResourceText.fs in the F# source" outFilename outXmlFilename filename (if not condition1 then 1 elif not condition2 then 2 elif not condition3 then 3 elif not condition4 then 4 else 5) )
printMessage (sprintf "Reading %s" filename)
let lines = File.ReadAllLines(filename)
|> Array.mapi (fun i s -> i,s) // keep line numbers
|> Array.filter (fun (i,s) -> not(s.StartsWith "#")) // filter out comments
printMessage (sprintf "Parsing %s" filename)
let stringInfos = lines |> Array.map (fun (i,s) -> ParseLine filename i s)
// now we have array of (lineNum, ident, str, holes, netFormatString) // str has %d, netFormatString has {0}
printMessage (sprintf "Validating %s" filename)
// validate that all the idents are unique
let allIdents = new System.Collections.Generic.Dictionary<string,int>()
for (line,(_,ident),_,_,_) in stringInfos do
if allIdents.ContainsKey(ident) then
Err(filename,line,sprintf "Identifier '%s' is already used previously on line %d - each identifier must be unique" ident allIdents.[ident])
allIdents.Add(ident,line)
printMessage (sprintf "Validating uniqueness of %s" filename)
// validate that all the strings themselves are unique
let allStrs = new System.Collections.Generic.Dictionary<string,(int*string)>()
for (line,(_,ident),str,_,_) in stringInfos do
if allStrs.ContainsKey(str) then
let prevLine,prevIdent = allStrs.[str]
Err(filename,line,sprintf "String '%s' already appears on line %d with identifier '%s' - each string must be unique" str prevLine prevIdent)
allStrs.Add(str,(line,ident))
printMessage (sprintf "Generating %s" outFilename)
use outStream = File.Create outFilename
use out = new StreamWriter(outStream)
fprintfn out "// This is a generated file; the original input is '%s'" filename
fprintfn out "namespace %s" justfilename
fprintfn out "%s" stringBoilerPlatePrefix
fprintfn out "type internal SR private() ="
let theResourceName = justfilename
fprintfn out "%s" (StringBoilerPlate theResourceName)
printMessage (sprintf "Generating resource methods for %s" outFilename)
// gen each resource method
stringInfos |> Seq.iter (fun (lineNum, (optErrNum,ident), str, holes, netFormatString) ->
let formalArgs = new System.Text.StringBuilder()
let actualArgs = new System.Text.StringBuilder()
let firstTime = ref true
let n = ref 0
formalArgs.Append "(" |> ignore
for hole in holes do
if !firstTime then
firstTime := false
else
formalArgs.Append ", " |> ignore
actualArgs.Append " " |> ignore
formalArgs.Append(sprintf "a%d : %s" !n hole) |> ignore
actualArgs.Append(sprintf "a%d" !n) |> ignore
n := !n + 1
formalArgs.Append ")" |> ignore
fprintfn out " /// %s" str
fprintfn out " /// (Originally from %s:%d)" filename (lineNum+1)
let justPercentsFromFormatString =
(holes |> Array.fold (fun acc holeType ->
acc + match holeType with
| "System.Int32" -> ",,,%d"
| "System.Double" -> ",,,%f"
| "System.String" -> ",,,%s"
| _ -> failwith "unreachable") "") + ",,,"
let errPrefix = match optErrNum with
| None -> ""
| Some n -> sprintf "%d, " n
fprintfn out " static member %s%s = (%sGetStringFunc(\"%s\",\"%s\") %s)" ident (formalArgs.ToString()) errPrefix ident justPercentsFromFormatString (actualArgs.ToString())
)
printMessage (sprintf "Generating .resx for %s" outFilename)
fprintfn out ""
// gen validation method
fprintfn out " /// Call this method once to validate that all known resources are valid; throws if not"
fprintfn out " static member RunStartupValidation() ="
stringInfos |> Seq.iter (fun (lineNum, (optErrNum,ident), str, holes, netFormatString) ->
fprintfn out " ignore(GetString(\"%s\"))" ident
)
fprintfn out " ()" // in case there are 0 strings, we need the generated code to parse
// gen to resx
let xd = new System.Xml.XmlDocument()
xd.LoadXml(xmlBoilerPlateString)
stringInfos |> Seq.iter (fun (lineNum, (optErrNum,ident), str, holes, netFormatString) ->
let xn = xd.CreateElement("data")
xn.SetAttribute("name",ident) |> ignore
xn.SetAttribute("xml:space","preserve") |> ignore
let xnc = xd.CreateElement "value"
xn.AppendChild xnc |> ignore
xnc.AppendChild(xd.CreateTextNode netFormatString) |> ignore
xd.LastChild.AppendChild xn |> ignore
)
use outXmlStream = File.Create outXmlFilename
xd.Save outXmlStream
printMessage (sprintf "Done %s" outFilename)
Some (filename, outFilename, outXmlFilename)
with e ->
PrintErr(filename, 0, sprintf "An exception occurred when processing '%s'\n%s" filename (e.ToString()))
None
[<Required>]
member this.EmbeddedText
with get() = _embeddedText
and set(value) = _embeddedText <- value
[<Required>]
member this.IntermediateOutputPath
with get() = _outputPath
and set(value) = _outputPath <- value
[<Output>]
member this.GeneratedSource
with get() = _generatedSource
[<Output>]
member this.GeneratedResx
with get() = _generatedResx
interface ITask with
member this.BuildEngine
with get() = _buildEngine
and set(value) = _buildEngine <- value
member this.HostObject
with get() = _hostObject
and set(value) = _hostObject <- value
member this.Execute() =
let generatedFiles =
this.EmbeddedText
|> Array.choose (fun item -> generateResxAndSource item.ItemSpec)
let generatedSource, generatedResx =
[| for (textFile, source, resx) in generatedFiles do
let sourceItem =
let item = TaskItem(source)
item.SetMetadata("AutoGen", "true")
item.SetMetadata("DesignTime", "true")
item.SetMetadata("DependentUpon", resx)
item :> ITaskItem
let resxItem =
let item = TaskItem(resx)
item.SetMetadata("ManifestResourceName", Path.GetFileNameWithoutExtension(resx))
item.SetMetadata("SourceDocumentPath", textFile)
item :> ITaskItem
yield (sourceItem, resxItem) |]
|> Array.unzip
let generatedResult = (generatedFiles.Length = this.EmbeddedText.Length)
_generatedSource <- generatedSource
_generatedResx <- generatedResx
generatedResult