Skip to content

Commit 12a9b47

Browse files
committed
Add NameCollisions parameter. Fixes #1.
1 parent fd49dc8 commit 12a9b47

File tree

11 files changed

+254
-61
lines changed

11 files changed

+254
-61
lines changed

.editorconfig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
root = true
22

33
[*]
4+
charset = utf-8
45
indent_style = space
56
indent_size = 2
6-
charset = utf-8
77
trim_trailing_whitespace = true
88
insert_final_newline = true
9+
10+
[*.{sln,fsproj,csproj,vbproj}]
11+
charset = utf-8-bom

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ The source CSS to process. Can be a file path, web URL, or CSS text.
101101
* `Naming.CamelCase`: convert to camel case names with all non-alphanumeric characters removed.
102102
* `Naming.PascalCase`: convert to Pascal case names with all non-alphanumeric characters removed.
103103

104-
If a naming option produces collisions, such as `card-text` and `card_text` both mapping to `CardText` in Pascal case, then the duplicate names will receive `_2`, `_3`, etc. suffixes as needed.
104+
Note that non-verbatim naming options can produce name collisions. See the [`nameCollisions`](#nameCollisions) parameter for details.
105105

106106
### resolutionFolder
107107

@@ -137,6 +137,14 @@ let activator = "activator"
137137
...
138138
```
139139

140+
### nameCollisions
141+
142+
If a naming option produces collisions, such as `card-text` and `card_text` CSS classes both mapping to a `CardText` property in Pascal case, then the duplicate names will be handled according to this option.
143+
144+
- `NameCollisions.BasicSuffix`: (default) The base property name will refer to the closest text match, and additional properties will receive `_2`, `_3`, etc. suffixes as needed. Note that if this option is used during ongoing CSS development, it can cause existing properties to silently change to refer to different classes if collisions are introduced that affect the base name and number determination.
145+
- `NameCollisions.ExtendedSuffix`: All property names involved in a collision will receive an extended numbered suffix such as `__1_of_2`, `__2_of_2`. This option is safer for ongoing development since any introduced collision will change all involved names and produce immediate compiler errors where the previous names were used.
146+
- `NameCollisions.Omit`: All colliding properties will be omitted from the generated type. This option is safer for ongoing development since any introduced collision will remove the original property and produce immediate compiler errors wherever it was used.
147+
140148
## Notes
141149

142150
As with all type providers, the source CSS file must be available at both design time and build time.

invoke.build.ps1

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1-
# Install PowerShell for your platform:
1+
# Install PowerShell Core:
22
# https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell
33
# Install Invoke-Build:
44
# https://github.com/nightroman/Invoke-Build
55
# (Optional) Add "Set-Alias ib Invoke-Build" to your PS profile.
66
# At a PS prompt, run any build task (optionally use "ib" alias):
77
# Invoke-Build build
8-
# Invoke-Build test
98
# Invoke-Build ? # this lists available tasks
109

10+
param (
11+
$NuGetApiPushKey = ( property NuGetApiPushKey 'MISSING' ),
12+
$LocalPackageDir = ( property LocalPackageDir 'C:\code\LocalPackages' )
13+
)
14+
15+
$baseProjectName = "TypedCssClasses"
16+
$basePackageName = "Zanaptak.$baseProjectName"
17+
18+
task . Build
19+
1120
task Clean {
1221
exec { dotnet clean .\src -c Release }
1322
}
@@ -24,12 +33,72 @@ task BuildTest {
2433
exec { dotnet build .\src -c ReleaseTest }
2534
}
2635

36+
task Test CleanTest, BuildTest, {
37+
exec { dotnet test .\test\TypedCssClasses.Tests -c ReleaseTest }
38+
}
39+
2740
task Pack Clean, Build, {
2841
exec { dotnet pack .\src -c Release }
2942
}
3043

31-
task Test CleanTest, BuildTest, {
32-
exec { dotnet test .\test\TypedCssClasses.Tests -c ReleaseTest }
44+
task PackInternal Clean, Build, GetVersion, {
45+
$yearStart = Get-Date -Year ( ( Get-Date ).Year ) -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0
46+
$now = Get-Date
47+
$buildSuffix = [ int ] ( ( $now - $yearStart ).TotalSeconds )
48+
$internalVersion = "$Version.$buildSuffix"
49+
exec { dotnet pack .\src -c Release -p:PackageVersion=$internalVersion }
50+
$filename = "$basePackageName.$internalVersion.nupkg"
51+
Copy-Item .\src\bin\Release\$filename $LocalPackageDir
52+
Write-Build Green "Copied $filename to $LocalPackageDir"
3353
}
3454

35-
task . Build
55+
task IncrementMinor GetVersion, {
56+
if ( $Version -match "^(\d+)\.(\d+)\." ) {
57+
$projectFile = "$BuildRoot\src\$baseProjectName.fsproj"
58+
$major = $Matches[ 1 ]
59+
$minor = $Matches[ 2 ]
60+
$newMinor = ( [ int ] $minor ) + 1
61+
$newVersion = "$major.$newMinor.0"
62+
63+
$xml = New-Object System.Xml.XmlDocument
64+
$xml.PreserveWhitespace = $true
65+
$xml.Load( $projectFile )
66+
67+
$node = $xml.SelectSingleNode( '/Project/PropertyGroup/Version' )
68+
$node.InnerText = $newVersion
69+
70+
$settings = New-Object System.Xml.XmlWriterSettings
71+
$settings.OmitXmlDeclaration = $true
72+
$settings.Encoding = New-Object System.Text.UTF8Encoding( $true )
73+
74+
$writer = [ System.Xml.XmlWriter ]::Create( $projectFile , $settings )
75+
try {
76+
$xml.Save( $writer )
77+
} finally {
78+
$writer.Dispose()
79+
}
80+
Write-Build Green "Updated version to $newVersion"
81+
}
82+
else {
83+
throw "invalid version: $Version"
84+
}
85+
}
86+
87+
task GetVersion {
88+
$script:Version = Select-Xml -Path ".\src\$baseProjectName.fsproj" -XPath /Project/PropertyGroup/Version | % { $_.Node.InnerXml.Trim() }
89+
}
90+
91+
task UploadNuGet EnsureCommitted, GetVersion, {
92+
if ( $NuGetApiPushKey -eq "MISSING" ) { throw "NuGet key not provided" }
93+
Set-Location ./src/bin/Release
94+
$filename = "$basePackageName.$Version.nupkg"
95+
if ( -not ( Test-Path $filename ) ) { throw "nupkg file not found" }
96+
$lastHour = ( Get-Date ).AddHours( -1 )
97+
if ( ( Get-ChildItem $filename ).LastWriteTime -lt $lastHour ) { throw "nupkg file too old" }
98+
exec { dotnet nuget push $filename -k $NuGetApiPushKey -s https://api.nuget.org/v3/index.json }
99+
}
100+
101+
task EnsureCommitted {
102+
$gitoutput = exec { git status -s -uall }
103+
if ( $gitoutput ) { throw "uncommitted changes exist in working directory" }
104+
}

src/CssClassesTypeProvider.fs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ open FSharp.Core.CompilerServices
88
open System
99
open System.Reflection
1010

11-
type Naming =
12-
| Verbatim = 0
13-
| Underscores = 1
14-
| CamelCase = 2
15-
| PascalCase = 3
16-
1711
module TypeProvider =
1812

1913
[< TypeProvider >]
@@ -54,17 +48,11 @@ module TypeProvider =
5448
let naming = args.[ 1 ] :?> Naming
5549
let resolutionFolder = args.[ 2 ] :?> string
5650
let getProperties = args.[ 3 ] :?> bool
51+
let nameCollisions = args.[ 4 ] :?> NameCollisions
5752

5853
let getSpec _ value =
5954

60-
let transformer =
61-
match naming with
62-
| Naming.Underscores -> Utils.symbolsToUnderscores
63-
| Naming.CamelCase -> Utils.toCamelCase
64-
| Naming.PascalCase -> Utils.toPascalCase
65-
| _ -> id
66-
67-
let cssClasses = Utils.parseCss value transformer
55+
let cssClasses = Utils.parseCss value naming nameCollisions
6856

6957
//using ( IO.logTime "TypeGeneration" source ) <| fun _ ->
7058

@@ -114,6 +102,7 @@ module TypeProvider =
114102
ProvidedStaticParameter( "naming" , typeof< Naming >, parameterDefaultValue = Naming.Verbatim )
115103
ProvidedStaticParameter( "resolutionFolder" , typeof< string >, parameterDefaultValue = "" )
116104
ProvidedStaticParameter( "getProperties" , typeof< bool >, parameterDefaultValue = false )
105+
ProvidedStaticParameter( "nameCollisions" , typeof< NameCollisions >, parameterDefaultValue = NameCollisions.BasicSuffix )
117106
]
118107

119108
let helpText = """
@@ -123,6 +112,8 @@ module TypeProvider =
123112
One of: Naming.Verbatim (default), Naming.Underscores, Naming.CamelCase, Naming.PascalCase.</param>
124113
<param name='resolutionFolder'>A directory that is used when resolving relative file references.</param>
125114
<param name='getProperties'>Adds a GetProperties() method that returns a seq of all generated property name/value pairs.</param>
115+
<param name='nameCollisions'>Behavior of name collisions that arise from naming strategy.
116+
One of: NameCollisions.BasicSuffix (default), NameCollisions.ExtendedSuffix, NameCollisions.Omit. </param>
126117
"""
127118

128119
do parentType.AddXmlDoc helpText

src/TypedCssClasses.fsproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>netstandard2.0;net452</TargetFrameworks>
55
<DisableImplicitSystemValueTupleReference>true</DisableImplicitSystemValueTupleReference>
6-
<Version>0.1.0</Version>
6+
<Version>0.2.0</Version>
77
<PackageId>Zanaptak.TypedCssClasses</PackageId>
88
<Authors>zanaptak</Authors>
99
<Product>Zanaptak.TypedCssClasses</Product>
@@ -14,6 +14,7 @@
1414
<PackageProjectUrl>https://github.com/zanaptak/TypedCssClasses</PackageProjectUrl>
1515
<Configurations>Debug;Release;ReleaseTest;DebugLog</Configurations>
1616
<RepositoryUrl>https://github.com/zanaptak/TypedCssClasses.git</RepositoryUrl>
17+
<RepositoryType>git</RepositoryType>
1718
<PackageReleaseNotes>https://github.com/zanaptak/TypedCssClasses/releases</PackageReleaseNotes>
1819
</PropertyGroup>
1920

@@ -56,6 +57,7 @@
5657
</ItemGroup>
5758

5859
<ItemGroup>
60+
<Compile Include="Types.fs" />
5961
<Compile Include="Utils.fs" />
6062
<Compile Include="CssClassesTypeProvider.fs" />
6163
</ItemGroup>

src/TypedCssClasses.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "TypedCssClasses", "TypedCss
77
EndProject
88
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CF019DE1-D98F-40B5-A1D4-FC4BE9AC74B6}"
99
ProjectSection(SolutionItems) = preProject
10+
..\invoke.build.ps1 = ..\invoke.build.ps1
1011
..\README.md = ..\README.md
1112
EndProjectSection
1213
EndProject

src/Types.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Zanaptak.TypedCssClasses
2+
3+
type Naming =
4+
| Verbatim = 0
5+
| Underscores = 1
6+
| CamelCase = 2
7+
| PascalCase = 3
8+
9+
type NameCollisions =
10+
| BasicSuffix = 0
11+
| ExtendedSuffix = 1
12+
| Omit = 2

src/Utils.fs

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ open System
44
open System.Globalization
55
open System.Text.RegularExpressions
66
open Zanaptak.TypedCssClasses.Internal.FSharp.Data.Runtime
7+
open System.Collections.Generic
78

89
type Property = { Name : string ; Value : string }
910

@@ -281,35 +282,102 @@ let classNamesFromCss text =
281282
|> Seq.collect classNamesFromSelectorText
282283
|> Seq.distinct
283284

284-
let parseCss text transformer =
285-
let uniqueName = NameUtils.uniqueGenerator' ()
285+
let basicSuffixGenerator () =
286+
let nameSet = new HashSet<_>()
287+
fun ( baseName : string ) ->
288+
let mutable name = baseName
289+
let mutable trySuffix = 1
290+
while nameSet.Contains name do
291+
trySuffix <- trySuffix + 1
292+
name <- baseName + "_" + string trySuffix
293+
nameSet.Add name |> ignore
294+
name
295+
296+
type ExtendedSuffixGenerator () =
297+
let nameSet = new HashSet<_>()
298+
299+
member this.Single ( baseName : string ) =
300+
let mutable name = baseName
301+
let mutable trySuffix = ""
302+
while nameSet.Contains name do
303+
// shouldn't happen, but just in case
304+
if trySuffix = "" then
305+
trySuffix <- "__1_of_1"
306+
else
307+
trySuffix <- "_" + trySuffix
308+
name <- baseName + trySuffix
309+
nameSet.Add name |> ignore
310+
name
311+
312+
member this.Multiple ( baseName : string ) count =
313+
let mutable leadingUnderscores = "__"
314+
let digits = ( float count |> Math.Log10 |> int ) + 1
315+
let numStr num = ( string num ).PadLeft( digits , '0' )
316+
let nameArray underscores = Array.init count ( fun i -> sprintf "%s%s%s_of_%s" baseName underscores ( numStr ( i + 1 ) ) ( numStr count ) )
317+
let mutable names = nameArray leadingUnderscores
318+
while names |> Array.exists ( fun name -> nameSet.Contains name ) do
319+
// If any in a group match an existing name, try again with additional underscore
320+
leadingUnderscores <- leadingUnderscores + "_"
321+
names <- nameArray leadingUnderscores
322+
names |> Array.iter ( fun name -> nameSet.Add name |> ignore )
323+
names
324+
325+
let parseCss text naming nameCollisions =
326+
327+
let transformer =
328+
match naming with
329+
| Naming.Underscores -> symbolsToUnderscores
330+
| Naming.CamelCase -> toCamelCase
331+
| Naming.PascalCase -> toPascalCase
332+
| _ -> id
286333

287334
let initialProperties =
288335
text
289336
|> classNamesFromCss
290-
|> Seq.map ( fun s ->
291-
{ Name = transformer s ; Value = s }
292-
)
337+
|> Seq.map ( fun s -> { Name = transformer s ; Value = s } )
293338
|> Seq.filter ( fun p -> not ( p.Name.Contains( "``" ) ) ) // impossible to represent as verbatim property name, user will have to use string value
339+
|> Seq.toArray
294340

295-
let sameNames , differentNames = initialProperties |> Seq.toArray |> Array.partition ( fun p -> p.Name = p.Value )
341+
match nameCollisions with
296342

297-
// A name that exactly matches raw value is exempt from conflict resolution, reserve unique name immediately.
298-
// register unique base names in case later suffixed name conflicts
299-
// e.g. xyz_2 followed by group of xyz, xyz, xyz, they need to know xyz_2 is already reserved
300-
let sameNamesFinal = sameNames |> Array.map ( fun p -> { p with Name = uniqueName p.Name } )
343+
| NameCollisions.Omit ->
344+
initialProperties
345+
|> Array.groupBy ( fun p -> p.Name )
346+
|> Array.filter ( fun ( _ , props ) -> props.Length = 1 )
347+
|> Array.collect snd
301348

302-
let differentNamesFinal =
303-
differentNames
349+
| NameCollisions.ExtendedSuffix ->
350+
let nameGen = ExtendedSuffixGenerator()
351+
initialProperties
304352
|> Array.groupBy ( fun p -> p.Name )
353+
|> Array.sortBy ( fun ( propName , props ) -> props.Length , propName )
305354
|> Array.collect ( fun ( propName , props ) ->
306355
if Array.length props = 1 then
307-
[| { props.[ 0 ] with Name = uniqueName propName } |]
356+
[| { props.[ 0 ] with Name = nameGen.Single propName } |]
308357
else
309-
// entries with same property name, closest to underying text value gets first chance at unique base name, others get numbered suffix
310-
props
311-
|> Array.sortBy ( fun p -> levenshtein propName p.Value , p.Value )
312-
|> Array.map ( fun p -> { p with Name = uniqueName propName } )
358+
let sorted = props |> Array.sortBy ( fun p -> levenshtein propName p.Value , p.Value )
359+
let names = nameGen.Multiple propName props.Length
360+
Array.zip sorted names
361+
|> Array.map ( fun ( p , name ) -> { p with Name = name } )
313362
)
314363

315-
Seq.append sameNamesFinal differentNamesFinal
364+
| _ ->
365+
let nameGen = basicSuffixGenerator ()
366+
// A name that exactly matches raw value is exempt from conflict resolution, reserve unique name immediately.
367+
// register unique base names in case later suffixed name conflicts
368+
// e.g. xyz_2 followed by group of xyz, xyz, xyz, they need to know xyz_2 is already reserved
369+
let sameNames , differentNames = initialProperties |> Array.partition ( fun p -> p.Name = p.Value )
370+
let sameNamesFinal = sameNames |> Array.map ( fun p -> { p with Name = nameGen p.Name } )
371+
let differentNamesFinal =
372+
differentNames
373+
|> Array.groupBy ( fun p -> p.Name )
374+
|> Array.collect ( fun ( propName , props ) ->
375+
if Array.length props = 1 then
376+
[| { props.[ 0 ] with Name = nameGen propName } |]
377+
else
378+
// entries with same property name, closest to underying text value gets first chance at unique base name, others get numbered suffix
379+
props
380+
|> Array.sortBy ( fun p -> levenshtein propName p.Value , p.Value )
381+
|> Array.map ( fun p -> { p with Name = nameGen propName } )
382+
)
383+
Array.append sameNamesFinal differentNamesFinal

src/vendor/FSharp.Data/NameUtils.fs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,6 @@ let uniqueGenerator (niceName:string->string) =
9898
set.Add name |> ignore
9999
name
100100

101-
let uniqueGenerator' () =
102-
let set = new HashSet<_>()
103-
fun ( baseName : string ) ->
104-
let mutable name = baseName
105-
let mutable trySuffix = 1
106-
while set.Contains name do
107-
trySuffix <- trySuffix + 1
108-
name <- baseName + "_" + string trySuffix
109-
set.Add name |> ignore
110-
name
111-
112101
let capitalizeFirstLetter (s:string) =
113102
match s.Length with
114103
| 0 -> ""

test/TestWithFable/src/App.fsproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>netstandard2.0</TargetFramework>
44
</PropertyGroup>
@@ -15,4 +15,4 @@
1515
<HintPath>..\..\..\src\bin\DebugLog\netstandard2.0\Zanaptak.TypedCssClasses.dll</HintPath>
1616
</Reference>
1717
</ItemGroup>
18-
</Project>
18+
</Project>

0 commit comments

Comments
 (0)