From b6b2958592296c7530a1c5b63f1a4cfa41864668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Tue, 24 Mar 2026 13:33:01 +0100 Subject: [PATCH] First commit --- src/FibLib/FibLib.fsproj | 18 ++++ src/FibLib/Library.fs | 192 +++++++++++++++++++++++++++++++++++++++ src/Fibble/Fibble.fsproj | 16 ++++ src/Fibble/Program.fs | 66 ++++++++++++++ 4 files changed, 292 insertions(+) create mode 100644 src/FibLib/FibLib.fsproj create mode 100644 src/FibLib/Library.fs create mode 100644 src/Fibble/Fibble.fsproj create mode 100644 src/Fibble/Program.fs diff --git a/src/FibLib/FibLib.fsproj b/src/FibLib/FibLib.fsproj new file mode 100644 index 0000000..d4a509f --- /dev/null +++ b/src/FibLib/FibLib.fsproj @@ -0,0 +1,18 @@ + + + + net10.0 + true + + + + + + + + + + + + + diff --git a/src/FibLib/Library.fs b/src/FibLib/Library.fs new file mode 100644 index 0000000..a018825 --- /dev/null +++ b/src/FibLib/Library.fs @@ -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() + + 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>(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 * 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) (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) (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) (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 + +module Converters = + open Ast + + type HtmlTemplateConverter(template: string, prelude: Map) = + + 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" 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" tag idAttr (renderInlines children) tag + | Paragraph children -> + sprintf "

%s

" (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) + diff --git a/src/Fibble/Fibble.fsproj b/src/Fibble/Fibble.fsproj new file mode 100644 index 0000000..b6d18c5 --- /dev/null +++ b/src/Fibble/Fibble.fsproj @@ -0,0 +1,16 @@ + + + + Exe + net10.0 + + + + + + + + + + + diff --git a/src/Fibble/Program.fs b/src/Fibble/Program.fs new file mode 100644 index 0000000..cd916f9 --- /dev/null +++ b/src/Fibble/Program.fs @@ -0,0 +1,66 @@ +open Fibble.FibLib + +let makeElem (name : string) = + ("name", fun _ text -> sprintf "<%s>%s" name text name) +let makeElems lst = + List.map makeElem lst + + + + + + +let defaultPrelude : Map = + [ "b" + "em" + "i" + "strong" ] + |> makeElems + |> Map.ofList + +let myPrelude : Map = + 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 "%s" body + + "link", fun args body -> + let url = if args.Length > 0 then args.[0].Trim('"') else "#" + sprintf """%s""" url body + ] + +let template = """ + +{{title}} + +

{{title}}

+ {{body}} + +""" + +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