diff --git a/src/Bif/Bif.fsproj b/src/Bif/Bif.fsproj
index 8d26c7e..b6d18c5 100644
--- a/src/Bif/Bif.fsproj
+++ b/src/Bif/Bif.fsproj
@@ -13,8 +13,4 @@
-
-
-
-
diff --git a/src/Bif/Program.fs b/src/Bif/Program.fs
index 4f6aa83..3e108fb 100644
--- a/src/Bif/Program.fs
+++ b/src/Bif/Program.fs
@@ -1,13 +1,9 @@
open System.Net
open System.IO
-open System.Runtime.CompilerServices
-open FSharp.Compiler.Text
open Fibble.FibLib
open Fibble.FibLib.Ast // Ger oss tillgång till Element, Text, RawHtml etc.
open Fibble.FibLib.Pandoc
-open Fibble.FibLib.HtmlPrinter
open Fibble.FibLib.Utils
-open Fibble.FibLib.ConstructionHelpers
// ==========================================
// 1. Prelude (Dina egna taggar)
@@ -16,25 +12,27 @@ open Fibble.FibLib.ConstructionHelpers
let myPrelude : Map =
Map [
- "quotient", fun _ args _ _ ->
+ "quotient", fun _ args _ ->
match args with
- | [one; two] ->
+ | [_, one; _, two] ->
Text (sprintf "%d" (int one / int two))
| _ ->
Text "[Fel: quotient kräver två argument]"
- "bold", fun _ _ _ children ->
- Strong(children)
- "kursiv", fun _ _ _ children -> Emph(children)
- "image", image
- "value", value
- "link", link
- "list", fun _ _ _ c -> System.Console.WriteLine(c)
- RawHtml "hej"
+ "bold", fun _ args children ->
+ Element("b", args, children)
+ "image", positional (fun _ args _ -> Element("img", [("src", args[0])], []))
+ "value", positional (fun meta args _ ->
+ match Map.tryFind args.Head meta with
+ | Some(v) -> Text v
+ | None -> Text $"value {args.Head} not found in metadata")
+ "link", positional (fun _ args children ->
+ let url = if args.Length > 0 then args.[0].Trim '"' else "#"
+ Element("a", [("href", args.Head)], children))
// @br har varken argument eller barn, så vi returnerar bara rå HTML direkt
- "br", linebreak
- "table", fun _ _ _ children -> Text "hej"
- "md", fun _ _ _ children -> RawHtml (mdToHtml (stringifyNodes children))
+ "br", nameToElement "br"
+ "table", fun _ _ children -> Text "hej"
+ "md", fun meta args children -> RawHtml (mdToHtml (stringifyNodes children))
]
// ==========================================
@@ -45,7 +43,7 @@ module File =
let readFile path =
match Path.Exists(path) with
| true -> File.ReadAllText(path)
- | _ -> failwith $"{Path.GetFullPath path} does not exist"
+ | _ -> failwith $"{path} does not exist"
let pageTemplate = File.readFile "_page-template"
@@ -69,12 +67,10 @@ let processDocument (source: string) =
| Section(l, a, children) ->
Section(l, a, children
|> List.map (Execution.transform metadata myPrelude evaluator))
- | _ -> failwith "haha"
-
)
// Steg 3: Be printern skriva ut trädet till HTML
- let bodyHtml = HtmlPrinter.render evaluatedBlocks
+ let bodyHtml = HtmlPrinter.render (metadata, evaluatedBlocks)
// Steg 4: Fyll i din HTML-mall
let mutable finalHtml = pageTemplate.Replace("{{body}}", bodyHtml)
diff --git a/src/Bif/document.fib b/src/Bif/document.fib
index 9bd2254..923d706 100644
--- a/src/Bif/document.fib
+++ b/src/Bif/document.fib
@@ -20,7 +20,7 @@ let increment () =
Detta är en paragraf som har lite text i sig och lite @md{inbäddad *markdown* som kanske} funkar. Det är bra så.
@md{
-babeuoastnuhaoesn
+en tabell
hej hopp
--- ----
@@ -32,18 +32,8 @@ ueo aoeu
elleR?
-@list{
-Första saken
-Andra saken med @bold{text}
-Tredje saken
-}
-
@section{Användning}
Första anropet: @(increment())
Andra anropet: @(increment())
-@bold{detta är ibdenrerat.
- detta också.
- hej}
-
Test av utskrift: @value[date]
diff --git a/src/FibLib/FibLib.fsproj b/src/FibLib/FibLib.fsproj
index 2765919..d424556 100644
--- a/src/FibLib/FibLib.fsproj
+++ b/src/FibLib/FibLib.fsproj
@@ -8,15 +8,12 @@
-
-
-
diff --git a/src/FibLib/HtmlPrinter.fs b/src/FibLib/HtmlPrinter.fs
deleted file mode 100644
index d435718..0000000
--- a/src/FibLib/HtmlPrinter.fs
+++ /dev/null
@@ -1,59 +0,0 @@
-// ReSharper disable FSharpInterpolatedString
-
-module Fibble.FibLib.HtmlPrinter
-
-open System.Net
-
-module HtmlPrinter =
- open Ast
- open System.Net
- let renderAttributes (args: (string * string) list) =
- args
- |> List.map (fun k -> sprintf "%s=\"%s\"" (fst k) (snd k))
- |> String.concat " "
- let rec renderInline =
- function
- | Text t -> WebUtility.HtmlEncode t
- | Strong(t) -> sprintf "%s" (renderInline t)
- | RawHtml h ->
- h
- | _ -> ""
-
- let renderFigure a c l =
- failwith "haha"
-
- let renderListItem item =
- sprintf "%s" item
-
- let renderList kind attributes nodesList =
- let content = nodesList
- |> List.map renderListItem
- |> String.concat "\n"
- $"<{kind} {(renderAttributes attributes)}>{content}{kind}>"
-
-
- let rec render blocks =
- let doubleRender blocksblock =
- List.map render blocksblock
-
- let renderNode = function
- | Paragraph [RawHtml html] ->
- html
- | Paragraph nodes when nodes |> List.forall (function
- | RawHtml _ -> true
- | Text t when System.String.IsNullOrWhiteSpace(t) -> true
- | _ -> false) ->
- nodes
- |> List.choose (function RawHtml h -> Some h | _ -> None)
- |> String.concat "\n"
- | Paragraph c -> sprintf "%s
" (c |> List.map renderInline |> String.concat "")
- | Section(l, _, c) -> sprintf "%s" l (c |> List.map renderInline |> String.concat "") l
- | CodeBlock(attributes, text) -> sprintf "%s" text
- | Figure(attributes, caption, blocks) -> renderFigure attributes caption blocks
- | ListBlock(l) -> match l with
- | BulletList(attr, blocknodes) -> renderList "ul" attr.kvp (doubleRender blocknodes)
- | Orderedlist(attr, start, blocknodes) -> renderList "ol" attr.kvp (doubleRender blocknodes)
- | Plain nodes -> List.map renderInline nodes |> String.concat " "
- blocks
- |> List.map renderNode
- |> String.concat "\n"
diff --git a/src/FibLib/Library.fs b/src/FibLib/Library.fs
index 87fa452..fd5a9ef 100644
--- a/src/FibLib/Library.fs
+++ b/src/FibLib/Library.fs
@@ -10,55 +10,32 @@ open System.Collections.Generic
// 1. AST & Utils
// ==========================================
module Ast =
-
- type Attr = { id: string; classes: string list; kvp: (string * string) list }
-
-
type InlineNode =
| Text of string
| RawHtml of string
- | Emph of InlineNode list
- | Underline of InlineNode list
- | Strong of InlineNode list
- | Strikeout of InlineNode list
- | Superscript of InlineNode list
- | Subscript of InlineNode list
- | Link of attributes: Attr * target: Target
- | Code of attributes: Attr * text: string
- | Image of attributes: Attr * altText: InlineNode list * target: Url
- | Note of BlockNode list
- | SoftBreak
- | LineBreak
| Expr of code: string * result: string option
- | Command of tag: string * args: string list * kwargs: Map * children: InlineNode list
- and BlockNode =
- | CodeBlock of attributes: Attr * text: string
- | Figure of attributes: Attr * caption: InlineNode list * blocks: BlockNode list
- | ListBlock of ListKind
- | Plain of InlineNode list
- | Paragraph of children: InlineNode list
+ | Element of tag: string * args: (string * string) list * children: InlineNode list
+
+ type BlockNode =
| Section of level: int * args: (string * string) list * children: InlineNode list
- and ListKind =
- | Orderedlist of attributes: Attr * start: int * blocksList: (BlockNode list) list
- | BulletList of attributes: Attr * blocksList: (BlockNode list) list
- and Url = string
- and Target = Url * InlineNode list
+ | Paragraph of children: InlineNode list
type Document = BlockNode list
- type TagRenderer = Map
- -> string list
- -> Map
- -> InlineNode list -> InlineNode
+ type TagRenderer = Map -> (string * string) list -> InlineNode list -> InlineNode
let rec stringifyNodes (nodes: InlineNode list) =
+ let tupleToString (t: string * string) = sprintf "%s=\"%s\"" (fst t) (snd t)
nodes
|> List.map (function
| Text t -> t
| RawHtml h -> h
+ | Element(tag, args, children) ->
+ // Omvandla inre taggar till HTML
+ let attrs = if args.IsEmpty then "" else " " + String.concat " " (List.map tupleToString args)
+ sprintf "<%s%s>%s%s>" tag attrs (stringifyNodes children) tag
| Expr(_, Some res) -> res
| Expr(code, None) -> sprintf "@(%s)" code // Fallback om den inte evaluerats
- | _ -> failwith "haha"
)
|> String.concat ""
@@ -66,63 +43,32 @@ module Ast =
module Utils =
open Ast
- let dedentNodes (nodes: InlineNode list) =
- let fullText =
- nodes |> List.choose (function Text t -> Some t | _ -> None) |> String.concat ""
-
- let lines = fullText.Replace("\r\n", "\n").Split('\n')
-
- // 1. Räkna BARA ut minIndent från rader som kommer efter en radbrytning (skippa rad 0)
+ let smartDedent (input: string) =
+ let lines = input.Replace("\r\n", "\n").Split '\n' |> List.ofArray
+
+ // 1. Hitta den minsta indenteringen bland alla rader som har text
let minIndent =
- if lines.Length <= 1 then 0
- else
- lines |> Array.skip 1
- |> Array.filter (fun l -> not (System.String.IsNullOrWhiteSpace(l)))
- |> Array.map (fun l -> l.Length - l.TrimStart().Length)
- |> function [||] -> 0 | arr -> Array.min arr
+ lines
+ |> List.filter (fun l -> not (System.String.IsNullOrWhiteSpace(l)))
+ |> List.map (fun l -> l.Length - l.TrimStart().Length)
+ |> function
+ | [] -> 0
+ | indents -> List.min indents
- let mutable isFirstText = true
- let indentStr = "\n" + String.replicate minIndent " "
-
- // 2. Applicera formateringen
- let dedented =
- nodes |> List.map (function
- | Text t ->
- let t1 = t.Replace("\r\n", "\n")
-
- // Ta bort inledande mellanslag på den allra första texten direkt efter '{'
- let t2 =
- if isFirstText then
- isFirstText <- false
- t1.TrimStart(' ', '\t')
- else t1
-
- // Ta bort minIndent antal mellanslag efter varje radbrytning i noden
- let t3 = if minIndent > 0 then t2.Replace(indentStr, "\n") else t2
- Text t3
- | otherNode -> otherNode
+ // 2. Dra av exakt så många mellanslag från alla rader
+ // för att dessa inte ska vara med i det slutgiltiga dokumentet.
+ let dedented =
+ lines
+ |> List.map (fun l ->
+ if System.String.IsNullOrWhiteSpace(l) then ""
+ else l.Substring(minIndent)
)
-
- // 3. Städa bort överflödiga radbrytningar och blanksteg i ytterkanterna
- let rec trimStart = function
- | Text t :: rest ->
- let trimmed = t.TrimStart('\n', '\r', ' ', '\t')
- if trimmed = "" then trimStart rest else Text trimmed :: rest
- | other -> other
-
- let rec trimEnd = function
- | Text t :: rest ->
- let trimmed = t.TrimEnd('\n', '\r', ' ', '\t')
- if trimmed = "" then trimEnd rest else Text trimmed :: rest
- | other -> other
-
- dedented |> trimStart |> List.rev |> trimEnd |> List.rev
+
+ // 3. Slå ihop och städa bort överflödiga radbrytningar i början/slutet
+ (String.concat "\n" dedented).Trim('\n', '\r')
let positional f: TagRenderer =
- fun _ (args: string list) _ children -> f args children
-
- let onlyArgs f =
- fun _ args kwargs children -> f args kwargs
+ fun meta (args: (string*string) list) children -> f meta (List.map snd args) children
let getArgIdx (args: (string*string) list) index defaultVal =
let unnamed = args |> List.filter (fun (k, _) -> k = "")
@@ -141,6 +87,10 @@ module Utils =
let withArg2 (k1: string) (d1: string) (k2: string) (d2: string) (f: string -> string -> InlineNode list -> InlineNode) =
fun _ args children -> f (getArg args k1 0 d1) (getArg args k2 1 d2) children
+ let nameToElement (n:string) : TagRenderer =
+ fun meta args children -> Element(n, args, children)
+
+
@@ -162,49 +112,52 @@ module Parser =
let isSection (name: string) = name.EndsWith("section")
let pNewline = newline
-
- let pNamedArg =
- // Leta efter "nyckel=värde"
- attempt (many1Chars (asciiLetter <|> digit <|> anyOf "-_")
- .>> spaces .>> pchar '=' .>> spaces)
- .>>. manyChars (noneOf ",]")
- |>> fun (k, v) -> (k, v.Trim())
-
- let pPositionalArg =
- // Bara "värde"
- manyChars (noneOf ",]") |>> fun v -> ("", v.Trim())
-
- let pSingleArg = spaces >>. (pNamedArg <|> pPositionalArg) .>> spaces
-
- let pArgs =
- between (pstring "[") (pstring "]") (sepBy pSingleArg (pchar ','))
- >>= fun args ->
- // Validera att positionella argument alltid kommer först
- let rec validate canBePositional = function
- | [] -> preturn args // Allt är okej, returnera listan
- | ("", _) :: tail ->
- if not canBePositional then
- fail "Syntaxfel: Positionella argument får inte komma efter namngivna argument."
- else validate true tail
- | _ :: tail ->
- validate false tail
-
- validate true args
+ let pArg =
+ spaces >>.
+ choice [
+ attempt (many1Chars (asciiLetter <|> digit <|> anyOf "-_")
+ .>> spaces .>> pchar '=' .>> spaces
+ .>>. manyChars (noneOf ",]"))
+ |>> fun (k, v) -> (k, v.Trim())
+
+ // Fallback: bara "värde" (ges en tom nyckel)
+ manyChars (noneOf ",]") |>> fun v -> ("", v.Trim())
+ ] .>> spaces
+ let pArgs = between (pstring "[") (pstring "]") (sepBy pArg (pstring ","))
// --- 1. Måsvinge-parser (för @kommandon) ---
- // Lägg till en referens för pBody högst upp bland dina referenser
- // (Bör ligga precis under let pInline, pInlineRef = ...)
- let pBody, pBodyRef = createParserForwardedToRef()
+ let pRawBody, pRawBodyRef = createParserForwardedToRef()
+ pRawBodyRef.Value <-
+ many (choice [
+ many1Chars (noneOf "{}")
+ pchar '{' >>. pRawBodyRef.Value .>> pchar '}' |>> sprintf "{%s}"
+ ]) |>> String.concat ""
+
+ let pBody =
+ between (pstring "{") (pstring "}") pRawBodyRef.Value >>= fun raw ->
+ // En mer tillåtande parser isolerad för innehållet inuti {...}
+ let pInnerInline =
+ choice [
+ attempt pInline
+ // Fångar upp dubbla radbrytningar och måsvingar som pInline normalt blockerar
+ many1Chars (anyOf "\r\n{}") |>> Text
+ ]
+
+ match run (many pInnerInline .>> eof) (Utils.smartDedent raw) with
+ | Success(n, _, _) -> preturn n
+ | Failure(m, _, _) -> fail m
// --- 2. Parentes-parser (för @(...) med sträng-stöd) ---
let pParenBody, pParenBodyRef = createParserForwardedToRef()
+ // En inre parser som känner igen F#-strängar och escape-tecken (\")
let pFSharpString =
let normal = many1Chars (noneOf "\"\\")
let escaped = pstring "\\" >>. anyChar |>> sprintf "\\%c"
pstring "\"" .>>. manyStrings (normal <|> escaped) .>>. pstring "\""
|>> fun ((start, inner), end_) -> start + inner + end_
+ // Själva loopen letar nu efter strängar FÖRST, sen vanlig text, och sist inre parenteser
pParenBodyRef.Value <-
manyStrings (choice [
pFSharpString
@@ -219,38 +172,19 @@ module Parser =
// --- Övriga inline-parsers ---
let pMultilineCode =
pstring "@\"\"\"" >>. manyCharsTill anyChar (pstring "\"\"\"")
- |>> fun c -> Expr(c, None)
-
- // pInlineCommand använder nu forward-referensen pBodyRef
+ |>> fun c -> Expr(Utils.smartDedent c, None)
+
let pInlineCommand =
- attempt (pchar '@' >>. many1Chars asciiLetter)
+ 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 rawArgs = defaultArg argsOpt []
- let posArgs = rawArgs |> List.choose (fun (k, v) -> if k = "" then Some v else None)
- let kwargs = rawArgs |> List.filter (fun (k, _) -> k <> "") |> Map.ofList
-
- // dedentNodes anropas här från Utils
- let children = defaultArg bodyOpt [] |> Utils.dedentNodes
-
- Command(name, posArgs, kwargs, children)
+ .>>. opt pBody
+ |>> fun ((n, a), b) -> Element(n, defaultArg a [], defaultArg b [])
- // Nu när pInlineCommand, pExpr och pMultilineCode är definierade
- // kan vi skapa pInnerInline
- let pInnerInline =
- choice [
- attempt pInlineCommand
- attempt pExpr
- pMultilineCode
- many1Chars (noneOf "@}") |>> Text
- pchar '@' |>> fun _ -> Text "@"
- ]
-
- // Tilldela värdet till pBodyRef
- pBodyRef.Value <- between (pstring "{") (pstring "}") (many pInnerInline)
-
- // Tilldela värdet till pInlineRef
+ // MÅSTE tilldelas efter att alla pExpr, pInlineCommand etc. är definierade
pInlineRef.Value <- choice [
pMultilineCode
pExpr
@@ -310,13 +244,47 @@ module Execution =
open Ast
let rec transform (metadata: Map) (prelude: Map) (eval: IEvaluator) = function
- | Command(name, args, kwargs, children) when prelude.ContainsKey name ->
- prelude.[name] metadata args kwargs (children |> List.map (transform metadata prelude eval))
- | Command(n, _, _ ,_) -> failwithf "%s is not a defined command" n
+ | Element(n, a, c) when prelude.ContainsKey n ->
+ prelude.[n] metadata a (c |> List.map (transform metadata prelude eval))
+ | Element(n, a, c) ->
+ Element(n, a, c |> List.map (transform metadata prelude eval))
| Expr(c, _) -> RawHtml (eval.Evaluate c)
| n -> n
+module HtmlPrinter =
+ open Ast
+ let voidElements = [ "area"; "base"; "br"; "col"; "command"; "embed"; "hr"; "img"; "input"; "keygen"; "link"; "meta"; "param"; "source"; "track"; "wbr"]
+ let renderAttributes args =
+ args
+ |> List.map (fun m -> sprintf "%s=\"%s\"" (fst m) (snd m))
+ |> String.concat " "
+ let rec renderInline =
+ function
+ | Text t -> WebUtility.HtmlEncode t
+ | RawHtml h -> h
+ | Element(t,a,c) when List.contains t voidElements -> sprintf "<%s %s />" t (renderAttributes a)
+ | Element(t, a, c) -> sprintf "<%s %s>%s%s>" t (renderAttributes a) (c |> List.map renderInline |> String.concat "") t
+ | _ -> ""
+ let render (header, blocks) =
+ blocks
+ |> List.map (function
+ | Paragraph [RawHtml html] ->
+ html
+
+ // 2. (Frivillig) Mer robust guard om parsern råkar lämna kvar
+ // blanksteg (Text " ") runt ditt @md-block i samma paragraf
+ | Paragraph nodes when nodes |> List.forall (function
+ | RawHtml _ -> true
+ | Text t when System.String.IsNullOrWhiteSpace(t) -> true
+ | _ -> false) ->
+
+ nodes
+ |> List.choose (function RawHtml h -> Some h | _ -> None)
+ |> String.concat "\n"
+ | Paragraph c -> sprintf "%s
" (c |> List.map renderInline |> String.concat "")
+ | Section(l, _, c) -> sprintf "%s" l (c |> List.map renderInline |> String.concat "") l)
+ |> String.concat "\n"
module Evaluators =
@@ -325,8 +293,8 @@ module Evaluators =
open FSharp.Compiler.Interactive.Shell
type FsiEvaluator() =
- let sbOut = StringBuilder()
- let sbErr = StringBuilder()
+ let sbOut = new StringBuilder()
+ let sbErr = new StringBuilder()
let inStream = new StringReader("")
let outStream = new StringWriter(sbOut)
let errStream = new StringWriter(sbErr)
diff --git a/src/FibLib/Pandoc.fs b/src/FibLib/Pandoc.fs
index 0df9736..6b510e4 100644
--- a/src/FibLib/Pandoc.fs
+++ b/src/FibLib/Pandoc.fs
@@ -1,8 +1,5 @@
namespace Fibble.FibLib
-open System
-open System.Xml.Schema
-
module Pandoc =
open System.Diagnostics
@@ -20,16 +17,14 @@ module Pandoc =
use proc = Process.Start startInfo
// Skriv markdown till Pandoc
use stdin = proc.StandardInput
- stdin.WriteLine markdownText
+ stdin.Write markdownText
stdin.Close() // Måste stängas så Pandoc vet att texten är slut
// Läs ut resultatet
let htmlOutput = proc.StandardOutput.ReadToEnd()
let errorOutput = proc.StandardError.ReadToEnd()
-
-
proc.WaitForExit()
- Console.WriteLine(htmlOutput)
+
if proc.ExitCode = 0 then
htmlOutput.Trim()
else
@@ -40,5 +35,4 @@ module Pandoc =
sprintf "\n%s
and %s
" ex.Message markdownText
let mdToHtml markdownText =
- let res= toHtml "markdown" markdownText
- res
+ toHtml "markdown" markdownText
diff --git a/src/FibLib/constructorHelpers.fs b/src/FibLib/constructorHelpers.fs
deleted file mode 100644
index 2b57d06..0000000
--- a/src/FibLib/constructorHelpers.fs
+++ /dev/null
@@ -1,53 +0,0 @@
-namespace Fibble.FibLib
-
-open FSharp.Compiler.Text
-open Fibble.FibLib.Ast
-
-module Helpers =
- let emptyAttr ={id=""; classes=[]; kvp=[]}
- let ah cls kvp =
- {id=""; classes = [cls]; kvp = Map.toList kvp }
- let onlyChildren constructor : TagRenderer =
- fun _ _ _ children -> constructor(children)
-
-open Helpers
-
-module ConstructionHelpers =
- let linebreak _ _ _ _ = LineBreak
- let softbreak _ _ _ _ = SoftBreak
-
- let value : TagRenderer =
- fun meta args _ _ ->
- match Map.tryFind args.Head meta with
- | Some(v) -> Text v
- | None -> Text $"value {args.Head} not found in metadata"
-
-
- let emph = onlyChildren Emph
- let underline = onlyChildren Underline
- let strong = onlyChildren Strong
- let strikeout = onlyChildren Strikeout
- let superscript = onlyChildren Superscript
- let subscript = onlyChildren Subscript
-
- let image : TagRenderer =
- fun _ args kwargs children ->
- let attributes = ah "inlineImage" kwargs
- Image(attributes,children, args[0])
- let code : TagRenderer =
- fun _ _ kwargs children ->
- let attributes = ah "inlineCode" kwargs
- match children with
- | [Text(c)] -> InlineNode.Code(attributes, c)
- | _ -> failwith "Code tag was not Text,"
-
- let link : TagRenderer =
- fun _ args kwargs children ->
- let attributes = ah "link" kwargs
- Link(attributes,Target(args[0], children))
-
-
-
- // blocks
- let paragraph _ _ _ children = Paragraph(children)
- let plain _ _ _ children = Plain(children)
diff --git a/src/Fibble/Program.fs b/src/Fibble/Program.fs
index b0e93e0..f92e40c 100644
--- a/src/Fibble/Program.fs
+++ b/src/Fibble/Program.fs
@@ -127,6 +127,10 @@ let processDocument (source: string) =
// Hjälpfunktion: Gå igenom trädet och byt ut @value{...} mot faktiskt data från YAML
let rec resolveMeta = function
+ | MetaRef key ->
+ match metadata.TryFind key with
+ | Some v -> Text v
+ | None -> Text (sprintf "[Saknad meta: %s]" key)
| Element(tag, args, children) ->
Element(tag, args, children |> List.map resolveMeta)
| other -> other