Skip to content

Commit 2c8e690

Browse files
authored
Merge pull request #846 from nojaf/previous-next-page
Add previous and next page url substitutions
2 parents 676a00f + 2b01ba2 commit 2c8e690

File tree

14 files changed

+351
-82
lines changed

14 files changed

+351
-82
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ tests/FSharp.Literate.Tests/output1/
4747
.vscode/
4848
.DS_Store
4949
tests/FSharp.Literate.Tests/output2/
50+
tests/FSharp.Literate.Tests/previous-next-output/

docs/content.fsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ See [Styling](styling.html) for information about template parameters and stylin
146146
| `fsdocs-source-basename` | Name of original input source, excluding its extensions, relative to the `docs` root |
147147
| `fsdocs-tooltips` | Generated hidden div elements for tooltips |
148148
| `fsdocs-watch-script` | The websocket script used in watch mode to trigger hot reload |
149+
| `fsdocs-previous-page-link` | A relative link to the previous page based on the frontmatter index data |
150+
| `fsdocs-next-page-link` | A relative link to the next page based on the frontmatter index data |
149151

150152
The following substitutions are extracted from your project files and may or may not be used by the default
151153
template:

src/FSharp.Formatting.Common/Templating.fs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,48 @@ type ParamKey =
2323
/// A list of parameters for substituting in templates, indexed by parameter keys
2424
type Substitutions = (ParamKey * string) list
2525

26+
/// Meta data from files that contains front matter
27+
/// Used to determine upfront which files have front matter so that previous and next substitutes can be discovered.
28+
type FrontMatterFile =
29+
{ FileName: string
30+
Category: string
31+
CategoryIndex: int
32+
Index: int }
33+
34+
/// Parses the category, categoryindex and index from the frontmatter lines
35+
static member ParseFromLines (fileName: string) (lines: string seq) =
36+
let (|ValidIndex|_|) (value: string) =
37+
match Int32.TryParse value with
38+
| true, i -> Some i
39+
| false, _ -> None
40+
41+
let keyValues =
42+
lines
43+
// Skip opening lines
44+
|> Seq.skipWhile (fun line ->
45+
let line = line.Trim()
46+
line = "(**" || line = "---")
47+
|> Seq.takeWhile (fun line ->
48+
// Allow empty lines in frontmatter
49+
let isBlankLine = String.IsNullOrWhiteSpace line
50+
isBlankLine || line.Contains(":"))
51+
|> Seq.filter (String.IsNullOrWhiteSpace >> not)
52+
|> Seq.map (fun line ->
53+
let parts = line.Split(":")
54+
parts.[0].ToLowerInvariant(), parts.[1])
55+
|> Map.ofSeq
56+
57+
match
58+
Map.tryFind "category" keyValues, Map.tryFind "categoryindex" keyValues, Map.tryFind "index" keyValues
59+
with
60+
| Some category, Some(ValidIndex categoryindex), Some(ValidIndex index) ->
61+
Some
62+
{ FileName = fileName
63+
Category = category.Trim()
64+
CategoryIndex = categoryindex
65+
Index = index }
66+
| _ -> None
67+
2668
/// <summary>
2769
/// Defines the parameter keys known to FSharp.Formatting processing code
2870
/// </summary>
@@ -134,6 +176,12 @@ module ParamKeys =
134176
/// A parameter key known to FSharp.Formatting, available in _menu-item_template.html
135177
let ``fsdocs-menu-item-id`` = ParamKey "fsdocs-menu-item-id"
136178

179+
/// A parameter key known to FSharp.Formatting, available when frontmatter is used correctly
180+
let ``fsdocs-previous-page-link`` = ParamKey "fsdocs-previous-page-link"
181+
182+
/// A parameter key known to FSharp.Formatting, available when frontmatter is used correctly
183+
let ``fsdocs-next-page-link`` = ParamKey "fsdocs-next-page-link"
184+
137185
module internal SimpleTemplating =
138186

139187
#if NETSTANDARD2_0

src/FSharp.Formatting.Literate/Contexts.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ type internal LiterateDocModel =
6363
Category: string option
6464

6565
/// The category index in the front matter (determines the order of categories)
66-
CategoryIndex: string option
66+
CategoryIndex: int option
6767

6868
/// The index in the front matter (Determines the order of files within a category)
69-
Index: string option
69+
Index: int option
7070

7171
/// The relative output path
7272
OutputPath: string

src/FSharp.Formatting.Literate/Formatting.fs

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ module internal Formatting =
4040
mdlinkResolver = mdlinkResolver
4141
)
4242
| OutputKind.Html ->
43-
let sb = new System.Text.StringBuilder()
43+
let sb = System.Text.StringBuilder()
4444
use wr = new StringWriter(sb)
4545

4646
HtmlFormatting.formatAsHtml
@@ -105,11 +105,17 @@ module internal Formatting =
105105
| LiterateSource.Markdown text -> text
106106
| LiterateSource.Script snippets ->
107107
[ for Snippet(_name, lines) in snippets do
108-
for (Line(line, _)) in lines do
108+
for Line(line, _) in lines do
109109
yield line ]
110110
|> String.concat "\n"
111111

112-
let transformDocument (doc: LiterateDocument) (outputPath: string) ctx =
112+
let transformDocument
113+
// This array was sorted in BuildCommand.fs
114+
(filesWithFrontMatter: FrontMatterFile array)
115+
(doc: LiterateDocument)
116+
(outputPath: string)
117+
ctx
118+
=
113119

114120
let findInFrontMatter key =
115121
match doc.Paragraphs with
@@ -126,10 +132,14 @@ module internal Formatting =
126132
None)
127133
| _ -> None
128134

129-
let category = findInFrontMatter "category"
135+
let mkValidIndex (value: string) =
136+
match System.Int32.TryParse value with
137+
| true, i -> Some i
138+
| false, _ -> None
130139

131-
let categoryIndex = findInFrontMatter "categoryindex"
132-
let index = findInFrontMatter "index"
140+
let category = findInFrontMatter "category"
141+
let categoryIndex = findInFrontMatter "categoryindex" |> Option.bind mkValidIndex
142+
let index = findInFrontMatter "index" |> Option.bind mkValidIndex
133143
let titleFromFrontMatter = findInFrontMatter "title"
134144

135145
// If we want to include the source code of the script, then process
@@ -167,10 +177,62 @@ module internal Formatting =
167177
// Replace all special elements with ordinary Html/Latex Markdown
168178
let doc = Transformations.replaceLiterateParagraphs ctx doc
169179

180+
// construct previous and next urls
181+
let nextPreviousPageSubstitutions =
182+
let getLinksFromCurrentPageIdx currentPageIdx =
183+
match currentPageIdx with
184+
| None -> []
185+
| Some currentPageIdx ->
186+
let previousPage =
187+
filesWithFrontMatter
188+
|> Array.tryItem (currentPageIdx - 1)
189+
|> Option.bind (fun { FileName = fileName } ->
190+
ctx.MarkdownDirectLinkResolver fileName
191+
|> Option.map (fun link -> ParamKeys.``fsdocs-previous-page-link``, link))
192+
|> Option.toList
193+
194+
let nextPage =
195+
filesWithFrontMatter
196+
|> Array.tryItem (currentPageIdx + 1)
197+
|> Option.bind (fun { FileName = fileName } ->
198+
ctx.MarkdownDirectLinkResolver fileName
199+
|> Option.map (fun link -> ParamKeys.``fsdocs-next-page-link``, link))
200+
|> Option.toList
201+
202+
previousPage @ nextPage
203+
204+
match index, categoryIndex with
205+
| None, None
206+
| None, Some _ ->
207+
// Typical uses case here is the main index page.
208+
// If there is no frontmatter there, we want to propose the first available page
209+
filesWithFrontMatter
210+
|> Array.tryHead
211+
|> Option.bind (fun { FileName = fileName } ->
212+
ctx.MarkdownDirectLinkResolver fileName
213+
|> Option.map (fun link -> ParamKeys.``fsdocs-next-page-link``, link))
214+
|> Option.toList
215+
216+
| Some currentPageIdx, None ->
217+
let currentPageIdx =
218+
filesWithFrontMatter
219+
|> Array.tryFindIndex (fun { Index = idx } -> idx = currentPageIdx)
220+
221+
getLinksFromCurrentPageIdx currentPageIdx
222+
| Some currentPageIdx, Some currentCategoryIdx ->
223+
let currentPageIdx =
224+
filesWithFrontMatter
225+
|> Array.tryFindIndex (fun { Index = idx; CategoryIndex = cIdx } ->
226+
cIdx = currentCategoryIdx && idx = currentPageIdx)
227+
228+
getLinksFromCurrentPageIdx currentPageIdx
229+
170230
let substitutions0 =
171-
[ ParamKeys.``fsdocs-page-title``, pageTitle; ParamKeys.``fsdocs-page-source``, doc.SourceFile ]
172-
@ ctx.Substitutions
173-
@ sourceSubstitutions
231+
[ yield ParamKeys.``fsdocs-page-title``, pageTitle
232+
yield ParamKeys.``fsdocs-page-source``, doc.SourceFile
233+
yield! ctx.Substitutions
234+
yield! sourceSubstitutions
235+
yield! nextPreviousPageSubstitutions ]
174236

175237
let formattedDocument =
176238
format

src/FSharp.Formatting.Literate/Literate.fs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ open System.Collections.Generic
55
open System.IO
66
open System.Runtime.CompilerServices
77
open FSharp.Formatting.Markdown
8-
open FSharp.Formatting.CodeFormat
98
open FSharp.Formatting.Templating
109

1110
/// <summary>
@@ -260,7 +259,7 @@ type Literate private () =
260259
let doc =
261260
MarkdownDocument(doc.Paragraphs @ [ InlineHtmlBlock(doc.FormattedTips, None, None) ], doc.DefinedLinks)
262261

263-
let sb = new System.Text.StringBuilder()
262+
let sb = System.Text.StringBuilder()
264263
use wr = new StringWriter(sb)
265264

266265
HtmlFormatting.formatAsHtml
@@ -424,7 +423,8 @@ type Literate private () =
424423
crefResolver,
425424
mdlinkResolver,
426425
parseOptions,
427-
onError
426+
onError,
427+
filesWithFrontMatter: FrontMatterFile array
428428
) =
429429

430430
let parseOptions =
@@ -457,7 +457,7 @@ type Literate private () =
457457
None
458458

459459
let doc = downloadImagesForDoc imageSaver doc
460-
let docModel = Formatting.transformDocument doc output ctx
460+
let docModel = Formatting.transformDocument filesWithFrontMatter doc output ctx
461461
docModel
462462

463463
/// Parse and transform an F# script file
@@ -477,7 +477,8 @@ type Literate private () =
477477
rootInputFolder,
478478
crefResolver,
479479
mdlinkResolver,
480-
onError
480+
onError,
481+
filesWithFrontMatter: FrontMatterFile array
481482
) =
482483

483484
let parseOptions =
@@ -509,7 +510,7 @@ type Literate private () =
509510
None
510511

511512
let doc = downloadImagesForDoc imageSaver doc
512-
let docModel = Formatting.transformDocument doc output ctx
513+
let docModel = Formatting.transformDocument filesWithFrontMatter doc output ctx
513514
docModel
514515

515516
/// Convert a markdown file into HTML or another output kind
@@ -529,14 +530,16 @@ type Literate private () =
529530
?rootInputFolder,
530531
?crefResolver,
531532
?mdlinkResolver,
532-
?onError
533+
?onError,
534+
?filesWithFrontMatter
533535
) =
534536

535537
let outputKind = defaultArg outputKind OutputKind.Html
536538
let output = defaultOutput output input outputKind
537539
let crefResolver = defaultArg crefResolver (fun _ -> None)
538540
let mdlinkResolver = defaultArg mdlinkResolver (fun _ -> None)
539541
let substitutions = defaultArg substitutions []
542+
let filesWithFrontMatter = defaultArg filesWithFrontMatter Array.empty
540543

541544
let res =
542545
Literate.ParseAndTransformMarkdownFile(
@@ -554,7 +557,8 @@ type Literate private () =
554557
crefResolver = crefResolver,
555558
mdlinkResolver = mdlinkResolver,
556559
parseOptions = MarkdownParseOptions.AllowYamlFrontMatter,
557-
onError = onError
560+
onError = onError,
561+
filesWithFrontMatter = filesWithFrontMatter
558562
)
559563

560564
SimpleTemplating.UseFileAsSimpleTemplate(res.Substitutions, template, output)
@@ -582,14 +586,16 @@ type Literate private () =
582586
?rootInputFolder,
583587
?crefResolver,
584588
?mdlinkResolver,
585-
?onError
589+
?onError,
590+
?filesWithFrontMatter
586591
) =
587592

588593
let outputKind = defaultArg outputKind OutputKind.Html
589594
let output = defaultOutput output input outputKind
590595
let crefResolver = defaultArg crefResolver (fun _ -> None)
591596
let mdlinkResolver = defaultArg mdlinkResolver (fun _ -> None)
592597
let substitutions = defaultArg substitutions []
598+
let filesWithFrontMatter = defaultArg filesWithFrontMatter Array.empty
593599

594600
let res =
595601
Literate.ParseAndTransformScriptFile(
@@ -607,7 +613,8 @@ type Literate private () =
607613
rootInputFolder = rootInputFolder,
608614
crefResolver = crefResolver,
609615
mdlinkResolver = mdlinkResolver,
610-
onError = onError
616+
onError = onError,
617+
filesWithFrontMatter = filesWithFrontMatter
611618
)
612619

613620
SimpleTemplating.UseFileAsSimpleTemplate(res.Substitutions, template, output)

src/FSharp.Formatting.Literate/ParseScript.fs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
namespace FSharp.Formatting.Literate
22

33
open System.Collections.Generic
4+
open System.Text
5+
open FSharp.Compiler.CodeAnalysis
6+
open FSharp.Compiler.Syntax
7+
open FSharp.Compiler.SyntaxTrivia
8+
open FSharp.Compiler.Text
9+
open FSharp.Formatting.Templating
410
open FSharp.Patterns
511
open FSharp.Formatting.CodeFormat
612
open FSharp.Formatting.Markdown
@@ -389,3 +395,50 @@ type internal ParseScript(parseOptions, ctx: CompilerContext) =
389395
diagnostics = diagnostics,
390396
rootInputFolder = rootInputFolder
391397
)
398+
399+
/// Tries and parse the file to find and process the first block comment.
400+
static member ParseFrontMatter(fileName: string) : FrontMatterFile option =
401+
try
402+
let sourceText = (System.IO.File.ReadAllText >> SourceText.ofString) fileName
403+
let checker = FSharp.Formatting.Internal.CompilerServiceExtensions.FSharpAssemblyHelper.checker
404+
405+
let parseResult =
406+
checker.ParseFile(
407+
fileName,
408+
sourceText,
409+
{ FSharpParsingOptions.Default with
410+
SourceFiles = [| fileName |] }
411+
)
412+
|> Async.RunSynchronously
413+
414+
match parseResult.ParseTree with
415+
| ParsedInput.SigFile _ -> None
416+
| ParsedInput.ImplFile(ParsedImplFileInput.ParsedImplFileInput(trivia = { CodeComments = codeComments })) ->
417+
codeComments
418+
|> List.tryPick (function
419+
| CommentTrivia.BlockComment mBlockComment -> Some mBlockComment
420+
| CommentTrivia.LineComment _ -> None)
421+
|> Option.bind (fun mBlockComment ->
422+
// Grab the comment text from the ISourceText
423+
let commentText =
424+
let startLine = mBlockComment.StartLine - 1
425+
let line = sourceText.GetLineString startLine
426+
427+
if mBlockComment.StartLine = mBlockComment.EndLine then
428+
let length = mBlockComment.EndColumn - mBlockComment.StartColumn
429+
line.Substring(mBlockComment.StartColumn, length)
430+
else
431+
let firstLineContent = line.Substring(mBlockComment.StartColumn)
432+
let sb = StringBuilder().AppendLine(firstLineContent)
433+
434+
(sb, [ mBlockComment.StartLine .. mBlockComment.EndLine - 2 ])
435+
||> List.fold (fun sb lineNumber -> sb.AppendLine(sourceText.GetLineString lineNumber))
436+
|> fun sb ->
437+
let lastLine = sourceText.GetLineString(mBlockComment.EndLine - 1)
438+
sb.Append(lastLine.Substring(0, mBlockComment.EndColumn)).ToString()
439+
440+
let lines = commentText.Split '\n'
441+
FrontMatterFile.ParseFromLines fileName lines)
442+
with ex ->
443+
printfn "Failed to find frontmatter in %s, %A" fileName ex
444+
None

0 commit comments

Comments
 (0)