├── .editorconfig ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc └── configuration.md ├── invoke.build.ps1 ├── sample ├── FableSass │ ├── .config │ │ └── dotnet-tools.json │ ├── .editorconfig │ ├── .gitignore │ ├── FableSass.fsproj │ ├── FableSass.sln │ ├── README.md │ ├── content │ │ ├── _base.sass │ │ ├── assets │ │ │ └── fable.ico │ │ ├── index.html │ │ └── styles.sass │ ├── package-lock.json │ ├── package.json │ ├── sass-process.js │ ├── sassdemo.gif │ ├── src │ │ ├── App.fs │ │ └── Main.fs │ └── webpack.config.js └── FableTailwind │ ├── .config │ └── dotnet-tools.json │ ├── .editorconfig │ ├── .gitignore │ ├── FableTailwind.fsproj │ ├── FableTailwind.sln │ ├── README.md │ ├── content │ ├── assets │ │ └── fable.ico │ ├── index.html │ └── tailwind-source.css │ ├── demo.gif │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── App.fs │ └── Main.fs │ ├── tailwind-process.js │ ├── tailwind.config.js │ └── webpack.config.js ├── src ├── CssClassesTypeProvider.fs ├── Fable.Core.fs ├── TypedCssClasses.fsproj ├── TypedCssClasses.sln ├── Types.fs ├── Utils.fs └── vendor │ ├── FSharp.Data │ ├── Caching.fs │ ├── Helpers.fs │ ├── Http.fs │ ├── IO.fs │ └── LICENSE.md │ ├── FSharp.TypeProviders.SDK │ ├── LICENSE.md │ ├── ProvidedTypes.fs │ └── ProvidedTypes.fsi │ └── README.md └── test ├── TestWithFable ├── .gitignore ├── README.md ├── TestWithFable.sln ├── package-lock.json ├── package.json ├── public │ ├── fable.ico │ └── index.html ├── src │ ├── TestWithFable.fsproj │ ├── folder │ │ ├── folder.fs │ │ ├── folder1.css │ │ └── folder2.css │ ├── import │ │ ├── import.fs │ │ ├── import.js │ │ ├── import1.css │ │ └── import2.css │ ├── main.css │ ├── main.fs │ └── sass │ │ ├── sass.fs │ │ └── sass.sass └── webpack.config.js └── TypedCssClasses.Tests ├── Tests.fs ├── TypedCssClasses.Tests.fsproj ├── TypedCssClasses.Tests.sln └── testdata ├── bootstrap-431-classes-reference.txt ├── bootstrap-431-min-css.txt ├── tailwind-10-classes-reference.txt └── tailwind-10-min-css.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{sln,fsproj,csproj,vbproj}] 11 | charset = utf-8-bom 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log.* 2 | 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | ## 7 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # StyleCop 67 | StyleCopReport.xml 68 | 69 | # Files built by Visual Studio 70 | *_i.c 71 | *_p.c 72 | *_h.h 73 | *.ilk 74 | *.meta 75 | *.obj 76 | *.iobj 77 | *.pch 78 | *.pdb 79 | *.ipdb 80 | *.pgc 81 | *.pgd 82 | *.rsp 83 | *.sbr 84 | *.tlb 85 | *.tli 86 | *.tlh 87 | *.tmp 88 | *.tmp_proj 89 | *_wpftmp.csproj 90 | *.log 91 | *.vspscc 92 | *.vssscc 93 | .builds 94 | *.pidb 95 | *.svclog 96 | *.scc 97 | 98 | # Chutzpah Test files 99 | _Chutzpah* 100 | 101 | # Visual C++ cache files 102 | ipch/ 103 | *.aps 104 | *.ncb 105 | *.opendb 106 | *.opensdf 107 | *.sdf 108 | *.cachefile 109 | *.VC.db 110 | *.VC.VC.opendb 111 | 112 | # Visual Studio profiler 113 | *.psess 114 | *.vsp 115 | *.vspx 116 | *.sap 117 | 118 | # Visual Studio Trace Files 119 | *.e2e 120 | 121 | # TFS 2012 Local Workspace 122 | $tf/ 123 | 124 | # Guidance Automation Toolkit 125 | *.gpState 126 | 127 | # ReSharper is a .NET coding add-in 128 | _ReSharper*/ 129 | *.[Rr]e[Ss]harper 130 | *.DotSettings.user 131 | 132 | # JustCode is a .NET coding add-in 133 | .JustCode 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Visual Studio code coverage results 146 | *.coverage 147 | *.coveragexml 148 | 149 | # NCrunch 150 | _NCrunch_* 151 | .*crunch*.local.xml 152 | nCrunchTemp_* 153 | 154 | # MightyMoose 155 | *.mm.* 156 | AutoTest.Net/ 157 | 158 | # Web workbench (sass) 159 | .sass-cache/ 160 | 161 | # Installshield output folder 162 | [Ee]xpress/ 163 | 164 | # DocProject is a documentation generator add-in 165 | DocProject/buildhelp/ 166 | DocProject/Help/*.HxT 167 | DocProject/Help/*.HxC 168 | DocProject/Help/*.hhc 169 | DocProject/Help/*.hhk 170 | DocProject/Help/*.hhp 171 | DocProject/Help/Html2 172 | DocProject/Help/html 173 | 174 | # Click-Once directory 175 | publish/ 176 | 177 | # Publish Web Output 178 | *.[Pp]ublish.xml 179 | *.azurePubxml 180 | # Note: Comment the next line if you want to checkin your web deploy settings, 181 | # but database connection strings (with potential passwords) will be unencrypted 182 | *.pubxml 183 | *.publishproj 184 | 185 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 186 | # checkin your Azure Web App publish settings, but sensitive information contained 187 | # in these scripts will be unencrypted 188 | PublishScripts/ 189 | 190 | # NuGet Packages 191 | *.nupkg 192 | # NuGet Symbol Packages 193 | *.snupkg 194 | # The packages folder can be ignored because of Package Restore 195 | **/[Pp]ackages/* 196 | # except build/, which is used as an MSBuild target. 197 | !**/[Pp]ackages/build/ 198 | # Uncomment if necessary however generally it will be regenerated when needed 199 | #!**/[Pp]ackages/repositories.config 200 | # NuGet v3's project.json files produces more ignorable files 201 | *.nuget.props 202 | *.nuget.targets 203 | 204 | # Microsoft Azure Build Output 205 | csx/ 206 | *.build.csdef 207 | 208 | # Microsoft Azure Emulator 209 | ecf/ 210 | rcf/ 211 | 212 | # Windows Store app package directories and files 213 | AppPackages/ 214 | BundleArtifacts/ 215 | Package.StoreAssociation.xml 216 | _pkginfo.txt 217 | *.appx 218 | *.appxbundle 219 | *.appxupload 220 | 221 | # Visual Studio cache files 222 | # files ending in .cache can be ignored 223 | *.[Cc]ache 224 | # but keep track of directories ending in .cache 225 | !?*.[Cc]ache/ 226 | 227 | # Others 228 | ClientBin/ 229 | ~$* 230 | *~ 231 | *.dbmdl 232 | *.dbproj.schemaview 233 | *.jfm 234 | *.pfx 235 | *.publishsettings 236 | orleans.codegen.cs 237 | 238 | # Including strong name files can present a security risk 239 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 240 | #*.snk 241 | 242 | # Since there are multiple workflows, uncomment next line to ignore bower_components 243 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 244 | #bower_components/ 245 | 246 | # RIA/Silverlight projects 247 | Generated_Code/ 248 | 249 | # Backup & report files from converting an old project file 250 | # to a newer Visual Studio version. Backup files are not needed, 251 | # because we have git ;-) 252 | _UpgradeReport_Files/ 253 | Backup*/ 254 | UpgradeLog*.XML 255 | UpgradeLog*.htm 256 | ServiceFabricBackup/ 257 | *.rptproj.bak 258 | 259 | # SQL Server files 260 | *.mdf 261 | *.ldf 262 | *.ndf 263 | 264 | # Business Intelligence projects 265 | *.rdl.data 266 | *.bim.layout 267 | *.bim_*.settings 268 | *.rptproj.rsuser 269 | *- [Bb]ackup.rdl 270 | *- [Bb]ackup ([0-9]).rdl 271 | *- [Bb]ackup ([0-9][0-9]).rdl 272 | 273 | # Microsoft Fakes 274 | FakesAssemblies/ 275 | 276 | # GhostDoc plugin setting file 277 | *.GhostDoc.xml 278 | 279 | # Node.js Tools for Visual Studio 280 | .ntvs_analysis.dat 281 | node_modules/ 282 | 283 | # Visual Studio 6 build log 284 | *.plg 285 | 286 | # Visual Studio 6 workspace options file 287 | *.opt 288 | 289 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 290 | *.vbw 291 | 292 | # Visual Studio LightSwitch build output 293 | **/*.HTMLClient/GeneratedArtifacts 294 | **/*.DesktopClient/GeneratedArtifacts 295 | **/*.DesktopClient/ModelManifest.xml 296 | **/*.Server/GeneratedArtifacts 297 | **/*.Server/ModelManifest.xml 298 | _Pvt_Extensions 299 | 300 | # Paket dependency manager 301 | .paket/paket.exe 302 | paket-files/ 303 | 304 | # FAKE - F# Make 305 | .fake/ 306 | 307 | # CodeRush personal settings 308 | .cr/personal 309 | 310 | # Python Tools for Visual Studio (PTVS) 311 | __pycache__/ 312 | *.pyc 313 | 314 | # Cake - Uncomment if you are using it 315 | # tools/** 316 | # !tools/packages.config 317 | 318 | # Tabs Studio 319 | *.tss 320 | 321 | # Telerik's JustMock configuration file 322 | *.jmconfig 323 | 324 | # BizTalk build output 325 | *.btp.cs 326 | *.btm.cs 327 | *.odx.cs 328 | *.xsd.cs 329 | 330 | # OpenCover UI analysis results 331 | OpenCover/ 332 | 333 | # Azure Stream Analytics local run output 334 | ASALocalRun/ 335 | 336 | # MSBuild Binary and Structured Log 337 | *.binlog 338 | 339 | # NVidia Nsight GPU debugger configuration file 340 | *.nvuser 341 | 342 | # MFractors (Xamarin productivity tool) working folder 343 | .mfractor/ 344 | 345 | # Local History for Visual Studio 346 | .localhistory/ 347 | 348 | # BeatPulse healthcheck temp database 349 | healthchecksdb 350 | 351 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 352 | MigrationBackup/ 353 | 354 | # Ionide (cross platform F# VS Code tools) working folder 355 | .ionide/ 356 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog - Zanaptak.TypedCssClasses 2 | 3 | [![GitHub](https://img.shields.io/badge/-github-gray?logo=github)](https://github.com/zanaptak/TypedCssClasses) [![NuGet](https://img.shields.io/nuget/v/Zanaptak.TypedCssClasses?logo=nuget)](https://www.nuget.org/packages/Zanaptak.TypedCssClasses) 4 | 5 | ## 1.0.0 (2021-07-30) 6 | 7 | - Add [`fableCssModule`](https://github.com/zanaptak/TypedCssClasses/blob/main/doc/configuration.md#fablecssmodule) parameter for CSS Module support in Fable projects ([#11](https://github.com/zanaptak/TypedCssClasses/pull/11)) @alfonsogarciacaro 8 | - Fix stale cache data when changing `naming` and `nameCollisions` parameters ([#12](https://github.com/zanaptak/TypedCssClasses/issues/12)) 9 | 10 | ## 0.4.0 (2020-07-26) 11 | 12 | - Add caching of parsed results from files and process execution ([#7](https://github.com/zanaptak/TypedCssClasses/issues/7), [#8](https://github.com/zanaptak/TypedCssClasses/pull/8)) 13 | - Change from properties to fields for better completion list performance 14 | - See [note on performance](https://github.com/zanaptak/TypedCssClasses/pull/8#issue-456779399) with large class counts 15 | 16 | ## 0.3.1 (2020-07-16) 17 | 18 | - Add process id to log messages 19 | 20 | ## 0.3.0 (2020-07-14) 21 | 22 | - Add configurable [external command](https://github.com/zanaptak/TypedCssClasses/blob/main/doc/configuration.md#external-command-support-for-css-preprocessing) capability for CSS preprocessing ([#4](https://github.com/zanaptak/TypedCssClasses/issues/4)) 23 | - Support environment variables in parameters 24 | - Support OS-specific values in parameters 25 | - Add [`logFile`](https://github.com/zanaptak/TypedCssClasses/blob/main/doc/configuration.md#logfile) option 26 | - Enable Source Link 27 | - __Breaking change__: Target .NET Standard 2.0 only, per Type Provider SDK guidance (may affect very old build chains) 28 | - __Breaking change__: The environment variable and OS-specific parameter support may affect existing parameter values if they use the same syntax. See the [`osDelimiters`](https://github.com/zanaptak/TypedCssClasses/blob/main/doc/configuration.md#osdelimiters) and [`expandVariables`](https://github.com/zanaptak/TypedCssClasses/blob/main/doc/configuration.md#expandvariables) options to address this. 29 | 30 | ## 0.2.0 (2019-09-27) 31 | 32 | - Add [`nameCollisions`](https://github.com/zanaptak/TypedCssClasses/blob/main/doc/configuration.md#namecollisions) parameter for handling duplicate property names ([#1](https://github.com/zanaptak/TypedCssClasses/issues/1)) 33 | 34 | ## 0.1.0 (2019-08-21) 35 | 36 | - Packaging update (no change in functionality): 37 | - Remove System.ValueTuple dependency 38 | 39 | ## 0.0.3 (2019-08-10) 40 | 41 | - Add [`getProperties`](https://github.com/zanaptak/TypedCssClasses/blob/main/doc/configuration.md#getproperties) option providing seq of generated properties, useful for code generation 42 | - Add .NET Framework target 43 | 44 | ## 0.0.2 (2019-08-06) 45 | 46 | - Recognize classes inside `@supports` rules 47 | - Provide duplicate property names with `_2`, `_3`, etc. suffixes 48 | - Preserve non-ASCII letters with `Naming.Underscores` 49 | - Add `_` prefix to names with invalid identifier start characters (except with `Naming.Verbatim`) 50 | - Add `_` suffix to names that are F# keywords (except with `Naming.Verbatim`) 51 | 52 | ## 0.0.1 (2019-07-29) 53 | 54 | - Initial release 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 zanaptak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zanaptak.TypedCssClasses 2 | 3 | [![GitHub](https://img.shields.io/badge/-github-gray?logo=github)](https://github.com/zanaptak/TypedCssClasses) [![NuGet](https://img.shields.io/nuget/v/Zanaptak.TypedCssClasses?logo=nuget)](https://www.nuget.org/packages/Zanaptak.TypedCssClasses) 4 | 5 | A CSS class type provider for F# web development. 6 | 7 | Bring external stylesheet classes into your F# code as design-time discoverable compiler-verified properties. 8 | 9 | ## Code examples 10 | 11 | The following code examples show how the type provider enables type-safe CSS classes. Anywhere you would have previously used a class name string, you can now use something like a `Bootstrap`-, `FA`-, or `tw`-prefixed property, with your IDE providing completion for valid classes. 12 | 13 | The syntax in these examples is [Fable.React](https://fable.io/blog/Announcing-Fable-React-5.html) view syntax, but any web framework can be used because the provided properties just compile to the underlying class names as strings. 14 | 15 | ### Bootstrap CSS 16 | 17 | ```fs 18 | // A "Bootstrap" type pointing at a remote Bootstrap CSS file. 19 | type Bootstrap = CssClasses<"https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css", Naming.PascalCase> 20 | 21 | // All CSS classes become Bootstrap.* properties, with design-time completion. 22 | div [ ClassName Bootstrap.Card ] [ 23 | div [ ClassName Bootstrap.CardBody ] [ 24 | h5 [ ClassName Bootstrap.CardTitle ] [ str "A clever title" ] 25 | p [ ClassName Bootstrap.CardText ] [ str "Lorem ipsum dolor sit amet." ] 26 | ] 27 | ] 28 | ``` 29 | 30 | ### Font Awesome CSS 31 | 32 | ```fs 33 | // An "FA" type pointing at a local Font Awesome CSS file. 34 | type FA = CssClasses<"static/font-awesome/css/all.css", Naming.Underscores> 35 | 36 | // All CSS classes become FA.* properties, with design-time completion. 37 | i [ classList [ 38 | FA.far, true 39 | FA.fa_grin, true ] ] [] 40 | 41 | i [ classList [ 42 | FA.fas, true 43 | FA.fa_thumbs_up, true ] ] [] 44 | ``` 45 | 46 | ### Tailwind CSS 47 | 48 | ```fs 49 | // A "tw" type pointing at a remote Tailwind CSS file. 50 | type tw = CssClasses<"https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css", Naming.Verbatim> 51 | 52 | // All CSS classes become tw.* properties, with design-time completion. 53 | div [ ClassName <| String.concat " " [ 54 | tw.``bg-red-200`` 55 | tw.``hover:bg-red-500`` 56 | tw.``hover:font-bold`` 57 | 58 | tw.``sm:text-2xl`` 59 | tw.``sm:bg-green-200`` 60 | tw.``sm:hover:bg-green-500`` 61 | 62 | tw.``lg:text-4xl`` 63 | tw.``lg:bg-blue-200`` 64 | tw.``lg:hover:bg-blue-500`` ] 65 | ] [ str "Resize me! Hover me!" ] 66 | ``` 67 | 68 | ## Samples 69 | 70 | Preconfigured sample projects to see it in action and use as a starting point: 71 | 72 | - [Fable Sass sample](https://github.com/zanaptak/TypedCssClasses/tree/main/sample/FableSass) - Demonstrates TypedCssClasses with Sass compilation in a Fable project. 73 | 74 | - [Fable Tailwind sample](https://github.com/zanaptak/TypedCssClasses/tree/main/sample/FableTailwind) - Demonstrates TypedCssClasses with Tailwind CSS in a Fable project. 75 | 76 | ## Getting started 77 | 78 | Add the [NuGet package](https://www.nuget.org/packages/Zanaptak.TypedCssClasses) to your project: 79 | ``` 80 | dotnet add package Zanaptak.TypedCssClasses 81 | ``` 82 | 83 | If the project was already open in an IDE, you might want to close and restart it to make sure the type provider loads into the process correctly. 84 | 85 | Write some code: 86 | ```fs 87 | open Zanaptak.TypedCssClasses 88 | 89 | // Define a type for a CSS source. 90 | // Can be file path, web URL, or CSS text. 91 | type css = CssClasses<"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"> 92 | 93 | // Enjoy your typed properties! 94 | let x = css.``display-1`` 95 | ``` 96 | 97 | It should work with any IDE that supports type providers. (Tested in Visual Studio Code with Ionide-fsharp extension on Windows and Linux, and in Visual Studio on Windows.) 98 | 99 | ## Configuration 100 | 101 | See the [configuration instructions](https://github.com/zanaptak/TypedCssClasses/blob/main/doc/configuration.md) for full details on configuration parameters to customize the behavior of the type provider. 102 | 103 | ## Notes 104 | 105 | This type provider does not use formal CSS parsing; it identifies classes by typical selector patterns using (somewhat complex) regular expressions. It tests fine against several major CSS frameworks but is not guaranteed foolproof in case there is some obscure CSS syntax not accounted for. 106 | 107 | Web URLs are expected to use static CDN or otherwise unchanging content and are cached on the filesystem with a 90-day expiration. If tracking of CSS changes is required, you must use local CSS files in your project. 108 | 109 | If using Fable 2.x, update fable-compiler to version 2.3.17 or later to avoid an issue with the type provider failing to resolve relative file paths. 110 | 111 | CSS `@import` rules are not processed internally by the type provider. If desired, they can be processed via external command; see the [TestWithFable test project](https://github.com/zanaptak/TypedCssClasses/tree/main/test/TestWithFable) for an example using [PostCSS](https://postcss.org/) with the [postcss-import](https://github.com/postcss/postcss-import) plugin. 112 | -------------------------------------------------------------------------------- /doc/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration - Zanaptak.TypedCssClasses 2 | 3 | [![GitHub](https://img.shields.io/badge/-github-gray?logo=github)](https://github.com/zanaptak/TypedCssClasses) [![NuGet](https://img.shields.io/nuget/v/Zanaptak.TypedCssClasses?logo=nuget)](https://www.nuget.org/packages/Zanaptak.TypedCssClasses) 4 | 5 | ## Getting started 6 | 7 | Add the [NuGet package](https://www.nuget.org/packages/Zanaptak.TypedCssClasses) to your project: 8 | ``` 9 | dotnet add package Zanaptak.TypedCssClasses 10 | ``` 11 | 12 | If the project was already open in an IDE, you might want to close and restart it to make sure the type provider loads into the process correctly. 13 | 14 | Write some code: 15 | ```fs 16 | open Zanaptak.TypedCssClasses 17 | 18 | // Define a type for a CSS source. 19 | // Can be file path, web URL, or CSS text. 20 | type css = CssClasses<"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"> 21 | 22 | // Enjoy your typed properties! 23 | let x = css.``display-1`` 24 | ``` 25 | 26 | It should work with any IDE that supports type providers. (Tested in Visual Studio Code with Ionide-fsharp extension on Windows and Linux, and in Visual Studio on Windows.) 27 | 28 | ## External command support for CSS preprocessing 29 | 30 | The internal logic of this type provider requires standard CSS syntax to extract class names from. For alternate syntaxes (such as Sass/SCSS), you can specify an external command to compile the source into standard CSS with the `commandFile` parameter: 31 | 32 | ``` 33 | // Assuming a "sass" command is available in PATH. 34 | type css = CssClasses<"example.sass", commandFile="sass"> 35 | ``` 36 | 37 | The command can be an executable file in your PATH, a file path relative to the resolution folder (see [`resolutionFolder`](#resolutionfolder) parameter), or an absolute path. The working directory of the process will be the resolution folder of the type provider. On Windows, the .exe extension can be omitted, but other extensions such as .cmd/.bat must be specified. 38 | 39 | The `source` parameter will be passed as an argument to the command. The standard output returned by the command will be used as the CSS data for extracting class names. A non-zero exit code returned by the command indicates an error and the standard error text will be reported as an exception. 40 | 41 | In addition to the CSS output, the command can optionally return leading lines to indicate additional files to watch beyond the `source` parameter (see [File watching](#file-watching) section below), such as Sass partials. Any initial full line that exactly matches a local path will be interpreted as a file to watch; the type provider will only start processing CSS on the first line that doesn't match a local path. You would likely accomplish this via custom scripts; see the sample projects for examples. 42 | 43 | Two additional parameters are available to further customize the command: `argumentPrefix` and `argumentSuffix`. If specified, these strings will be placed before and after the `source` argument, separated by a space. That is, the full command will be the result of the following parameters: 44 | 45 | ``` 46 | commandFile [argumentPrefix ][source][ argumentSuffix] 47 | ``` 48 | 49 | Source and arguments are concatenated as-is (after OS-specific and environment variable processing described below); you are responsible for any quoting or escaping if necessary. 50 | 51 | ## Multiple development environments 52 | 53 | To support development in different environments, you can include operating system-specific alternatives in parameters using comma-separated OS=value pairs after an initially-specified default, in the form of `defaultvalue,OS1=value1,OS2=value2`: 54 | 55 | ``` 56 | // Use "sass.cmd" on Windows, "sass" everywhere else. 57 | type css = CssClasses<"example.sass", commandFile="sass,Windows=sass.cmd"> 58 | ``` 59 | 60 | Supported values are (case-insensitive): `FREEBSD`, `LINUX`, `OSX`, and `WINDOWS`. (Based on [OSPlatform.Create()](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.osplatform.create?view=netstandard-2.0) as of .NET Standard 2.0.) 61 | 62 | If this syntax conflicts with your parameter values, you can use the `osDelimiters` parameter. It takes a two-character string specifying alternatives for the comma and the equals sign in that order, or an empty string to disable OS parsing. 63 | 64 | ## Environment variables 65 | 66 | You can use environment variables in parameters using `%VARIABLE%` syntax. Note that the Windows-style `%...%` syntax must be used, even for non-Windows systems (e.g. Linux must use `%HOME%` rather than `$HOME`). Variables not found in the environment will not be processed and the original syntax will be left in the string. 67 | 68 | You can disable environment variable expansion by setting the `expandVariables` parameter to false. 69 | 70 | ## File watching 71 | 72 | If the `source` parameter specifies a local file, the type provider will monitor the file and refresh the CSS classes when it changes (rerunning any specified command if applicable). 73 | 74 | If a `commandFile` is specified, any leading lines from the output of the command that exactly specify a local file path will also be watched. 75 | 76 | ## Parameters 77 | 78 | ### source 79 | 80 | The source CSS to process. Can be a file path, web URL, or CSS text. 81 | 82 | ### naming 83 | 84 | * `Naming.Verbatim`: (default) use class names verbatim from source CSS, requiring backtick-quotes for names with special characters. 85 | * `Naming.Underscores`: replace all non-alphanumeric characters with underscores. 86 | * `Naming.CamelCase`: convert to camel case names with all non-alphanumeric characters removed. 87 | * `Naming.PascalCase`: convert to Pascal case names with all non-alphanumeric characters removed. 88 | 89 | Note that non-verbatim naming options can produce name collisions. See the [`nameCollisions`](#nameCollisions) parameter for details. 90 | 91 | ### resolutionFolder 92 | 93 | A custom folder to use for resolving relative file paths. The default is the project root folder. 94 | 95 | To have nested code files referencing CSS files in the same directory without having to specify the entire path from project root, you can use the built-in F# `__SOURCE_DIRECTORY__` value: 96 | 97 | ```fs 98 | type css = CssClasses<"file-in-same-dir.css", resolutionFolder=__SOURCE_DIRECTORY__> 99 | ``` 100 | 101 | ### getProperties 102 | 103 | If true, the type will include a `GetProperties()` method that returns a sequence of the generated property name/value pairs. This can be used, for example, to generate hard-coded CSS class bindings via `.fsx` script: 104 | 105 | ```fs 106 | // Before release of F# 5, use preview flag: dotnet fsi --langversion:preview SCRIPT_NAME.fsx 107 | #r "nuget: Zanaptak.TypedCssClasses" 108 | open Zanaptak.TypedCssClasses 109 | type css = CssClasses<"https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css", Naming.PascalCase, getProperties=true> 110 | printfn "module Bootstrap" 111 | css.GetProperties() |> Seq.iter (fun p -> printfn "let [] %s = \"%s\"" p.Name p.Value) 112 | ``` 113 | 114 | Example output: 115 | ```fs 116 | module Bootstrap 117 | let [] Accordion = "accordion" 118 | let [] Active = "active" 119 | let [] Alert = "alert" 120 | let [] AlertDanger = "alert-danger" 121 | ... 122 | ``` 123 | 124 | ### nameCollisions 125 | 126 | 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. 127 | 128 | - `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. 129 | - `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. 130 | - `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. 131 | 132 | ### logFile 133 | 134 | Path to a log file the type provider should write to. 135 | 136 | ### commandFile 137 | 138 | An executable file to run that will process the `source` file before extracting CSS class names. See [External command support](#external-command-support-for-css-preprocessing). 139 | 140 | ### argumentPrefix 141 | 142 | An argument string to include before the `source` parameter, separated by a space, when running a command. Only applicable when `commandFile` is specified. 143 | 144 | ### argumentSuffix 145 | 146 | An argument string to include after the `source` parameter, separated by a space, when running a command. Only applicable when `commandFile` is specified. 147 | 148 | ### osDelimiters 149 | 150 | A two-character string specifying the delimiter characters used to indicate operating system-specific parameter values. Default is `,=` as in `defaultvalue,OS1=value1,OS2=value2`. If set to empty string, disables parsing for OS values. 151 | 152 | Applies to parameters: `source`, `resolutionFolder`, `logFile`, `commandFile`, `argumentPrefix`, `argumentSuffix` 153 | 154 | ### expandVariables 155 | 156 | Boolean to indicate whether evironment variables in the form of `%VARIABLE%` in parameter values should be expanded. Default true. 157 | 158 | Applies to parameters: `source`, `resolutionFolder`, `logFile`, `commandFile`, `argumentPrefix`, `argumentSuffix` 159 | 160 | ### fableCssModule 161 | 162 | In a Fable project, this parameter causes the provided properties to be compiled to Fable import expressions instead of class name strings, allowing processing as a [CSS Module](https://github.com/css-modules/css-modules). (This only affects the type provider property substitutions; you must also configure your JavaScript bundler for CSS Module support.) 163 | 164 | Enabling this option will cause the `source` parameter to be used in a JavaScript `import` statement after Fable compilation, so it must be specified in a way that both the type provider and the JavaScript bundler can resolve. It is recommended to use a path relative to the source file, with current directory dot prefix, as follows: 165 | 166 | ```fs 167 | type css = CssClasses<"./style.module.css", resolutionFolder=__SOURCE_DIRECTORY__, fableCssModule=true> 168 | ``` 169 | -------------------------------------------------------------------------------- /invoke.build.ps1: -------------------------------------------------------------------------------- 1 | # Install PowerShell Core: 2 | # https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell 3 | # Install Invoke-Build: 4 | # https://github.com/nightroman/Invoke-Build 5 | # (Optional) Add "Set-Alias ib Invoke-Build" to your PS profile. 6 | # At a PS prompt, run any build task (optionally use "ib" alias): 7 | # Invoke-Build build 8 | # Invoke-Build ? # this lists available tasks 9 | 10 | param ( 11 | $NuGetApiPushKey = ( property NuGetApiPushKey 'MISSING' ) , 12 | $LocalPackageDir = ( property LocalPackageDir 'C:/code/LocalPackages' ) , 13 | $Configuration = "Release" 14 | ) 15 | 16 | [ System.Environment ]::CurrentDirectory = $BuildRoot 17 | 18 | $baseProjectName = "TypedCssClasses" 19 | $basePackageName = "Zanaptak.$baseProjectName" 20 | $mainProjectFilePath = "src/$baseProjectName.fsproj" 21 | 22 | function trimLeadingZero { 23 | param ( $item ) 24 | $item = $item.TrimStart( '0' ) 25 | if ( $item -eq "" ) { "0" } else { $item } 26 | } 27 | 28 | function combinePrefixSuffix { 29 | param ( $prefix , $suffix ) 30 | "$prefix-$suffix".TrimEnd( '-' ) 31 | } 32 | 33 | function writeProjectFileProperty { 34 | param ( $projectFile , $propertyName , $propertyValue ) 35 | $xml = New-Object System.Xml.XmlDocument 36 | $xml.PreserveWhitespace = $true 37 | $xml.Load( $projectFile ) 38 | 39 | $nodePath = '/Project/PropertyGroup/' + $propertyName 40 | $node = $xml.SelectSingleNode( $nodePath ) 41 | $node.InnerText = $propertyValue 42 | 43 | $settings = New-Object System.Xml.XmlWriterSettings 44 | $settings.OmitXmlDeclaration = $true 45 | $settings.Encoding = New-Object System.Text.UTF8Encoding( $true ) 46 | 47 | $writer = [ System.Xml.XmlWriter ]::Create( $projectFile , $settings ) 48 | try { 49 | $xml.Save( $writer ) 50 | } finally { 51 | $writer.Dispose() 52 | } 53 | } 54 | 55 | function readProjectFileProperty { 56 | param ( $projectFile , $propertyName ) 57 | $nodePath = '/Project/PropertyGroup/' + $propertyName 58 | $propertyValue = 59 | Select-Xml -Path $projectFile -XPath $nodePath | 60 | Select-Object -First 1 | 61 | & { process { $_.Node.InnerXml.Trim() } } 62 | $propertyValue 63 | } 64 | 65 | function changelogTopVersionAndDate { 66 | 67 | $topSectionVersion = "" 68 | $topSectionDate = "" 69 | 70 | foreach ( $line in ( Get-Content .\CHANGELOG.md ) ) { 71 | $versionMatch = ( [ regex ] "^#+ (\d+(?:\.\d+){2,}(?:-\S+)?) \((\S+)\)" ).Match( $line ) 72 | if ( $versionMatch.Success ) { 73 | $topSectionVersion , $topSectionDate = $versionMatch.Groups[ 1 ].Value , $versionMatch.Groups[ 2 ].Value 74 | break 75 | } 76 | } 77 | 78 | $topSectionVersion , $topSectionDate 79 | } 80 | 81 | task . Build 82 | 83 | task Clean { 84 | exec { dotnet clean ./src -c $Configuration } 85 | } 86 | 87 | task Build { 88 | exec { dotnet build ./src -c $Configuration } 89 | } 90 | 91 | task Test { 92 | $script:Configuration = "ReleaseTest" 93 | } , Clean , Build , { 94 | exec { dotnet test ./test/TypedCssClasses.Tests -c $Configuration } 95 | } 96 | 97 | task IncrementMajor LoadVersion , { 98 | $version = [ System.Version ] $VersionPrefix 99 | $newVersionPrefix = [ System.Version ]::new( ( $version.Major + 1 ) , 0 , 0 ).ToString( 3 ) 100 | writeProjectFileProperty $mainProjectFilePath "VersionPrefix" $newVersionPrefix 101 | writeProjectFileProperty $mainProjectFilePath "VersionSuffix" "" 102 | } , ReportProjectFileVersion 103 | 104 | task IncrementMinor LoadVersion , { 105 | $version = [ System.Version ] $VersionPrefix 106 | $newVersionPrefix = [ System.Version ]::new( $version.Major , ( $version.Minor + 1 ) , 0 ).ToString( 3 ) 107 | writeProjectFileProperty $mainProjectFilePath "VersionPrefix" $newVersionPrefix 108 | writeProjectFileProperty $mainProjectFilePath "VersionSuffix" "" 109 | } , ReportProjectFileVersion 110 | 111 | task IncrementPatch LoadVersion , { 112 | $version = [ System.Version ] $VersionPrefix 113 | $newVersionPrefix = [ System.Version ]::new( $version.Major , $version.Minor , ( $version.Build + 1 ) ).ToString( 3 ) 114 | writeProjectFileProperty $mainProjectFilePath "VersionPrefix" $newVersionPrefix 115 | writeProjectFileProperty $mainProjectFilePath "VersionSuffix" "" 116 | } , ReportProjectFileVersion 117 | 118 | task IncrementPre LoadVersion , { 119 | $now = Get-Date 120 | $pre1 = $now.ToString( "yyyyMMdd" ) 121 | $pre2 = trimLeadingZero $now.ToString( "HHmmssff" ) 122 | $newVersionSuffix = "pre.{0}.{1}" -f $pre1 , $pre2 123 | writeProjectFileProperty $mainProjectFilePath "VersionSuffix" $newVersionSuffix 124 | } , ReportProjectFileVersion 125 | 126 | task ClearPre LoadVersion , { 127 | writeProjectFileProperty $mainProjectFilePath "VersionSuffix" "" 128 | } , ReportProjectFileVersion 129 | 130 | task ReportProjectFileVersion { 131 | $actualVersionPrefix = readProjectFileProperty $mainProjectFilePath "VersionPrefix" 132 | $actualVersionSuffix = readProjectFileProperty $mainProjectFilePath "VersionSuffix" 133 | $actualFullVersion = combinePrefixSuffix $actualVersionPrefix $actualVersionSuffix 134 | Write-Build Green "Version: $actualFullVersion" 135 | } 136 | 137 | task LoadVersion { 138 | $script:VersionPrefix = readProjectFileProperty $mainProjectFilePath "VersionPrefix" 139 | $script:VersionSuffix = readProjectFileProperty $mainProjectFilePath "VersionSuffix" 140 | $script:FullVersion = combinePrefixSuffix $VersionPrefix $VersionSuffix 141 | } 142 | 143 | task Pack { 144 | $script:Configuration = "Release" 145 | } , Clean , Build , { 146 | exec { dotnet pack ./src -c Release } 147 | } 148 | 149 | task PackInternal { 150 | $script:Configuration = "Debug" 151 | } , Clean , Build , LoadVersion , { 152 | $yearStart = Get-Date -Year ( ( Get-Date ).Year ) -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0 153 | $now = Get-Date 154 | $seconds = [ int ] ( $now - $yearStart ).TotalSeconds 155 | if ( $VersionSuffix ) { 156 | $internalVersionPrefix = $VersionPrefix 157 | $internalVersionSuffix = "$VersionSuffix.$seconds" 158 | } 159 | else { 160 | $internalVersionPrefix = "$VersionPrefix.$seconds" 161 | $internalVersionSuffix = $VersionSuffix 162 | } 163 | exec { dotnet pack ./src -c $Configuration -p:VersionPrefix=$internalVersionPrefix -p:VersionSuffix=$internalVersionSuffix } 164 | $internalFullVersion = combinePrefixSuffix $internalVersionPrefix $internalVersionSuffix 165 | $filename = "$basePackageName.$internalFullVersion.nupkg" 166 | Copy-Item ./src/bin/$Configuration/$filename $LocalPackageDir 167 | Write-Build Green "Copied $filename to $LocalPackageDir" 168 | } 169 | 170 | task UploadNuGet EnsureCommitted , LoadVersion , { 171 | if ( $NuGetApiPushKey -eq "MISSING" ) { throw "NuGet key not provided" } 172 | Set-Location ./src/bin/Release 173 | $filename = "$basePackageName.$FullVersion.nupkg" 174 | if ( -not ( Test-Path $filename ) ) { throw "nupkg file not found" } 175 | $lastHour = ( Get-Date ).AddHours( -1 ) 176 | if ( ( Get-ChildItem $filename ).LastWriteTime -lt $lastHour ) { throw "nupkg file too old" } 177 | exec { dotnet nuget push $filename -k $NuGetApiPushKey -s https://api.nuget.org/v3/index.json } 178 | } 179 | 180 | task EnsureCommitted { 181 | $gitoutput = exec { git status -s -uall } 182 | if ( $gitoutput ) { throw "uncommitted changes exist in working directory" } 183 | } 184 | 185 | task UpdateProjectFromChangelog { 186 | $version , $date = changelogTopVersionAndDate 187 | if ( $version -match '-' ) { 188 | $prefix , $suffix = $version -split '-' 189 | } 190 | else { 191 | $prefix , $suffix = $version , "" 192 | } 193 | 194 | writeProjectFileProperty $mainProjectFilePath "VersionPrefix" $prefix 195 | writeProjectFileProperty $mainProjectFilePath "VersionSuffix" $suffix 196 | 197 | $anchor = ( $version -replace '\.','' ) + "-$date" 198 | $url = "https://github.com/zanaptak/TypedCssClasses/blob/main/CHANGELOG.md#$anchor" 199 | 200 | writeProjectFileProperty $mainProjectFilePath "PackageReleaseNotes" $url 201 | 202 | Write-Build Green "****" 203 | Write-Build Green "**** Assumed changelog URL (VERIFY): $url" 204 | Write-Build Green "****" 205 | 206 | } , ReportProjectFileVersion 207 | -------------------------------------------------------------------------------- /sample/FableSass/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fable": { 6 | "version": "3.2.9", 7 | "commands": [ 8 | "fable" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /sample/FableSass/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{sln,fsproj,csproj,vbproj}] 11 | charset = utf-8-bom 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /sample/FableSass/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.fs.js 3 | *.fs.js.map 4 | TypedCssClasses.log 5 | 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # JustCode is a .NET coding add-in 135 | .JustCode 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Visual Studio code coverage results 148 | *.coverage 149 | *.coveragexml 150 | 151 | # NCrunch 152 | _NCrunch_* 153 | .*crunch*.local.xml 154 | nCrunchTemp_* 155 | 156 | # MightyMoose 157 | *.mm.* 158 | AutoTest.Net/ 159 | 160 | # Web workbench (sass) 161 | .sass-cache/ 162 | 163 | # Installshield output folder 164 | [Ee]xpress/ 165 | 166 | # DocProject is a documentation generator add-in 167 | DocProject/buildhelp/ 168 | DocProject/Help/*.HxT 169 | DocProject/Help/*.HxC 170 | DocProject/Help/*.hhc 171 | DocProject/Help/*.hhk 172 | DocProject/Help/*.hhp 173 | DocProject/Help/Html2 174 | DocProject/Help/html 175 | 176 | # Click-Once directory 177 | publish/ 178 | 179 | # Publish Web Output 180 | *.[Pp]ublish.xml 181 | *.azurePubxml 182 | # Note: Comment the next line if you want to checkin your web deploy settings, 183 | # but database connection strings (with potential passwords) will be unencrypted 184 | *.pubxml 185 | *.publishproj 186 | 187 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 188 | # checkin your Azure Web App publish settings, but sensitive information contained 189 | # in these scripts will be unencrypted 190 | PublishScripts/ 191 | 192 | # NuGet Packages 193 | *.nupkg 194 | # NuGet Symbol Packages 195 | *.snupkg 196 | # The packages folder can be ignored because of Package Restore 197 | **/[Pp]ackages/* 198 | # except build/, which is used as an MSBuild target. 199 | !**/[Pp]ackages/build/ 200 | # Uncomment if necessary however generally it will be regenerated when needed 201 | #!**/[Pp]ackages/repositories.config 202 | # NuGet v3's project.json files produces more ignorable files 203 | *.nuget.props 204 | *.nuget.targets 205 | 206 | # Microsoft Azure Build Output 207 | csx/ 208 | *.build.csdef 209 | 210 | # Microsoft Azure Emulator 211 | ecf/ 212 | rcf/ 213 | 214 | # Windows Store app package directories and files 215 | AppPackages/ 216 | BundleArtifacts/ 217 | Package.StoreAssociation.xml 218 | _pkginfo.txt 219 | *.appx 220 | *.appxbundle 221 | *.appxupload 222 | 223 | # Visual Studio cache files 224 | # files ending in .cache can be ignored 225 | *.[Cc]ache 226 | # but keep track of directories ending in .cache 227 | !?*.[Cc]ache/ 228 | 229 | # Others 230 | ClientBin/ 231 | ~$* 232 | *~ 233 | *.dbmdl 234 | *.dbproj.schemaview 235 | *.jfm 236 | *.pfx 237 | *.publishsettings 238 | orleans.codegen.cs 239 | 240 | # Including strong name files can present a security risk 241 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 242 | #*.snk 243 | 244 | # Since there are multiple workflows, uncomment next line to ignore bower_components 245 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 246 | #bower_components/ 247 | 248 | # RIA/Silverlight projects 249 | Generated_Code/ 250 | 251 | # Backup & report files from converting an old project file 252 | # to a newer Visual Studio version. Backup files are not needed, 253 | # because we have git ;-) 254 | _UpgradeReport_Files/ 255 | Backup*/ 256 | UpgradeLog*.XML 257 | UpgradeLog*.htm 258 | ServiceFabricBackup/ 259 | *.rptproj.bak 260 | 261 | # SQL Server files 262 | *.mdf 263 | *.ldf 264 | *.ndf 265 | 266 | # Business Intelligence projects 267 | *.rdl.data 268 | *.bim.layout 269 | *.bim_*.settings 270 | *.rptproj.rsuser 271 | *- [Bb]ackup.rdl 272 | *- [Bb]ackup ([0-9]).rdl 273 | *- [Bb]ackup ([0-9][0-9]).rdl 274 | 275 | # Microsoft Fakes 276 | FakesAssemblies/ 277 | 278 | # GhostDoc plugin setting file 279 | *.GhostDoc.xml 280 | 281 | # Node.js Tools for Visual Studio 282 | .ntvs_analysis.dat 283 | node_modules/ 284 | 285 | # Visual Studio 6 build log 286 | *.plg 287 | 288 | # Visual Studio 6 workspace options file 289 | *.opt 290 | 291 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 292 | *.vbw 293 | 294 | # Visual Studio LightSwitch build output 295 | **/*.HTMLClient/GeneratedArtifacts 296 | **/*.DesktopClient/GeneratedArtifacts 297 | **/*.DesktopClient/ModelManifest.xml 298 | **/*.Server/GeneratedArtifacts 299 | **/*.Server/ModelManifest.xml 300 | _Pvt_Extensions 301 | 302 | # Paket dependency manager 303 | .paket/paket.exe 304 | paket-files/ 305 | 306 | # FAKE - F# Make 307 | .fake/ 308 | 309 | # CodeRush personal settings 310 | .cr/personal 311 | 312 | # Python Tools for Visual Studio (PTVS) 313 | __pycache__/ 314 | *.pyc 315 | 316 | # Cake - Uncomment if you are using it 317 | # tools/** 318 | # !tools/packages.config 319 | 320 | # Tabs Studio 321 | *.tss 322 | 323 | # Telerik's JustMock configuration file 324 | *.jmconfig 325 | 326 | # BizTalk build output 327 | *.btp.cs 328 | *.btm.cs 329 | *.odx.cs 330 | *.xsd.cs 331 | 332 | # OpenCover UI analysis results 333 | OpenCover/ 334 | 335 | # Azure Stream Analytics local run output 336 | ASALocalRun/ 337 | 338 | # MSBuild Binary and Structured Log 339 | *.binlog 340 | 341 | # NVidia Nsight GPU debugger configuration file 342 | *.nvuser 343 | 344 | # MFractors (Xamarin productivity tool) working folder 345 | .mfractor/ 346 | 347 | # Local History for Visual Studio 348 | .localhistory/ 349 | 350 | # BeatPulse healthcheck temp database 351 | healthchecksdb 352 | 353 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 354 | MigrationBackup/ 355 | 356 | # Ionide (cross platform F# VS Code tools) working folder 357 | .ionide/ 358 | -------------------------------------------------------------------------------- /sample/FableSass/FableSass.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sample/FableSass/FableSass.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29123.88 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FableSass", "FableSass.fsproj", "{E842D90F-BD83-4DB4-BC96-459B3D772A03}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3E6CCADF-250E-4646-84F3-3BEE70E243D4}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | .gitignore = .gitignore 12 | package.json = package.json 13 | README.md = README.md 14 | sass-process.js = sass-process.js 15 | webpack.config.js = webpack.config.js 16 | EndProjectSection 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {E842D90F-BD83-4DB4-BC96-459B3D772A03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {E842D90F-BD83-4DB4-BC96-459B3D772A03}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {E842D90F-BD83-4DB4-BC96-459B3D772A03}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {E842D90F-BD83-4DB4-BC96-459B3D772A03}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | GlobalSection(ExtensibilityGlobals) = postSolution 33 | SolutionGuid = {1E39F012-820E-49EF-B8DC-5DDBF51C7178} 34 | EndGlobalSection 35 | EndGlobal 36 | -------------------------------------------------------------------------------- /sample/FableSass/README.md: -------------------------------------------------------------------------------- 1 | # TypedCssClasses Fable Sass Sample 2 | 3 | ![Demo](sassdemo.gif) 4 | 5 | This sample illustrates the following concepts: 6 | 7 | - Using [TypedCssClasses](https://github.com/zanaptak/TypedCssClasses) for type-safe CSS class properties in a [Fable](https://fable.io/) single page application. 8 | 9 | - Using the [Sass](https://sass-lang.com/) CSS preprocessor to generate CSS. The sample uses Sass syntax; SCSS syntax would also work with the same setup. 10 | 11 | - Using [Feliz](https://github.com/Zaid-Ajaj/Feliz/) for type-safe HTML and React components. 12 | 13 | ## Getting started 14 | 15 | Install [.NET Core SDK](https://dotnet.microsoft.com/download). 16 | 17 | Install [Node.js](https://nodejs.org/). 18 | 19 | Run `dotnet tool restore`. 20 | 21 | Run `dotnet restore`. 22 | 23 | Run `npm install`. 24 | 25 | Run `npm start`, and then browse to `http://localhost:8080`. You should get a counter with the header styled as white monospace text on blue background. 26 | 27 | Optionally, run `npm run build` to produce a production deployment version. 28 | 29 | ## Project structure 30 | 31 | * `src/App.fs` 32 | 33 | The application code, consisting of `App` and `Counter` React components. 34 | 35 | In the code, note the `type Css = CssClasses<...>` declaration for the TypedCssClasses type provider and the `Css`-prefixed CSS class properties. 36 | 37 | * `content/styles.sass` and `content/_base.sass` 38 | 39 | The source Sass files that are used to generate the CSS. 40 | 41 | * `sass-process.js` 42 | 43 | The Node-based script file executed by the TypedCssClasses type provider at design and compile time to preprocess the Sass source file into standard CSS, which is then used to generate the CSS class properties. 44 | 45 | This script is used instead of the Sass CLI in order to additionally communicate any referenced files (`_base.sass` in this case) to the type provider as files to watch for changes. 46 | 47 | * `dist/` 48 | 49 | The folder containing the final distributable output after running `npm run build`. You can look at the `style.[some hash value].css` file to see the final generated CSS, and you can open `index.html` in the browser to run the application. 50 | -------------------------------------------------------------------------------- /sample/FableSass/content/_base.sass: -------------------------------------------------------------------------------- 1 | $font-stack: monospace 2 | $primary-color: blue 3 | $secondary-color: white 4 | 5 | div 6 | .base 7 | font: 1.25rem $font-stack 8 | color: $primary-color 9 | background-color: $secondary-color 10 | .number 11 | display: inline-block 12 | width: 5rem 13 | text-align: center 14 | 15 | button 16 | width: 3rem 17 | -------------------------------------------------------------------------------- /sample/FableSass/content/assets/fable.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanaptak/TypedCssClasses/ee7a84f3314fe748c92026e31a96dac3f453a833/sample/FableSass/content/assets/fable.ico -------------------------------------------------------------------------------- /sample/FableSass/content/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FableSass 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /sample/FableSass/content/styles.sass: -------------------------------------------------------------------------------- 1 | @use 'base' 2 | 3 | div 4 | .inverse 5 | background-color: base.$primary-color 6 | color: base.$secondary-color 7 | -------------------------------------------------------------------------------- /sample/FableSass/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "dotnet fable watch FableSass.fsproj --sourceMaps --run webpack serve", 5 | "build": "dotnet fable FableSass.fsproj --run webpack --node-env production", 6 | "browsers": "browserslist --update-db" 7 | }, 8 | "browserslist": [ 9 | "> 0.25%", 10 | "not dead" 11 | ], 12 | "dependencies": { 13 | "@babel/core": "^7.14.8", 14 | "@babel/preset-env": "^7.14.8", 15 | "@babel/preset-react": "^7.14.5", 16 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-rc.2", 17 | "babel-loader": "^8.2.2", 18 | "browserslist": "^4.16.6", 19 | "copy-webpack-plugin": "^9.0.1", 20 | "core-js": "^3.15.2", 21 | "css-loader": "^6.2.0", 22 | "html-webpack-plugin": "^5.3.2", 23 | "mini-css-extract-plugin": "^2.1.0", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "react-refresh": "^0.10.0", 27 | "sass": "^1.36.0", 28 | "sass-loader": "^12.1.0", 29 | "source-map-loader": "^3.0.0", 30 | "style-loader": "^3.2.1", 31 | "webpack": "^5.47.0", 32 | "webpack-cli": "^4.7.2", 33 | "webpack-dev-server": "^4.0.0-rc.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sample/FableSass/sass-process.js: -------------------------------------------------------------------------------- 1 | const sass = require('sass') 2 | const fs = require('fs') 3 | const inputFile = fs.realpathSync(process.argv[2]) 4 | 5 | // run the preprocessor 6 | const result = sass.renderSync({ file: inputFile }) 7 | 8 | // write included files as leading output lines 9 | result.stats.includedFiles 10 | .filter(f => f !== inputFile) 11 | .forEach(f => console.log(f)) 12 | 13 | // write the final CSS output 14 | console.log("/* ---- end included files, begin generated CSS ---- */") 15 | console.log(result.css.toString()) 16 | -------------------------------------------------------------------------------- /sample/FableSass/sassdemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanaptak/TypedCssClasses/ee7a84f3314fe748c92026e31a96dac3f453a833/sample/FableSass/sassdemo.gif -------------------------------------------------------------------------------- /sample/FableSass/src/App.fs: -------------------------------------------------------------------------------- 1 | module App 2 | 3 | open Zanaptak.TypedCssClasses 4 | 5 | // Configure type provider to use result of sass transformation. 6 | type Css = 7 | CssClasses< 8 | "content/styles.sass" 9 | , Naming.PascalCase 10 | , commandFile = "node" 11 | , argumentPrefix = "sass-process.js" 12 | //, logFile = "TypedCssClasses.log" // uncomment to enable logging 13 | > 14 | 15 | open Feliz 16 | 17 | [] 18 | let Counter() = 19 | let (count, setCount) = React.useState(0) 20 | 21 | Html.div [ 22 | prop.classes [ 23 | Css.Base 24 | ] 25 | prop.children [ 26 | // Header row 27 | Html.div [ 28 | prop.classes [ 29 | Css.Inverse 30 | ] 31 | prop.text "Counter" 32 | ] 33 | // Counter row 34 | Html.div [ 35 | Html.button [ 36 | prop.onClick (fun _ -> setCount(count + 1)) 37 | prop.text "+" 38 | ] 39 | Html.div [ 40 | prop.classes [ 41 | Css.Number 42 | ] 43 | prop.text count 44 | ] 45 | Html.button [ 46 | prop.onClick (fun _ -> setCount(count - 1)) 47 | prop.text "-" 48 | ] 49 | ] 50 | ] 51 | ] 52 | 53 | [] 54 | let App() = 55 | Html.div [ 56 | Counter() 57 | ] 58 | -------------------------------------------------------------------------------- /sample/FableSass/src/Main.fs: -------------------------------------------------------------------------------- 1 | module Main 2 | 3 | open Feliz 4 | 5 | ReactDOM.render(App.App(), Browser.Dom.document.getElementById "app") 6 | -------------------------------------------------------------------------------- /sample/FableSass/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); // Inject 12 | 13 | -------------------------------------------------------------------------------- /test/TestWithFable/src/TestWithFable.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | preview 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ..\..\..\src\bin\Debug\netstandard2.0\Zanaptak.TypedCssClasses.dll 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/TestWithFable/src/folder/folder.fs: -------------------------------------------------------------------------------- 1 | module Folder 2 | open Fable.React 3 | open Fable.React.Props 4 | open Zanaptak.TypedCssClasses 5 | 6 | type css = CssClasses<"folder/folder1.css", logFile="folder/folder1.log"> 7 | let view model dispatch = 8 | div [ Class css.folder1class ] [ str "class: " ; str (nameof css.folder1class) ] 9 | 10 | type css2 = CssClasses<"folder2.css", logFile="folder2.log", resolutionFolder=__SOURCE_DIRECTORY__> 11 | let view2 model dispatch = 12 | div [ Class css2.folder2class ] [ str "class: " ; str (nameof css2.folder2class) ] 13 | -------------------------------------------------------------------------------- /test/TestWithFable/src/folder/folder1.css: -------------------------------------------------------------------------------- 1 | .folder1class { background-color: skyblue; } 2 | -------------------------------------------------------------------------------- /test/TestWithFable/src/folder/folder2.css: -------------------------------------------------------------------------------- 1 | .folder2class { background-color: skyblue; } 2 | -------------------------------------------------------------------------------- /test/TestWithFable/src/import/import.fs: -------------------------------------------------------------------------------- 1 | module Import 2 | open Fable.React 3 | open Fable.React.Props 4 | open Zanaptak.TypedCssClasses 5 | 6 | type css = CssClasses<"import1.css", logFile="import.log", commandFile="node", argumentPrefix="import.js", resolutionFolder=__SOURCE_DIRECTORY__> 7 | let view model dispatch = 8 | div [] [ 9 | div [ Class css.import1class ] [ str "class: " ; str (nameof css.import1class) ] 10 | div [ Class css.import2class ] [ str "class: " ; str (nameof css.import2class) ] 11 | ] 12 | -------------------------------------------------------------------------------- /test/TestWithFable/src/import/import.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss') 2 | const postcssImport = require('postcss-import') 3 | const fs = require('fs') 4 | const inputFile = fs.realpathSync(process.argv[2]) 5 | const css = fs.readFileSync(inputFile) 6 | postcss() 7 | .use(postcssImport) 8 | .process(css, { from: inputFile }) 9 | .then(result => { 10 | // write included files as leading output lines 11 | result.messages 12 | .filter(msg => msg.type === "dependency" && msg.file !== inputFile) 13 | .forEach(msg => console.log(msg.file)) 14 | // write the final CSS output 15 | console.log("/* ---- end included files, begin generated CSS ---- */") 16 | console.log(result.css) 17 | }) 18 | .catch(reason => { 19 | console.error(reason) 20 | process.exit(1) 21 | }) 22 | -------------------------------------------------------------------------------- /test/TestWithFable/src/import/import1.css: -------------------------------------------------------------------------------- 1 | @import "import2.css"; 2 | 3 | .import1class { background-color: skyblue; } 4 | -------------------------------------------------------------------------------- /test/TestWithFable/src/import/import2.css: -------------------------------------------------------------------------------- 1 | .import2class { background-color: skyblue; } 2 | -------------------------------------------------------------------------------- /test/TestWithFable/src/main.css: -------------------------------------------------------------------------------- 1 | .mainclass { background-color: skyblue; } 2 | -------------------------------------------------------------------------------- /test/TestWithFable/src/main.fs: -------------------------------------------------------------------------------- 1 | module Main 2 | open Fable.React 3 | open Fable.React.Props 4 | open Zanaptak.TypedCssClasses 5 | 6 | type css = CssClasses<"main.css", logFile="main.log"> 7 | let view model dispatch = 8 | div [] [ 9 | div [ Class css.mainclass ] [ str "class: " ; str (nameof css.mainclass) ] 10 | Folder.view model dispatch 11 | Folder.view2 model dispatch 12 | Import.view model dispatch 13 | Sass.view model dispatch 14 | ] 15 | 16 | open Fable.Core.JsInterop 17 | importSideEffects "./main.css" 18 | importSideEffects "./folder/folder1.css" 19 | importSideEffects "./folder/folder2.css" 20 | importSideEffects "./import/import1.css" 21 | importSideEffects "./sass/sass.sass" 22 | 23 | open Elmish 24 | open Elmish.React 25 | Program.mkSimple ignore ( fun _ _ -> () ) view 26 | |> Program.withReactSynchronous "elmish-app" 27 | |> Program.run 28 | -------------------------------------------------------------------------------- /test/TestWithFable/src/sass/sass.fs: -------------------------------------------------------------------------------- 1 | module Sass 2 | open Fable.React 3 | open Fable.React.Props 4 | open Zanaptak.TypedCssClasses 5 | 6 | type css = CssClasses<"sass.sass", logFile="sass.log", commandFile="sass,Windows=sass.cmd", resolutionFolder=__SOURCE_DIRECTORY__> 7 | let view model dispatch = 8 | div [ Class css.sassclass ] [ str "class: " ; str (nameof css.sassclass) ] 9 | -------------------------------------------------------------------------------- /test/TestWithFable/src/sass/sass.sass: -------------------------------------------------------------------------------- 1 | .sassclass 2 | background-color: skyblue 3 | -------------------------------------------------------------------------------- /test/TestWithFable/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | mode: "development", 5 | entry: "./src/TestWithFable.fsproj", 6 | output: { 7 | path: path.join(__dirname, "./public"), 8 | filename: "bundle.js", 9 | }, 10 | devServer: { 11 | static: { 12 | directory: path.resolve(__dirname, './public'), 13 | } 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.fs(x|proj)?$/, 19 | use: "fable-loader" 20 | } 21 | , { 22 | test: /\.css$/i, 23 | use: ['style-loader', 'css-loader'], 24 | } 25 | , { 26 | test: /\.s[ac]ss$/i, 27 | use: ['style-loader', 'css-loader', 'sass-loader'], 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/TypedCssClasses.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | module TypedCssClasses.Tests 2 | 3 | open NUnit.Framework 4 | open Zanaptak.TypedCssClasses 5 | open Zanaptak.TypedCssClasses.Utils 6 | open Zanaptak.TypedCssClasses.Internal.FSharpData.ProvidedTypes 7 | open System 8 | open System.Reflection 9 | open System.IO 10 | 11 | let parseCss text naming collisions = 12 | tryParseCssClassNames text 13 | |> Option.map( getPropertiesFromClassNames naming collisions ) 14 | |> Option.defaultValue [||] 15 | 16 | let [< Test >] ``find classes bootstrap`` () = 17 | let assembly = Assembly.GetExecutingAssembly() 18 | 19 | let refClasses = 20 | let resource = assembly.GetManifestResourceStream( "TypedCssClasses.Tests.testdata.bootstrap-431-classes-reference.txt" ) 21 | use reader = new StreamReader( resource ) 22 | reader.ReadToEnd().Split( [| '\n' ; '\r' |] , StringSplitOptions.RemoveEmptyEntries ) 23 | 24 | let cssToParse = 25 | let resource = assembly.GetManifestResourceStream( "TypedCssClasses.Tests.testdata.bootstrap-431-min-css.txt" ) 26 | use reader = new StreamReader( resource ) 27 | reader.ReadToEnd() 28 | 29 | let parsed = parseCss cssToParse Naming.Verbatim NameCollisions.BasicSuffix 30 | 31 | let refSet = Set.ofArray refClasses 32 | let parsedSet = parsed |> Seq.map ( fun p -> p.Value ) |> Set.ofSeq 33 | 34 | let refExtra = Set.difference refSet parsedSet 35 | let parsedExtra = Set.difference parsedSet refSet 36 | 37 | Assert.That( parsedExtra , Is.Empty ) 38 | Assert.That( refExtra , Is.Empty ) 39 | 40 | let [< Test >] ``find classes tailwind`` () = 41 | let assembly = Assembly.GetExecutingAssembly() 42 | 43 | let refClasses = 44 | let resource = assembly.GetManifestResourceStream( "TypedCssClasses.Tests.testdata.tailwind-10-classes-reference.txt" ) 45 | use reader = new StreamReader( resource ) 46 | reader.ReadToEnd().Split( [| '\n' ; '\r' |] , StringSplitOptions.RemoveEmptyEntries ) 47 | 48 | let cssToParse = 49 | let resource = assembly.GetManifestResourceStream( "TypedCssClasses.Tests.testdata.tailwind-10-min-css.txt" ) 50 | use reader = new StreamReader( resource ) 51 | reader.ReadToEnd() 52 | 53 | let parsed = parseCss cssToParse Naming.Verbatim NameCollisions.BasicSuffix 54 | 55 | let refSet = Set.ofArray refClasses 56 | let parsedSet = parsed |> Seq.map ( fun p -> p.Value ) |> Set.ofSeq 57 | 58 | let refExtra = Set.difference refSet parsedSet 59 | let parsedExtra = Set.difference parsedSet refSet 60 | 61 | Assert.That( parsedExtra , Is.Empty ) 62 | Assert.That( refExtra , Is.Empty ) 63 | 64 | 65 | let [< Test >] ``Underscores: keep non ascii letters`` () = 66 | Assert.That( symbolsToUnderscores "αβⅢ" , Is.EqualTo "αβⅢ" ) 67 | 68 | let [< Test >] ``Underscores: replace connector punctuation`` () = 69 | Assert.That( symbolsToUnderscores "a⁀b_c" , Is.EqualTo "a_b_c" ) 70 | 71 | let [< Test >] ``Underscores: replace single quote`` () = 72 | Assert.That( symbolsToUnderscores "a'b" , Is.EqualTo "a_b" ) 73 | 74 | let [< Test >] ``Underscores: replace other punctuation`` () = 75 | Assert.That( symbolsToUnderscores "a:b‖c" , Is.EqualTo "a_b_c" ) 76 | 77 | let [< Test >] ``Underscores: replace supplementary char`` () = 78 | Assert.That( symbolsToUnderscores "a😀b𐐥c" , Is.EqualTo "a_b_c" ) 79 | 80 | let [< Test >] ``Underscores: prefix illegal first char`` () = 81 | Assert.That( symbolsToUnderscores "123" , Is.EqualTo "_123" ) 82 | 83 | let [< Test >] ``Underscores: suffix keyword`` () = 84 | Assert.That( symbolsToUnderscores "inline" , Is.EqualTo "inline_" ) 85 | 86 | let [< Test >] ``Underscores: preserve case`` () = 87 | Assert.That( symbolsToUnderscores "CARD-BODY" , Is.EqualTo "CARD_BODY" ) 88 | 89 | 90 | let [< Test >] ``PascalCase: upper non ascii letter at word boundary`` () = 91 | Assert.That( toPascalCase "ΑΒ-ΓΔ" , Is.EqualTo "ΑβΓδ" ) 92 | 93 | let [< Test >] ``PascalCase: connector punctuation to capitalization boundary`` () = 94 | Assert.That( toPascalCase "aa⁀bb_cc" , Is.EqualTo "AaBbCc" ) 95 | 96 | let [< Test >] ``PascalCase: single quote to capitalization boundary`` () = 97 | Assert.That( toPascalCase "aa'bb" , Is.EqualTo "AaBb" ) 98 | 99 | let [< Test >] ``PascalCase: other punctuation to capitalization boundary`` () = 100 | Assert.That( toPascalCase "aa:bb‖cc" , Is.EqualTo "AaBbCc" ) 101 | 102 | let [< Test >] ``PascalCase: supplementary char to capitalization boundary`` () = 103 | Assert.That( toPascalCase "aa😀bb𐐥cc" , Is.EqualTo "AaBbCc" ) 104 | 105 | let [< Test >] ``PascalCase: prefix illegal first char`` () = 106 | Assert.That( toPascalCase "123" , Is.EqualTo "_123" ) 107 | 108 | let [< Test >] ``PascalCase: all upper to mixed case`` () = 109 | Assert.That( toPascalCase "CARD-BODY" , Is.EqualTo "CardBody" ) 110 | 111 | let [< Test >] ``PascalCase: lower + upper preserved as word boundary between then`` () = 112 | Assert.That( toPascalCase "CardBody" , Is.EqualTo "CardBody" ) 113 | 114 | let [< Test >] ``PascalCase: upper + lower starts word boundary after string of uppers`` () = 115 | Assert.That( toPascalCase "CARDBody" , Is.EqualTo "CardBody" ) 116 | 117 | 118 | let [< Test >] ``CamelCase: upper non ascii letter at word boundary`` () = 119 | Assert.That( toCamelCase "ΑΒ-ΓΔ" , Is.EqualTo "αβΓδ" ) 120 | 121 | let [< Test >] ``CamelCase: connector punctuation to capitalization boundary`` () = 122 | Assert.That( toCamelCase "aa⁀bb_cc" , Is.EqualTo "aaBbCc" ) 123 | 124 | let [< Test >] ``CamelCase: single quote to capitalization boundary`` () = 125 | Assert.That( toCamelCase "aa'bb" , Is.EqualTo "aaBb" ) 126 | 127 | let [< Test >] ``CamelCase: other punctuation to capitalization boundary`` () = 128 | Assert.That( toCamelCase "aa:bb‖cc" , Is.EqualTo "aaBbCc" ) 129 | 130 | let [< Test >] ``CamelCase: supplementary char to capitalization boundary`` () = 131 | Assert.That( toCamelCase "aa😀bb𐐥cc" , Is.EqualTo "aaBbCc" ) 132 | 133 | let [< Test >] ``CamelCase: prefix illegal first char`` () = 134 | Assert.That( toCamelCase "123" , Is.EqualTo "_123" ) 135 | 136 | let [< Test >] ``CamelCase: all upper to mixed case`` () = 137 | Assert.That( toCamelCase "CARD-BODY" , Is.EqualTo "cardBody" ) 138 | 139 | let [< Test >] ``CamelCase: lower + upper preserved as word boundary between then`` () = 140 | Assert.That( toCamelCase "CardBody" , Is.EqualTo "cardBody" ) 141 | 142 | let [< Test >] ``CamelCase: upper + lower starts word boundary after string of uppers`` () = 143 | Assert.That( toCamelCase "CARDBody" , Is.EqualTo "cardBody" ) 144 | 145 | let [< Test >] ``CamelCase: suffix keyword`` () = 146 | Assert.That( toCamelCase "INLINE" , Is.EqualTo "inline_" ) 147 | 148 | 149 | let [< Test >] ``Verbatim: remove double-backticks entry`` () = 150 | let parsedCss = parseCss ".abc``xyz {} .abc`xyz {} .abcxyz {}" Naming.Verbatim NameCollisions.BasicSuffix 151 | Assert.That( parsedCss , Does.Not.Contain { Name = "abc``xyz" ; Value = "abc``xyz" } ) 152 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "abcxyz" ) ) 153 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "abc`xyz" ) ) 154 | Assert.That( parsedCss , Has.Length.EqualTo 2 ) 155 | 156 | let [< Test >] ``Underscores: matching name gets non-suffixed name`` () = 157 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.Underscores NameCollisions.BasicSuffix 158 | Assert.That( parsedCss , Does.Contain { Name = "card_body" ; Value = "card_body" } ) 159 | Assert.That( parsedCss , Does.Not.Contain { Name = "card_body" ; Value = "card-body" } ) 160 | Assert.That( parsedCss , Does.Contain { Name = "card_body_2" ; Value = "card-body" } ) 161 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "cardBody" ) ) 162 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "CardBody" ) ) 163 | 164 | let [< Test >] ``PascalCase: matching name gets non-suffixed name`` () = 165 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.PascalCase NameCollisions.BasicSuffix 166 | Assert.That( parsedCss , Does.Contain { Name = "CardBody" ; Value = "CardBody" } ) 167 | Assert.That( parsedCss , Does.Not.Contain { Name = "CardBody" ; Value = "cardBody" } ) 168 | Assert.That( parsedCss , Does.Contain { Name = "CardBody_2" ; Value = "cardBody" } ) 169 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "CardBody_3" ) ) 170 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "CardBody_4" ) ) 171 | Assert.IsFalse( parsedCss |> Seq.exists ( fun p -> p.Name = "CardBody_5" ) ) 172 | 173 | let [< Test >] ``CamelCase: matching name gets non-suffixed name`` () = 174 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.CamelCase NameCollisions.BasicSuffix 175 | Assert.That( parsedCss , Does.Contain { Name = "cardBody" ; Value = "cardBody" } ) 176 | Assert.That( parsedCss , Does.Not.Contain { Name = "cardBody" ; Value = "CardBody" } ) 177 | Assert.That( parsedCss , Does.Contain { Name = "cardBody_2" ; Value = "CardBody" } ) 178 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "cardBody_3" ) ) 179 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "cardBody_4" ) ) 180 | Assert.IsFalse( parsedCss |> Seq.exists ( fun p -> p.Name = "cardBody_5" ) ) 181 | 182 | let [< Test >] ``unicode escapes`` () = 183 | let parsedCss = parseCss @".a\:b\00216bc {} " Naming.Verbatim NameCollisions.BasicSuffix 184 | Assert.That( parsedCss , Does.Contain { Name = "a:bⅫc" ; Value = "a:bⅫc" } ) 185 | 186 | let [< Test >] ``Underscores: extended suffix`` () = 187 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.Underscores NameCollisions.ExtendedSuffix 188 | Assert.That( parsedCss.Length , Is.EqualTo 4 ) 189 | Assert.That( parsedCss , Does.Contain { Name = "card_body__1_of_2" ; Value = "card_body" } ) 190 | Assert.That( parsedCss , Does.Contain { Name = "card_body__2_of_2" ; Value = "card-body" } ) 191 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "cardBody" ) ) 192 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "CardBody" ) ) 193 | 194 | let [< Test >] ``Underscores: extended suffix conflict with existing`` () = 195 | let parsedCss = parseCss @".card_body__1_of_2 {} .card_body__2_of_2 {} .card\:body {} .card\^body {}" Naming.Underscores NameCollisions.ExtendedSuffix 196 | Assert.That( parsedCss.Length , Is.EqualTo 4 ) 197 | Assert.That( parsedCss , Does.Contain { Name = "card_body__1_of_2" ; Value = "card_body__1_of_2" } ) 198 | Assert.That( parsedCss , Does.Contain { Name = "card_body__2_of_2" ; Value = "card_body__2_of_2" } ) 199 | // extra underscore due to conflict with existing property 200 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "card_body___1_of_2" ) ) 201 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "card_body___2_of_2" ) ) 202 | 203 | let [< Test >] ``Underscores: omit`` () = 204 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.Underscores NameCollisions.Omit 205 | Assert.That( parsedCss.Length , Is.EqualTo 2 ) 206 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "cardBody" ) ) 207 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "CardBody" ) ) 208 | Assert.IsFalse( parsedCss |> Seq.exists ( fun p -> p.Value = "card-body" ) ) 209 | Assert.IsFalse( parsedCss |> Seq.exists ( fun p -> p.Value = "card_body" ) ) 210 | 211 | let [< Test >] ``PascalCase: extended suffix`` () = 212 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.PascalCase NameCollisions.ExtendedSuffix 213 | Assert.That( parsedCss.Length , Is.EqualTo 4 ) 214 | Assert.That( parsedCss , Does.Contain { Name = "CardBody__1_of_4" ; Value = "CardBody" } ) 215 | Assert.That( parsedCss , Does.Contain { Name = "CardBody__2_of_4" ; Value = "cardBody" } ) 216 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "CardBody__3_of_4" ) ) 217 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "CardBody__4_of_4" ) ) 218 | 219 | let [< Test >] ``PascalCase: omit`` () = 220 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.PascalCase NameCollisions.Omit 221 | Assert.That( parsedCss.Length , Is.EqualTo 0 ) 222 | 223 | let [< Test >] ``CamelCase: extended suffix`` () = 224 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.CamelCase NameCollisions.ExtendedSuffix 225 | Assert.That( parsedCss.Length , Is.EqualTo 4 ) 226 | Assert.That( parsedCss , Does.Contain { Name = "cardBody__1_of_4" ; Value = "cardBody" } ) 227 | Assert.That( parsedCss , Does.Contain { Name = "cardBody__2_of_4" ; Value = "CardBody" } ) 228 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "cardBody__3_of_4" ) ) 229 | Assert.IsTrue( parsedCss |> Seq.exists ( fun p -> p.Name = "cardBody__4_of_4" ) ) 230 | 231 | let [< Test >] ``CamelCase: omit`` () = 232 | let parsedCss = parseCss ".card-body {} .card_body {} .cardBody {} .CardBody {}" Naming.CamelCase NameCollisions.Omit 233 | Assert.That( parsedCss.Length , Is.EqualTo 0 ) 234 | -------------------------------------------------------------------------------- /test/TypedCssClasses.Tests/TypedCssClasses.Tests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | false 6 | true 7 | Debug;Release;ReleaseTest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ..\..\src\bin\Debug\netstandard2.0\Zanaptak.TypedCssClasses.dll 30 | 31 | 32 | 33 | 34 | 35 | ..\..\src\bin\ReleaseTest\netstandard2.0\Zanaptak.TypedCssClasses.dll 36 | 37 | 38 | 39 | 40 | 41 | ..\..\src\bin\Release\netstandard2.0\Zanaptak.TypedCssClasses.dll 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/TypedCssClasses.Tests/TypedCssClasses.Tests.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29123.88 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "TypedCssClasses.Tests", "TypedCssClasses.Tests.fsproj", "{1D0B9375-7396-4A46-939D-F158844562C3}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | ReleaseTest|Any CPU = ReleaseTest|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {1D0B9375-7396-4A46-939D-F158844562C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {1D0B9375-7396-4A46-939D-F158844562C3}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {1D0B9375-7396-4A46-939D-F158844562C3}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {1D0B9375-7396-4A46-939D-F158844562C3}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {1D0B9375-7396-4A46-939D-F158844562C3}.ReleaseTest|Any CPU.ActiveCfg = ReleaseTest|Any CPU 20 | {1D0B9375-7396-4A46-939D-F158844562C3}.ReleaseTest|Any CPU.Build.0 = ReleaseTest|Any CPU 21 | EndGlobalSection 22 | GlobalSection(SolutionProperties) = preSolution 23 | HideSolutionNode = FALSE 24 | EndGlobalSection 25 | GlobalSection(ExtensibilityGlobals) = postSolution 26 | SolutionGuid = {D6D60F33-2562-4B24-BE71-BC16A629A587} 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /test/TypedCssClasses.Tests/testdata/tailwind-10-classes-reference.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanaptak/TypedCssClasses/ee7a84f3314fe748c92026e31a96dac3f453a833/test/TypedCssClasses.Tests/testdata/tailwind-10-classes-reference.txt --------------------------------------------------------------------------------