First commit
This commit is contained in:
commit
b6b2958592
4 changed files with 292 additions and 0 deletions
18
src/FibLib/FibLib.fsproj
Normal file
18
src/FibLib/FibLib.fsproj
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Library.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FParsec" Version="1.1.1" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="10.0.5" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
192
src/FibLib/Library.fs
Normal file
192
src/FibLib/Library.fs
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
namespace Fibble.FibLib
|
||||||
|
open System.Net
|
||||||
|
open FParsec
|
||||||
|
open YamlDotNet.Serialization
|
||||||
|
open System.Collections.Generic
|
||||||
|
|
||||||
|
module Ast =
|
||||||
|
type InlineNode =
|
||||||
|
| Text of string
|
||||||
|
| RawHtml of string
|
||||||
|
| Expr of code:string * result:string option
|
||||||
|
| MetaRef of string
|
||||||
|
| Element of tag:string * args:string list * children:InlineNode list
|
||||||
|
|
||||||
|
type BlockNode =
|
||||||
|
| Section of level:int * args:string list * children:InlineNode list
|
||||||
|
| Paragraph of children:InlineNode list
|
||||||
|
|
||||||
|
type Document = BlockNode list
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. Parser (FParsec)
|
||||||
|
// ==========================================
|
||||||
|
module Parser =
|
||||||
|
open Ast
|
||||||
|
|
||||||
|
let pInline, pInlineRef = createParserForwardedToRef<InlineNode, unit>()
|
||||||
|
|
||||||
|
let getSectionLevel (name: string) =
|
||||||
|
if name = "section" then 1
|
||||||
|
elif name = "subsection" then 2
|
||||||
|
elif name.StartsWith("sub") && name.EndsWith("section") then (name.Length - 7) / 3 + 1
|
||||||
|
else 1
|
||||||
|
|
||||||
|
let isSection (name: string) = name.EndsWith("section")
|
||||||
|
|
||||||
|
// --- Nya parsers för argument och body ---
|
||||||
|
let pArg = spaces >>. manyChars (noneOf ",]") .>> spaces
|
||||||
|
let pArgs = between (pstring "[") (pstring "]") (sepBy pArg (pstring ","))
|
||||||
|
let pBody = between (pstring "{") (pstring "}") (many pInline)
|
||||||
|
|
||||||
|
// --- Inline Parsers ---
|
||||||
|
let pExpr = attempt (pstring "@(") >>. manyChars (noneOf ")") .>> pstring ")" |>> fun c -> Expr(c, None)
|
||||||
|
let pMetaRef = attempt (pstring "@value{") >>. many1Chars (noneOf "}") .>> pstring "}" |>> MetaRef
|
||||||
|
|
||||||
|
let pInlineCommand =
|
||||||
|
attempt (
|
||||||
|
pchar '@' >>. many1Chars asciiLetter >>= fun name ->
|
||||||
|
if isSection name then fail "Sektioner är block-element."
|
||||||
|
else preturn name
|
||||||
|
)
|
||||||
|
.>>. opt pArgs
|
||||||
|
.>>. opt pBody
|
||||||
|
|>> fun ((name, argsOpt), bodyOpt) ->
|
||||||
|
let args = defaultArg argsOpt []
|
||||||
|
let body = defaultArg bodyOpt []
|
||||||
|
Element(name, args, body)
|
||||||
|
|
||||||
|
let pText = many1Chars (noneOf "@{}\n\r") |>> Text
|
||||||
|
let pNewline = newline
|
||||||
|
let pSingleNewline = attempt (pNewline .>> notFollowedBy pNewline) |>> fun _ -> Text "\n"
|
||||||
|
|
||||||
|
pInlineRef.Value <- choice [ pExpr; pMetaRef; pInlineCommand; pText; pSingleNewline ]
|
||||||
|
|
||||||
|
// --- Block Parsers ---
|
||||||
|
let pSectionBlock =
|
||||||
|
attempt (
|
||||||
|
pchar '@' >>. many1Chars asciiLetter >>= fun name ->
|
||||||
|
if isSection name then preturn name
|
||||||
|
else fail "Inte en sektion."
|
||||||
|
)
|
||||||
|
.>>. opt pArgs
|
||||||
|
.>>. opt pBody
|
||||||
|
|>> fun ((name, argsOpt), bodyOpt) ->
|
||||||
|
let args = defaultArg argsOpt []
|
||||||
|
let body = defaultArg bodyOpt []
|
||||||
|
Section(getSectionLevel name, args, body)
|
||||||
|
|
||||||
|
let pParagraphBlock = many1 pInline |>> Paragraph
|
||||||
|
|
||||||
|
let pBlock = choice [ pSectionBlock; pParagraphBlock ]
|
||||||
|
|
||||||
|
// --- YAML Header Parser ---
|
||||||
|
let pYamlHeader =
|
||||||
|
// Letar efter radbrytning, följt av ---, följt av radbrytning (eller filens slut)
|
||||||
|
let endMarker = attempt (newline >>. pstring "---" >>. (skipNewline <|> eof))
|
||||||
|
// Matchar --- i början, läser allt som text fram till endMarker
|
||||||
|
pstring "---" >>. newline >>. manyCharsTill anyChar endMarker
|
||||||
|
|>> fun yamlStr ->
|
||||||
|
let deserializer = DeserializerBuilder().Build()
|
||||||
|
try
|
||||||
|
let dict = deserializer.Deserialize<Dictionary<string, string>>(yamlStr)
|
||||||
|
if isNull dict then Map.empty
|
||||||
|
else dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value) |> Map.ofSeq
|
||||||
|
with _ -> Map.empty
|
||||||
|
|
||||||
|
// --- Dokument Parser ---
|
||||||
|
let pDocument =
|
||||||
|
spaces >>. opt pYamlHeader .>> spaces .>>. sepEndBy pBlock (many1 pNewline)
|
||||||
|
|>> fun (headerOpt, blocks) ->
|
||||||
|
(defaultArg headerOpt Map.empty, blocks)
|
||||||
|
|
||||||
|
let parse (input: string) : Map<string, string> * Document =
|
||||||
|
match run pDocument input with
|
||||||
|
| Success(result, _, _) -> result
|
||||||
|
| Failure(errorMsg, _, _) -> failwithf "Kunde inte parsa dokumentet:\n%s" errorMsg
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. Evaluator
|
||||||
|
// ==========================================
|
||||||
|
type IEvaluator =
|
||||||
|
abstract member Evaluate: code:string -> string
|
||||||
|
|
||||||
|
module Execution =
|
||||||
|
open Ast
|
||||||
|
|
||||||
|
let rec evaluateInline (metadata: Map<string, string>) (evaluator: IEvaluator) (node: InlineNode) =
|
||||||
|
match node with
|
||||||
|
| Expr (code, _) ->
|
||||||
|
let result = evaluator.Evaluate(code)
|
||||||
|
RawHtml result
|
||||||
|
| Text t -> Text t
|
||||||
|
| RawHtml h -> RawHtml h
|
||||||
|
| MetaRef key ->
|
||||||
|
match metadata.TryFind key with
|
||||||
|
| Some value -> Text value
|
||||||
|
| None -> Text (sprintf "[Saknad meta: %s]" key)
|
||||||
|
| Element (tag, args, children) ->
|
||||||
|
Element (tag, args, children |> List.map (evaluateInline metadata evaluator))
|
||||||
|
| Text t -> Text t
|
||||||
|
|
||||||
|
let evaluateBlock (metadata: Map<string, string>) (evaluator: IEvaluator) (node: BlockNode) =
|
||||||
|
match node with
|
||||||
|
| Section (level, args, children) -> Section (level, args, children |> List.map (evaluateInline metadata evaluator))
|
||||||
|
| Paragraph children -> Paragraph (children |> List.map (evaluateInline metadata evaluator))
|
||||||
|
|
||||||
|
let evaluateDocument (metadata: Map<string, string>) (evaluator: IEvaluator) (doc: Document) =
|
||||||
|
doc |> List.map (evaluateBlock metadata evaluator)
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. HTML Converter
|
||||||
|
// ==========================================
|
||||||
|
type TagRenderer = string list -> string -> string
|
||||||
|
|
||||||
|
type IConverter =
|
||||||
|
abstract member Convert: doc:Ast.Document * metadata:Map<string, string> -> string
|
||||||
|
|
||||||
|
module Converters =
|
||||||
|
open Ast
|
||||||
|
|
||||||
|
type HtmlTemplateConverter(template: string, prelude: Map<string, TagRenderer>) =
|
||||||
|
|
||||||
|
let rec renderInline = function
|
||||||
|
| Text t -> WebUtility.HtmlEncode(t)
|
||||||
|
| RawHtml h -> h
|
||||||
|
| Expr (_, Some res) -> WebUtility.HtmlEncode(res)
|
||||||
|
| Expr (_, None) -> failwith "Oexekverat uttryck"
|
||||||
|
| MetaRef _ -> failwith "Ogiltig nod vid konvertering"
|
||||||
|
| Element (tag, args, children) ->
|
||||||
|
let renderedChildren = renderInlines children
|
||||||
|
match prelude.TryFind tag with
|
||||||
|
| Some customFunc -> customFunc args renderedChildren
|
||||||
|
| None -> sprintf "<%s>%s</%s>" tag renderedChildren tag // Fallback ignorerar args
|
||||||
|
|
||||||
|
and renderInlines inlines =
|
||||||
|
inlines |> List.map renderInline |> String.concat ""
|
||||||
|
|
||||||
|
let renderBlock = function
|
||||||
|
| Section (level, args, children) ->
|
||||||
|
let tag = sprintf "h%d" level
|
||||||
|
let idAttr = if args.Length > 0 then sprintf " id=\"%s\"" (args.[0].Trim('"')) else ""
|
||||||
|
sprintf "<%s%s>%s</%s>" tag idAttr (renderInlines children) tag
|
||||||
|
| Paragraph children ->
|
||||||
|
sprintf "<p>%s</p>" (renderInlines children)
|
||||||
|
|
||||||
|
interface IConverter with
|
||||||
|
member _.Convert(doc, metadata) =
|
||||||
|
let bodyHtml = doc |> List.map renderBlock |> String.concat "\n\n"
|
||||||
|
let mutable finalHtml = template.Replace("{{body}}", bodyHtml)
|
||||||
|
for kvp in metadata do
|
||||||
|
finalHtml <- finalHtml.Replace(sprintf "{{%s}}" kvp.Key, WebUtility.HtmlEncode(kvp.Value))
|
||||||
|
finalHtml
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 5. Pipeline & Test
|
||||||
|
// ==========================================
|
||||||
|
module FibLib=
|
||||||
|
let processDocument (source: string) (template: string) (evaluator: IEvaluator) (converter: IConverter) =
|
||||||
|
let (metadata, rawAst) = Parser.parse source
|
||||||
|
let evaluatedAst = Execution.evaluateDocument metadata evaluator rawAst
|
||||||
|
converter.Convert(evaluatedAst, metadata)
|
||||||
|
|
||||||
16
src/Fibble/Fibble.fsproj
Normal file
16
src/Fibble/Fibble.fsproj
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FibLib\FibLib.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
66
src/Fibble/Program.fs
Normal file
66
src/Fibble/Program.fs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
open Fibble.FibLib
|
||||||
|
|
||||||
|
let makeElem (name : string) =
|
||||||
|
("name", fun _ text -> sprintf "<%s>%s</%s>" name text name)
|
||||||
|
let makeElems lst =
|
||||||
|
List.map makeElem lst
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let defaultPrelude : Map<string, TagRenderer> =
|
||||||
|
[ "b"
|
||||||
|
"em"
|
||||||
|
"i"
|
||||||
|
"strong" ]
|
||||||
|
|> makeElems
|
||||||
|
|> Map.ofList
|
||||||
|
|
||||||
|
let myPrelude : Map<string, TagRenderer> =
|
||||||
|
Map.ofList [
|
||||||
|
"quotient", fun args _ ->
|
||||||
|
if args.Length >= 2 then sprintf "%d" (int args.[0] / int args.[1])
|
||||||
|
else "[Fel: quotient kräver två argument]"
|
||||||
|
|
||||||
|
"bold", fun _ body -> sprintf "<b>%s</b>" body
|
||||||
|
|
||||||
|
"link", fun args body ->
|
||||||
|
let url = if args.Length > 0 then args.[0].Trim('"') else "#"
|
||||||
|
sprintf """<a href="%s">%s</a>""" url body
|
||||||
|
]
|
||||||
|
|
||||||
|
let template = """<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>{{title}}</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
{{body}}
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
type MockEvaluator() =
|
||||||
|
interface IEvaluator with
|
||||||
|
member _.Evaluate(code) = if code.Trim() = "2 + 2" then "4" else "Okänd"
|
||||||
|
|
||||||
|
let sourceCode = """---
|
||||||
|
title: Min Rapport
|
||||||
|
author: Ada
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
@section["intro"]{Inledning}
|
||||||
|
Detta dokument av @value{author} visar hur positionella argument fungerar.
|
||||||
|
|
||||||
|
Resultatet av quotient är @quotient[20, 6].
|
||||||
|
En vanlig F#-beräkning: @(2 + 2)
|
||||||
|
|
||||||
|
@bold{Fet text} och en @link["https://fsharp.org"]{länk till F#}.
|
||||||
|
En tagg helt utan argument eller kropp: @br"""
|
||||||
|
|
||||||
|
let converter = Converters.HtmlTemplateConverter(template, myPrelude)
|
||||||
|
let evaluator = MockEvaluator()
|
||||||
|
|
||||||
|
let output = FibLib.processDocument sourceCode template evaluator converter
|
||||||
|
printfn "%s" output
|
||||||
Loading…
Add table
Add a link
Reference in a new issue