Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 8.1.0-beta

- Add `PreferOptionals` parameter to `JsonProvider` and `XmlProvider` (defaults to `true` to match existing behavior; set to `false` to use empty string or `NaN` for missing values, like the CsvProvider default) (closes #649)
- Add `MarkdownProvider` type provider for typed access to YAML front matter in Markdown files (closes #1657)

## 8.0.0 - Feb 25 2026

Expand Down
145 changes: 145 additions & 0 deletions docs/library/MarkdownProvider.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
(**
---
category: Type Providers
categoryindex: 1
index: 8
---
*)
(*** condition: prepare ***)
#r "../../src/FSharp.Data/bin/Release/netstandard2.0/FSharp.Data.Http.dll"
#r "../../src/FSharp.Data/bin/Release/netstandard2.0/FSharp.Data.Runtime.Utilities.dll"
#r "../../src/FSharp.Data/bin/Release/netstandard2.0/FSharp.Data.Json.Core.dll"
#r "../../src/FSharp.Data/bin/Release/netstandard2.0/FSharp.Data.dll"
(*** condition: fsx ***)
#if FSX
#r "nuget: FSharp.Data,{{fsdocs-package-version}}"
#endif
(*** condition: ipynb ***)
#if IPYNB
#r "nuget: FSharp.Data,{{fsdocs-package-version}}"

Formatter.SetPreferredMimeTypesFor(typeof<obj>, "text/plain")
Formatter.Register(fun (x: obj) (writer: TextWriter) -> fprintfn writer "%120A" x)
#endif
(**
# Markdown Type Provider

The `MarkdownProvider` gives you statically typed access to [YAML front matter](https://jekyllrb.com/docs/front-matter/)
in Markdown files. It infers the types of front matter fields from a sample file and exposes them as
typed properties. The body of the document (everything after the front matter delimiter `---`) is
always available as a `Body: string` property.

This is particularly useful for static site generators, documentation tools, blog engines, and any
application that stores metadata in Markdown files (e.g. Hugo, Jekyll, Eleventy).

## Basic Usage

Given a Markdown file `post.md`:

```markdown
---
title: Hello World
date: 2024-01-15
author: Jane Doe
tags: [fsharp, data, markdown]
draft: false
views: 1234
---

# Hello World

This is the body of the post.
```

You can write:
*)

open FSharp.Data

type Post = MarkdownProvider<"../tests/FSharp.Data.Tests/Data/BlogPost.md">

let post = Post.GetSample()

(** The front matter fields are strongly typed: *)

let title = post.Title // string: "Hello World"
let date = post.Date // System.DateTime: 2024-01-15
let author = post.Author // string: "Jane Doe"
let tags = post.Tags // string[]: [| "fsharp"; "data"; "markdown" |]
let draft = post.Draft // bool: false
let views = post.Views // int: 1234

(** The body is always accessible as a `string` property: *)

let body = post.Body // string: the markdown content after ---

(**
## Loading Multiple Files

Use `Load` to load any file matching the same schema as the sample:
*)

// let post2 = Post.Load("another-post.md")

(**
## Inline Samples

You can use an inline Markdown string as the sample:
*)

type InlineDoc =
MarkdownProvider<"""---
title: My Title
count: 42
enabled: true
---
Body here.""">

let doc = InlineDoc.Parse("""---
title: Hello
count: 100
enabled: false
---
Different body.""")

// doc.Title // "Hello"
// doc.Count // 100
// doc.Enabled // false
// doc.Body // "Different body."

(**
## Supported Front Matter Types

The `MarkdownProvider` infers types using the same rules as `JsonProvider`:

| YAML value | F# type |
|---|---|
| `title: Hello` | `string` |
| `count: 42` | `int` |
| `rating: 4.5` | `decimal` |
| `enabled: true` | `bool` |
| `date: 2024-01-15` | `System.DateTime` |
| `tags: [a, b, c]` | `string[]` |
| `weight: null` | `string option` |

## Static Parameters

| Parameter | Default | Description |
|---|---|---|
| `Sample` | `""` | Path to a sample `.md` file or an inline Markdown string |
| `RootName` | `"Root"` | Name for the generated root type |
| `Culture` | `""` | Culture for parsing dates and numbers |
| `Encoding` | `""` | File encoding (default: UTF-8) |
| `ResolutionFolder` | `""` | Directory for resolving relative file paths at design time |
| `EmbeddedResource` | `""` | Embedded resource name for the sample |
| `InferTypesFromValues` | `true` | Enable type inference from values (e.g. `"123"` β†’ `int`) |
| `UseOriginalNames` | `false` | Use front matter key names as-is (no PascalCase normalisation) |
| `PreferOptionals` | `true` | Use option types for missing/null values |

## Notes

- Only YAML front matter (delimited by `---`) is supported. TOML front matter (`+++`) is not.
- The YAML parser handles strings, numbers, booleans, null, inline arrays (`[a, b]`), and block arrays. Nested objects are not currently supported.
- The `Body` property returns the raw Markdown text after the closing `---` delimiter (including any leading newline). Use `.Trim()` if needed.
- `With*` methods are generated for each property, enabling non-destructive updates.
*)
1 change: 1 addition & 0 deletions src/FSharp.Data.DesignTime/FSharp.Data.DesignTime.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<Compile Include="WorldBank/WorldBankProvider.fs" />
<Compile Include="Html/HtmlGenerator.fs" />
<Compile Include="Html/HtmlProvider.fs" />
<Compile Include="Markdown/MarkdownProvider.fs" />
<Compile Include="../AssemblyInfo.DesignTime.fs" />
<None Include="../Test.fsx" />
<None Include="paket.references" />
Expand Down
155 changes: 155 additions & 0 deletions src/FSharp.Data.DesignTime/Markdown/MarkdownProvider.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// --------------------------------------------------------------------------------------
// Markdown type provider – typed access to YAML front matter in markdown files
// --------------------------------------------------------------------------------------
namespace ProviderImplementation

open System
open System.IO
open FSharp.Core.CompilerServices
open ProviderImplementation
open ProviderImplementation.ProvidedTypes
open ProviderImplementation.ProviderHelpers
open FSharp.Data
open FSharp.Data.Runtime
open FSharp.Data.Runtime.BaseTypes
open FSharp.Data.Runtime.StructuralTypes
open FSharp.Data.Runtime.StructuralInference
open System.Net

#nowarn "10001"

[<TypeProvider>]
type public MarkdownProvider(cfg: TypeProviderConfig) as this =
inherit
DisposableTypeProviderForNamespaces(cfg, assemblyReplacementMap = [ "FSharp.Data.DesignTime", "FSharp.Data" ])

do AssemblyResolver.init ()
let asm = System.Reflection.Assembly.GetExecutingAssembly()
let ns = "FSharp.Data"

let markdownProvTy =
ProvidedTypeDefinition(asm, ns, "MarkdownProvider", None, hideObjectMethods = true, nonNullable = true)

let buildTypes (typeName: string) (args: obj[]) =

// Enable TLS 1.2 for samples requested through https.
ServicePointManager.SecurityProtocol <- ServicePointManager.SecurityProtocol ||| SecurityProtocolType.Tls12

let tpType =
ProvidedTypeDefinition(asm, ns, typeName, None, hideObjectMethods = true, nonNullable = true)

let sample = args.[0] :?> string

let rootName = args.[1] :?> string

let rootName =
if String.IsNullOrWhiteSpace rootName then
"Root"
else
NameUtils.singularize rootName

let cultureStr = args.[2] :?> string
let encodingStr = args.[3] :?> string
let resolutionFolder = args.[4] :?> string
let resource = args.[5] :?> string
let inferTypesFromValues = args.[6] :?> bool
let useOriginalNames = args.[7] :?> bool
let preferOptionals = args.[8] :?> bool

let cultureInfo = TextRuntime.GetCulture cultureStr
let unitsOfMeasureProvider = ProviderHelpers.unitsOfMeasureProvider

let inferenceMode =
InferenceMode'.FromPublicApi(InferenceMode.BackwardCompatible, inferTypesFromValues)

let getSpec _ value =

// Parse the sample markdown, extracting the front matter as a JsonValue record
let sampleJson = MarkdownDocument.ParseSample value

let inferedType =
use _holder = IO.logTime "Inference" sample

let rawInfered =
JsonInference.inferType
unitsOfMeasureProvider
inferenceMode
cultureInfo
(not preferOptionals)
""
sampleJson

#if NET6_0_OR_GREATER
StructuralInference.downgradeNet6Types rawInfered
#else
rawInfered
#endif

use _holder = IO.logTime "TypeGeneration" sample

let ctx =
JsonGenerationContext.Create(
cultureStr,
tpType,
unitsOfMeasureProvider,
inferenceMode,
?useOriginalNames = Some useOriginalNames
)

let result = JsonTypeBuilder.generateJsonType ctx false false rootName inferedType

// Add a Body property to the root generated type (the type returned by Load/Parse).
// At runtime the underlying representation is MarkdownDocument (which implements IJsonDocument),
// so downcasting to MarkdownDocument is always safe for root-level instances.
match result.ConvertedType with
| :? ProvidedTypeDefinition as rootTy ->
let bodyProp =
ProvidedProperty(
"Body",
typeof<string>,
getterCode = fun args -> <@@ ((%%args.[0]: IJsonDocument) :?> MarkdownDocument).Body @@>
)

bodyProp.AddXmlDoc "The markdown body content (all text after the front matter delimiter `---`)."
rootTy.AddMember bodyProp
| _ -> ()

{ GeneratedType = tpType
RepresentationType = result.ConvertedType
CreateFromTextReader = fun reader -> result.Convert <@@ MarkdownDocument.Load(%reader) @@>
CreateListFromTextReader = None
CreateFromTextReaderForSampleList = fun reader -> result.Convert <@@ MarkdownDocument.Load(%reader) @@>
CreateFromValue = None }

generateType "Markdown" (Sample sample) getSpec this cfg encodingStr resolutionFolder resource typeName None

// Static parameters exposed to the user
let parameters =
[ ProvidedStaticParameter("Sample", typeof<string>, parameterDefaultValue = "")
ProvidedStaticParameter("RootName", typeof<string>, parameterDefaultValue = "Root")
ProvidedStaticParameter("Culture", typeof<string>, parameterDefaultValue = "")
ProvidedStaticParameter("Encoding", typeof<string>, parameterDefaultValue = "")
ProvidedStaticParameter("ResolutionFolder", typeof<string>, parameterDefaultValue = "")
ProvidedStaticParameter("EmbeddedResource", typeof<string>, parameterDefaultValue = "")
ProvidedStaticParameter("InferTypesFromValues", typeof<bool>, parameterDefaultValue = true)
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false)
ProvidedStaticParameter("PreferOptionals", typeof<bool>, parameterDefaultValue = true) ]

let helpText =
"""<summary>Typed representation of a Markdown document with YAML front matter.</summary>
<param name='Sample'>Location of a Markdown sample file or a string containing a sample Markdown document with YAML front matter.</param>
<param name='RootName'>The name to be used for the root type. Defaults to `Root`.</param>
<param name='Culture'>The culture used for parsing numbers and dates. Defaults to the invariant culture.</param>
<param name='Encoding'>The encoding used to read the sample. You can specify either the character set name or the codepage number. Defaults to UTF8 for files.</param>
<param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param>
<param name='EmbeddedResource'>When specified, the type provider first attempts to load the sample from the specified resource
(e.g. 'MyCompany.MyAssembly, resource_name.md'). This is useful when exposing types generated by the type provider.</param>
<param name='InferTypesFromValues'>If true, turns on additional type inference from values (e.g. "123" inferred as int). Defaults to true.</param>
<param name='UseOriginalNames'>When true, YAML front matter key names are used as-is for generated property names instead of being normalized to PascalCase. Defaults to false.</param>
<param name='PreferOptionals'>When true (default), missing or null front matter values are represented as option types. When false, missing values use empty string or NaN.</param>"""

do markdownProvTy.AddXmlDoc helpText
do markdownProvTy.DefineStaticParameters(parameters, buildTypes)

// Register the main type with the F# compiler
do this.AddNamespace(ns, [ markdownProvTy ])
1 change: 1 addition & 0 deletions src/FSharp.Data.Json.Core/FSharp.Data.Json.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Compile Include="JsonExtensions.fs" />
<Compile Include="JsonDocument.fs" />
<Compile Include="JsonRuntime.fs" />
<Compile Include="MarkdownDocument.fs" />
<Compile Include="JsonInference.fs" />
<Compile Include="JsonSchema.fs" />
<Compile Include="../AssemblyInfo.Json.Core.fs" />
Expand Down
Loading
Loading