My agentic slop goes here. Not intended for anyone else!

more

+1
.devcontainer/init-firewall.sh
···
"opam.ocaml.org" \
"deb.debian.org" \
"dl.geotessera.org" \
+
"tangled.org" \
"api.fastmail.com" \
"packages.apache.org" \
"statsig.com"; do
+5 -1
.devcontainer/setup-ocaml.sh
···
opam init --disable-sandboxing -y
eval $(opam env)
+
opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git
+
echo "Installing OCaml LSP server and common tools..."
opam install -y \
ocaml-lsp-server \
···
bytesrw \
toml \
crockford \
-
jsonfeed
+
jsonfeed \
+
yamlt \
+
xdge
echo "Setting up shell environment..."
echo 'eval $(opam env)' >> ~/.bashrc
+1
term/sortal-mosaic/.gitignore
···
+
_build
+11
term/sortal-mosaic/bin/dune
···
+
(executable
+
(name main)
+
(public_name sortal-browser)
+
(libraries
+
sortal_mosaic
+
sortal
+
eio
+
eio_main
+
logs
+
logs.fmt
+
fmt.tty))
+23
term/sortal-mosaic/bin/main.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Sortal Browser - Terminal UI for browsing contacts *)
+
+
let () =
+
(* Set up logging *)
+
Fmt_tty.setup_std_outputs ();
+
Logs.set_reporter (Logs_fmt.reporter ());
+
Logs.set_level (Some Logs.Warning);
+
+
Eio_main.run @@ fun env ->
+
let fs = Eio.Stdenv.fs env in
+
let store = Sortal.create fs "sortal" in
+
+
(* Run the TEA app *)
+
Mosaic_tea.run
+
~exit_on_ctrl_c:false
+
~mouse:(Some `Sgr_any)
+
~bracketed_paste:true
+
(Sortal_mosaic.app store)
+27
term/sortal-mosaic/dune-project
···
+
(lang dune 3.20)
+
+
(name sortal_mosaic)
+
+
(generate_opam_files true)
+
+
(license ISC)
+
(authors "Anil Madhavapeddy")
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
+
(package
+
(name sortal_mosaic)
+
(synopsis "Terminal UI for browsing Sortal contacts")
+
(description
+
"A terminal-based contact browser combining Sortal (contact management)
+
and Mosaic (TEA-based terminal UI framework). Features master-detail
+
layout, search, and temporal data display.")
+
(depends
+
(ocaml (>= 5.1.0))
+
eio
+
eio_main
+
sortal
+
mosaic
+
matrix
+
toffee
+
fmt
+
logs))
+5
term/sortal-mosaic/dune-workspace
···
+
(lang dune 3.20)
+
+
(env
+
(dev
+
(flags (:standard -w -69))))
+12
term/sortal-mosaic/lib/dune
···
+
(library
+
(name sortal_mosaic)
+
(public_name sortal_mosaic)
+
(libraries
+
sortal
+
mosaic.tea
+
mosaic.ui
+
matrix
+
matrix.ansi
+
matrix.input
+
toffee
+
unix))
+796
term/sortal-mosaic/lib/sortal_mosaic.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Sortal-Mosaic: Terminal UI for browsing contacts *)
+
+
open Mosaic_tea
+
module Contact = Sortal.Contact
+
module Temporal = Sortal.Temporal
+
+
(* ========================================================================= *)
+
(* Model Types *)
+
(* ========================================================================= *)
+
+
type focus_area =
+
| Search_input
+
| Contact_list
+
| Detail_view
+
| Log_panel
+
+
type log_level = Info | Warn | Error
+
+
type log_entry = {
+
timestamp : float;
+
level : log_level;
+
message : string;
+
}
+
+
type model = {
+
(* Data *)
+
store : Sortal.Store.t;
+
all_contacts : Contact.t list;
+
filtered_contacts : Contact.t list;
+
(* Selection *)
+
selected_index : int;
+
selected_contact : Contact.t option;
+
(* Search *)
+
search_query : string;
+
(* UI State *)
+
focus : focus_area;
+
detail_scroll_y : int;
+
log_scroll_y : int;
+
(* Logs *)
+
logs : log_entry list;
+
max_logs : int;
+
(* Layout *)
+
terminal_width : int;
+
terminal_height : int;
+
list_width_pct : int;
+
log_height : int;
+
log_expanded : bool;
+
}
+
+
(* ========================================================================= *)
+
(* Messages *)
+
(* ========================================================================= *)
+
+
type msg =
+
(* Navigation *)
+
| Select_next
+
| Select_prev
+
| Select_contact of int
+
| Page_up
+
| Page_down
+
| Go_to_top
+
| Go_to_bottom
+
(* Focus *)
+
| Focus_search
+
| Focus_list
+
| Focus_detail
+
| Focus_log
+
| Cycle_focus
+
| Cycle_focus_back
+
(* Search *)
+
| Update_search of string
+
| Clear_search
+
| Submit_search
+
(* Scrolling *)
+
| Scroll_detail of int
+
| Scroll_log of int
+
(* Data *)
+
| Refresh_contacts
+
(* Log panel *)
+
| Toggle_log_expand
+
(* System *)
+
| Resize of int * int
+
| Quit
+
+
(* ========================================================================= *)
+
(* Helpers *)
+
(* ========================================================================= *)
+
+
let log_entry level message =
+
{ timestamp = Unix.gettimeofday (); level; message }
+
+
let add_log model entry =
+
let logs = entry :: model.logs in
+
let logs =
+
if List.length logs > model.max_logs then
+
List.filteri (fun i _ -> i < model.max_logs) logs
+
else logs
+
in
+
{ model with logs }
+
+
let format_temporal_range range =
+
match range with
+
| None -> ""
+
| Some r -> (
+
match (r.Temporal.from, r.until) with
+
| Some f, Some u ->
+
Printf.sprintf " [%s-%s]"
+
(Temporal.format_date f)
+
(Temporal.format_date u)
+
| Some f, None ->
+
Printf.sprintf " [%s-]" (Temporal.format_date f)
+
| None, Some u ->
+
Printf.sprintf " [-%s]" (Temporal.format_date u)
+
| None, None -> "")
+
+
(* ========================================================================= *)
+
(* Init *)
+
(* ========================================================================= *)
+
+
let init store () =
+
let all_contacts =
+
Sortal.list store |> List.sort (fun a b -> Contact.compare a b)
+
in
+
let log_msg =
+
log_entry Info
+
(Printf.sprintf "Loaded %d contacts" (List.length all_contacts))
+
in
+
let model =
+
{
+
store;
+
all_contacts;
+
filtered_contacts = all_contacts;
+
selected_index = 0;
+
selected_contact = List.nth_opt all_contacts 0;
+
search_query = "";
+
focus = Contact_list;
+
detail_scroll_y = 0;
+
log_scroll_y = 0;
+
logs = [ log_msg ];
+
max_logs = 100;
+
terminal_width = 80;
+
terminal_height = 24;
+
list_width_pct = 30;
+
log_height = 4;
+
log_expanded = false;
+
}
+
in
+
(model, Cmd.none)
+
+
(* ========================================================================= *)
+
(* Update *)
+
(* ========================================================================= *)
+
+
let rec update msg model =
+
match msg with
+
| Update_search query ->
+
let filtered =
+
if query = "" then model.all_contacts
+
else Sortal.search_all model.store query
+
in
+
let log_msg =
+
log_entry Info
+
(Printf.sprintf "Search '%s' matched %d contacts" query
+
(List.length filtered))
+
in
+
let selected = List.nth_opt filtered 0 in
+
let model =
+
{
+
model with
+
search_query = query;
+
filtered_contacts = filtered;
+
selected_index = 0;
+
selected_contact = selected;
+
detail_scroll_y = 0;
+
}
+
in
+
(add_log model log_msg, Cmd.none)
+
| Clear_search ->
+
let model =
+
{
+
model with
+
search_query = "";
+
filtered_contacts = model.all_contacts;
+
selected_index = 0;
+
selected_contact = List.nth_opt model.all_contacts 0;
+
focus = Contact_list;
+
}
+
in
+
(model, Cmd.focus "contact-select")
+
| Submit_search -> ({ model with focus = Contact_list }, Cmd.focus "contact-select")
+
| Select_contact idx ->
+
let contact = List.nth_opt model.filtered_contacts idx in
+
( {
+
model with
+
selected_index = idx;
+
selected_contact = contact;
+
detail_scroll_y = 0;
+
},
+
Cmd.none )
+
| Select_next ->
+
let max_idx = max 0 (List.length model.filtered_contacts - 1) in
+
let new_idx = min (model.selected_index + 1) max_idx in
+
update (Select_contact new_idx) model
+
| Select_prev ->
+
let new_idx = max (model.selected_index - 1) 0 in
+
update (Select_contact new_idx) model
+
| Page_down ->
+
let page_size = 10 in
+
let max_idx = max 0 (List.length model.filtered_contacts - 1) in
+
let new_idx = min (model.selected_index + page_size) max_idx in
+
update (Select_contact new_idx) model
+
| Page_up ->
+
let page_size = 10 in
+
let new_idx = max (model.selected_index - page_size) 0 in
+
update (Select_contact new_idx) model
+
| Go_to_top -> update (Select_contact 0) model
+
| Go_to_bottom ->
+
let max_idx = max 0 (List.length model.filtered_contacts - 1) in
+
update (Select_contact max_idx) model
+
| Focus_search -> ({ model with focus = Search_input }, Cmd.focus "search-input")
+
| Focus_list -> ({ model with focus = Contact_list }, Cmd.focus "contact-select")
+
| Focus_detail -> ({ model with focus = Detail_view }, Cmd.focus "detail-scroll")
+
| Focus_log -> ({ model with focus = Log_panel }, Cmd.focus "log-scroll")
+
| Cycle_focus ->
+
let next_focus =
+
match model.focus with
+
| Search_input -> Contact_list
+
| Contact_list -> Detail_view
+
| Detail_view -> Log_panel
+
| Log_panel -> Search_input
+
in
+
let focus_id =
+
match next_focus with
+
| Search_input -> "search-input"
+
| Contact_list -> "contact-select"
+
| Detail_view -> "detail-scroll"
+
| Log_panel -> "log-scroll"
+
in
+
({ model with focus = next_focus }, Cmd.focus focus_id)
+
| Cycle_focus_back ->
+
let prev_focus =
+
match model.focus with
+
| Search_input -> Log_panel
+
| Contact_list -> Search_input
+
| Detail_view -> Contact_list
+
| Log_panel -> Detail_view
+
in
+
let focus_id =
+
match prev_focus with
+
| Search_input -> "search-input"
+
| Contact_list -> "contact-select"
+
| Detail_view -> "detail-scroll"
+
| Log_panel -> "log-scroll"
+
in
+
({ model with focus = prev_focus }, Cmd.focus focus_id)
+
| Scroll_detail delta ->
+
let new_scroll = max 0 (model.detail_scroll_y + delta) in
+
({ model with detail_scroll_y = new_scroll }, Cmd.none)
+
| Scroll_log delta ->
+
let new_scroll = max 0 (model.log_scroll_y + delta) in
+
({ model with log_scroll_y = new_scroll }, Cmd.none)
+
| Toggle_log_expand ->
+
({ model with log_expanded = not model.log_expanded }, Cmd.none)
+
| Refresh_contacts ->
+
let all_contacts =
+
Sortal.list model.store |> List.sort (fun a b -> Contact.compare a b)
+
in
+
let filtered =
+
if model.search_query = "" then all_contacts
+
else Sortal.search_all model.store model.search_query
+
in
+
let log_msg =
+
log_entry Info
+
(Printf.sprintf "Refreshed: %d contacts" (List.length all_contacts))
+
in
+
let model =
+
{
+
model with
+
all_contacts;
+
filtered_contacts = filtered;
+
selected_index = 0;
+
selected_contact = List.nth_opt filtered 0;
+
}
+
in
+
(add_log model log_msg, Cmd.none)
+
| Resize (w, h) ->
+
({ model with terminal_width = w; terminal_height = h }, Cmd.none)
+
| Quit -> (model, Cmd.quit)
+
+
(* ========================================================================= *)
+
(* View Helpers *)
+
(* ========================================================================= *)
+
+
let section_header title =
+
box ~flex_direction:Column ~margin:(margin 0) ~gap:(gap 0)
+
[
+
text ~content:title
+
~text_style:(Ansi.Style.make ~bold:true ~fg:Ansi.Color.cyan ())
+
();
+
text ~content:(String.make (String.length title) '-')
+
~text_style:(Ansi.Style.make ~fg:Ansi.Color.cyan ())
+
();
+
]
+
+
let labeled_value ?badge ?badge_color ?suffix ?suffix_color ?value_color label
+
value =
+
let value_style =
+
match value_color with
+
| Some c -> Ansi.Style.make ~fg:c ()
+
| None -> Ansi.Style.default
+
in
+
box ~flex_direction:Row ~gap:(gap 1)
+
[
+
text ~content:label ();
+
text ~content:value ~text_style:value_style ();
+
(match badge with
+
| Some b ->
+
let style =
+
Ansi.Style.make
+
~fg:(Option.value badge_color ~default:Ansi.Color.green)
+
()
+
in
+
text ~content:b ~text_style:style ()
+
| None -> null);
+
(match suffix with
+
| Some s ->
+
let style =
+
Ansi.Style.make
+
~fg:(Option.value suffix_color ~default:(Ansi.Color.grayscale ~level:12))
+
()
+
in
+
text ~content:s ~text_style:style ()
+
| None -> null);
+
]
+
+
let indented_text content =
+
box ~padding:(padding 0) ~margin:(margin 0)
+
[
+
text ~content:(" " ^ content)
+
~text_style:(Ansi.Style.make ~fg:Ansi.Color.white ())
+
();
+
]
+
+
(* ========================================================================= *)
+
(* View Components *)
+
(* ========================================================================= *)
+
+
let search_bar model =
+
let count_text =
+
Printf.sprintf "%d contacts" (List.length model.filtered_contacts)
+
in
+
let focused = model.focus = Search_input in
+
box ~flex_direction:Row
+
~padding:
+
(Toffee.Geometry.Rect.make
+
~top:(Toffee.Style.Length_percentage.length 0.)
+
~bottom:(Toffee.Style.Length_percentage.length 0.)
+
~left:(Toffee.Style.Length_percentage.length 1.)
+
~right:(Toffee.Style.Length_percentage.length 1.))
+
~gap:(gap 2) ~align_items:Center ~border:true ~border_sides:[ `Bottom ]
+
~size:{ width = pct 100; height = px 3 }
+
[
+
text ~content:"Search:" ();
+
text_input ~id:"search-input" ~autofocus:focused
+
~placeholder:"Type to filter..." ~value:model.search_query
+
~on_input:(fun s -> Some (Update_search s))
+
~on_submit:(fun _ -> Some Submit_search)
+
~size:{ width = px 30; height = px 1 }
+
~focused_background:Ansi.Color.black
+
~focused_text_color:Ansi.Color.white ();
+
box ~flex_grow:1. [];
+
text ~content:count_text
+
~text_style:(Ansi.Style.make ~fg:Ansi.Color.cyan ())
+
();
+
]
+
+
let contact_list model =
+
let options =
+
model.filtered_contacts
+
|> List.map (fun contact ->
+
let name = Contact.primary_name contact in
+
let kind = Contact.kind contact in
+
let kind_badge =
+
match kind with
+
| Contact.Person -> ""
+
| Organization -> " [Org]"
+
| Group -> " [Grp]"
+
| Role -> " [Role]"
+
in
+
let has_current =
+
Option.is_some (Contact.current_email contact)
+
|| Option.is_some (Contact.current_organization contact)
+
in
+
let indicator = if has_current then " *" else "" in
+
{
+
Select.name = name ^ kind_badge ^ indicator;
+
description = Some ("@" ^ Contact.handle contact);
+
})
+
in
+
let list_width = model.list_width_pct in
+
box ~border:true ~title:"Contacts"
+
~size:{ width = pct list_width; height = pct 100 }
+
[
+
select ~id:"contact-select" ~options ~selected_index:model.selected_index
+
~autofocus:(model.focus = Contact_list)
+
~show_description:true ~show_scroll_indicator:true ~wrap_selection:false
+
~selected_background:Ansi.Color.blue
+
~selected_text_color:Ansi.Color.white
+
~focused_background:(Ansi.Color.grayscale ~level:4)
+
~on_change:(fun idx -> Some (Select_contact idx))
+
~size:{ width = pct 100; height = pct 100 }
+
();
+
]
+
+
let current_info_section contact =
+
let current_email = Contact.current_email contact in
+
let current_org = Contact.current_organization contact in
+
if Option.is_none current_email && Option.is_none current_org then null
+
else
+
box ~flex_direction:Column ~gap:(gap 1)
+
[
+
section_header "CURRENT";
+
(match current_email with
+
| None -> null
+
| Some email ->
+
labeled_value "Email:" email ~badge:"[current]"
+
~badge_color:Ansi.Color.green);
+
(match current_org with
+
| None -> null
+
| Some org ->
+
box ~flex_direction:Column
+
[
+
labeled_value "Org:" org.name ~badge:"[current]"
+
~badge_color:Ansi.Color.green;
+
(match org.title with
+
| Some t -> indented_text ("Title: " ^ t)
+
| None -> null);
+
(match org.department with
+
| Some d -> indented_text ("Dept: " ^ d)
+
| None -> null);
+
]);
+
]
+
+
let services_section contact =
+
let services = Contact.current_services contact in
+
if services = [] then null
+
else
+
box ~flex_direction:Column ~gap:(gap 1)
+
[
+
section_header "SERVICES";
+
fragment
+
(List.map
+
(fun (svc : Contact.service) ->
+
let kind_str =
+
match svc.kind with
+
| Some k -> Contact.service_kind_to_string k
+
| None -> "link"
+
in
+
let handle_str =
+
match svc.handle with
+
| Some h -> Printf.sprintf "@%s" h
+
| None -> svc.url
+
in
+
let primary_suffix = if svc.primary then " (primary)" else "" in
+
labeled_value (kind_str ^ ":") handle_str ~suffix:primary_suffix
+
~suffix_color:Ansi.Color.yellow)
+
services);
+
]
+
+
let historical_section contact =
+
let all_emails = Contact.emails contact in
+
let historical_emails =
+
List.filter
+
(fun (e : Contact.email) ->
+
not (Temporal.is_current e.range))
+
all_emails
+
in
+
let all_orgs = Contact.organizations contact in
+
let historical_orgs =
+
List.filter
+
(fun (o : Contact.organization) ->
+
not (Temporal.is_current o.range))
+
all_orgs
+
in
+
if historical_emails = [] && historical_orgs = [] then null
+
else
+
box ~flex_direction:Column ~gap:(gap 1)
+
[
+
section_header "HISTORICAL";
+
fragment
+
(List.map
+
(fun (e : Contact.email) ->
+
let range_str = format_temporal_range e.range in
+
labeled_value "Email:" e.address ~suffix:range_str
+
~value_color:(Ansi.Color.grayscale ~level:12))
+
historical_emails);
+
fragment
+
(List.map
+
(fun (o : Contact.organization) ->
+
let title_str =
+
match o.title with Some t -> " - " ^ t | None -> ""
+
in
+
let range_str = format_temporal_range o.range in
+
labeled_value "Org:" (o.name ^ title_str) ~suffix:range_str
+
~value_color:(Ansi.Color.grayscale ~level:12))
+
historical_orgs);
+
]
+
+
let links_section contact =
+
let urls = Contact.urls contact in
+
if urls = [] then null
+
else
+
box ~flex_direction:Column ~gap:(gap 1)
+
[
+
section_header "LINKS";
+
fragment
+
(List.map
+
(fun (u : Contact.url_entry) ->
+
let label_str =
+
match u.label with Some l -> " (" ^ l ^ ")" | None -> ""
+
in
+
labeled_value "URL:" (u.url ^ label_str)
+
~value_color:Ansi.Color.blue)
+
urls);
+
]
+
+
let all_emails_section contact =
+
let emails = Contact.emails contact in
+
if emails = [] then null
+
else
+
box ~flex_direction:Column ~gap:(gap 1)
+
[
+
section_header "ALL EMAILS";
+
fragment
+
(List.map
+
(fun (e : Contact.email) ->
+
let type_str =
+
match e.type_ with
+
| Some Contact.Work -> " (work)"
+
| Some Personal -> " (personal)"
+
| Some Other -> " (other)"
+
| None -> ""
+
in
+
let is_current = Temporal.is_current e.range in
+
let range_str = format_temporal_range e.range in
+
if is_current then
+
labeled_value "Email:" (e.address ^ type_str)
+
~badge:"[current]" ~badge_color:Ansi.Color.green
+
else
+
labeled_value "Email:" (e.address ^ type_str) ~suffix:range_str
+
~value_color:(Ansi.Color.grayscale ~level:12))
+
emails);
+
]
+
+
let contact_detail model =
+
let detail_width = 100 - model.list_width_pct in
+
match model.selected_contact with
+
| None ->
+
box ~border:true ~title:"Details"
+
~size:{ width = pct detail_width; height = pct 100 }
+
~align_items:Center ~justify_content:Center
+
[
+
text ~content:"Select a contact"
+
~text_style:(Ansi.Style.make ~fg:(Ansi.Color.grayscale ~level:12) ())
+
();
+
]
+
| Some contact ->
+
let handle = Contact.handle contact in
+
let kind =
+
Contact.kind contact |> Contact.contact_kind_to_string
+
|> String.capitalize_ascii
+
in
+
let name = Contact.primary_name contact in
+
let aliases = Contact.names contact |> List.tl in
+
let title = Printf.sprintf "@%s - %s" handle kind in
+
box ~border:true ~title
+
~size:{ width = pct detail_width; height = pct 100 }
+
[
+
scroll_box ~id:"detail-scroll"
+
~autofocus:(model.focus = Detail_view)
+
~scroll_y:true
+
~size:{ width = pct 100; height = pct 100 }
+
[
+
box ~flex_direction:Column ~padding:(padding 1) ~gap:(gap 1)
+
[
+
(* Header *)
+
text ~content:name
+
~text_style:
+
(Ansi.Style.make ~bold:true ~fg:Ansi.Color.white ())
+
();
+
(if aliases <> [] then
+
text
+
~content:
+
(Printf.sprintf "aka: %s" (String.concat ", " aliases))
+
~text_style:
+
(Ansi.Style.make ~fg:(Ansi.Color.grayscale ~level:12) ~italic:true ())
+
()
+
else null);
+
text
+
~content:(String.make 50 '-')
+
~text_style:(Ansi.Style.make ~fg:Ansi.Color.cyan ())
+
();
+
(* Sections *)
+
current_info_section contact;
+
services_section contact;
+
all_emails_section contact;
+
historical_section contact;
+
links_section contact;
+
(* ORCID if present *)
+
(match Contact.orcid contact with
+
| Some orcid ->
+
labeled_value "ORCID:" ("https://orcid.org/" ^ orcid)
+
~value_color:Ansi.Color.blue
+
| None -> null);
+
];
+
];
+
]
+
+
let log_panel model =
+
let log_style level =
+
match level with
+
| Info -> Ansi.Style.make ~fg:Ansi.Color.cyan ()
+
| Warn -> Ansi.Style.make ~fg:Ansi.Color.yellow ()
+
| Error -> Ansi.Style.make ~fg:Ansi.Color.red ~bold:true ()
+
in
+
let level_str = function Info -> "INFO " | Warn -> "WARN " | Error -> "ERROR" in
+
let height = if model.log_expanded then 12 else model.log_height in
+
box ~border:true ~title:"Logs" ~size:{ width = pct 100; height = px height }
+
[
+
scroll_box ~id:"log-scroll"
+
~autofocus:(model.focus = Log_panel)
+
~scroll_y:true
+
~size:{ width = pct 100; height = pct 100 }
+
[
+
box ~flex_direction:Column
+
[
+
fragment
+
(List.rev_map
+
(fun entry ->
+
let time = Unix.localtime entry.timestamp in
+
let time_str =
+
Printf.sprintf "[%02d:%02d:%02d]" time.Unix.tm_hour
+
time.Unix.tm_min time.Unix.tm_sec
+
in
+
box ~flex_direction:Row ~gap:(gap 1)
+
[
+
text ~content:time_str
+
~text_style:(Ansi.Style.make ~fg:(Ansi.Color.grayscale ~level:12) ())
+
();
+
text ~content:(level_str entry.level)
+
~text_style:(log_style entry.level)
+
();
+
text ~content:entry.message ();
+
])
+
model.logs);
+
];
+
];
+
]
+
+
let help_bar _model =
+
let key_style = Ansi.Style.make ~fg:Ansi.Color.yellow () in
+
let sep = " | " in
+
box
+
~size:{ width = pct 100; height = px 1 }
+
~flex_direction:Row ~gap:(gap 0)
+
~background:Ansi.Color.black
+
[
+
text ~content:" j/k" ~text_style:key_style ();
+
text ~content:":nav" ();
+
text ~content:sep ();
+
text ~content:"/" ~text_style:key_style ();
+
text ~content:":search" ();
+
text ~content:sep ();
+
text ~content:"Tab" ~text_style:key_style ();
+
text ~content:":focus" ();
+
text ~content:sep ();
+
text ~content:"l" ~text_style:key_style ();
+
text ~content:":log" ();
+
text ~content:sep ();
+
text ~content:"r" ~text_style:key_style ();
+
text ~content:":refresh" ();
+
text ~content:sep ();
+
text ~content:"q" ~text_style:key_style ();
+
text ~content:":quit" ();
+
]
+
+
(* ========================================================================= *)
+
(* Main View *)
+
(* ========================================================================= *)
+
+
let view model =
+
box ~flex_direction:Column ~size:{ width = pct 100; height = pct 100 }
+
[
+
(* Search bar *)
+
search_bar model;
+
(* Main content: list + detail *)
+
box ~flex_direction:Row ~flex_grow:1.
+
~size:{ width = pct 100; height = auto }
+
[ contact_list model; contact_detail model ];
+
(* Log panel *)
+
log_panel model;
+
(* Help bar *)
+
help_bar model;
+
]
+
+
(* ========================================================================= *)
+
(* Subscriptions *)
+
(* ========================================================================= *)
+
+
let subscriptions model =
+
Sub.batch
+
[
+
Sub.on_key (fun ev ->
+
let data = Mosaic_ui.Event.Key.data ev in
+
let key = data.key in
+
let ctrl = data.modifier.ctrl in
+
let shift = data.modifier.shift in
+
match (model.focus, key, ctrl, shift) with
+
(* Global quit *)
+
| _, Char c, false, false when Uchar.equal c (Uchar.of_char 'q') ->
+
Some Quit
+
| _, Char c, true, false when Uchar.equal c (Uchar.of_char 'c') ->
+
Some Quit
+
(* Global search *)
+
| focus, Char c, false, false
+
when Uchar.equal c (Uchar.of_char '/') && focus <> Search_input ->
+
Some Focus_search
+
(* Global refresh *)
+
| _, Char c, false, false when Uchar.equal c (Uchar.of_char 'r') ->
+
Some Refresh_contacts
+
(* Tab cycling *)
+
| _, Tab, false, false -> Some Cycle_focus
+
| _, Tab, false, true -> Some Cycle_focus_back
+
(* Log expand toggle *)
+
| _, Char c, false, false when Uchar.equal c (Uchar.of_char 'l') ->
+
Some Toggle_log_expand
+
(* Search mode *)
+
| Search_input, Escape, false, false -> Some Clear_search
+
(* List navigation - only when list is focused *)
+
| Contact_list, Up, false, false -> Some Select_prev
+
| Contact_list, Down, false, false -> Some Select_next
+
| Contact_list, Char c, false, false
+
when Uchar.equal c (Uchar.of_char 'k') ->
+
Some Select_prev
+
| Contact_list, Char c, false, false
+
when Uchar.equal c (Uchar.of_char 'j') ->
+
Some Select_next
+
| Contact_list, Char c, false, false
+
when Uchar.equal c (Uchar.of_char 'g') ->
+
Some Go_to_top
+
| Contact_list, Char c, false, true
+
when Uchar.equal c (Uchar.of_char 'G') ->
+
Some Go_to_bottom
+
| Contact_list, Char c, true, false
+
when Uchar.equal c (Uchar.of_char 'u') ->
+
Some Page_up
+
| Contact_list, Char c, true, false
+
when Uchar.equal c (Uchar.of_char 'd') ->
+
Some Page_down
+
| Contact_list, Page_up, false, false -> Some Page_up
+
| Contact_list, Page_down, false, false -> Some Page_down
+
| Contact_list, Enter, false, false -> Some Focus_detail
+
(* Detail scrolling *)
+
| Detail_view, Up, false, false -> Some (Scroll_detail (-1))
+
| Detail_view, Down, false, false -> Some (Scroll_detail 1)
+
| Detail_view, Char c, false, false
+
when Uchar.equal c (Uchar.of_char 'k') ->
+
Some (Scroll_detail (-1))
+
| Detail_view, Char c, false, false
+
when Uchar.equal c (Uchar.of_char 'j') ->
+
Some (Scroll_detail 1)
+
| Detail_view, Escape, false, false -> Some Focus_list
+
(* Log scrolling *)
+
| Log_panel, Up, false, false -> Some (Scroll_log (-1))
+
| Log_panel, Down, false, false -> Some (Scroll_log 1)
+
| Log_panel, Char c, false, false
+
when Uchar.equal c (Uchar.of_char 'k') ->
+
Some (Scroll_log (-1))
+
| Log_panel, Char c, false, false
+
when Uchar.equal c (Uchar.of_char 'j') ->
+
Some (Scroll_log 1)
+
| Log_panel, Escape, false, false -> Some Focus_list
+
| _ -> None);
+
Sub.on_resize (fun ~width ~height -> Resize (width, height));
+
]
+
+
(* ========================================================================= *)
+
(* App *)
+
(* ========================================================================= *)
+
+
let app store = { init = init store; update; view; subscriptions }
+38
term/sortal-mosaic/sortal_mosaic.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "Terminal UI for browsing Sortal contacts"
+
description: """
+
A terminal-based contact browser combining Sortal (contact management)
+
and Mosaic (TEA-based terminal UI framework). Features master-detail
+
layout, search, and temporal data display."""
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy"]
+
license: "ISC"
+
depends: [
+
"dune" {>= "3.20"}
+
"ocaml" {>= "5.1.0"}
+
"eio"
+
"eio_main"
+
"sortal"
+
"mosaic"
+
"matrix"
+
"toffee"
+
"fmt"
+
"logs"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
x-maintenance-intent: ["(latest)"]
-3
yaml/ocaml-yamle/.gitignore
···
-
_build
-
*.cmi
-
*.cmo
-43
yaml/ocaml-yamle/TODO.md
···
-
# Yamle Implementation Progress
-
-
## Phase 1: Foundation
-
- [x] Project structure and dune files
-
- [ ] Position module - location tracking
-
- [ ] Span module - source ranges
-
- [ ] Error module - exception with position info
-
- [ ] Encoding module - UTF-8/16 detection
-
-
## Phase 2: Styles and Input
-
- [ ] Scalar_style module
-
- [ ] Layout_style module
-
- [ ] Chomping module
-
- [ ] Input module - character source abstraction
-
-
## Phase 3: Scanner (Lexer)
-
- [ ] Token module - token types
-
- [ ] Scanner module - tokenizer with lookahead
-
-
## Phase 4: Parser
-
- [ ] Event module - parser events
-
- [ ] Parser module - state machine
-
-
## Phase 5: Data Structures
-
- [ ] Value module - JSON-compatible representation
-
- [ ] Tag module - YAML tags
-
- [ ] Scalar module - scalar with metadata
-
- [ ] Sequence module - sequence with metadata
-
- [ ] Mapping module - mapping with metadata
-
- [ ] Yaml module - full YAML representation
-
- [ ] Document module - document wrapper
-
-
## Phase 6: Loader and Emitter
-
- [ ] Loader module - events to data structures
-
- [ ] Emitter module - data structures to YAML string
-
-
## Phase 7: Top-Level API
-
- [ ] Yamle module - main API
-
- [ ] Stream submodule - streaming interface
-
-
## Phase 8: Testing
-
- [ ] Unit tests for each module
-
- [ ] Integration tests with YAML test suite
-64
yaml/ocaml-yamle/bin/dune
···
-
(executable
-
(name yamlcat)
-
(public_name yamlcat)
-
(libraries yamle cmdliner))
-
-
(executable
-
(name test_emit)
-
(libraries yamle))
-
-
(executable
-
(name test_remaining)
-
(libraries yamle))
-
-
(executable
-
(name test_scan)
-
(libraries yamle))
-
-
(executable
-
(name test_empty)
-
(libraries yamle))
-
-
(executable
-
(name test_specific)
-
(libraries yamle))
-
-
(executable
-
(name test_detailed)
-
(libraries yamle test_suite_lib))
-
-
(executable
-
(name test_empty_final)
-
(libraries yamle))
-
-
(executable
-
(name test_block_debug)
-
(libraries yamle))
-
-
(executable
-
(name test_failing)
-
(libraries yamle))
-
-
(executable
-
(name test_debug_cases)
-
(libraries yamle))
-
-
(executable
-
(name test_seq_comment)
-
(libraries yamle))
-
-
(executable
-
(name test_rzp5_exact)
-
(libraries yamle))
-
-
(executable
-
(name test_indent_comment)
-
(libraries yamle))
-
-
(executable
-
(name test_tokens_debug)
-
(libraries yamle))
-
-
(executable
-
(name test_error_detail)
-
(libraries yamle))
-14
yaml/ocaml-yamle/bin/test_emit.ml
···
-
let () =
-
let yaml = {|
-
name: Alice
-
age: 30
-
hobbies:
-
- reading
-
- coding
-
|} in
-
let v = Yamle.of_string yaml in
-
print_endline "=== Using to_string (YAML output) ===";
-
print_endline (Yamle.to_string v);
-
print_endline "";
-
print_endline "=== Using pp (JSON-like) ===";
-
Format.printf "%a@." Yamle.pp v
-184
yaml/ocaml-yamle/bin/yamlcat.ml
···
-
(** yamlcat - parse and reprint YAML files *)
-
-
open Cmdliner
-
-
type output_format = Yaml | Json | Flow | Debug
-
-
let rec json_to_string buf (v : Yamle.value) =
-
match v with
-
| `Null -> Buffer.add_string buf "null"
-
| `Bool b -> Buffer.add_string buf (if b then "true" else "false")
-
| `Float f ->
-
if Float.is_integer f && Float.abs f < 1e15 then
-
Buffer.add_string buf (Printf.sprintf "%.0f" f)
-
else
-
Buffer.add_string buf (Printf.sprintf "%g" f)
-
| `String s -> Buffer.add_string buf (Printf.sprintf "%S" s)
-
| `A items ->
-
Buffer.add_char buf '[';
-
List.iteri (fun i item ->
-
if i > 0 then Buffer.add_string buf ", ";
-
json_to_string buf item
-
) items;
-
Buffer.add_char buf ']'
-
| `O pairs ->
-
Buffer.add_char buf '{';
-
List.iteri (fun i (k, v) ->
-
if i > 0 then Buffer.add_string buf ", ";
-
Buffer.add_string buf (Printf.sprintf "%S: " k);
-
json_to_string buf v
-
) pairs;
-
Buffer.add_char buf '}'
-
-
let value_to_json v =
-
let buf = Buffer.create 256 in
-
json_to_string buf v;
-
Buffer.contents buf
-
-
let process_string ~format ~resolve_aliases ~max_nodes ~max_depth content =
-
try
-
(* Always parse as multi-document stream *)
-
let documents = Yamle.documents_of_string content in
-
-
match format with
-
| Yaml ->
-
(* Convert through Value to apply tag-based type coercion *)
-
let first = ref true in
-
List.iter (fun doc ->
-
if not !first then print_string "---\n";
-
first := false;
-
match Yamle.Document.root doc with
-
| None -> print_endline ""
-
| Some yaml ->
-
let value = Yamle.to_json ~resolve_aliases ~max_nodes ~max_depth yaml in
-
print_string (Yamle.to_string value)
-
) documents
-
| Flow ->
-
(* Convert through Value to apply tag-based type coercion *)
-
let first = ref true in
-
List.iter (fun doc ->
-
if not !first then print_string "---\n";
-
first := false;
-
match Yamle.Document.root doc with
-
| None -> print_endline ""
-
| Some yaml ->
-
let value = Yamle.to_json ~resolve_aliases ~max_nodes ~max_depth yaml in
-
print_string (Yamle.to_string ~layout_style:Yamle.Layout_style.Flow value)
-
) documents
-
| Json ->
-
let first = ref true in
-
List.iter (fun doc ->
-
match Yamle.Document.root doc with
-
| None -> ()
-
| Some yaml ->
-
if not !first then print_endline "---";
-
first := false;
-
let value = Yamle.to_json ~resolve_aliases ~max_nodes ~max_depth yaml in
-
print_endline (value_to_json value)
-
) documents
-
| Debug ->
-
List.iteri (fun i doc ->
-
Format.printf "Document %d:@." (i + 1);
-
Format.printf "%a@." Yamle.Document.pp doc
-
) documents
-
with
-
| Yamle.Yamle_error e ->
-
Printf.eprintf "Error: %s\n" (Yamle.Error.to_string e);
-
exit 1
-
-
let process_file ~format ~resolve_aliases ~max_nodes ~max_depth filename =
-
let content =
-
if filename = "-" then
-
In_channel.input_all In_channel.stdin
-
else
-
In_channel.with_open_text filename In_channel.input_all
-
in
-
process_string ~format ~resolve_aliases ~max_nodes ~max_depth content
-
-
let run format _all resolve_aliases max_nodes max_depth files =
-
let files = if files = [] then ["-"] else files in
-
List.iter (process_file ~format ~resolve_aliases ~max_nodes ~max_depth) files;
-
`Ok ()
-
-
(* Command-line arguments *)
-
-
let format_arg =
-
let doc = "Output format: yaml (default), json, flow, or debug." in
-
let formats = [
-
("yaml", Yaml);
-
("json", Json);
-
("flow", Flow);
-
("debug", Debug);
-
] in
-
Arg.(value & opt (enum formats) Yaml & info ["format"; "f"] ~docv:"FORMAT" ~doc)
-
-
let json_arg =
-
let doc = "Output as JSON (shorthand for --format=json)." in
-
Arg.(value & flag & info ["json"] ~doc)
-
-
let flow_arg =
-
let doc = "Output in flow style (shorthand for --format=flow)." in
-
Arg.(value & flag & info ["flow"] ~doc)
-
-
let debug_arg =
-
let doc = "Output internal representation (shorthand for --format=debug)." in
-
Arg.(value & flag & info ["debug"] ~doc)
-
-
let all_arg =
-
let doc = "Output all documents (for multi-document YAML)." in
-
Arg.(value & flag & info ["all"; "a"] ~doc)
-
-
let no_resolve_aliases_arg =
-
let doc = "Don't resolve aliases (keep them as references)." in
-
Arg.(value & flag & info ["no-resolve-aliases"] ~doc)
-
-
let max_nodes_arg =
-
let doc = "Maximum number of nodes during alias expansion (default: 10000000). \
-
Protection against billion laughs attack." in
-
Arg.(value & opt int Yamle.default_max_alias_nodes & info ["max-nodes"] ~docv:"N" ~doc)
-
-
let max_depth_arg =
-
let doc = "Maximum alias nesting depth (default: 100). \
-
Protection against deeply nested alias chains." in
-
Arg.(value & opt int Yamle.default_max_alias_depth & info ["max-depth"] ~docv:"N" ~doc)
-
-
let files_arg =
-
let doc = "YAML file(s) to process. Use '-' for stdin." in
-
Arg.(value & pos_all file [] & info [] ~docv:"FILE" ~doc)
-
-
let combined_format format json flow debug =
-
if json then Json
-
else if flow then Flow
-
else if debug then Debug
-
else format
-
-
let term =
-
let combine format json flow debug all no_resolve max_nodes max_depth files =
-
let format = combined_format format json flow debug in
-
let resolve_aliases = not no_resolve in
-
run format all resolve_aliases max_nodes max_depth files
-
in
-
Term.(ret (const combine $ format_arg $ json_arg $ flow_arg $ debug_arg $
-
all_arg $ no_resolve_aliases_arg $ max_nodes_arg $ max_depth_arg $ files_arg))
-
-
let info =
-
let doc = "Parse and reprint YAML files" in
-
let man = [
-
`S Manpage.s_description;
-
`P "$(tname) parses YAML files and reprints them in various formats. \
-
It can be used to validate YAML, convert between styles, or convert to JSON.";
-
`S Manpage.s_examples;
-
`P "Parse and reprint a YAML file:";
-
`Pre " $(tname) config.yaml";
-
`P "Convert YAML to JSON:";
-
`Pre " $(tname) --json config.yaml";
-
`P "Process multi-document YAML:";
-
`Pre " $(tname) --all multi.yaml";
-
`P "Limit alias expansion (protection against malicious YAML):";
-
`Pre " $(tname) --max-nodes 1000 --max-depth 10 untrusted.yaml";
-
`S Manpage.s_bugs;
-
`P "Report bugs at https://github.com/avsm/ocaml-yaml/issues";
-
] in
-
Cmd.info "yamlcat" ~version:"0.1.0" ~doc ~man
-
-
let () = exit (Cmd.eval (Cmd.v info term))
-19
yaml/ocaml-yamle/dune-project
···
-
(lang dune 3.0)
-
(name yamle)
-
(version 0.1.0)
-
-
(generate_opam_files true)
-
-
(source (github ocaml/yamle))
-
(license ISC)
-
(authors "Yamle Authors")
-
(maintainers "yamle@example.com")
-
-
(package
-
(name yamle)
-
(synopsis "Pure OCaml YAML 1.2 parser and emitter")
-
(description "A pure OCaml implementation of YAML 1.2 parsing and emission, with no C dependencies.")
-
(depends
-
(ocaml (>= 4.14.0))
-
(dune (>= 3.0))
-
(alcotest :with-test)))
-26
yaml/ocaml-yamle/lib/chomping.ml
···
-
(** Block scalar chomping indicators *)
-
-
type t =
-
| Strip (** Remove final line break and trailing empty lines *)
-
| Clip (** Keep final line break, remove trailing empty lines (default) *)
-
| Keep (** Keep final line break and trailing empty lines *)
-
-
let to_string = function
-
| Strip -> "strip"
-
| Clip -> "clip"
-
| Keep -> "keep"
-
-
let pp fmt t =
-
Format.pp_print_string fmt (to_string t)
-
-
let of_char = function
-
| '-' -> Some Strip
-
| '+' -> Some Keep
-
| _ -> None
-
-
let to_char = function
-
| Strip -> Some '-'
-
| Clip -> None
-
| Keep -> Some '+'
-
-
let equal a b = a = b
-54
yaml/ocaml-yamle/lib/document.ml
···
-
(** YAML document with directives and content *)
-
-
type t = {
-
version : (int * int) option;
-
tags : (string * string) list;
-
root : Yaml.t option;
-
implicit_start : bool;
-
implicit_end : bool;
-
}
-
-
let make
-
?(version : (int * int) option)
-
?(tags : (string * string) list = [])
-
?(implicit_start = true)
-
?(implicit_end = true)
-
root =
-
{ version; tags; root; implicit_start; implicit_end }
-
-
let version t = t.version
-
let tags t = t.tags
-
let root t = t.root
-
let implicit_start t = t.implicit_start
-
let implicit_end t = t.implicit_end
-
-
let with_version version t = { t with version = Some version }
-
let with_tags tags t = { t with tags }
-
let with_root root t = { t with root = Some root }
-
-
let pp fmt t =
-
Format.fprintf fmt "@[<v 2>document(@,";
-
(match t.version with
-
| Some (maj, min) -> Format.fprintf fmt "version=%d.%d,@ " maj min
-
| None -> ());
-
if t.tags <> [] then begin
-
Format.fprintf fmt "tags=[";
-
List.iteri (fun i (h, p) ->
-
if i > 0 then Format.fprintf fmt ", ";
-
Format.fprintf fmt "%s -> %s" h p
-
) t.tags;
-
Format.fprintf fmt "],@ "
-
end;
-
Format.fprintf fmt "implicit_start=%b,@ " t.implicit_start;
-
Format.fprintf fmt "implicit_end=%b,@ " t.implicit_end;
-
(match t.root with
-
| Some root -> Format.fprintf fmt "root=%a" Yaml.pp root
-
| None -> Format.fprintf fmt "root=<empty>");
-
Format.fprintf fmt "@]@,)"
-
-
let equal a b =
-
Option.equal (fun (a1, a2) (b1, b2) -> a1 = b1 && a2 = b2) a.version b.version &&
-
List.equal (fun (h1, p1) (h2, p2) -> h1 = h2 && p1 = p2) a.tags b.tags &&
-
Option.equal Yaml.equal a.root b.root &&
-
a.implicit_start = b.implicit_start &&
-
a.implicit_end = b.implicit_end
-26
yaml/ocaml-yamle/lib/dune
···
-
(library
-
(name yamle)
-
(public_name yamle)
-
(modules
-
position
-
span
-
error
-
encoding
-
scalar_style
-
layout_style
-
chomping
-
input
-
token
-
scanner
-
event
-
parser
-
value
-
tag
-
scalar
-
sequence
-
mapping
-
yaml
-
document
-
loader
-
emitter
-
yamle))
-764
yaml/ocaml-yamle/lib/emitter.ml
···
-
(** Emitter - converts YAML data structures to string output *)
-
-
type config = {
-
encoding : Encoding.t;
-
scalar_style : Scalar_style.t;
-
layout_style : Layout_style.t;
-
indent : int;
-
width : int;
-
canonical : bool;
-
}
-
-
let default_config = {
-
encoding = Encoding.Utf8;
-
scalar_style = Scalar_style.Any;
-
layout_style = Layout_style.Any;
-
indent = 2;
-
width = 80;
-
canonical = false;
-
}
-
-
type state =
-
| Initial
-
| Stream_started
-
| Document_started
-
| In_block_sequence of int (* indent level *)
-
| In_block_mapping_key of int
-
| In_block_mapping_value of int
-
| In_block_mapping_first_key of int (* first key after "- ", no indent needed *)
-
| In_flow_sequence
-
| In_flow_mapping_key
-
| In_flow_mapping_value
-
| Document_ended
-
| Stream_ended
-
-
type t = {
-
config : config;
-
buffer : Buffer.t;
-
mutable state : state;
-
mutable states : state list;
-
mutable indent : int;
-
mutable flow_level : int;
-
mutable need_separator : bool;
-
}
-
-
let create ?(config = default_config) () = {
-
config;
-
buffer = Buffer.create 1024;
-
state = Initial;
-
states = [];
-
indent = 0;
-
flow_level = 0;
-
need_separator = false;
-
}
-
-
let contents t = Buffer.contents t.buffer
-
-
let reset t =
-
Buffer.clear t.buffer;
-
t.state <- Initial;
-
t.states <- [];
-
t.indent <- 0;
-
t.flow_level <- 0;
-
t.need_separator <- false
-
-
(** Output helpers *)
-
-
let write t s = Buffer.add_string t.buffer s
-
let write_char t c = Buffer.add_char t.buffer c
-
-
let write_indent t =
-
for _ = 1 to t.indent do
-
write_char t ' '
-
done
-
-
let write_newline t =
-
write_char t '\n'
-
-
let push_state t s =
-
t.states <- t.state :: t.states;
-
t.state <- s
-
-
let pop_state t =
-
match t.states with
-
| s :: rest ->
-
t.state <- s;
-
t.states <- rest
-
| [] ->
-
t.state <- Stream_ended
-
-
(** Check if string needs quoting *)
-
let needs_quoting s =
-
if String.length s = 0 then true
-
else
-
let first = s.[0] in
-
(* Check first character *)
-
if first = '-' || first = '?' || first = ':' || first = ',' ||
-
first = '[' || first = ']' || first = '{' || first = '}' ||
-
first = '#' || first = '&' || first = '*' || first = '!' ||
-
first = '|' || first = '>' || first = '\'' || first = '"' ||
-
first = '%' || first = '@' || first = '`' || first = ' ' then
-
true
-
else
-
(* Check for special values *)
-
let lower = String.lowercase_ascii s in
-
if lower = "null" || lower = "true" || lower = "false" ||
-
lower = "yes" || lower = "no" || lower = "on" || lower = "off" ||
-
lower = "~" || lower = ".inf" || lower = "-.inf" || lower = ".nan" then
-
true
-
else
-
(* Check for characters that need quoting *)
-
try
-
String.iter (fun c ->
-
if c = ':' || c = '#' || c = '\n' || c = '\r' then
-
raise Exit
-
) s;
-
(* Check if it looks like a number *)
-
(try ignore (Float.of_string s); true with _ -> false)
-
with Exit -> true
-
-
(** Check if string contains characters requiring double quotes *)
-
let needs_double_quotes s =
-
try
-
String.iter (fun c ->
-
if c = '\n' || c = '\r' || c = '\t' || c = '\\' ||
-
c < ' ' || c = '"' then
-
raise Exit
-
) s;
-
false
-
with Exit -> true
-
-
(** Write scalar with appropriate quoting *)
-
let write_scalar t ?(style = Scalar_style.Any) value =
-
let effective_style =
-
if style = Scalar_style.Any then
-
if needs_double_quotes value then Scalar_style.Double_quoted
-
else if needs_quoting value then Scalar_style.Single_quoted
-
else Scalar_style.Plain
-
else style
-
in
-
match effective_style with
-
| Scalar_style.Plain | Scalar_style.Any ->
-
write t value
-
-
| Scalar_style.Single_quoted ->
-
write_char t '\'';
-
String.iter (fun c ->
-
if c = '\'' then write t "''"
-
else write_char t c
-
) value;
-
write_char t '\''
-
-
| Scalar_style.Double_quoted ->
-
write_char t '"';
-
String.iter (fun c ->
-
match c with
-
| '"' -> write t "\\\""
-
| '\\' -> write t "\\\\"
-
| '\n' -> write t "\\n"
-
| '\r' -> write t "\\r"
-
| '\t' -> write t "\\t"
-
| c when c < ' ' -> write t (Printf.sprintf "\\x%02x" (Char.code c))
-
| c -> write_char t c
-
) value;
-
write_char t '"'
-
-
| Scalar_style.Literal ->
-
write t "|";
-
write_newline t;
-
let lines = String.split_on_char '\n' value in
-
List.iter (fun line ->
-
write_indent t;
-
write t line;
-
write_newline t
-
) lines
-
-
| Scalar_style.Folded ->
-
write t ">";
-
write_newline t;
-
let lines = String.split_on_char '\n' value in
-
List.iter (fun line ->
-
write_indent t;
-
write t line;
-
write_newline t
-
) lines
-
-
(** Write anchor if present *)
-
let write_anchor t anchor =
-
match anchor with
-
| Some name ->
-
write_char t '&';
-
write t name;
-
write_char t ' '
-
| None -> ()
-
-
(** Write tag if present and not implicit *)
-
let write_tag t ~implicit tag =
-
if not implicit then
-
match tag with
-
| Some tag_str ->
-
write_char t '!';
-
write t tag_str;
-
write_char t ' '
-
| None -> ()
-
-
(** Emit events *)
-
-
let emit t (ev : Event.t) =
-
match ev with
-
| Event.Stream_start _ ->
-
t.state <- Stream_started
-
-
| Event.Stream_end ->
-
t.state <- Stream_ended
-
-
| Event.Document_start { version; implicit } ->
-
if not implicit then begin
-
(match version with
-
| Some (maj, min) ->
-
write t (Printf.sprintf "%%YAML %d.%d\n" maj min)
-
| None -> ());
-
write t "---";
-
write_newline t
-
end;
-
t.state <- Document_started
-
-
| Event.Document_end { implicit } ->
-
if not implicit then begin
-
write t "...";
-
write_newline t
-
end;
-
t.state <- Document_ended
-
-
| Event.Alias { anchor } ->
-
if t.flow_level > 0 then begin
-
if t.need_separator then write t ", ";
-
t.need_separator <- true;
-
write_char t '*';
-
write t anchor
-
end else begin
-
(match t.state with
-
| In_block_sequence _ ->
-
write_indent t;
-
write t "- *";
-
write t anchor;
-
write_newline t
-
| In_block_mapping_key _ ->
-
write_indent t;
-
write_char t '*';
-
write t anchor;
-
write t ": ";
-
t.state <- In_block_mapping_value t.indent
-
| In_block_mapping_value indent ->
-
write_char t '*';
-
write t anchor;
-
write_newline t;
-
t.state <- In_block_mapping_key indent
-
| _ ->
-
write_char t '*';
-
write t anchor;
-
write_newline t)
-
end
-
-
| Event.Scalar { anchor; tag; value; plain_implicit; style; _ } ->
-
if t.flow_level > 0 then begin
-
(match t.state with
-
| In_flow_mapping_key ->
-
if t.need_separator then write t ", ";
-
write_anchor t anchor;
-
write_tag t ~implicit:plain_implicit tag;
-
write_scalar t ~style value;
-
write t ": ";
-
t.need_separator <- false;
-
t.state <- In_flow_mapping_value
-
| In_flow_mapping_value ->
-
write_anchor t anchor;
-
write_tag t ~implicit:plain_implicit tag;
-
write_scalar t ~style value;
-
t.need_separator <- true;
-
t.state <- In_flow_mapping_key
-
| _ ->
-
if t.need_separator then write t ", ";
-
t.need_separator <- true;
-
write_anchor t anchor;
-
write_tag t ~implicit:plain_implicit tag;
-
write_scalar t ~style value)
-
end else begin
-
match t.state with
-
| In_block_sequence _ ->
-
write_indent t;
-
write t "- ";
-
write_anchor t anchor;
-
write_tag t ~implicit:plain_implicit tag;
-
write_scalar t ~style value;
-
write_newline t
-
| In_block_mapping_key indent ->
-
write_indent t;
-
write_anchor t anchor;
-
write_tag t ~implicit:plain_implicit tag;
-
write_scalar t ~style value;
-
write_char t ':';
-
t.state <- In_block_mapping_value indent
-
| In_block_mapping_first_key indent ->
-
(* First key after "- ", no indent needed *)
-
write_anchor t anchor;
-
write_tag t ~implicit:plain_implicit tag;
-
write_scalar t ~style value;
-
write_char t ':';
-
t.state <- In_block_mapping_value indent
-
| In_block_mapping_value indent ->
-
write_char t ' ';
-
write_anchor t anchor;
-
write_tag t ~implicit:plain_implicit tag;
-
write_scalar t ~style value;
-
write_newline t;
-
t.state <- In_block_mapping_key indent
-
| _ ->
-
write_anchor t anchor;
-
write_tag t ~implicit:plain_implicit tag;
-
write_scalar t ~style value;
-
write_newline t
-
end
-
-
| Event.Sequence_start { anchor; tag; implicit; style } ->
-
let use_flow = style = Layout_style.Flow || t.flow_level > 0 in
-
if t.flow_level > 0 then begin
-
(match t.state with
-
| In_flow_mapping_key ->
-
if t.need_separator then write t ", ";
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write_char t '[';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_mapping_value; (* After ] we'll be in value position but sequence handles it *)
-
t.state <- In_flow_sequence
-
| In_flow_mapping_value ->
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write_char t '[';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_mapping_key;
-
t.state <- In_flow_sequence
-
| _ ->
-
if t.need_separator then write t ", ";
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write_char t '[';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_sequence)
-
end else begin
-
match t.state with
-
| In_block_sequence _ ->
-
write_indent t;
-
write t "- ";
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
if use_flow then begin
-
write_char t '[';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_sequence
-
end else begin
-
write_newline t;
-
push_state t (In_block_sequence t.indent);
-
t.indent <- t.indent + t.config.indent
-
end
-
| In_block_mapping_key indent ->
-
write_indent t;
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write t ":";
-
write_newline t;
-
push_state t (In_block_mapping_key indent);
-
t.indent <- t.indent + t.config.indent;
-
t.state <- In_block_sequence t.indent
-
| In_block_mapping_first_key indent ->
-
(* First key after "- " with sequence value - no indent *)
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write t ":";
-
write_newline t;
-
push_state t (In_block_mapping_key indent);
-
t.indent <- t.indent + t.config.indent;
-
t.state <- In_block_sequence t.indent
-
| In_block_mapping_value indent ->
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
if use_flow then begin
-
write_char t ' ';
-
write_char t '[';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
(* Save key state to return to after flow sequence *)
-
t.state <- In_block_mapping_key indent;
-
push_state t In_flow_sequence
-
end else begin
-
write_newline t;
-
(* Save key state to return to after nested sequence *)
-
t.state <- In_block_mapping_key indent;
-
push_state t (In_block_sequence (t.indent + t.config.indent));
-
t.indent <- t.indent + t.config.indent
-
end
-
| _ ->
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
if use_flow then begin
-
write_char t '[';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_sequence
-
end else begin
-
push_state t (In_block_sequence t.indent);
-
t.state <- In_block_sequence t.indent
-
end
-
end
-
-
| Event.Sequence_end ->
-
if t.flow_level > 0 then begin
-
write_char t ']';
-
t.flow_level <- t.flow_level - 1;
-
t.need_separator <- true;
-
pop_state t;
-
(* Write newline if returning to block context *)
-
(match t.state with
-
| In_block_mapping_key _ | In_block_sequence _ -> write_newline t
-
| _ -> ())
-
end else begin
-
t.indent <- t.indent - t.config.indent;
-
pop_state t
-
end
-
-
| Event.Mapping_start { anchor; tag; implicit; style } ->
-
let use_flow = style = Layout_style.Flow || t.flow_level > 0 in
-
if t.flow_level > 0 then begin
-
(match t.state with
-
| In_flow_mapping_key ->
-
if t.need_separator then write t ", ";
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write_char t '{';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_mapping_value;
-
t.state <- In_flow_mapping_key
-
| In_flow_mapping_value ->
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write_char t '{';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_mapping_key;
-
t.state <- In_flow_mapping_key
-
| _ ->
-
if t.need_separator then write t ", ";
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write_char t '{';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_mapping_key)
-
end else begin
-
match t.state with
-
| In_block_sequence _ ->
-
write_indent t;
-
write t "- ";
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
if use_flow then begin
-
write_char t '{';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_mapping_key
-
end else begin
-
(* Don't write newline - first key goes on same line as "- " *)
-
push_state t (In_block_sequence t.indent);
-
t.indent <- t.indent + t.config.indent;
-
t.state <- In_block_mapping_first_key t.indent
-
end
-
| In_block_mapping_key indent ->
-
write_indent t;
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write t ":";
-
write_newline t;
-
push_state t (In_block_mapping_key indent);
-
t.indent <- t.indent + t.config.indent;
-
t.state <- In_block_mapping_key t.indent
-
| In_block_mapping_first_key indent ->
-
(* First key after "- " with mapping value - no indent *)
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
write t ":";
-
write_newline t;
-
push_state t (In_block_mapping_key indent);
-
t.indent <- t.indent + t.config.indent;
-
t.state <- In_block_mapping_key t.indent
-
| In_block_mapping_value indent ->
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
if use_flow then begin
-
write_char t ' ';
-
write_char t '{';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
(* Save key state to return to after flow mapping *)
-
t.state <- In_block_mapping_key indent;
-
push_state t In_flow_mapping_key
-
end else begin
-
write_newline t;
-
(* Save key state to return to after nested mapping *)
-
t.state <- In_block_mapping_key indent;
-
push_state t (In_block_mapping_key (t.indent + t.config.indent));
-
t.indent <- t.indent + t.config.indent
-
end
-
| _ ->
-
write_anchor t anchor;
-
write_tag t ~implicit tag;
-
if use_flow then begin
-
write_char t '{';
-
t.flow_level <- t.flow_level + 1;
-
t.need_separator <- false;
-
push_state t In_flow_mapping_key
-
end else begin
-
push_state t (In_block_mapping_key t.indent);
-
t.state <- In_block_mapping_key t.indent
-
end
-
end
-
-
| Event.Mapping_end ->
-
if t.flow_level > 0 then begin
-
write_char t '}';
-
t.flow_level <- t.flow_level - 1;
-
t.need_separator <- true;
-
pop_state t;
-
(* Write newline if returning to block context *)
-
(match t.state with
-
| In_block_mapping_key _ | In_block_sequence _ -> write_newline t
-
| _ -> ())
-
end else begin
-
t.indent <- t.indent - t.config.indent;
-
pop_state t
-
end
-
-
(** High-level emission *)
-
-
let rec emit_yaml_node t (yaml : Yaml.t) =
-
match yaml with
-
| `Scalar s ->
-
emit t (Event.Scalar {
-
anchor = Scalar.anchor s;
-
tag = Scalar.tag s;
-
value = Scalar.value s;
-
plain_implicit = Scalar.plain_implicit s;
-
quoted_implicit = Scalar.quoted_implicit s;
-
style = Scalar.style s;
-
})
-
-
| `Alias name ->
-
emit t (Event.Alias { anchor = name })
-
-
| `A seq ->
-
let members = Sequence.members seq in
-
let style =
-
(* Force flow style for empty sequences *)
-
if members = [] then Layout_style.Flow
-
else Sequence.style seq
-
in
-
emit t (Event.Sequence_start {
-
anchor = Sequence.anchor seq;
-
tag = Sequence.tag seq;
-
implicit = Sequence.implicit seq;
-
style;
-
});
-
List.iter (emit_yaml_node t) members;
-
emit t Event.Sequence_end
-
-
| `O map ->
-
let members = Mapping.members map in
-
let style =
-
(* Force flow style for empty mappings *)
-
if members = [] then Layout_style.Flow
-
else Mapping.style map
-
in
-
emit t (Event.Mapping_start {
-
anchor = Mapping.anchor map;
-
tag = Mapping.tag map;
-
implicit = Mapping.implicit map;
-
style;
-
});
-
List.iter (fun (k, v) ->
-
emit_yaml_node t k;
-
emit_yaml_node t v
-
) members;
-
emit t Event.Mapping_end
-
-
let emit_yaml t yaml =
-
emit t (Event.Stream_start { encoding = t.config.encoding });
-
emit t (Event.Document_start { version = None; implicit = true });
-
emit_yaml_node t yaml;
-
emit t (Event.Document_end { implicit = true });
-
emit t Event.Stream_end
-
-
let rec emit_value_node t (value : Value.t) =
-
match value with
-
| `Null ->
-
emit t (Event.Scalar {
-
anchor = None; tag = None;
-
value = "null";
-
plain_implicit = true; quoted_implicit = false;
-
style = Scalar_style.Plain;
-
})
-
-
| `Bool b ->
-
emit t (Event.Scalar {
-
anchor = None; tag = None;
-
value = if b then "true" else "false";
-
plain_implicit = true; quoted_implicit = false;
-
style = Scalar_style.Plain;
-
})
-
-
| `Float f ->
-
let value =
-
match Float.classify_float f with
-
| FP_nan -> ".nan"
-
| FP_infinite -> if f > 0.0 then ".inf" else "-.inf"
-
| _ ->
-
if Float.is_integer f && Float.abs f < 1e15 then
-
Printf.sprintf "%.0f" f
-
else
-
Printf.sprintf "%g" f
-
in
-
emit t (Event.Scalar {
-
anchor = None; tag = None;
-
value;
-
plain_implicit = true; quoted_implicit = false;
-
style = Scalar_style.Plain;
-
})
-
-
| `String s ->
-
let style =
-
if needs_double_quotes s then Scalar_style.Double_quoted
-
else if needs_quoting s then Scalar_style.Single_quoted
-
else Scalar_style.Plain
-
in
-
emit t (Event.Scalar {
-
anchor = None; tag = None;
-
value = s;
-
plain_implicit = style = Scalar_style.Plain;
-
quoted_implicit = style <> Scalar_style.Plain;
-
style;
-
})
-
-
| `A items ->
-
let style =
-
(* Force flow style for empty sequences *)
-
if items = [] then Layout_style.Flow
-
else if t.config.layout_style = Layout_style.Flow then Layout_style.Flow
-
else Layout_style.Block
-
in
-
emit t (Event.Sequence_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style;
-
});
-
List.iter (emit_value_node t) items;
-
emit t Event.Sequence_end
-
-
| `O pairs ->
-
let style =
-
(* Force flow style for empty mappings *)
-
if pairs = [] then Layout_style.Flow
-
else if t.config.layout_style = Layout_style.Flow then Layout_style.Flow
-
else Layout_style.Block
-
in
-
emit t (Event.Mapping_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style;
-
});
-
List.iter (fun (k, v) ->
-
emit t (Event.Scalar {
-
anchor = None; tag = None;
-
value = k;
-
plain_implicit = not (needs_quoting k);
-
quoted_implicit = needs_quoting k;
-
style = if needs_quoting k then Scalar_style.Double_quoted else Scalar_style.Plain;
-
});
-
emit_value_node t v
-
) pairs;
-
emit t Event.Mapping_end
-
-
let emit_value t value =
-
emit t (Event.Stream_start { encoding = t.config.encoding });
-
emit t (Event.Document_start { version = None; implicit = true });
-
emit_value_node t value;
-
emit t (Event.Document_end { implicit = true });
-
emit t Event.Stream_end
-
-
(** Strip anchors from a YAML tree *)
-
let rec strip_anchors (yaml : Yaml.t) : Yaml.t =
-
match yaml with
-
| `Scalar s ->
-
if Scalar.anchor s = None then yaml
-
else
-
`Scalar (Scalar.make
-
?tag:(Scalar.tag s)
-
~plain_implicit:(Scalar.plain_implicit s)
-
~quoted_implicit:(Scalar.quoted_implicit s)
-
~style:(Scalar.style s)
-
(Scalar.value s))
-
| `Alias _ -> yaml
-
| `A seq ->
-
`A (Sequence.make
-
?tag:(Sequence.tag seq)
-
~implicit:(Sequence.implicit seq)
-
~style:(Sequence.style seq)
-
(List.map strip_anchors (Sequence.members seq)))
-
| `O map ->
-
`O (Mapping.make
-
?tag:(Mapping.tag map)
-
~implicit:(Mapping.implicit map)
-
~style:(Mapping.style map)
-
(List.map (fun (k, v) -> (strip_anchors k, strip_anchors v)) (Mapping.members map)))
-
-
let emit_document ?(resolve_aliases = true) t doc =
-
emit t (Event.Document_start {
-
version = Document.version doc;
-
implicit = Document.implicit_start doc;
-
});
-
(match Document.root doc with
-
| Some yaml ->
-
let yaml = if resolve_aliases then
-
yaml |> Yaml.resolve_aliases |> strip_anchors
-
else yaml in
-
emit_yaml_node t yaml
-
| None ->
-
emit t (Event.Scalar {
-
anchor = None; tag = None;
-
value = "";
-
plain_implicit = true; quoted_implicit = false;
-
style = Scalar_style.Plain;
-
}));
-
emit t (Event.Document_end { implicit = Document.implicit_end doc })
-
-
(** Convenience functions *)
-
-
let value_to_string ?(config = default_config) value =
-
let t = create ~config () in
-
emit_value t value;
-
contents t
-
-
let yaml_to_string ?(config = default_config) yaml =
-
let t = create ~config () in
-
emit_yaml t yaml;
-
contents t
-
-
let documents_to_string ?(config = default_config) ?(resolve_aliases = true) documents =
-
let t = create ~config () in
-
emit t (Event.Stream_start { encoding = config.encoding });
-
List.iter (emit_document ~resolve_aliases t) documents;
-
emit t Event.Stream_end;
-
contents t
-54
yaml/ocaml-yamle/lib/encoding.ml
···
-
(** Character encoding detection and handling *)
-
-
type t =
-
| Utf8
-
| Utf16be
-
| Utf16le
-
| Utf32be
-
| Utf32le
-
-
let to_string = function
-
| Utf8 -> "UTF-8"
-
| Utf16be -> "UTF-16BE"
-
| Utf16le -> "UTF-16LE"
-
| Utf32be -> "UTF-32BE"
-
| Utf32le -> "UTF-32LE"
-
-
let pp fmt t =
-
Format.pp_print_string fmt (to_string t)
-
-
(** Detect encoding from BOM or first bytes.
-
Returns (encoding, bom_length) *)
-
let detect s =
-
let len = String.length s in
-
if len = 0 then (Utf8, 0)
-
else
-
let b0 = Char.code s.[0] in
-
let b1 = if len > 1 then Char.code s.[1] else 0 in
-
let b2 = if len > 2 then Char.code s.[2] else 0 in
-
let b3 = if len > 3 then Char.code s.[3] else 0 in
-
(* Check for BOM first *)
-
if b0 = 0xEF && b1 = 0xBB && b2 = 0xBF then
-
(Utf8, 3)
-
else if b0 = 0xFE && b1 = 0xFF then
-
(Utf16be, 2)
-
else if b0 = 0xFF && b1 = 0xFE then
-
if b2 = 0x00 && b3 = 0x00 then
-
(Utf32le, 4)
-
else
-
(Utf16le, 2)
-
else if b0 = 0x00 && b1 = 0x00 && b2 = 0xFE && b3 = 0xFF then
-
(Utf32be, 4)
-
(* No BOM - detect from content pattern *)
-
else if b0 = 0x00 && b1 = 0x00 && b2 = 0x00 && b3 <> 0x00 then
-
(Utf32be, 0)
-
else if b0 <> 0x00 && b1 = 0x00 && b2 = 0x00 && b3 = 0x00 then
-
(Utf32le, 0)
-
else if b0 = 0x00 && b1 <> 0x00 then
-
(Utf16be, 0)
-
else if b0 <> 0x00 && b1 = 0x00 then
-
(Utf16le, 0)
-
else
-
(Utf8, 0)
-
-
let equal a b = a = b
-196
yaml/ocaml-yamle/lib/error.ml
···
-
(** Error handling with position information *)
-
-
(** Error classification *)
-
type kind =
-
(* Scanner errors *)
-
| Unexpected_character of char
-
| Unexpected_eof
-
| Invalid_escape_sequence of string
-
| Invalid_unicode_escape of string
-
| Invalid_hex_escape of string
-
| Invalid_tag of string
-
| Invalid_anchor of string
-
| Invalid_alias of string
-
| Invalid_comment
-
| Unclosed_single_quote
-
| Unclosed_double_quote
-
| Unclosed_flow_sequence
-
| Unclosed_flow_mapping
-
| Invalid_indentation of int * int (** expected, got *)
-
| Invalid_flow_indentation (** Content in flow collection must be indented *)
-
| Tab_in_indentation
-
| Invalid_block_scalar_header of string
-
| Invalid_quoted_scalar_indentation of string
-
| Invalid_directive of string
-
| Invalid_yaml_version of string
-
| Invalid_tag_directive of string
-
| Reserved_directive of string
-
| Illegal_flow_key_line (** Key and : must be on same line in flow context *)
-
| Block_sequence_disallowed (** Block sequence entries not allowed in this context *)
-
-
(* Parser errors *)
-
| Unexpected_token of string
-
| Expected_document_start
-
| Expected_document_end
-
| Expected_block_entry
-
| Expected_key
-
| Expected_value
-
| Expected_node
-
| Expected_scalar
-
| Expected_sequence_end
-
| Expected_mapping_end
-
| Duplicate_anchor of string
-
| Undefined_alias of string
-
| Alias_cycle of string
-
| Multiple_documents
-
| Mapping_key_too_long
-
-
(* Loader errors *)
-
| Invalid_scalar_conversion of string * string (** value, target type *)
-
| Type_mismatch of string * string (** expected, got *)
-
| Unresolved_alias of string
-
| Key_not_found of string
-
| Alias_expansion_node_limit of int (** max nodes exceeded *)
-
| Alias_expansion_depth_limit of int (** max depth exceeded *)
-
-
(* Emitter errors *)
-
| Invalid_encoding of string
-
| Scalar_contains_invalid_chars of string
-
| Anchor_not_set
-
| Invalid_state of string
-
-
(* Generic *)
-
| Custom of string
-
-
(** Full error with location *)
-
type t = {
-
kind : kind;
-
span : Span.t option;
-
context : string list;
-
source : string option;
-
}
-
-
(** The exception raised by yamle *)
-
exception Yamle_error of t
-
-
let () =
-
Printexc.register_printer (function
-
| Yamle_error e ->
-
let loc = match e.span with
-
| None -> ""
-
| Some span -> " at " ^ Span.to_string span
-
in
-
Some (Printf.sprintf "Yamle_error: %s%s"
-
(match e.kind with Custom s -> s | _ -> "error") loc)
-
| _ -> None)
-
-
let make ?span ?(context=[]) ?source kind =
-
{ kind; span; context; source }
-
-
let raise ?span ?context ?source kind =
-
Stdlib.raise (Yamle_error (make ?span ?context ?source kind))
-
-
let raise_at pos kind =
-
let span = Span.point pos in
-
raise ~span kind
-
-
let raise_span span kind =
-
raise ~span kind
-
-
let with_context ctx f =
-
try f () with
-
| Yamle_error e ->
-
Stdlib.raise (Yamle_error { e with context = ctx :: e.context })
-
-
let kind_to_string = function
-
| Unexpected_character c -> Printf.sprintf "unexpected character %C" c
-
| Unexpected_eof -> "unexpected end of input"
-
| Invalid_escape_sequence s -> Printf.sprintf "invalid escape sequence: %s" s
-
| Invalid_unicode_escape s -> Printf.sprintf "invalid unicode escape: %s" s
-
| Invalid_hex_escape s -> Printf.sprintf "invalid hex escape: %s" s
-
| Invalid_tag s -> Printf.sprintf "invalid tag: %s" s
-
| Invalid_anchor s -> Printf.sprintf "invalid anchor: %s" s
-
| Invalid_alias s -> Printf.sprintf "invalid alias: %s" s
-
| Invalid_comment -> "comments must be separated from other tokens by whitespace"
-
| Unclosed_single_quote -> "unclosed single quote"
-
| Unclosed_double_quote -> "unclosed double quote"
-
| Unclosed_flow_sequence -> "unclosed flow sequence '['"
-
| Unclosed_flow_mapping -> "unclosed flow mapping '{'"
-
| Invalid_indentation (expected, got) ->
-
Printf.sprintf "invalid indentation: expected %d, got %d" expected got
-
| Invalid_flow_indentation -> "invalid indentation in flow construct"
-
| Tab_in_indentation -> "tab character in indentation"
-
| Invalid_block_scalar_header s ->
-
Printf.sprintf "invalid block scalar header: %s" s
-
| Invalid_quoted_scalar_indentation s ->
-
Printf.sprintf "%s" s
-
| Invalid_directive s -> Printf.sprintf "invalid directive: %s" s
-
| Invalid_yaml_version s -> Printf.sprintf "invalid YAML version: %s" s
-
| Invalid_tag_directive s -> Printf.sprintf "invalid TAG directive: %s" s
-
| Reserved_directive s -> Printf.sprintf "reserved directive: %s" s
-
| Illegal_flow_key_line -> "key and ':' must be on the same line in flow context"
-
| Block_sequence_disallowed -> "block sequence entries are not allowed in this context"
-
| Unexpected_token s -> Printf.sprintf "unexpected token: %s" s
-
| Expected_document_start -> "expected document start '---'"
-
| Expected_document_end -> "expected document end '...'"
-
| Expected_block_entry -> "expected block entry '-'"
-
| Expected_key -> "expected mapping key"
-
| Expected_value -> "expected mapping value"
-
| Expected_node -> "expected node"
-
| Expected_scalar -> "expected scalar"
-
| Expected_sequence_end -> "expected sequence end ']'"
-
| Expected_mapping_end -> "expected mapping end '}'"
-
| Duplicate_anchor s -> Printf.sprintf "duplicate anchor: &%s" s
-
| Undefined_alias s -> Printf.sprintf "undefined alias: *%s" s
-
| Alias_cycle s -> Printf.sprintf "alias cycle detected: *%s" s
-
| Multiple_documents -> "multiple documents found when single expected"
-
| Mapping_key_too_long -> "mapping key too long (max 1024 characters)"
-
| Invalid_scalar_conversion (value, typ) ->
-
Printf.sprintf "cannot convert %S to %s" value typ
-
| Type_mismatch (expected, got) ->
-
Printf.sprintf "type mismatch: expected %s, got %s" expected got
-
| Unresolved_alias s -> Printf.sprintf "unresolved alias: *%s" s
-
| Key_not_found s -> Printf.sprintf "key not found: %s" s
-
| Alias_expansion_node_limit n ->
-
Printf.sprintf "alias expansion exceeded node limit (%d nodes)" n
-
| Alias_expansion_depth_limit n ->
-
Printf.sprintf "alias expansion exceeded depth limit (%d levels)" n
-
| Invalid_encoding s -> Printf.sprintf "invalid encoding: %s" s
-
| Scalar_contains_invalid_chars s ->
-
Printf.sprintf "scalar contains invalid characters: %s" s
-
| Anchor_not_set -> "anchor not set"
-
| Invalid_state s -> Printf.sprintf "invalid state: %s" s
-
| Custom s -> s
-
-
let to_string t =
-
let loc = match t.span with
-
| None -> ""
-
| Some span -> " at " ^ Span.to_string span
-
in
-
let ctx = match t.context with
-
| [] -> ""
-
| ctxs -> " (in " ^ String.concat " > " (List.rev ctxs) ^ ")"
-
in
-
kind_to_string t.kind ^ loc ^ ctx
-
-
let pp fmt t =
-
Format.fprintf fmt "Yamle error: %s" (to_string t)
-
-
let extract_line source line_num =
-
let lines = String.split_on_char '\n' source in
-
if line_num >= 1 && line_num <= List.length lines then
-
Some (List.nth lines (line_num - 1))
-
else
-
None
-
-
let pp_with_source ~source fmt t =
-
pp fmt t;
-
match t.span with
-
| None -> ()
-
| Some span ->
-
match extract_line source span.start.line with
-
| None -> ()
-
| Some line ->
-
Format.fprintf fmt "\n %d | %s\n" span.start.line line;
-
let padding = String.make (span.start.column - 1) ' ' in
-
Format.fprintf fmt " | %s^" padding
-77
yaml/ocaml-yamle/lib/event.ml
···
-
(** YAML parser events *)
-
-
type t =
-
| Stream_start of { encoding : Encoding.t }
-
| Stream_end
-
| Document_start of {
-
version : (int * int) option;
-
implicit : bool;
-
}
-
| Document_end of { implicit : bool }
-
| Alias of { anchor : string }
-
| Scalar of {
-
anchor : string option;
-
tag : string option;
-
value : string;
-
plain_implicit : bool;
-
quoted_implicit : bool;
-
style : Scalar_style.t;
-
}
-
| Sequence_start of {
-
anchor : string option;
-
tag : string option;
-
implicit : bool;
-
style : Layout_style.t;
-
}
-
| Sequence_end
-
| Mapping_start of {
-
anchor : string option;
-
tag : string option;
-
implicit : bool;
-
style : Layout_style.t;
-
}
-
| Mapping_end
-
-
type spanned = {
-
event : t;
-
span : Span.t;
-
}
-
-
let pp fmt = function
-
| Stream_start { encoding } ->
-
Format.fprintf fmt "stream-start(%a)" Encoding.pp encoding
-
| Stream_end ->
-
Format.fprintf fmt "stream-end"
-
| Document_start { version; implicit } ->
-
Format.fprintf fmt "document-start(version=%s, implicit=%b)"
-
(match version with None -> "none" | Some (maj, min) -> Printf.sprintf "%d.%d" maj min)
-
implicit
-
| Document_end { implicit } ->
-
Format.fprintf fmt "document-end(implicit=%b)" implicit
-
| Alias { anchor } ->
-
Format.fprintf fmt "alias(%s)" anchor
-
| Scalar { anchor; tag; value; style; _ } ->
-
Format.fprintf fmt "scalar(anchor=%s, tag=%s, style=%a, value=%S)"
-
(Option.value anchor ~default:"none")
-
(Option.value tag ~default:"none")
-
Scalar_style.pp style
-
value
-
| Sequence_start { anchor; tag; implicit; style } ->
-
Format.fprintf fmt "sequence-start(anchor=%s, tag=%s, implicit=%b, style=%a)"
-
(Option.value anchor ~default:"none")
-
(Option.value tag ~default:"none")
-
implicit
-
Layout_style.pp style
-
| Sequence_end ->
-
Format.fprintf fmt "sequence-end"
-
| Mapping_start { anchor; tag; implicit; style } ->
-
Format.fprintf fmt "mapping-start(anchor=%s, tag=%s, implicit=%b, style=%a)"
-
(Option.value anchor ~default:"none")
-
(Option.value tag ~default:"none")
-
implicit
-
Layout_style.pp style
-
| Mapping_end ->
-
Format.fprintf fmt "mapping-end"
-
-
let pp_spanned fmt { event; span } =
-
Format.fprintf fmt "%a at %a" pp event Span.pp span
-151
yaml/ocaml-yamle/lib/input.ml
···
-
(** Character input source with lookahead *)
-
-
type t = {
-
source : string;
-
mutable pos : int; (** Current byte position *)
-
mutable position : Position.t; (** Line/column tracking *)
-
length : int;
-
}
-
-
let of_string source =
-
let encoding, bom_len = Encoding.detect source in
-
(* For now, we only support UTF-8. Skip BOM if present. *)
-
ignore encoding;
-
{
-
source;
-
pos = bom_len;
-
position = Position.initial;
-
length = String.length source;
-
}
-
-
let position t = t.position
-
-
let is_eof t = t.pos >= t.length
-
-
let peek t =
-
if t.pos >= t.length then None
-
else Some t.source.[t.pos]
-
-
let peek_exn t =
-
if t.pos >= t.length then
-
Error.raise_at t.position Unexpected_eof
-
else
-
t.source.[t.pos]
-
-
let peek_nth t n =
-
let idx = t.pos + n in
-
if idx >= t.length then None
-
else Some t.source.[idx]
-
-
let peek_string t n =
-
if t.pos + n > t.length then
-
String.sub t.source t.pos (t.length - t.pos)
-
else
-
String.sub t.source t.pos n
-
-
let next t =
-
if t.pos >= t.length then None
-
else begin
-
let c = t.source.[t.pos] in
-
t.pos <- t.pos + 1;
-
t.position <- Position.advance_char c t.position;
-
Some c
-
end
-
-
let next_exn t =
-
match next t with
-
| Some c -> c
-
| None -> Error.raise_at t.position Unexpected_eof
-
-
let skip t n =
-
for _ = 1 to n do
-
ignore (next t)
-
done
-
-
let skip_while t pred =
-
while not (is_eof t) && pred (Option.get (peek t)) do
-
ignore (next t)
-
done
-
-
(** Character classification *)
-
-
let is_break c = c = '\n' || c = '\r'
-
-
let is_blank c = c = ' ' || c = '\t'
-
-
let is_whitespace c = is_break c || is_blank c
-
-
let is_digit c = c >= '0' && c <= '9'
-
-
let is_hex c =
-
(c >= '0' && c <= '9') ||
-
(c >= 'a' && c <= 'f') ||
-
(c >= 'A' && c <= 'F')
-
-
let is_alpha c =
-
(c >= 'a' && c <= 'z') ||
-
(c >= 'A' && c <= 'Z')
-
-
let is_alnum c = is_alpha c || is_digit c
-
-
(** YAML indicator characters *)
-
let is_indicator c =
-
match c with
-
| '-' | '?' | ':' | ',' | '[' | ']' | '{' | '}'
-
| '#' | '&' | '*' | '!' | '|' | '>' | '\'' | '"'
-
| '%' | '@' | '`' -> true
-
| _ -> false
-
-
(** Characters that cannot start a plain scalar *)
-
let is_flow_indicator c =
-
match c with
-
| ',' | '[' | ']' | '{' | '}' -> true
-
| _ -> false
-
-
(** Check if next char satisfies predicate *)
-
let next_is pred t =
-
match peek t with
-
| None -> false
-
| Some c -> pred c
-
-
let next_is_break t = next_is is_break t
-
let next_is_blank t = next_is is_blank t
-
let next_is_whitespace t = next_is is_whitespace t
-
let next_is_digit t = next_is is_digit t
-
let next_is_hex t = next_is is_hex t
-
let next_is_alpha t = next_is is_alpha t
-
let next_is_indicator t = next_is is_indicator t
-
-
(** Check if at document boundary (--- or ...) *)
-
let at_document_boundary t =
-
if t.position.column <> 1 then false
-
else
-
let s = peek_string t 4 in
-
let prefix = String.sub s 0 (min 3 (String.length s)) in
-
(prefix = "---" || prefix = "...") &&
-
(String.length s < 4 || is_whitespace s.[3] || String.length s = 3)
-
-
(** Consume line break, handling \r\n as single break *)
-
let consume_break t =
-
match peek t with
-
| Some '\r' ->
-
ignore (next t);
-
(match peek t with
-
| Some '\n' -> ignore (next t)
-
| _ -> ())
-
| Some '\n' ->
-
ignore (next t)
-
| _ -> ()
-
-
(** Get remaining content from current position *)
-
let remaining t =
-
if t.pos >= t.length then ""
-
else String.sub t.source t.pos (t.length - t.pos)
-
-
(** Mark current position for span creation *)
-
let mark t = t.position
-
-
(** Get the character before the current position *)
-
let peek_back t =
-
if t.pos <= 0 then None
-
else Some t.source.[t.pos - 1]
-24
yaml/ocaml-yamle/lib/layout_style.ml
···
-
(** Collection layout styles *)
-
-
type t =
-
| Any (** Let emitter choose *)
-
| Block (** Indentation-based *)
-
| Flow (** Inline with brackets *)
-
-
let to_string = function
-
| Any -> "any"
-
| Block -> "block"
-
| Flow -> "flow"
-
-
let pp fmt t =
-
Format.pp_print_string fmt (to_string t)
-
-
let equal a b = a = b
-
-
let compare a b =
-
let to_int = function
-
| Any -> 0
-
| Block -> 1
-
| Flow -> 2
-
in
-
Int.compare (to_int a) (to_int b)
-276
yaml/ocaml-yamle/lib/loader.ml
···
-
(** Loader - converts parser events to YAML data structures *)
-
-
(** Stack frame for building nested structures *)
-
type frame =
-
| Sequence_frame of {
-
anchor : string option;
-
tag : string option;
-
implicit : bool;
-
style : Layout_style.t;
-
items : Yaml.t list;
-
}
-
| Mapping_frame of {
-
anchor : string option;
-
tag : string option;
-
implicit : bool;
-
style : Layout_style.t;
-
pairs : (Yaml.t * Yaml.t) list;
-
pending_key : Yaml.t option;
-
}
-
-
type state = {
-
mutable stack : frame list;
-
mutable current : Yaml.t option;
-
mutable documents : Document.t list;
-
mutable doc_version : (int * int) option;
-
mutable doc_implicit_start : bool;
-
}
-
-
let create_state () = {
-
stack = [];
-
current = None;
-
documents = [];
-
doc_version = None;
-
doc_implicit_start = true;
-
}
-
-
(** Process a single event *)
-
let rec process_event state (ev : Event.spanned) =
-
match ev.event with
-
| Event.Stream_start _ -> ()
-
-
| Event.Stream_end -> ()
-
-
| Event.Document_start { version; implicit } ->
-
state.doc_version <- version;
-
state.doc_implicit_start <- implicit
-
-
| Event.Document_end { implicit } ->
-
let doc = Document.make
-
?version:state.doc_version
-
~implicit_start:state.doc_implicit_start
-
~implicit_end:implicit
-
state.current
-
in
-
state.documents <- doc :: state.documents;
-
state.current <- None;
-
state.doc_version <- None;
-
state.doc_implicit_start <- true
-
-
| Event.Alias { anchor } ->
-
let node : Yaml.t = `Alias anchor in
-
add_node state node
-
-
| Event.Scalar { anchor; tag; value; plain_implicit; quoted_implicit; style } ->
-
let scalar = Scalar.make
-
?anchor ?tag
-
~plain_implicit ~quoted_implicit
-
~style value
-
in
-
let node : Yaml.t = `Scalar scalar in
-
add_node state node
-
-
| Event.Sequence_start { anchor; tag; implicit; style } ->
-
let frame = Sequence_frame {
-
anchor; tag; implicit; style;
-
items = [];
-
} in
-
state.stack <- frame :: state.stack
-
-
| Event.Sequence_end ->
-
(match state.stack with
-
| Sequence_frame { anchor; tag; implicit; style; items } :: rest ->
-
let seq = Sequence.make ?anchor ?tag ~implicit ~style (List.rev items) in
-
let node : Yaml.t = `A seq in
-
state.stack <- rest;
-
add_node state node
-
| _ -> Error.raise (Invalid_state "unexpected sequence end"))
-
-
| Event.Mapping_start { anchor; tag; implicit; style } ->
-
let frame = Mapping_frame {
-
anchor; tag; implicit; style;
-
pairs = [];
-
pending_key = None;
-
} in
-
state.stack <- frame :: state.stack
-
-
| Event.Mapping_end ->
-
(match state.stack with
-
| Mapping_frame { anchor; tag; implicit; style; pairs; pending_key = None } :: rest ->
-
let map = Mapping.make ?anchor ?tag ~implicit ~style (List.rev pairs) in
-
let node : Yaml.t = `O map in
-
state.stack <- rest;
-
add_node state node
-
| Mapping_frame { pending_key = Some _; _ } :: _ ->
-
Error.raise (Invalid_state "mapping ended with pending key")
-
| _ -> Error.raise (Invalid_state "unexpected mapping end"))
-
-
(** Add a node to current context *)
-
and add_node state node =
-
match state.stack with
-
| [] ->
-
state.current <- Some node
-
-
| Sequence_frame f :: rest ->
-
state.stack <- Sequence_frame { f with items = node :: f.items } :: rest
-
-
| Mapping_frame f :: rest ->
-
(match f.pending_key with
-
| None ->
-
(* This is a key *)
-
state.stack <- Mapping_frame { f with pending_key = Some node } :: rest
-
| Some key ->
-
(* This is a value *)
-
state.stack <- Mapping_frame {
-
f with
-
pairs = (key, node) :: f.pairs;
-
pending_key = None;
-
} :: rest)
-
-
(** Load single document as Value.
-
-
@param resolve_aliases Whether to resolve aliases (default true)
-
@param max_nodes Maximum nodes during alias expansion (default 10M)
-
@param max_depth Maximum alias nesting depth (default 100)
-
*)
-
let value_of_string
-
?(resolve_aliases = true)
-
?(max_nodes = Yaml.default_max_alias_nodes)
-
?(max_depth = Yaml.default_max_alias_depth)
-
s =
-
let parser = Parser.of_string s in
-
let state = create_state () in
-
Parser.iter (process_event state) parser;
-
match state.documents with
-
| [] -> `Null
-
| [doc] ->
-
(match Document.root doc with
-
| None -> `Null
-
| Some yaml ->
-
Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml)
-
| _ -> Error.raise Multiple_documents
-
-
(** Load single document as Yaml.
-
-
@param resolve_aliases Whether to resolve aliases (default false for Yaml.t)
-
@param max_nodes Maximum nodes during alias expansion (default 10M)
-
@param max_depth Maximum alias nesting depth (default 100)
-
*)
-
let yaml_of_string
-
?(resolve_aliases = false)
-
?(max_nodes = Yaml.default_max_alias_nodes)
-
?(max_depth = Yaml.default_max_alias_depth)
-
s =
-
let parser = Parser.of_string s in
-
let state = create_state () in
-
Parser.iter (process_event state) parser;
-
match state.documents with
-
| [] -> `Scalar (Scalar.make "")
-
| [doc] ->
-
(match Document.root doc with
-
| None -> `Scalar (Scalar.make "")
-
| Some yaml ->
-
if resolve_aliases then
-
Yaml.resolve_aliases ~max_nodes ~max_depth yaml
-
else
-
yaml)
-
| _ -> Error.raise Multiple_documents
-
-
(** Load all documents *)
-
let documents_of_string s =
-
let parser = Parser.of_string s in
-
let state = create_state () in
-
Parser.iter (process_event state) parser;
-
List.rev state.documents
-
-
(** Load single Value from parser.
-
-
@param resolve_aliases Whether to resolve aliases (default true)
-
@param max_nodes Maximum nodes during alias expansion (default 10M)
-
@param max_depth Maximum alias nesting depth (default 100)
-
*)
-
let load_value
-
?(resolve_aliases = true)
-
?(max_nodes = Yaml.default_max_alias_nodes)
-
?(max_depth = Yaml.default_max_alias_depth)
-
parser =
-
let state = create_state () in
-
let rec loop () =
-
match Parser.next parser with
-
| None -> None
-
| Some ev ->
-
process_event state ev;
-
match ev.event with
-
| Event.Document_end _ ->
-
(match state.documents with
-
| doc :: _ ->
-
state.documents <- [];
-
Some (match Document.root doc with
-
| None -> `Null
-
| Some yaml ->
-
Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml)
-
| [] -> None)
-
| Event.Stream_end -> None
-
| _ -> loop ()
-
in
-
loop ()
-
-
(** Load single Yaml from parser *)
-
let load_yaml parser =
-
let state = create_state () in
-
let rec loop () =
-
match Parser.next parser with
-
| None -> None
-
| Some ev ->
-
process_event state ev;
-
match ev.event with
-
| Event.Document_end _ ->
-
(match state.documents with
-
| doc :: _ ->
-
state.documents <- [];
-
Some (match Document.root doc with
-
| None -> `Scalar (Scalar.make "")
-
| Some yaml -> yaml)
-
| [] -> None)
-
| Event.Stream_end -> None
-
| _ -> loop ()
-
in
-
loop ()
-
-
(** Load single Document from parser *)
-
let load_document parser =
-
let state = create_state () in
-
let rec loop () =
-
match Parser.next parser with
-
| None -> None
-
| Some ev ->
-
process_event state ev;
-
match ev.event with
-
| Event.Document_end _ ->
-
(match state.documents with
-
| doc :: _ ->
-
state.documents <- [];
-
Some doc
-
| [] -> None)
-
| Event.Stream_end -> None
-
| _ -> loop ()
-
in
-
loop ()
-
-
(** Iterate over documents *)
-
let iter_documents f parser =
-
let rec loop () =
-
match load_document parser with
-
| None -> ()
-
| Some doc -> f doc; loop ()
-
in
-
loop ()
-
-
(** Fold over documents *)
-
let fold_documents f init parser =
-
let rec loop acc =
-
match load_document parser with
-
| None -> acc
-
| Some doc -> loop (f acc doc)
-
in
-
loop init
-92
yaml/ocaml-yamle/lib/mapping.ml
···
-
(** YAML mapping (object) values with metadata *)
-
-
type ('k, 'v) t = {
-
anchor : string option;
-
tag : string option;
-
implicit : bool;
-
style : Layout_style.t;
-
members : ('k * 'v) list;
-
}
-
-
let make
-
?(anchor : string option)
-
?(tag : string option)
-
?(implicit = true)
-
?(style = Layout_style.Any)
-
members =
-
{ anchor; tag; implicit; style; members }
-
-
let members t = t.members
-
let anchor t = t.anchor
-
let tag t = t.tag
-
let implicit t = t.implicit
-
let style t = t.style
-
-
let with_anchor anchor t = { t with anchor = Some anchor }
-
let with_tag tag t = { t with tag = Some tag }
-
let with_style style t = { t with style }
-
-
let map_keys f t = { t with members = List.map (fun (k, v) -> (f k, v)) t.members }
-
let map_values f t = { t with members = List.map (fun (k, v) -> (k, f v)) t.members }
-
let map f t = { t with members = List.map (fun (k, v) -> f k v) t.members }
-
-
let length t = List.length t.members
-
-
let is_empty t = t.members = []
-
-
let find pred t =
-
match List.find_opt (fun (k, _) -> pred k) t.members with
-
| Some (_, v) -> Some v
-
| None -> None
-
-
let find_key pred t =
-
List.find_opt (fun (k, _) -> pred k) t.members
-
-
let mem pred t =
-
List.exists (fun (k, _) -> pred k) t.members
-
-
let keys t = List.map fst t.members
-
-
let values t = List.map snd t.members
-
-
let iter f t = List.iter (fun (k, v) -> f k v) t.members
-
-
let fold f init t = List.fold_left (fun acc (k, v) -> f acc k v) init t.members
-
-
let pp pp_key pp_val fmt t =
-
Format.fprintf fmt "@[<hv 2>mapping(@,";
-
(match t.anchor with
-
| Some a -> Format.fprintf fmt "anchor=%s,@ " a
-
| None -> ());
-
(match t.tag with
-
| Some tag -> Format.fprintf fmt "tag=%s,@ " tag
-
| None -> ());
-
Format.fprintf fmt "style=%a,@ " Layout_style.pp t.style;
-
Format.fprintf fmt "members={@,";
-
List.iteri (fun i (k, v) ->
-
if i > 0 then Format.fprintf fmt ",@ ";
-
Format.fprintf fmt "@[<hv 2>%a:@ %a@]" pp_key k pp_val v
-
) t.members;
-
Format.fprintf fmt "@]@,})"
-
-
let equal eq_k eq_v a b =
-
Option.equal String.equal a.anchor b.anchor &&
-
Option.equal String.equal a.tag b.tag &&
-
a.implicit = b.implicit &&
-
Layout_style.equal a.style b.style &&
-
List.equal (fun (k1, v1) (k2, v2) -> eq_k k1 k2 && eq_v v1 v2) a.members b.members
-
-
let compare cmp_k cmp_v a b =
-
let c = Option.compare String.compare a.anchor b.anchor in
-
if c <> 0 then c else
-
let c = Option.compare String.compare a.tag b.tag in
-
if c <> 0 then c else
-
let c = Bool.compare a.implicit b.implicit in
-
if c <> 0 then c else
-
let c = Layout_style.compare a.style b.style in
-
if c <> 0 then c else
-
let cmp_pair (k1, v1) (k2, v2) =
-
let c = cmp_k k1 k2 in
-
if c <> 0 then c else cmp_v v1 v2
-
in
-
List.compare cmp_pair a.members b.members
-781
yaml/ocaml-yamle/lib/parser.ml
···
-
(** YAML parser - converts tokens to semantic events via state machine *)
-
-
(** Parser states *)
-
type state =
-
| Stream_start
-
| Implicit_document_start
-
| Document_start
-
| Document_content
-
| Document_content_done (* After parsing a node, check for unexpected content *)
-
| Document_end
-
| Block_node
-
| Block_node_or_indentless_sequence
-
| Flow_node
-
| Block_sequence_first_entry
-
| Block_sequence_entry
-
| Indentless_sequence_entry
-
| Block_mapping_first_key
-
| Block_mapping_key
-
| Block_mapping_value
-
| Flow_sequence_first_entry
-
| Flow_sequence_entry
-
| Flow_sequence_entry_mapping_key
-
| Flow_sequence_entry_mapping_value
-
| Flow_sequence_entry_mapping_end
-
| Flow_mapping_first_key
-
| Flow_mapping_key
-
| Flow_mapping_value
-
| Flow_mapping_empty_value
-
| End
-
-
type t = {
-
scanner : Scanner.t;
-
mutable state : state;
-
mutable states : state list; (** State stack *)
-
mutable marks : Span.t list; (** Mark stack for span tracking *)
-
mutable version : (int * int) option;
-
mutable tag_directives : (string * string) list;
-
mutable current_token : Token.spanned option;
-
mutable finished : bool;
-
mutable explicit_doc_end : bool; (** True if last doc ended with explicit ... *)
-
mutable stream_start : bool; (** True if we haven't emitted any documents yet *)
-
}
-
-
let create scanner = {
-
scanner;
-
state = Stream_start;
-
states = [];
-
marks = [];
-
version = None;
-
tag_directives = [
-
("!", "!");
-
("!!", "tag:yaml.org,2002:");
-
];
-
current_token = None;
-
finished = false;
-
explicit_doc_end = false;
-
stream_start = true;
-
}
-
-
let of_string s = create (Scanner.of_string s)
-
-
(** Get current token, fetching if needed *)
-
let current_token t =
-
match t.current_token with
-
| Some tok -> tok
-
| None ->
-
let tok = Scanner.next t.scanner in
-
t.current_token <- tok;
-
match tok with
-
| Some tok -> tok
-
| None -> Error.raise Unexpected_eof
-
-
(** Peek at current token *)
-
let peek_token t =
-
match t.current_token with
-
| Some _ -> t.current_token
-
| None ->
-
t.current_token <- Scanner.next t.scanner;
-
t.current_token
-
-
(** Skip current token *)
-
let skip_token t =
-
t.current_token <- None
-
-
(** Check if current token matches *)
-
let check t pred =
-
match peek_token t with
-
| Some tok -> pred tok.token
-
| None -> false
-
-
(** Check for specific token *)
-
let check_token t token_match =
-
check t token_match
-
-
(** Push state onto stack *)
-
let push_state t s =
-
t.states <- s :: t.states
-
-
(** Pop state from stack *)
-
let pop_state t =
-
match t.states with
-
| s :: rest ->
-
t.states <- rest;
-
s
-
| [] -> End
-
-
(** Resolve a tag *)
-
let resolve_tag t ~handle ~suffix =
-
if handle = "" then
-
(* Verbatim tag - suffix is already the full URI *)
-
suffix
-
else
-
match List.assoc_opt handle t.tag_directives with
-
| Some prefix -> prefix ^ suffix
-
| None when handle = "!" -> "!" ^ suffix
-
| None -> Error.raise (Invalid_tag (handle ^ suffix))
-
-
(** Process directives at document start *)
-
let process_directives t =
-
t.version <- None;
-
t.tag_directives <- [("!", "!"); ("!!", "tag:yaml.org,2002:")];
-
-
while check t (function
-
| Token.Version_directive _ | Token.Tag_directive _ -> true
-
| _ -> false)
-
do
-
let tok = current_token t in
-
skip_token t;
-
match tok.token with
-
| Token.Version_directive { major; minor } ->
-
if t.version <> None then
-
Error.raise_span tok.span (Invalid_yaml_version "duplicate YAML directive");
-
t.version <- Some (major, minor)
-
| Token.Tag_directive { handle; prefix } ->
-
(* Skip empty tag directives (these are reserved/unknown directives that were ignored) *)
-
if handle = "" && prefix = "" then
-
() (* Ignore reserved directives *)
-
else begin
-
if List.mem_assoc handle t.tag_directives &&
-
handle <> "!" && handle <> "!!" then
-
Error.raise_span tok.span (Invalid_tag_directive ("duplicate tag handle: " ^ handle));
-
t.tag_directives <- (handle, prefix) :: t.tag_directives
-
end
-
| _ -> ()
-
done
-
-
(** Parse anchor and/or tag properties *)
-
let parse_properties t =
-
let anchor = ref None in
-
let tag = ref None in
-
-
while check t (function
-
| Token.Anchor _ | Token.Tag _ -> true
-
| _ -> false)
-
do
-
let tok = current_token t in
-
skip_token t;
-
match tok.token with
-
| Token.Anchor name ->
-
if !anchor <> None then
-
Error.raise_span tok.span (Duplicate_anchor name);
-
anchor := Some name
-
| Token.Tag { handle; suffix } ->
-
if !tag <> None then
-
Error.raise_span tok.span (Invalid_tag "duplicate tag");
-
let resolved =
-
if handle = "" && suffix = "" then None
-
else if handle = "!" && suffix = "" then Some "!"
-
else Some (resolve_tag t ~handle ~suffix)
-
in
-
tag := resolved
-
| _ -> ()
-
done;
-
(!anchor, !tag)
-
-
(** Empty scalar event *)
-
let empty_scalar_event ~anchor ~tag span =
-
Event.Scalar {
-
anchor;
-
tag;
-
value = "";
-
plain_implicit = tag = None;
-
quoted_implicit = false;
-
style = Scalar_style.Plain;
-
}, span
-
-
(** Parse stream start *)
-
let parse_stream_start t =
-
let tok = current_token t in
-
skip_token t;
-
match tok.token with
-
| Token.Stream_start encoding ->
-
t.state <- Implicit_document_start;
-
Event.Stream_start { encoding }, tok.span
-
| _ ->
-
Error.raise_span tok.span (Unexpected_token "expected stream start")
-
-
(** Parse document start (implicit or explicit) *)
-
let parse_document_start t ~implicit =
-
process_directives t;
-
-
if not implicit then begin
-
let tok = current_token t in
-
match tok.token with
-
| Token.Document_start ->
-
skip_token t
-
| _ ->
-
Error.raise_span tok.span Expected_document_start
-
end;
-
-
let span = match peek_token t with
-
| Some tok -> tok.span
-
| None -> Span.point Position.initial
-
in
-
-
(* After first document, stream_start is false *)
-
t.stream_start <- false;
-
push_state t Document_end;
-
t.state <- Document_content;
-
Event.Document_start { version = t.version; implicit }, span
-
-
(** Parse document end *)
-
let parse_document_end t =
-
let implicit = not (check t (function Token.Document_end -> true | _ -> false)) in
-
let span = match peek_token t with
-
| Some tok -> tok.span
-
| None -> Span.point Position.initial
-
in
-
-
if not implicit then skip_token t;
-
-
(* Track if this document ended explicitly with ... *)
-
t.explicit_doc_end <- not implicit;
-
t.state <- Implicit_document_start;
-
Event.Document_end { implicit }, span
-
-
(** Parse node in various contexts *)
-
let parse_node t ~block ~indentless =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Alias name ->
-
skip_token t;
-
t.state <- pop_state t;
-
Event.Alias { anchor = name }, tok.span
-
-
| Token.Anchor _ | Token.Tag _ ->
-
let anchor, tag = parse_properties t in
-
let tok = current_token t in
-
(match tok.token with
-
| Token.Block_entry when indentless ->
-
t.state <- Indentless_sequence_entry;
-
Event.Sequence_start {
-
anchor; tag;
-
implicit = tag = None;
-
style = Layout_style.Block;
-
}, tok.span
-
-
| Token.Block_sequence_start when block ->
-
t.state <- Block_sequence_first_entry;
-
skip_token t;
-
Event.Sequence_start {
-
anchor; tag;
-
implicit = tag = None;
-
style = Layout_style.Block;
-
}, tok.span
-
-
| Token.Block_mapping_start when block ->
-
t.state <- Block_mapping_first_key;
-
skip_token t;
-
Event.Mapping_start {
-
anchor; tag;
-
implicit = tag = None;
-
style = Layout_style.Block;
-
}, tok.span
-
-
| Token.Flow_sequence_start ->
-
t.state <- Flow_sequence_first_entry;
-
skip_token t;
-
Event.Sequence_start {
-
anchor; tag;
-
implicit = tag = None;
-
style = Layout_style.Flow;
-
}, tok.span
-
-
| Token.Flow_mapping_start ->
-
t.state <- Flow_mapping_first_key;
-
skip_token t;
-
Event.Mapping_start {
-
anchor; tag;
-
implicit = tag = None;
-
style = Layout_style.Flow;
-
}, tok.span
-
-
| Token.Scalar { style; value } ->
-
skip_token t;
-
t.state <- pop_state t;
-
let plain_implicit = tag = None && style = Scalar_style.Plain in
-
let quoted_implicit = tag = None && style <> Scalar_style.Plain in
-
Event.Scalar {
-
anchor; tag; value;
-
plain_implicit; quoted_implicit; style;
-
}, tok.span
-
-
| _ ->
-
(* Empty node *)
-
t.state <- pop_state t;
-
empty_scalar_event ~anchor ~tag tok.span)
-
-
| Token.Block_sequence_start when block ->
-
t.state <- Block_sequence_first_entry;
-
skip_token t;
-
Event.Sequence_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style = Layout_style.Block;
-
}, tok.span
-
-
| Token.Block_mapping_start when block ->
-
t.state <- Block_mapping_first_key;
-
skip_token t;
-
Event.Mapping_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style = Layout_style.Block;
-
}, tok.span
-
-
| Token.Flow_sequence_start ->
-
t.state <- Flow_sequence_first_entry;
-
skip_token t;
-
Event.Sequence_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style = Layout_style.Flow;
-
}, tok.span
-
-
| Token.Flow_mapping_start ->
-
t.state <- Flow_mapping_first_key;
-
skip_token t;
-
Event.Mapping_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style = Layout_style.Flow;
-
}, tok.span
-
-
| Token.Block_entry when indentless ->
-
t.state <- Indentless_sequence_entry;
-
Event.Sequence_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style = Layout_style.Block;
-
}, tok.span
-
-
| Token.Scalar { style; value } ->
-
skip_token t;
-
t.state <- pop_state t;
-
let plain_implicit = style = Scalar_style.Plain in
-
let quoted_implicit = style <> Scalar_style.Plain in
-
Event.Scalar {
-
anchor = None; tag = None; value;
-
plain_implicit; quoted_implicit; style;
-
}, tok.span
-
-
| _ ->
-
(* Empty node *)
-
t.state <- pop_state t;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
-
(** Parse block sequence entry *)
-
let parse_block_sequence_entry t =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Block_entry ->
-
skip_token t;
-
if check t (function
-
| Token.Block_entry | Token.Block_end -> true
-
| _ -> false)
-
then begin
-
t.state <- Block_sequence_entry;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
push_state t Block_sequence_entry;
-
parse_node t ~block:true ~indentless:false
-
end
-
| Token.Block_end ->
-
skip_token t;
-
t.state <- pop_state t;
-
Event.Sequence_end, tok.span
-
| _ ->
-
Error.raise_span tok.span Expected_block_entry
-
-
(** Parse block mapping key *)
-
let parse_block_mapping_key t =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Key ->
-
skip_token t;
-
if check t (function
-
| Token.Key | Token.Value | Token.Block_end -> true
-
| _ -> false)
-
then begin
-
t.state <- Block_mapping_value;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
push_state t Block_mapping_value;
-
parse_node t ~block:true ~indentless:true
-
end
-
(* Handle value without explicit key - key is empty/null *)
-
| Token.Value ->
-
t.state <- Block_mapping_value;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
| Token.Block_end ->
-
skip_token t;
-
t.state <- pop_state t;
-
Event.Mapping_end, tok.span
-
| _ ->
-
Error.raise_span tok.span Expected_key
-
-
(** Parse block mapping value *)
-
let parse_block_mapping_value t =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Value ->
-
skip_token t;
-
if check t (function
-
| Token.Key | Token.Value | Token.Block_end -> true
-
| _ -> false)
-
then begin
-
t.state <- Block_mapping_key;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
push_state t Block_mapping_key;
-
parse_node t ~block:true ~indentless:true
-
end
-
| _ ->
-
(* Implicit empty value *)
-
t.state <- Block_mapping_key;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
-
(** Parse indentless sequence entry *)
-
let parse_indentless_sequence_entry t =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Block_entry ->
-
skip_token t;
-
if check t (function
-
| Token.Block_entry | Token.Key | Token.Value | Token.Block_end -> true
-
| _ -> false)
-
then begin
-
t.state <- Indentless_sequence_entry;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
push_state t Indentless_sequence_entry;
-
parse_node t ~block:true ~indentless:false
-
end
-
| _ ->
-
t.state <- pop_state t;
-
Event.Sequence_end, tok.span
-
-
(** Parse flow sequence *)
-
let rec parse_flow_sequence_entry t ~first =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Flow_sequence_end ->
-
skip_token t;
-
t.state <- pop_state t;
-
Event.Sequence_end, tok.span
-
| Token.Flow_entry when not first ->
-
skip_token t;
-
parse_flow_sequence_entry_internal t
-
| _ when first ->
-
parse_flow_sequence_entry_internal t
-
| _ ->
-
Error.raise_span tok.span Expected_sequence_end
-
-
and parse_flow_sequence_entry_internal t =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Flow_sequence_end ->
-
(* Trailing comma case - don't emit empty scalar, just go back to sequence entry state *)
-
skip_token t;
-
t.state <- pop_state t;
-
Event.Sequence_end, tok.span
-
| Token.Flow_entry ->
-
(* Double comma or comma after comma - invalid *)
-
Error.raise_span tok.span (Unexpected_token "unexpected ',' in flow sequence")
-
| Token.Key ->
-
skip_token t;
-
t.state <- Flow_sequence_entry_mapping_key;
-
Event.Mapping_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style = Layout_style.Flow;
-
}, tok.span
-
| Token.Value ->
-
(* Implicit empty key mapping: [ : value ] *)
-
t.state <- Flow_sequence_entry_mapping_key;
-
Event.Mapping_start {
-
anchor = None; tag = None;
-
implicit = true;
-
style = Layout_style.Flow;
-
}, tok.span
-
| _ ->
-
push_state t Flow_sequence_entry;
-
parse_node t ~block:false ~indentless:false
-
-
(** Parse flow sequence entry mapping *)
-
let parse_flow_sequence_entry_mapping_key t =
-
let tok = current_token t in
-
if check t (function
-
| Token.Value | Token.Flow_entry | Token.Flow_sequence_end -> true
-
| _ -> false)
-
then begin
-
t.state <- Flow_sequence_entry_mapping_value;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
push_state t Flow_sequence_entry_mapping_value;
-
parse_node t ~block:false ~indentless:false
-
end
-
-
let parse_flow_sequence_entry_mapping_value t =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Value ->
-
skip_token t;
-
if check t (function
-
| Token.Flow_entry | Token.Flow_sequence_end -> true
-
| _ -> false)
-
then begin
-
t.state <- Flow_sequence_entry_mapping_end;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
push_state t Flow_sequence_entry_mapping_end;
-
parse_node t ~block:false ~indentless:false
-
end
-
| _ ->
-
t.state <- Flow_sequence_entry_mapping_end;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
-
let parse_flow_sequence_entry_mapping_end t =
-
let tok = current_token t in
-
t.state <- Flow_sequence_entry;
-
Event.Mapping_end, tok.span
-
-
(** Parse flow mapping *)
-
let rec parse_flow_mapping_key t ~first =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Flow_mapping_end ->
-
skip_token t;
-
t.state <- pop_state t;
-
Event.Mapping_end, tok.span
-
| Token.Flow_entry when not first ->
-
skip_token t;
-
parse_flow_mapping_key_internal t
-
| _ when first ->
-
parse_flow_mapping_key_internal t
-
| _ ->
-
Error.raise_span tok.span Expected_mapping_end
-
-
and parse_flow_mapping_key_internal t =
-
let tok = current_token t in
-
match tok.token with
-
| Token.Flow_mapping_end ->
-
(* Trailing comma case - don't emit empty scalar, just return to key state *)
-
skip_token t;
-
t.state <- pop_state t;
-
Event.Mapping_end, tok.span
-
| Token.Flow_entry ->
-
(* Double comma or comma after comma - invalid *)
-
Error.raise_span tok.span (Unexpected_token "unexpected ',' in flow mapping")
-
| Token.Key ->
-
skip_token t;
-
if check t (function
-
| Token.Value | Token.Flow_entry | Token.Flow_mapping_end -> true
-
| _ -> false)
-
then begin
-
t.state <- Flow_mapping_value;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
push_state t Flow_mapping_value;
-
parse_node t ~block:false ~indentless:false
-
end
-
| _ ->
-
push_state t Flow_mapping_value;
-
parse_node t ~block:false ~indentless:false
-
-
let parse_flow_mapping_value t ~empty =
-
let tok = current_token t in
-
if empty then begin
-
t.state <- Flow_mapping_key;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else
-
match tok.token with
-
| Token.Value ->
-
skip_token t;
-
if check t (function
-
| Token.Flow_entry | Token.Flow_mapping_end -> true
-
| _ -> false)
-
then begin
-
t.state <- Flow_mapping_key;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
push_state t Flow_mapping_key;
-
parse_node t ~block:false ~indentless:false
-
end
-
| _ ->
-
t.state <- Flow_mapping_key;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
-
(** Main state machine dispatcher *)
-
let rec parse t =
-
match t.state with
-
| Stream_start ->
-
parse_stream_start t
-
-
| Implicit_document_start ->
-
(* Skip any document end markers before checking what's next *)
-
while check t (function Token.Document_end -> true | _ -> false) do
-
t.explicit_doc_end <- true; (* Seeing ... counts as explicit end *)
-
skip_token t
-
done;
-
-
let tok = current_token t in
-
(match tok.token with
-
| Token.Stream_end ->
-
skip_token t;
-
t.state <- End;
-
t.finished <- true;
-
Event.Stream_end, tok.span
-
| Token.Version_directive _ | Token.Tag_directive _ ->
-
(* Directives are only allowed at stream start or after explicit ... (MUS6/01) *)
-
if not t.stream_start && not t.explicit_doc_end then
-
Error.raise_span tok.span (Invalid_directive "directives require explicit document end '...' before them");
-
parse_document_start t ~implicit:false
-
| Token.Document_start ->
-
parse_document_start t ~implicit:false
-
(* These tokens are invalid at document start - they indicate leftover junk *)
-
| Token.Flow_sequence_end | Token.Flow_mapping_end | Token.Flow_entry
-
| Token.Block_end | Token.Value ->
-
Error.raise_span tok.span (Unexpected_token "unexpected token at document start")
-
| _ ->
-
parse_document_start t ~implicit:true)
-
-
| Document_start ->
-
parse_document_start t ~implicit:false
-
-
| Document_content ->
-
if check t (function
-
| Token.Version_directive _ | Token.Tag_directive _
-
| Token.Document_start | Token.Document_end | Token.Stream_end -> true
-
| _ -> false)
-
then begin
-
let tok = current_token t in
-
t.state <- pop_state t;
-
empty_scalar_event ~anchor:None ~tag:None tok.span
-
end else begin
-
(* Push Document_content_done so we return there after parsing the node.
-
This allows us to check for unexpected content after the node. *)
-
push_state t Document_content_done;
-
parse_node t ~block:true ~indentless:false
-
end
-
-
| Document_content_done ->
-
(* After parsing a node in document content, check for unexpected content *)
-
if check t (function
-
| Token.Version_directive _ | Token.Tag_directive _
-
| Token.Document_start | Token.Document_end | Token.Stream_end -> true
-
| _ -> false)
-
then begin
-
(* Valid document boundary - continue to Document_end *)
-
t.state <- pop_state t;
-
parse t (* Continue to emit the next event *)
-
end else begin
-
(* Unexpected content after document value - this is an error (KS4U, BS4K) *)
-
let tok = current_token t in
-
Error.raise_span tok.span
-
(Unexpected_token "content not allowed after document value")
-
end
-
-
| Document_end ->
-
parse_document_end t
-
-
| Block_node ->
-
parse_node t ~block:true ~indentless:false
-
-
| Block_node_or_indentless_sequence ->
-
parse_node t ~block:true ~indentless:true
-
-
| Flow_node ->
-
parse_node t ~block:false ~indentless:false
-
-
| Block_sequence_first_entry ->
-
t.state <- Block_sequence_entry;
-
parse_block_sequence_entry t
-
-
| Block_sequence_entry ->
-
parse_block_sequence_entry t
-
-
| Indentless_sequence_entry ->
-
parse_indentless_sequence_entry t
-
-
| Block_mapping_first_key ->
-
t.state <- Block_mapping_key;
-
parse_block_mapping_key t
-
-
| Block_mapping_key ->
-
parse_block_mapping_key t
-
-
| Block_mapping_value ->
-
parse_block_mapping_value t
-
-
| Flow_sequence_first_entry ->
-
parse_flow_sequence_entry t ~first:true
-
-
| Flow_sequence_entry ->
-
parse_flow_sequence_entry t ~first:false
-
-
| Flow_sequence_entry_mapping_key ->
-
parse_flow_sequence_entry_mapping_key t
-
-
| Flow_sequence_entry_mapping_value ->
-
parse_flow_sequence_entry_mapping_value t
-
-
| Flow_sequence_entry_mapping_end ->
-
parse_flow_sequence_entry_mapping_end t
-
-
| Flow_mapping_first_key ->
-
parse_flow_mapping_key t ~first:true
-
-
| Flow_mapping_key ->
-
parse_flow_mapping_key t ~first:false
-
-
| Flow_mapping_value ->
-
parse_flow_mapping_value t ~empty:false
-
-
| Flow_mapping_empty_value ->
-
parse_flow_mapping_value t ~empty:true
-
-
| End ->
-
let span = Span.point Position.initial in
-
t.finished <- true;
-
Event.Stream_end, span
-
-
(** Get next event *)
-
let next t =
-
if t.finished then None
-
else begin
-
let event, span = parse t in
-
Some { Event.event; span }
-
end
-
-
(** Peek at next event *)
-
let peek t =
-
(* Parser is not easily peekable without full state save/restore *)
-
(* For now, we don't support peek - could add caching if needed *)
-
if t.finished then None
-
else
-
(* Just call next and the caller will have to deal with it *)
-
next t
-
-
(** Iterate over all events *)
-
let iter f t =
-
let rec loop () =
-
match next t with
-
| None -> ()
-
| Some ev -> f ev; loop ()
-
in
-
loop ()
-
-
(** Fold over all events *)
-
let fold f init t =
-
let rec loop acc =
-
match next t with
-
| None -> acc
-
| Some ev -> loop (f acc ev)
-
in
-
loop init
-
-
(** Convert to list *)
-
let to_list t =
-
fold (fun acc ev -> ev :: acc) [] t |> List.rev
-42
yaml/ocaml-yamle/lib/position.ml
···
-
(** Position tracking for source locations *)
-
-
type t = {
-
index : int; (** Byte offset from start *)
-
line : int; (** 1-indexed line number *)
-
column : int; (** 1-indexed column number *)
-
}
-
-
let initial = { index = 0; line = 1; column = 1 }
-
-
let advance_byte t =
-
{ t with index = t.index + 1; column = t.column + 1 }
-
-
let advance_line t =
-
{ index = t.index + 1; line = t.line + 1; column = 1 }
-
-
let advance_char c t =
-
if c = '\n' then advance_line t
-
else advance_byte t
-
-
let advance_utf8 uchar t =
-
let len = Uchar.utf_8_byte_length uchar in
-
let code = Uchar.to_int uchar in
-
if code = 0x0A (* LF *) then
-
{ index = t.index + len; line = t.line + 1; column = 1 }
-
else
-
{ t with index = t.index + len; column = t.column + 1 }
-
-
let advance_bytes n t =
-
{ t with index = t.index + n; column = t.column + n }
-
-
let pp fmt t =
-
Format.fprintf fmt "line %d, column %d" t.line t.column
-
-
let to_string t =
-
Format.asprintf "%a" pp t
-
-
let compare a b =
-
Int.compare a.index b.index
-
-
let equal a b =
-
a.index = b.index
-61
yaml/ocaml-yamle/lib/scalar.ml
···
-
(** YAML scalar values with metadata *)
-
-
type t = {
-
anchor : string option;
-
tag : string option;
-
value : string;
-
plain_implicit : bool;
-
quoted_implicit : bool;
-
style : Scalar_style.t;
-
}
-
-
let make
-
?(anchor : string option)
-
?(tag : string option)
-
?(plain_implicit = true)
-
?(quoted_implicit = false)
-
?(style = Scalar_style.Plain)
-
value =
-
{ anchor; tag; value; plain_implicit; quoted_implicit; style }
-
-
let value t = t.value
-
let anchor t = t.anchor
-
let tag t = t.tag
-
let style t = t.style
-
let plain_implicit t = t.plain_implicit
-
let quoted_implicit t = t.quoted_implicit
-
-
let with_anchor anchor t = { t with anchor = Some anchor }
-
let with_tag tag t = { t with tag = Some tag }
-
let with_style style t = { t with style }
-
-
let pp fmt t =
-
Format.fprintf fmt "scalar(%S" t.value;
-
(match t.anchor with
-
| Some a -> Format.fprintf fmt ", anchor=%s" a
-
| None -> ());
-
(match t.tag with
-
| Some tag -> Format.fprintf fmt ", tag=%s" tag
-
| None -> ());
-
Format.fprintf fmt ", style=%a)" Scalar_style.pp t.style
-
-
let equal a b =
-
Option.equal String.equal a.anchor b.anchor &&
-
Option.equal String.equal a.tag b.tag &&
-
String.equal a.value b.value &&
-
a.plain_implicit = b.plain_implicit &&
-
a.quoted_implicit = b.quoted_implicit &&
-
Scalar_style.equal a.style b.style
-
-
let compare a b =
-
let c = Option.compare String.compare a.anchor b.anchor in
-
if c <> 0 then c else
-
let c = Option.compare String.compare a.tag b.tag in
-
if c <> 0 then c else
-
let c = String.compare a.value b.value in
-
if c <> 0 then c else
-
let c = Bool.compare a.plain_implicit b.plain_implicit in
-
if c <> 0 then c else
-
let c = Bool.compare a.quoted_implicit b.quoted_implicit in
-
if c <> 0 then c else
-
Scalar_style.compare a.style b.style
-33
yaml/ocaml-yamle/lib/scalar_style.ml
···
-
(** Scalar formatting styles *)
-
-
type t =
-
| Any (** Let emitter choose *)
-
| Plain (** Unquoted: foo *)
-
| Single_quoted (** 'foo' *)
-
| Double_quoted (** "foo" *)
-
| Literal (** | block *)
-
| Folded (** > block *)
-
-
let to_string = function
-
| Any -> "any"
-
| Plain -> "plain"
-
| Single_quoted -> "single-quoted"
-
| Double_quoted -> "double-quoted"
-
| Literal -> "literal"
-
| Folded -> "folded"
-
-
let pp fmt t =
-
Format.pp_print_string fmt (to_string t)
-
-
let equal a b = a = b
-
-
let compare a b =
-
let to_int = function
-
| Any -> 0
-
| Plain -> 1
-
| Single_quoted -> 2
-
| Double_quoted -> 3
-
| Literal -> 4
-
| Folded -> 5
-
in
-
Int.compare (to_int a) (to_int b)
-1568
yaml/ocaml-yamle/lib/scanner.ml
···
-
(** YAML tokenizer/scanner with lookahead for ambiguity resolution *)
-
-
(** Simple key tracking for mapping key disambiguation *)
-
type simple_key = {
-
sk_possible : bool;
-
sk_required : bool;
-
sk_token_number : int;
-
sk_position : Position.t;
-
}
-
-
(** Indent level tracking *)
-
type indent = {
-
indent : int;
-
needs_block_end : bool;
-
sequence : bool; (** true if this is a sequence indent *)
-
}
-
-
type t = {
-
input : Input.t;
-
mutable tokens : Token.spanned Queue.t;
-
mutable token_number : int;
-
mutable tokens_taken : int;
-
mutable stream_started : bool;
-
mutable stream_ended : bool;
-
mutable indent_stack : indent list; (** Stack of indentation levels *)
-
mutable flow_level : int; (** Nesting depth in [] or {} *)
-
mutable flow_indent : int; (** Column where outermost flow collection started *)
-
mutable simple_keys : simple_key option list; (** Per flow-level simple key tracking *)
-
mutable allow_simple_key : bool;
-
mutable leading_whitespace : bool; (** True when at start of line (only whitespace seen) *)
-
mutable document_has_content : bool; (** True if we've emitted content tokens in current document *)
-
mutable adjacent_value_allowed_at : Position.t option; (** Position where adjacent : is allowed *)
-
mutable pending_value : bool; (** True if we've emitted a KEY and are waiting for VALUE *)
-
mutable flow_mapping_stack : bool list; (** Stack of whether each flow level is a mapping *)
-
}
-
-
let create input =
-
{
-
input;
-
tokens = Queue.create ();
-
token_number = 0;
-
tokens_taken = 0;
-
stream_started = false;
-
stream_ended = false;
-
indent_stack = [];
-
flow_level = 0;
-
flow_indent = 0;
-
simple_keys = [None]; (* One entry for the base level *)
-
allow_simple_key = true;
-
leading_whitespace = true; (* Start at beginning of stream *)
-
document_has_content = false;
-
adjacent_value_allowed_at = None;
-
pending_value = false;
-
flow_mapping_stack = [];
-
}
-
-
let of_string s = create (Input.of_string s)
-
-
let position t = Input.position t.input
-
-
(** Add a token to the queue *)
-
let emit t span token =
-
Queue.add { Token.token; span } t.tokens;
-
t.token_number <- t.token_number + 1
-
-
(** Get current column (1-indexed) *)
-
let column t = (Input.position t.input).column
-
-
(** Get current indent level *)
-
let current_indent t =
-
match t.indent_stack with
-
| [] -> -1
-
| { indent; _ } :: _ -> indent
-
-
(** Skip whitespace to end of line, checking for valid comments.
-
Returns true if any whitespace (including tabs) was found before a comment. *)
-
let skip_whitespace_and_comment t =
-
let has_whitespace = ref false in
-
(* Skip blanks (spaces and tabs) *)
-
while Input.next_is_blank t.input do
-
has_whitespace := true;
-
ignore (Input.next t.input)
-
done;
-
(* Check for comment *)
-
if Input.next_is (( = ) '#') t.input then begin
-
(* Validate: comment must be preceded by whitespace or be at start of line *)
-
if not !has_whitespace then begin
-
(* Check if we're at the start of input or after whitespace (blank or line break) *)
-
match Input.peek_back t.input with
-
| None -> () (* Start of input - OK *)
-
| Some c when Input.is_whitespace c -> () (* After whitespace - OK *)
-
| _ ->
-
(* Comment not preceded by whitespace - ERROR *)
-
Error.raise_at (Input.mark t.input) Invalid_comment
-
end;
-
(* Skip to end of line *)
-
while not (Input.is_eof t.input) && not (Input.next_is_break t.input) do
-
ignore (Input.next t.input)
-
done
-
end
-
-
(** Skip blanks (spaces/tabs) and return (found_tabs, found_spaces) *)
-
let skip_blanks_check_tabs t =
-
let found_tab = ref false in
-
let found_space = ref false in
-
while Input.next_is_blank t.input do
-
(match Input.peek t.input with
-
| Some '\t' -> found_tab := true
-
| Some ' ' -> found_space := true
-
| _ -> ());
-
ignore (Input.next t.input)
-
done;
-
(!found_tab, !found_space)
-
-
(** Skip whitespace and comments, return true if at newline *)
-
let rec skip_to_next_token t =
-
(* Check for tabs used as indentation in block context *)
-
(match Input.peek t.input with
-
| Some '\t' when t.flow_level = 0 && t.leading_whitespace &&
-
(column t - 1) < current_indent t ->
-
(* Tab found in indentation zone - this is invalid *)
-
(* Skip to end of line to check if line has content *)
-
let start_pos = Input.mark t.input in
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done;
-
(* If we have content on this line with a tab, raise error *)
-
if not (Input.next_is_break t.input) && not (Input.is_eof t.input) then
-
Error.raise_at start_pos Tab_in_indentation
-
| _ -> ());
-
-
(* Skip blanks and validate comments *)
-
skip_whitespace_and_comment t;
-
(* Skip line break in block context *)
-
if t.flow_level = 0 && Input.next_is_break t.input then begin
-
Input.consume_break t.input;
-
t.allow_simple_key <- true;
-
t.leading_whitespace <- true;
-
skip_to_next_token t
-
end
-
else if t.flow_level > 0 && Input.next_is_whitespace t.input then begin
-
(* In flow context, skip all whitespace including line breaks *)
-
if Input.next_is_break t.input then begin
-
Input.consume_break t.input;
-
(* Allow simple keys after line breaks in flow context *)
-
t.allow_simple_key <- true;
-
(* After line break in flow, check for tabs at start of line (Y79Y/03)
-
Tabs are not allowed as indentation - if tab is first char and results
-
in a column less than flow_indent, it's an error *)
-
if Input.next_is (( = ) '\t') t.input then begin
-
(* Tab at start of line in flow context - skip tabs and check position *)
-
let start_mark = Input.mark t.input in
-
while Input.next_is (( = ) '\t') t.input do
-
ignore (Input.next t.input)
-
done;
-
(* If only tabs were used (no spaces) and column < flow_indent, error *)
-
if not (Input.next_is_break t.input) && not (Input.is_eof t.input) &&
-
column t < t.flow_indent then
-
Error.raise_at start_mark Invalid_flow_indentation
-
end;
-
skip_to_next_token t
-
end else begin
-
ignore (Input.next t.input);
-
skip_to_next_token t
-
end
-
end
-
-
(** Roll the indentation level *)
-
let roll_indent t col ~sequence =
-
if t.flow_level = 0 && col > current_indent t then begin
-
t.indent_stack <- { indent = col; needs_block_end = true; sequence } :: t.indent_stack;
-
true
-
end else
-
false
-
-
(** Unroll indentation to given column *)
-
let unroll_indent t col =
-
while t.flow_level = 0 &&
-
match t.indent_stack with
-
| { indent; needs_block_end = true; _ } :: _ when indent > col -> true
-
| _ -> false
-
do
-
match t.indent_stack with
-
| { indent = _; needs_block_end = true; _ } :: rest ->
-
let pos = Input.position t.input in
-
let span = Span.point pos in
-
emit t span Token.Block_end;
-
t.indent_stack <- rest
-
| _ -> ()
-
done
-
-
(** Save a potential simple key *)
-
let save_simple_key t =
-
if t.allow_simple_key then begin
-
(* A simple key is required only if we're in a block context,
-
at the current indentation level, AND the current indent needs a block end.
-
This matches saphyr's logic and prevents false positives for values. *)
-
let required = t.flow_level = 0 &&
-
match t.indent_stack with
-
| { indent; needs_block_end = true; _ } :: _ ->
-
indent = column t
-
| _ -> false
-
in
-
let sk = {
-
sk_possible = true;
-
sk_required = required;
-
sk_token_number = t.token_number;
-
sk_position = Input.position t.input;
-
} in
-
(* Remove any existing simple key at current level *)
-
t.simple_keys <- (
-
match t.simple_keys with
-
| _ :: rest -> Some sk :: rest
-
| [] -> [Some sk]
-
)
-
end
-
-
(** Remove simple key at current level *)
-
let remove_simple_key t =
-
match t.simple_keys with
-
| Some sk :: _rest when sk.sk_required ->
-
Error.raise_at sk.sk_position Expected_key
-
| _ :: rest -> t.simple_keys <- None :: rest
-
| [] -> ()
-
-
(** Stale simple keys that span too many tokens *)
-
let stale_simple_keys t =
-
t.simple_keys <- List.map (fun sk_opt ->
-
match sk_opt with
-
| Some sk when sk.sk_possible &&
-
(Input.position t.input).line > sk.sk_position.line &&
-
t.flow_level = 0 ->
-
if sk.sk_required then
-
Error.raise_at sk.sk_position Expected_key;
-
None
-
| _ -> sk_opt
-
) t.simple_keys
-
-
(** Read anchor or alias name *)
-
let scan_anchor_alias t =
-
let start = Input.mark t.input in
-
let buf = Buffer.create 16 in
-
(* Per YAML 1.2 spec: anchor names can contain any character that is NOT:
-
- Whitespace (space, tab, line breaks)
-
- Flow indicators: []{}
-
- Comma (,)
-
This matches the saphyr implementation: is_yaml_non_space && !is_flow *)
-
while
-
match Input.peek t.input with
-
| Some c when not (Input.is_whitespace c) &&
-
not (Input.is_flow_indicator c) &&
-
c <> '\x00' ->
-
Buffer.add_char buf c;
-
ignore (Input.next t.input);
-
true
-
| _ -> false
-
do () done;
-
let name = Buffer.contents buf in
-
if String.length name = 0 then
-
Error.raise_at start (Invalid_anchor "empty anchor name");
-
(name, Span.make ~start ~stop:(Input.mark t.input))
-
-
(** Scan tag handle *)
-
let scan_tag_handle t =
-
let start = Input.mark t.input in
-
let buf = Buffer.create 16 in
-
(* Expect ! *)
-
(match Input.peek t.input with
-
| Some '!' ->
-
Buffer.add_char buf '!';
-
ignore (Input.next t.input)
-
| _ -> Error.raise_at start (Invalid_tag "expected '!'"));
-
(* Read word chars *)
-
while
-
match Input.peek t.input with
-
| Some c when Input.is_alnum c || c = '-' ->
-
Buffer.add_char buf c;
-
ignore (Input.next t.input);
-
true
-
| _ -> false
-
do () done;
-
(* Check for secondary ! *)
-
(match Input.peek t.input with
-
| Some '!' ->
-
Buffer.add_char buf '!';
-
ignore (Input.next t.input)
-
| _ -> ());
-
Buffer.contents buf
-
-
(** Scan tag suffix (after handle) *)
-
let scan_tag_suffix t =
-
let is_hex_digit c =
-
(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')
-
in
-
let hex_val c =
-
match c with
-
| '0'..'9' -> Char.code c - Char.code '0'
-
| 'A'..'F' -> Char.code c - Char.code 'A' + 10
-
| 'a'..'f' -> Char.code c - Char.code 'a' + 10
-
| _ -> 0
-
in
-
let buf = Buffer.create 32 in
-
while
-
match Input.peek t.input with
-
| Some '%' ->
-
(* Percent-encoded character *)
-
ignore (Input.next t.input);
-
(match Input.peek t.input, Input.peek_nth t.input 1 with
-
| Some c1, Some c2 when is_hex_digit c1 && is_hex_digit c2 ->
-
ignore (Input.next t.input);
-
ignore (Input.next t.input);
-
let code = (hex_val c1) * 16 + (hex_val c2) in
-
Buffer.add_char buf (Char.chr code);
-
true
-
| _ ->
-
(* Invalid percent encoding - keep the % *)
-
Buffer.add_char buf '%';
-
true)
-
| Some c when not (Input.is_whitespace c) &&
-
not (Input.is_flow_indicator c) ->
-
Buffer.add_char buf c;
-
ignore (Input.next t.input);
-
true
-
| _ -> false
-
do () done;
-
Buffer.contents buf
-
-
(** Scan a tag *)
-
let scan_tag t =
-
let start = Input.mark t.input in
-
ignore (Input.next t.input); (* consume ! *)
-
let handle, suffix =
-
match Input.peek t.input with
-
| Some '<' ->
-
(* Verbatim tag: !<...> - handle is empty, suffix is full URI *)
-
ignore (Input.next t.input);
-
let buf = Buffer.create 32 in
-
while
-
match Input.peek t.input with
-
| Some '>' -> false
-
| Some c ->
-
Buffer.add_char buf c;
-
ignore (Input.next t.input);
-
true
-
| None -> Error.raise_at (Input.mark t.input) (Invalid_tag "unclosed verbatim tag")
-
do () done;
-
ignore (Input.next t.input); (* consume > *)
-
("", Buffer.contents buf)
-
| Some c when Input.is_whitespace c || Input.is_flow_indicator c ->
-
(* Non-specific tag: ! *)
-
("!", "")
-
| Some '!' ->
-
(* Secondary handle: !! *)
-
ignore (Input.next t.input); (* consume second ! *)
-
let suffix = scan_tag_suffix t in
-
("!!", suffix)
-
| _ ->
-
(* Primary handle or just suffix: !foo or !e!foo *)
-
(* Read alphanumeric characters *)
-
let buf = Buffer.create 16 in
-
while
-
match Input.peek t.input with
-
| Some c when Input.is_alnum c || c = '-' ->
-
Buffer.add_char buf c;
-
ignore (Input.next t.input);
-
true
-
| _ -> false
-
do () done;
-
(* Check if next character is ! - if so, this is a named handle *)
-
(match Input.peek t.input with
-
| Some '!' ->
-
(* Named handle like !e! *)
-
ignore (Input.next t.input);
-
let handle_name = Buffer.contents buf in
-
let suffix = scan_tag_suffix t in
-
("!" ^ handle_name ^ "!", suffix)
-
| _ ->
-
(* Just ! followed by suffix *)
-
("!", Buffer.contents buf ^ scan_tag_suffix t))
-
in
-
(* Validate that tag is followed by whitespace, break, or (in flow) flow indicator *)
-
(match Input.peek t.input with
-
| None -> () (* EOF is ok *)
-
| Some c when Input.is_whitespace c || Input.is_break c -> ()
-
| Some c when t.flow_level > 0 && Input.is_flow_indicator c -> ()
-
| _ -> Error.raise_at start (Invalid_tag "expected whitespace or line break after tag"));
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
(handle, suffix, span)
-
-
(** Scan single-quoted scalar *)
-
let scan_single_quoted t =
-
let start = Input.mark t.input in
-
ignore (Input.next t.input); (* consume opening single-quote *)
-
let buf = Buffer.create 64 in
-
let whitespace = Buffer.create 16 in (* Track trailing whitespace *)
-
-
let flush_whitespace () =
-
if Buffer.length whitespace > 0 then begin
-
Buffer.add_buffer buf whitespace;
-
Buffer.clear whitespace
-
end
-
in
-
-
let rec loop () =
-
match Input.peek t.input with
-
| None -> Error.raise_at start Unclosed_single_quote
-
| Some '\'' ->
-
ignore (Input.next t.input);
-
(* Check for escaped quote ('') *)
-
(match Input.peek t.input with
-
| Some '\'' ->
-
flush_whitespace ();
-
Buffer.add_char buf '\'';
-
ignore (Input.next t.input);
-
loop ()
-
| _ ->
-
(* End of string - flush any trailing whitespace *)
-
flush_whitespace ())
-
| Some ' ' | Some '\t' ->
-
(* Track whitespace - don't add to buf yet *)
-
Buffer.add_char whitespace (Option.get (Input.peek t.input));
-
ignore (Input.next t.input);
-
loop ()
-
| Some '\n' | Some '\r' ->
-
(* Discard trailing whitespace before line break *)
-
Buffer.clear whitespace;
-
Input.consume_break t.input;
-
(* Skip leading whitespace on next line *)
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done;
-
(* Check for document boundary *)
-
if Input.at_document_boundary t.input then
-
Error.raise_at start Unclosed_single_quote;
-
(* Check indentation: continuation must be > block indent (QB6E, DK95) *)
-
let col = column t in
-
let indent = current_indent t in
-
if not (Input.is_eof t.input) && not (Input.next_is_break t.input) && col <= indent && indent >= 0 then
-
Error.raise_at (Input.mark t.input) (Invalid_quoted_scalar_indentation "invalid indentation in quoted scalar");
-
(* Count empty lines (consecutive line breaks) *)
-
let empty_lines = ref 0 in
-
while Input.next_is_break t.input do
-
incr empty_lines;
-
Input.consume_break t.input;
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done;
-
if Input.at_document_boundary t.input then
-
Error.raise_at start Unclosed_single_quote;
-
(* Check indentation after each empty line too *)
-
let col = column t in
-
let indent = current_indent t in
-
if not (Input.is_eof t.input) && not (Input.next_is_break t.input) && col <= indent && indent >= 0 then
-
Error.raise_at (Input.mark t.input) (Invalid_quoted_scalar_indentation "invalid indentation in quoted scalar")
-
done;
-
(* Apply folding rules *)
-
if !empty_lines > 0 then begin
-
(* Empty lines: preserve as newlines *)
-
for _ = 1 to !empty_lines do
-
Buffer.add_char buf '\n'
-
done
-
end else
-
(* Single break: fold to space (even at start of string) *)
-
Buffer.add_char buf ' ';
-
loop ()
-
| Some c ->
-
flush_whitespace ();
-
Buffer.add_char buf c;
-
ignore (Input.next t.input);
-
loop ()
-
in
-
loop ();
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
(Buffer.contents buf, span)
-
-
(** Decode hex escape of given length *)
-
let decode_hex t len =
-
let start = Input.mark t.input in
-
let buf = Buffer.create len in
-
for _ = 1 to len do
-
match Input.peek t.input with
-
| Some c when Input.is_hex c ->
-
Buffer.add_char buf c;
-
ignore (Input.next t.input)
-
| _ ->
-
Error.raise_at start (Invalid_hex_escape (Buffer.contents buf))
-
done;
-
let code = int_of_string ("0x" ^ Buffer.contents buf) in
-
if code <= 0x7F then
-
String.make 1 (Char.chr code)
-
else if code <= 0x7FF then
-
let b1 = 0xC0 lor (code lsr 6) in
-
let b2 = 0x80 lor (code land 0x3F) in
-
String.init 2 (fun i -> Char.chr (if i = 0 then b1 else b2))
-
else if code <= 0xFFFF then
-
let b1 = 0xE0 lor (code lsr 12) in
-
let b2 = 0x80 lor ((code lsr 6) land 0x3F) in
-
let b3 = 0x80 lor (code land 0x3F) in
-
String.init 3 (fun i -> Char.chr (match i with 0 -> b1 | 1 -> b2 | _ -> b3))
-
else
-
let b1 = 0xF0 lor (code lsr 18) in
-
let b2 = 0x80 lor ((code lsr 12) land 0x3F) in
-
let b3 = 0x80 lor ((code lsr 6) land 0x3F) in
-
let b4 = 0x80 lor (code land 0x3F) in
-
String.init 4 (fun i -> Char.chr (match i with 0 -> b1 | 1 -> b2 | 2 -> b3 | _ -> b4))
-
-
(** Scan double-quoted scalar *)
-
let scan_double_quoted t =
-
let start = Input.mark t.input in
-
ignore (Input.next t.input); (* consume opening double-quote *)
-
let buf = Buffer.create 64 in
-
let whitespace = Buffer.create 16 in (* Track pending whitespace *)
-
-
let flush_whitespace () =
-
if Buffer.length whitespace > 0 then begin
-
Buffer.add_buffer buf whitespace;
-
Buffer.clear whitespace
-
end
-
in
-
-
let rec loop () =
-
match Input.peek t.input with
-
| None -> Error.raise_at start Unclosed_double_quote
-
| Some '"' ->
-
(* Flush trailing whitespace before closing quote to preserve it *)
-
flush_whitespace ();
-
ignore (Input.next t.input)
-
| Some ' ' | Some '\t' as c_opt ->
-
(* Track whitespace - don't add to buf yet *)
-
let c = match c_opt with Some c -> c | None -> assert false in
-
Buffer.add_char whitespace c;
-
ignore (Input.next t.input);
-
loop ()
-
| Some '\\' ->
-
(* Escape sequence - this is non-whitespace content *)
-
flush_whitespace (); (* Commit any pending whitespace *)
-
ignore (Input.next t.input);
-
(match Input.peek t.input with
-
| None -> Error.raise_at start (Invalid_escape_sequence "\\<EOF>")
-
| Some '0' -> Buffer.add_char buf '\x00'; ignore (Input.next t.input)
-
| Some 'a' -> Buffer.add_char buf '\x07'; ignore (Input.next t.input)
-
| Some 'b' -> Buffer.add_char buf '\x08'; ignore (Input.next t.input)
-
| Some 't' | Some '\t' -> Buffer.add_char buf '\t'; ignore (Input.next t.input)
-
| Some 'n' -> Buffer.add_char buf '\n'; ignore (Input.next t.input)
-
| Some 'v' -> Buffer.add_char buf '\x0B'; ignore (Input.next t.input)
-
| Some 'f' -> Buffer.add_char buf '\x0C'; ignore (Input.next t.input)
-
| Some 'r' -> Buffer.add_char buf '\r'; ignore (Input.next t.input)
-
| Some 'e' -> Buffer.add_char buf '\x1B'; ignore (Input.next t.input)
-
| Some ' ' -> Buffer.add_char buf ' '; ignore (Input.next t.input)
-
| Some '"' -> Buffer.add_char buf '"'; ignore (Input.next t.input)
-
| Some '/' -> Buffer.add_char buf '/'; ignore (Input.next t.input)
-
| Some '\\' -> Buffer.add_char buf '\\'; ignore (Input.next t.input)
-
| Some 'N' -> Buffer.add_string buf "\xC2\x85"; ignore (Input.next t.input) (* NEL *)
-
| Some '_' -> Buffer.add_string buf "\xC2\xA0"; ignore (Input.next t.input) (* NBSP *)
-
| Some 'L' -> Buffer.add_string buf "\xE2\x80\xA8"; ignore (Input.next t.input) (* LS *)
-
| Some 'P' -> Buffer.add_string buf "\xE2\x80\xA9"; ignore (Input.next t.input) (* PS *)
-
| Some 'x' ->
-
ignore (Input.next t.input);
-
Buffer.add_string buf (decode_hex t 2)
-
| Some 'u' ->
-
ignore (Input.next t.input);
-
Buffer.add_string buf (decode_hex t 4)
-
| Some 'U' ->
-
ignore (Input.next t.input);
-
Buffer.add_string buf (decode_hex t 8)
-
| Some '\n' | Some '\r' ->
-
(* Line continuation escape *)
-
Input.consume_break t.input;
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done
-
| Some c ->
-
Error.raise_at (Input.mark t.input)
-
(Invalid_escape_sequence (Printf.sprintf "\\%c" c)));
-
loop ()
-
| Some '\n' | Some '\r' ->
-
(* Line break: discard any pending trailing whitespace *)
-
Buffer.clear whitespace;
-
Input.consume_break t.input;
-
(* Count consecutive line breaks (empty lines) *)
-
let empty_lines = ref 0 in
-
let continue = ref true in
-
let started_with_tab = ref false in
-
while !continue do
-
(* Track if we start with a tab (for DK95/01 check) *)
-
if Input.next_is (( = ) '\t') t.input then started_with_tab := true;
-
(* Skip blanks (spaces/tabs) on the line *)
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done;
-
(* Check if we hit another line break (empty line) *)
-
if Input.next_is_break t.input then begin
-
Input.consume_break t.input;
-
incr empty_lines;
-
started_with_tab := false (* Reset for next line *)
-
end else
-
continue := false
-
done;
-
(* Check for document boundary - this terminates the quoted string *)
-
if Input.at_document_boundary t.input then
-
Error.raise_at start Unclosed_double_quote;
-
(* Check indentation: continuation must be > block indent (QB6E, DK95)
-
Note: must be strictly greater than block indent, not just equal *)
-
let col = column t in
-
let indent = current_indent t in
-
let start_col = start.column in
-
(* DK95/01: if continuation started with tabs and column < start column, error *)
-
if not (Input.is_eof t.input) && !started_with_tab && col < start_col then
-
Error.raise_at (Input.mark t.input) (Invalid_quoted_scalar_indentation "invalid indentation in quoted scalar");
-
if not (Input.is_eof t.input) && col <= indent && indent >= 0 then
-
Error.raise_at (Input.mark t.input) (Invalid_quoted_scalar_indentation "invalid indentation in quoted scalar");
-
(* Per YAML spec: single break = space, break + empty lines = newlines *)
-
if !empty_lines > 0 then begin
-
(* Empty lines: output N newlines where N = number of empty lines *)
-
for _ = 1 to !empty_lines do
-
Buffer.add_char buf '\n'
-
done
-
end else
-
(* Single break folds to space *)
-
Buffer.add_char buf ' ';
-
loop ()
-
| Some c ->
-
(* Non-whitespace character *)
-
flush_whitespace (); (* Commit any pending whitespace *)
-
Buffer.add_char buf c;
-
ignore (Input.next t.input);
-
loop ()
-
in
-
loop ();
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
(Buffer.contents buf, span)
-
-
(** Check if character can appear in plain scalar at this position *)
-
let can_continue_plain t c ~in_flow =
-
match c with
-
| ':' ->
-
(* : is OK if not followed by whitespace or flow indicator *)
-
(match Input.peek_nth t.input 1 with
-
| None -> true
-
| Some c2 when Input.is_whitespace c2 -> false
-
| Some c2 when in_flow && Input.is_flow_indicator c2 -> false
-
| _ -> true)
-
| '#' ->
-
(* # is a comment indicator only if preceded by whitespace *)
-
(* Check the previous character to determine if this is a comment *)
-
(match Input.peek_back t.input with
-
| None -> true (* At start - can't be comment indicator, allow it *)
-
| Some c when Input.is_whitespace c -> false (* Preceded by whitespace - comment *)
-
| Some c when Input.is_break c -> false (* At start of line - comment *)
-
| _ -> true) (* Not preceded by whitespace - part of scalar *)
-
| c when in_flow && Input.is_flow_indicator c -> false
-
| _ when Input.is_break c -> false
-
| _ -> true
-
-
(** Scan plain scalar *)
-
let scan_plain_scalar t =
-
let start = Input.mark t.input in
-
let in_flow = t.flow_level > 0 in
-
let indent = current_indent t in
-
(* In flow context, scalars must be indented more than the current block indent.
-
This ensures that content at block indent or less ends the flow context. *)
-
if in_flow && (column t - 1) < indent then
-
Error.raise_at start Invalid_flow_indentation;
-
let buf = Buffer.create 64 in
-
let spaces = Buffer.create 16 in
-
let whitespace = Buffer.create 16 in (* Track whitespace within a line *)
-
let leading_blanks = ref false in
-
-
let rec scan_line () =
-
match Input.peek t.input with
-
| None -> ()
-
| Some c when Input.is_blank c && can_continue_plain t c ~in_flow ->
-
(* Blank character within a line - save to whitespace buffer *)
-
Buffer.add_char whitespace c;
-
ignore (Input.next t.input);
-
scan_line ()
-
| Some c when can_continue_plain t c ~in_flow ->
-
(* Non-blank character - process any pending breaks/whitespace first *)
-
begin
-
if Buffer.length spaces > 0 then begin
-
if !leading_blanks then begin
-
(* Fold line break *)
-
if Buffer.contents spaces = "\n" then
-
Buffer.add_char buf ' '
-
else begin
-
(* Multiple breaks - preserve all but first *)
-
let s = Buffer.contents spaces in
-
Buffer.add_substring buf s 1 (String.length s - 1)
-
end
-
end else
-
Buffer.add_buffer buf spaces;
-
Buffer.clear spaces
-
end;
-
(* Add any pending whitespace from within the line *)
-
if Buffer.length whitespace > 0 then begin
-
Buffer.add_buffer buf whitespace;
-
Buffer.clear whitespace
-
end;
-
(* Add the character *)
-
Buffer.add_char buf c;
-
ignore (Input.next t.input);
-
leading_blanks := false;
-
scan_line ()
-
end
-
| _ -> ()
-
in
-
-
let rec scan_lines () =
-
scan_line ();
-
(* Check for line continuation *)
-
if Input.next_is_break t.input then begin
-
(* Discard any trailing whitespace from the current line *)
-
Buffer.clear whitespace;
-
(* Save the line break *)
-
if !leading_blanks then begin
-
(* We already had a break - this is an additional break (empty line) *)
-
Buffer.add_char spaces '\n'
-
end else begin
-
(* First line break *)
-
Buffer.clear spaces;
-
Buffer.add_char spaces '\n';
-
leading_blanks := true
-
end;
-
Input.consume_break t.input;
-
(* Note: We do NOT set allow_simple_key here during plain scalar scanning.
-
Setting it here would incorrectly allow ':' that appears on a continuation
-
line to become a mapping indicator. The flag will be set properly after
-
the scalar ends and skip_to_next_token processes line breaks. *)
-
(* Skip leading blanks on the next line *)
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done;
-
let col = (Input.position t.input).column in
-
(* Check indentation - stop if we're at or before the containing block's indent *)
-
(* However, allow empty lines (line breaks) to continue even if dedented *)
-
if Input.next_is_break t.input then
-
scan_lines () (* Empty line - continue *)
-
else if not in_flow && col <= indent then
-
() (* Stop - dedented or at parent level in block context *)
-
else if Input.at_document_boundary t.input then
-
() (* Stop - document boundary *)
-
else
-
scan_lines ()
-
end
-
in
-
-
scan_lines ();
-
let value = Buffer.contents buf in
-
(* Trim trailing whitespace (spaces and tabs) *)
-
let value =
-
let len = String.length value in
-
let rec find_end i =
-
if i < 0 then 0
-
else match value.[i] with
-
| ' ' | '\t' -> find_end (i - 1)
-
| _ -> i + 1
-
in
-
let end_pos = find_end (len - 1) in
-
String.sub value 0 end_pos
-
in
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
(* Return value, span, and whether we ended with leading blanks (crossed a line break) *)
-
(value, span, !leading_blanks)
-
-
(** Scan block scalar (literal | or folded >) *)
-
let scan_block_scalar t literal =
-
let start = Input.mark t.input in
-
ignore (Input.next t.input); (* consume | or > *)
-
-
(* Parse header: optional indentation indicator and chomping *)
-
let explicit_indent = ref None in
-
let chomping = ref Chomping.Clip in
-
-
(* First character of header *)
-
(match Input.peek t.input with
-
| Some c when Input.is_digit c && c <> '0' ->
-
explicit_indent := Some (Char.code c - Char.code '0');
-
ignore (Input.next t.input)
-
| Some '-' -> chomping := Chomping.Strip; ignore (Input.next t.input)
-
| Some '+' -> chomping := Chomping.Keep; ignore (Input.next t.input)
-
| _ -> ());
-
-
(* Second character of header *)
-
(match Input.peek t.input with
-
| Some c when Input.is_digit c && c <> '0' && !explicit_indent = None ->
-
explicit_indent := Some (Char.code c - Char.code '0');
-
ignore (Input.next t.input)
-
| Some '-' when !chomping = Chomping.Clip ->
-
chomping := Chomping.Strip; ignore (Input.next t.input)
-
| Some '+' when !chomping = Chomping.Clip ->
-
chomping := Chomping.Keep; ignore (Input.next t.input)
-
| _ -> ());
-
-
(* Skip whitespace and optional comment *)
-
skip_whitespace_and_comment t;
-
-
(* Consume line break *)
-
if Input.next_is_break t.input then
-
Input.consume_break t.input
-
else if not (Input.is_eof t.input) then
-
Error.raise_at (Input.mark t.input)
-
(Invalid_block_scalar_header "expected newline after header");
-
-
let base_indent = current_indent t in
-
(* base_indent is the indent level from the stack, -1 if empty.
-
It's used directly for comparisons in implicit indent case. *)
-
let content_indent = ref (
-
match !explicit_indent with
-
| Some n ->
-
(* Explicit indent: base_indent is 1-indexed column, convert to 0-indexed.
-
content_indent = (base_indent - 1) + n, but at least n for document level. *)
-
let base_level = max 0 (base_indent - 1) in
-
base_level + n
-
| None -> 0 (* Will be determined by first non-empty line *)
-
) in
-
-
let buf = Buffer.create 256 in
-
let trailing_breaks = Buffer.create 16 in
-
let leading_blank = ref false in (* Was the previous line "more indented"? *)
-
let max_empty_line_indent = ref 0 in (* Track max indent of empty lines before first content *)
-
-
(* Skip to content indentation, skipping empty lines.
-
Returns the number of spaces actually skipped (important for detecting dedentation). *)
-
let rec skip_to_content_indent () =
-
if !content_indent > 0 then begin
-
(* Explicit indent - skip up to content_indent spaces *)
-
let spaces_skipped = ref 0 in
-
while !spaces_skipped < !content_indent && Input.next_is (( = ) ' ') t.input do
-
incr spaces_skipped;
-
ignore (Input.next t.input)
-
done;
-
-
(* Check if this line is empty (only spaces/tabs until break/eof) *)
-
if Input.next_is_break t.input then begin
-
(* Empty line - record the break and continue *)
-
Buffer.add_char trailing_breaks '\n';
-
Input.consume_break t.input;
-
skip_to_content_indent ()
-
end else if !spaces_skipped < !content_indent then begin
-
(* Line starts with fewer spaces than content_indent - dedented *)
-
!spaces_skipped
-
end else if Input.next_is_blank t.input then begin
-
(* Line has spaces/tabs beyond content_indent - could be whitespace content or empty line.
-
For literal scalars, whitespace-only lines ARE content (not empty).
-
For folded scalars, whitespace-only lines that are "more indented" are preserved. *)
-
if literal then
-
(* Literal: whitespace beyond content_indent is content, let read_lines handle it *)
-
!content_indent
-
else begin
-
(* Folded: check if rest is only blanks *)
-
let idx = ref 0 in
-
while match Input.peek_nth t.input !idx with
-
| Some c when Input.is_blank c -> incr idx; true
-
| _ -> false
-
do () done;
-
match Input.peek_nth t.input (!idx) with
-
| None | Some '\n' | Some '\r' ->
-
(* Empty/whitespace-only line in folded - skip spaces *)
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done;
-
Buffer.add_char trailing_breaks '\n';
-
Input.consume_break t.input;
-
skip_to_content_indent ()
-
| _ ->
-
(* Has non-whitespace content *)
-
!content_indent
-
end
-
end else
-
!content_indent
-
end else begin
-
(* Implicit indent - skip empty lines without consuming spaces.
-
Note: Only SPACES count as indentation. Tabs are content, not indentation.
-
So we only check for spaces when determining if a line is "empty". *)
-
if Input.next_is_break t.input then begin
-
Buffer.add_char trailing_breaks '\n';
-
Input.consume_break t.input;
-
skip_to_content_indent ()
-
end else if Input.next_is (( = ) ' ') t.input then begin
-
(* Check if line is empty (only spaces before break) *)
-
let idx = ref 0 in
-
while match Input.peek_nth t.input !idx with
-
| Some ' ' -> incr idx; true
-
| _ -> false
-
do () done;
-
match Input.peek_nth t.input (!idx) with
-
| None | Some '\n' | Some '\r' ->
-
(* Line has only spaces - empty line *)
-
(* Track max indent of empty lines for later validation *)
-
if !idx > !max_empty_line_indent then
-
max_empty_line_indent := !idx;
-
while Input.next_is (( = ) ' ') t.input do
-
ignore (Input.next t.input)
-
done;
-
Buffer.add_char trailing_breaks '\n';
-
Input.consume_break t.input;
-
skip_to_content_indent ()
-
| _ ->
-
(* Has content (including tabs which are content, not indentation) *)
-
0
-
end else if Input.next_is (( = ) '\t') t.input then begin
-
(* Tab at start of line in implicit indent mode - this is an error (Y79Y)
-
because tabs cannot be used as indentation in YAML *)
-
Error.raise_at (Input.mark t.input) Tab_in_indentation
-
end else
-
(* Not at break or space - other content character *)
-
0
-
end
-
in
-
-
(* Read content *)
-
let rec read_lines () =
-
let spaces_skipped = skip_to_content_indent () in
-
-
(* Check if we're at content *)
-
if Input.is_eof t.input then ()
-
else if Input.at_document_boundary t.input then ()
-
else begin
-
(* Count additional leading spaces beyond what was skipped *)
-
let extra_spaces = ref 0 in
-
while Input.next_is (( = ) ' ') t.input do
-
incr extra_spaces;
-
ignore (Input.next t.input)
-
done;
-
-
(* Calculate actual line indentation *)
-
let line_indent = spaces_skipped + !extra_spaces in
-
-
(* Determine content indent from first content line (implicit case) *)
-
let first_line = !content_indent = 0 in
-
(* base_indent is 1-indexed column, convert to 0-indexed for comparison with line_indent.
-
If base_indent = -1 (empty stack), then base_level = -1 means col 0 is valid. *)
-
let base_level = base_indent - 1 in
-
let should_process =
-
if !content_indent = 0 then begin
-
(* For implicit indent, content must be more indented than base_level. *)
-
if line_indent <= base_level then
-
false (* No content - first line not indented enough *)
-
else begin
-
(* Validate: first content line must be indented at least as much as
-
the maximum indent seen on empty lines before it (5LLU, S98Z, W9L4) *)
-
if line_indent < !max_empty_line_indent && line_indent > base_level then
-
Error.raise_at (Input.mark t.input)
-
(Invalid_block_scalar_header "wrongly indented line in block scalar");
-
content_indent := line_indent;
-
true
-
end
-
end else if line_indent < !content_indent then
-
false (* Dedented - done with content *)
-
else
-
true
-
in
-
-
if should_process then begin
-
(* Check if current line is "more indented" (has extra indent or starts with whitespace).
-
For folded scalars, lines that start with any whitespace (space or tab) after the
-
content indentation are "more indented" and preserve breaks.
-
Note: we check Input.next_is_blank BEFORE reading content to see if content starts with whitespace. *)
-
let trailing_blank = line_indent > !content_indent || Input.next_is_blank t.input in
-
-
(* Add trailing breaks to buffer *)
-
if Buffer.length buf > 0 then begin
-
if Buffer.length trailing_breaks > 0 then begin
-
if literal then
-
Buffer.add_buffer buf trailing_breaks
-
else begin
-
(* Folded scalar: fold only if both previous and current lines are not more-indented *)
-
if not !leading_blank && not trailing_blank then begin
-
let breaks = Buffer.contents trailing_breaks in
-
if String.length breaks = 1 then
-
Buffer.add_char buf ' '
-
else
-
Buffer.add_substring buf breaks 1 (String.length breaks - 1)
-
end else begin
-
(* Preserve breaks for more-indented lines *)
-
Buffer.add_buffer buf trailing_breaks
-
end
-
end
-
end else if not literal then
-
Buffer.add_char buf ' '
-
end else
-
Buffer.add_buffer buf trailing_breaks;
-
Buffer.clear trailing_breaks;
-
-
(* Add extra indentation for literal or more-indented folded lines *)
-
(* On the first line (when determining content_indent), we've already consumed all spaces,
-
so we should NOT add any back. On subsequent lines, we add only the spaces beyond content_indent. *)
-
if not first_line && (literal || (!extra_spaces > 0 && not literal)) then begin
-
for _ = 1 to !extra_spaces do
-
Buffer.add_char buf ' '
-
done
-
end;
-
-
(* Read line content *)
-
while not (Input.is_eof t.input) && not (Input.next_is_break t.input) do
-
Buffer.add_char buf (Input.next_exn t.input)
-
done;
-
-
(* Record trailing break *)
-
if Input.next_is_break t.input then begin
-
Buffer.add_char trailing_breaks '\n';
-
Input.consume_break t.input
-
end;
-
-
(* Update leading_blank for next iteration *)
-
leading_blank := trailing_blank;
-
-
read_lines ()
-
end
-
end
-
in
-
-
read_lines ();
-
-
(* Apply chomping *)
-
let value =
-
let content = Buffer.contents buf in
-
match !chomping with
-
| Chomping.Strip -> content
-
| Chomping.Clip ->
-
if String.length content > 0 then content ^ "\n" else content
-
| Chomping.Keep ->
-
content ^ Buffer.contents trailing_breaks
-
in
-
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
let style = if literal then Scalar_style.Literal else Scalar_style.Folded in
-
(value, style, span)
-
-
(** Scan directive (after %) *)
-
let scan_directive t =
-
let start = Input.mark t.input in
-
ignore (Input.next t.input); (* consume % *)
-
-
(* Read directive name *)
-
let name_buf = Buffer.create 16 in
-
while
-
match Input.peek t.input with
-
| Some c when Input.is_alnum c || c = '-' ->
-
Buffer.add_char name_buf c;
-
ignore (Input.next t.input);
-
true
-
| _ -> false
-
do () done;
-
let name = Buffer.contents name_buf in
-
-
(* Skip blanks *)
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done;
-
-
match name with
-
| "YAML" ->
-
(* Version directive: %YAML 1.2 *)
-
let major = ref 0 in
-
let minor = ref 0 in
-
(* Read major version *)
-
while Input.next_is_digit t.input do
-
major := !major * 10 + (Char.code (Input.next_exn t.input) - Char.code '0')
-
done;
-
(* Expect . *)
-
(match Input.peek t.input with
-
| Some '.' -> ignore (Input.next t.input)
-
| _ -> Error.raise_at (Input.mark t.input) (Invalid_yaml_version "expected '.'"));
-
(* Read minor version *)
-
while Input.next_is_digit t.input do
-
minor := !minor * 10 + (Char.code (Input.next_exn t.input) - Char.code '0')
-
done;
-
(* Validate: only whitespace and comments allowed before line break (MUS6) *)
-
skip_whitespace_and_comment t;
-
if not (Input.next_is_break t.input) && not (Input.is_eof t.input) then
-
Error.raise_at (Input.mark t.input) (Invalid_directive "expected comment or line break after version");
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
Token.Version_directive { major = !major; minor = !minor }, span
-
-
| "TAG" ->
-
(* Tag directive: %TAG !foo! tag:example.com,2000: *)
-
let handle = scan_tag_handle t in
-
(* Skip blanks *)
-
while Input.next_is_blank t.input do
-
ignore (Input.next t.input)
-
done;
-
(* Read prefix *)
-
let prefix_buf = Buffer.create 32 in
-
while
-
match Input.peek t.input with
-
| Some c when not (Input.is_whitespace c) ->
-
Buffer.add_char prefix_buf c;
-
ignore (Input.next t.input);
-
true
-
| _ -> false
-
do () done;
-
let prefix = Buffer.contents prefix_buf in
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
Token.Tag_directive { handle; prefix }, span
-
-
| _ ->
-
(* Reserved/Unknown directive - skip to end of line and ignore *)
-
(* Per YAML spec, reserved directives should be ignored with a warning *)
-
while not (Input.is_eof t.input) && not (Input.next_is_break t.input) do
-
ignore (Input.next t.input)
-
done;
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
(* Return an empty tag directive token to indicate directive was processed but ignored *)
-
Token.Tag_directive { handle = ""; prefix = "" }, span
-
-
(** Fetch the next token(s) into the queue *)
-
let rec fetch_next_token t =
-
skip_to_next_token t;
-
stale_simple_keys t;
-
let col = column t in
-
(* Unroll indents that are deeper than current column.
-
Note: we use col, not col-1, to allow entries at the same level. *)
-
unroll_indent t col;
-
-
(* We're about to process actual content, not leading whitespace *)
-
t.leading_whitespace <- false;
-
-
if Input.is_eof t.input then
-
fetch_stream_end t
-
else if Input.at_document_boundary t.input then
-
fetch_document_indicator t
-
else begin
-
match Input.peek t.input with
-
| None -> fetch_stream_end t
-
| Some '%' when (Input.position t.input).column = 1 ->
-
fetch_directive t
-
| Some '[' -> fetch_flow_collection_start t Token.Flow_sequence_start
-
| Some '{' -> fetch_flow_collection_start t Token.Flow_mapping_start
-
| Some ']' -> fetch_flow_collection_end t Token.Flow_sequence_end
-
| Some '}' -> fetch_flow_collection_end t Token.Flow_mapping_end
-
| Some ',' -> fetch_flow_entry t
-
| Some '-' when t.flow_level = 0 && check_block_entry t ->
-
fetch_block_entry t
-
| Some '?' when check_key t ->
-
fetch_key t
-
| Some ':' when check_value t ->
-
fetch_value t
-
| Some '*' -> fetch_alias t
-
| Some '&' -> fetch_anchor t
-
| Some '!' -> fetch_tag t
-
| Some '|' when t.flow_level = 0 -> fetch_block_scalar t true
-
| Some '>' when t.flow_level = 0 -> fetch_block_scalar t false
-
| Some '\'' -> fetch_single_quoted t
-
| Some '"' -> fetch_double_quoted t
-
| Some '-' when can_start_plain t ->
-
fetch_plain_scalar t
-
| Some '?' when can_start_plain t ->
-
fetch_plain_scalar t
-
| Some ':' when can_start_plain t ->
-
fetch_plain_scalar t
-
| Some c when can_start_plain_char c t ->
-
fetch_plain_scalar t
-
| Some c ->
-
Error.raise_at (Input.mark t.input) (Unexpected_character c)
-
end
-
-
and fetch_stream_end t =
-
if not t.stream_ended then begin
-
unroll_indent t (-1);
-
remove_simple_key t;
-
t.allow_simple_key <- false;
-
t.stream_ended <- true;
-
let span = Span.point (Input.mark t.input) in
-
emit t span Token.Stream_end
-
end
-
-
and fetch_document_indicator t =
-
unroll_indent t (-1);
-
remove_simple_key t;
-
t.allow_simple_key <- false;
-
let start = Input.mark t.input in
-
let indicator = Input.peek_string t.input 3 in
-
Input.skip t.input 3;
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
let token = if indicator = "---" then Token.Document_start else Token.Document_end in
-
(* Reset document content flag after document end marker *)
-
if indicator = "..." then begin
-
t.document_has_content <- false;
-
(* After document end marker, skip whitespace and check for end of line or comment *)
-
while Input.next_is_blank t.input do ignore (Input.next t.input) done;
-
(match Input.peek t.input with
-
| None -> () (* EOF is ok *)
-
| Some c when Input.is_break c -> ()
-
| Some '#' -> () (* Comment is ok *)
-
| _ -> Error.raise_at start (Invalid_directive "content not allowed after document end marker on same line"))
-
end;
-
emit t span token
-
-
and fetch_directive t =
-
(* Directives can only appear:
-
1. At stream start (before any document content)
-
2. After a document end marker (...)
-
If we've emitted content in the current document, we need a document end marker first *)
-
if t.document_has_content then
-
Error.raise_at (Input.mark t.input)
-
(Unexpected_token "directives must be separated from document content by document end marker (...)");
-
unroll_indent t (-1);
-
remove_simple_key t;
-
t.allow_simple_key <- false;
-
let token, span = scan_directive t in
-
emit t span token
-
-
and fetch_flow_collection_start t token_type =
-
save_simple_key t;
-
(* Record indent of outermost flow collection *)
-
if t.flow_level = 0 then
-
t.flow_indent <- column t;
-
t.flow_level <- t.flow_level + 1;
-
(* Track whether this is a mapping or sequence *)
-
let is_mapping = (token_type = Token.Flow_mapping_start) in
-
t.flow_mapping_stack <- is_mapping :: t.flow_mapping_stack;
-
t.allow_simple_key <- true;
-
t.simple_keys <- None :: t.simple_keys;
-
t.document_has_content <- true;
-
let start = Input.mark t.input in
-
ignore (Input.next t.input);
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
emit t span token_type
-
-
and fetch_flow_collection_end t token_type =
-
remove_simple_key t;
-
t.flow_level <- t.flow_level - 1;
-
t.flow_mapping_stack <- (match t.flow_mapping_stack with _ :: rest -> rest | [] -> []);
-
t.simple_keys <- (match t.simple_keys with _ :: rest -> rest | [] -> []);
-
t.allow_simple_key <- false;
-
let start = Input.mark t.input in
-
ignore (Input.next t.input);
-
(* Allow adjacent values after flow collection ends *)
-
if t.flow_level > 0 then
-
t.adjacent_value_allowed_at <- Some (Input.position t.input);
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
emit t span token_type
-
-
and fetch_flow_entry t =
-
remove_simple_key t;
-
t.allow_simple_key <- true;
-
let start = Input.mark t.input in
-
ignore (Input.next t.input);
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
emit t span Token.Flow_entry
-
-
and check_block_entry t =
-
(* - followed by whitespace or EOF *)
-
match Input.peek_nth t.input 1 with
-
| None -> true
-
| Some c -> Input.is_whitespace c
-
-
and fetch_block_entry t =
-
if t.flow_level = 0 then begin
-
(* Block entries require allow_simple_key to be true.
-
This prevents block sequences on the same line as a mapping value,
-
e.g., "key: - a" is invalid. *)
-
if not t.allow_simple_key then
-
Error.raise_at (Input.mark t.input) Block_sequence_disallowed;
-
let col = column t in
-
if roll_indent t col ~sequence:true then begin
-
let span = Span.point (Input.mark t.input) in
-
emit t span Token.Block_sequence_start
-
end
-
end;
-
remove_simple_key t;
-
t.allow_simple_key <- true;
-
t.document_has_content <- true;
-
let start = Input.mark t.input in
-
ignore (Input.next t.input);
-
-
(* Check for tabs after - : pattern like -\t- is invalid *)
-
let (found_tabs, _found_spaces) = skip_blanks_check_tabs t in
-
if found_tabs then begin
-
(* If we found tabs and next char is - followed by whitespace, error *)
-
match Input.peek t.input with
-
| Some '-' ->
-
(match Input.peek_nth t.input 1 with
-
| None -> Error.raise_at start Tab_in_indentation
-
| Some c when Input.is_whitespace c ->
-
Error.raise_at start Tab_in_indentation
-
| Some _ -> ())
-
| _ -> ()
-
end;
-
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
emit t span Token.Block_entry
-
-
and check_key t =
-
(* ? followed by whitespace or flow indicator in both block and flow *)
-
match Input.peek_nth t.input 1 with
-
| None -> true
-
| Some c ->
-
Input.is_whitespace c ||
-
(t.flow_level > 0 && Input.is_flow_indicator c)
-
-
and fetch_key t =
-
if t.flow_level = 0 then begin
-
if not t.allow_simple_key then
-
Error.raise_at (Input.mark t.input) Expected_key;
-
let col = column t in
-
if roll_indent t col ~sequence:false then begin
-
let span = Span.point (Input.mark t.input) in
-
emit t span Token.Block_mapping_start
-
end
-
end;
-
remove_simple_key t;
-
t.allow_simple_key <- t.flow_level = 0;
-
t.document_has_content <- true;
-
let start = Input.mark t.input in
-
ignore (Input.next t.input);
-
-
(* Check for tabs after ? : pattern like ?\t- or ?\tkey is invalid *)
-
let (found_tabs, _found_spaces) = skip_blanks_check_tabs t in
-
if found_tabs && t.flow_level = 0 then begin
-
(* In block context, tabs after ? are not allowed *)
-
Error.raise_at start Tab_in_indentation
-
end;
-
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
emit t span Token.Key;
-
t.pending_value <- true (* We've emitted a KEY, now waiting for VALUE *)
-
-
and check_value t =
-
(* : followed by whitespace in block, or whitespace/flow indicator in flow, or adjacent value *)
-
match Input.peek_nth t.input 1 with
-
| None -> true
-
| Some c ->
-
Input.is_whitespace c ||
-
(t.flow_level > 0 && Input.is_flow_indicator c) ||
-
(* Allow adjacent values in flow context at designated positions *)
-
(t.flow_level > 0 &&
-
match t.adjacent_value_allowed_at with
-
| Some pos -> pos.Position.line = (Input.position t.input).Position.line &&
-
pos.Position.column = (Input.position t.input).Position.column
-
| None -> false)
-
-
and fetch_value t =
-
let start = Input.mark t.input in
-
(* Check for simple key *)
-
let used_simple_key =
-
match t.simple_keys with
-
| Some sk :: _ when sk.sk_possible ->
-
(* In implicit flow mapping (inside a flow sequence), key and : must be on the same line.
-
In explicit flow mapping { }, key and : can span lines. *)
-
let is_implicit_flow_mapping = match t.flow_mapping_stack with
-
| false :: _ -> true (* false = we're in a sequence, so any mapping is implicit *)
-
| _ -> false
-
in
-
if is_implicit_flow_mapping && sk.sk_position.line < (Input.position t.input).line then
-
Error.raise_at start Illegal_flow_key_line;
-
(* Insert KEY token before the simple key value *)
-
let key_span = Span.point sk.sk_position in
-
let key_token = { Token.token = Token.Key; span = key_span } in
-
(* We need to insert at the right position *)
-
let tokens = Queue.to_seq t.tokens |> Array.of_seq in
-
Queue.clear t.tokens;
-
let insert_pos = sk.sk_token_number - t.tokens_taken in
-
Array.iteri (fun i tok ->
-
if i = insert_pos then Queue.add key_token t.tokens;
-
Queue.add tok t.tokens
-
) tokens;
-
if insert_pos >= Array.length tokens then
-
Queue.add key_token t.tokens;
-
t.token_number <- t.token_number + 1;
-
t.pending_value <- true; (* We've inserted a KEY token, now waiting for VALUE *)
-
(* Roll indent for implicit block mapping *)
-
if t.flow_level = 0 then begin
-
let col = sk.sk_position.column in
-
if roll_indent t col ~sequence:false then begin
-
let span = Span.point sk.sk_position in
-
(* Insert block mapping start before key *)
-
let bm_token = { Token.token = Token.Block_mapping_start; span } in
-
let tokens = Queue.to_seq t.tokens |> Array.of_seq in
-
Queue.clear t.tokens;
-
Array.iteri (fun i tok ->
-
if i = insert_pos then Queue.add bm_token t.tokens;
-
Queue.add tok t.tokens
-
) tokens;
-
if insert_pos >= Array.length tokens then
-
Queue.add bm_token t.tokens;
-
t.token_number <- t.token_number + 1
-
end
-
end;
-
t.simple_keys <- None :: (List.tl t.simple_keys);
-
true
-
| _ ->
-
(* No simple key - this is a complex value (or empty key) *)
-
if t.flow_level = 0 then begin
-
if not t.allow_simple_key then
-
Error.raise_at (Input.mark t.input) Expected_key;
-
let col = column t in
-
if roll_indent t col ~sequence:false then begin
-
let span = Span.point (Input.mark t.input) in
-
emit t span Token.Block_mapping_start
-
end
-
(* Note: We don't emit KEY here. Empty key handling is done by the parser,
-
which emits empty scalar when it sees VALUE without preceding KEY. *)
-
end;
-
false
-
in
-
remove_simple_key t;
-
(* In block context without simple key, allow simple keys for compact mappings like ": moon: white"
-
In flow context or after using a simple key, disallow simple keys *)
-
t.allow_simple_key <- (not used_simple_key) && (t.flow_level = 0);
-
t.document_has_content <- true;
-
let start = Input.mark t.input in
-
ignore (Input.next t.input);
-
-
(* Check for tabs after : : patterns like :\t- or :\tkey: are invalid in block context (Y79Y/09)
-
However, :\t bar (tab followed by space then content) is valid (6BCT) *)
-
let (found_tabs, found_spaces) = skip_blanks_check_tabs t in
-
if found_tabs && not found_spaces && t.flow_level = 0 then begin
-
(* In block context, tabs-only after : followed by indicator or alphanumeric are not allowed *)
-
match Input.peek t.input with
-
| Some ('-' | '?') ->
-
Error.raise_at start Tab_in_indentation
-
| Some c when (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ->
-
(* Tab-only followed by alphanumeric - likely a key, which is invalid *)
-
Error.raise_at start Tab_in_indentation
-
| _ -> ()
-
end;
-
-
(* Skip any comment that may follow the colon and whitespace *)
-
skip_whitespace_and_comment t;
-
-
let span = Span.make ~start ~stop:(Input.mark t.input) in
-
emit t span Token.Value;
-
t.pending_value <- false (* We've emitted a VALUE, no longer pending *)
-
-
and fetch_alias t =
-
save_simple_key t;
-
t.allow_simple_key <- false;
-
t.document_has_content <- true;
-
let start = Input.mark t.input in
-
ignore (Input.next t.input); (* consume * *)
-
let name, span = scan_anchor_alias t in
-
let span = Span.make ~start ~stop:span.stop in
-
emit t span (Token.Alias name)
-
-
and fetch_anchor t =
-
save_simple_key t;
-
t.allow_simple_key <- false;
-
t.document_has_content <- true;
-
let start = Input.mark t.input in
-
ignore (Input.next t.input); (* consume & *)
-
let name, span = scan_anchor_alias t in
-
let span = Span.make ~start ~stop:span.stop in
-
emit t span (Token.Anchor name)
-
-
and fetch_tag t =
-
save_simple_key t;
-
t.allow_simple_key <- false;
-
t.document_has_content <- true;
-
let handle, suffix, span = scan_tag t in
-
emit t span (Token.Tag { handle; suffix })
-
-
and fetch_block_scalar t literal =
-
remove_simple_key t;
-
t.allow_simple_key <- true;
-
t.document_has_content <- true;
-
let value, style, span = scan_block_scalar t literal in
-
emit t span (Token.Scalar { style; value })
-
-
and fetch_single_quoted t =
-
save_simple_key t;
-
t.allow_simple_key <- false;
-
t.document_has_content <- true;
-
let value, span = scan_single_quoted t in
-
(* Allow adjacent values after quoted scalars in flow context (for JSON compatibility) *)
-
skip_to_next_token t;
-
if t.flow_level > 0 then
-
t.adjacent_value_allowed_at <- Some (Input.position t.input);
-
emit t span (Token.Scalar { style = Scalar_style.Single_quoted; value })
-
-
and fetch_double_quoted t =
-
save_simple_key t;
-
t.allow_simple_key <- false;
-
t.document_has_content <- true;
-
let value, span = scan_double_quoted t in
-
(* Allow adjacent values after quoted scalars in flow context (for JSON compatibility) *)
-
skip_to_next_token t;
-
if t.flow_level > 0 then
-
t.adjacent_value_allowed_at <- Some (Input.position t.input);
-
emit t span (Token.Scalar { style = Scalar_style.Double_quoted; value })
-
-
and can_start_plain t =
-
(* Check if - ? : can start a plain scalar *)
-
match Input.peek_nth t.input 1 with
-
| None -> false
-
| Some c ->
-
not (Input.is_whitespace c) &&
-
(t.flow_level = 0 || not (Input.is_flow_indicator c))
-
-
and can_start_plain_char c _t =
-
(* Characters that can start a plain scalar *)
-
if Input.is_whitespace c then false
-
else if Input.is_indicator c then false
-
else true
-
-
and fetch_plain_scalar t =
-
save_simple_key t;
-
t.allow_simple_key <- false;
-
t.document_has_content <- true;
-
let value, span, ended_with_linebreak = scan_plain_scalar t in
-
(* If the plain scalar ended after crossing a line break (leading_blanks = true),
-
allow simple keys. This is important because the scanner already consumed the
-
line break and leading whitespace when checking for continuation. *)
-
if ended_with_linebreak then
-
t.allow_simple_key <- true;
-
emit t span (Token.Scalar { style = Scalar_style.Plain; value })
-
-
(** Check if we need more tokens to resolve simple keys *)
-
let need_more_tokens t =
-
if t.stream_ended then false
-
else if Queue.is_empty t.tokens then true
-
else
-
(* Check if any simple key could affect the first queued token *)
-
List.exists (function
-
| Some sk when sk.sk_possible ->
-
sk.sk_token_number >= t.tokens_taken
-
| _ -> false
-
) t.simple_keys
-
-
(** Ensure we have enough tokens to return one safely *)
-
let ensure_tokens t =
-
if not t.stream_started then begin
-
t.stream_started <- true;
-
let span = Span.point (Input.position t.input) in
-
let encoding, _ = Encoding.detect t.input.source in
-
emit t span (Token.Stream_start encoding)
-
end;
-
while need_more_tokens t do
-
fetch_next_token t
-
done
-
-
(** Get next token *)
-
let next t =
-
ensure_tokens t;
-
if Queue.is_empty t.tokens then
-
None
-
else begin
-
t.tokens_taken <- t.tokens_taken + 1;
-
Some (Queue.pop t.tokens)
-
end
-
-
(** Peek at next token *)
-
let peek t =
-
ensure_tokens t;
-
Queue.peek_opt t.tokens
-
-
(** Iterate over all tokens *)
-
let iter f t =
-
let rec loop () =
-
match next t with
-
| None -> ()
-
| Some tok -> f tok; loop ()
-
in
-
loop ()
-
-
(** Fold over all tokens *)
-
let fold f init t =
-
let rec loop acc =
-
match next t with
-
| None -> acc
-
| Some tok -> loop (f acc tok)
-
in
-
loop init
-
-
(** Convert to list *)
-
let to_list t =
-
fold (fun acc tok -> tok :: acc) [] t |> List.rev
-72
yaml/ocaml-yamle/lib/sequence.ml
···
-
(** YAML sequence (array) values with metadata *)
-
-
type 'a t = {
-
anchor : string option;
-
tag : string option;
-
implicit : bool;
-
style : Layout_style.t;
-
members : 'a list;
-
}
-
-
let make
-
?(anchor : string option)
-
?(tag : string option)
-
?(implicit = true)
-
?(style = Layout_style.Any)
-
members =
-
{ anchor; tag; implicit; style; members }
-
-
let members t = t.members
-
let anchor t = t.anchor
-
let tag t = t.tag
-
let implicit t = t.implicit
-
let style t = t.style
-
-
let with_anchor anchor t = { t with anchor = Some anchor }
-
let with_tag tag t = { t with tag = Some tag }
-
let with_style style t = { t with style }
-
-
let map f t = { t with members = List.map f t.members }
-
-
let length t = List.length t.members
-
-
let is_empty t = t.members = []
-
-
let nth t n = List.nth t.members n
-
-
let nth_opt t n = List.nth_opt t.members n
-
-
let iter f t = List.iter f t.members
-
-
let fold f init t = List.fold_left f init t.members
-
-
let pp pp_elem fmt t =
-
Format.fprintf fmt "@[<hv 2>sequence(@,";
-
(match t.anchor with
-
| Some a -> Format.fprintf fmt "anchor=%s,@ " a
-
| None -> ());
-
(match t.tag with
-
| Some tag -> Format.fprintf fmt "tag=%s,@ " tag
-
| None -> ());
-
Format.fprintf fmt "style=%a,@ " Layout_style.pp t.style;
-
Format.fprintf fmt "members=[@,%a@]@,)"
-
(Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ",@ ") pp_elem)
-
t.members
-
-
let equal eq a b =
-
Option.equal String.equal a.anchor b.anchor &&
-
Option.equal String.equal a.tag b.tag &&
-
a.implicit = b.implicit &&
-
Layout_style.equal a.style b.style &&
-
List.equal eq a.members b.members
-
-
let compare cmp a b =
-
let c = Option.compare String.compare a.anchor b.anchor in
-
if c <> 0 then c else
-
let c = Option.compare String.compare a.tag b.tag in
-
if c <> 0 then c else
-
let c = Bool.compare a.implicit b.implicit in
-
if c <> 0 then c else
-
let c = Layout_style.compare a.style b.style in
-
if c <> 0 then c else
-
List.compare cmp a.members b.members
-35
yaml/ocaml-yamle/lib/span.ml
···
-
(** Source spans representing ranges in input *)
-
-
type t = {
-
start : Position.t;
-
stop : Position.t;
-
}
-
-
let make ~start ~stop = { start; stop }
-
-
let point pos = { start = pos; stop = pos }
-
-
let merge a b =
-
let start = if Position.compare a.start b.start <= 0 then a.start else b.start in
-
let stop = if Position.compare a.stop b.stop >= 0 then a.stop else b.stop in
-
{ start; stop }
-
-
let extend span pos =
-
{ span with stop = pos }
-
-
let pp fmt t =
-
if t.start.line = t.stop.line then
-
Format.fprintf fmt "line %d, columns %d-%d"
-
t.start.line t.start.column t.stop.column
-
else
-
Format.fprintf fmt "lines %d-%d" t.start.line t.stop.line
-
-
let to_string t =
-
Format.asprintf "%a" pp t
-
-
let compare a b =
-
let c = Position.compare a.start b.start in
-
if c <> 0 then c else Position.compare a.stop b.stop
-
-
let equal a b =
-
Position.equal a.start b.start && Position.equal a.stop b.stop
-70
yaml/ocaml-yamle/lib/tag.ml
···
-
(** YAML tags for type information *)
-
-
type t = {
-
handle : string; (** e.g., "!" or "!!" or "!foo!" *)
-
suffix : string; (** e.g., "str", "int", "custom/type" *)
-
}
-
-
let make ~handle ~suffix = { handle; suffix }
-
-
let of_string s =
-
if String.length s = 0 then None
-
else if s.[0] <> '!' then None
-
else
-
(* Find the suffix after the handle *)
-
let len = String.length s in
-
if len = 1 then Some { handle = "!"; suffix = "" }
-
else if s.[1] = '!' then
-
(* !! handle *)
-
Some { handle = "!!"; suffix = String.sub s 2 (len - 2) }
-
else if s.[1] = '<' then
-
(* Verbatim tag !<...> *)
-
if len > 2 && s.[len - 1] = '>' then
-
Some { handle = "!"; suffix = String.sub s 2 (len - 3) }
-
else
-
None
-
else
-
(* Primary handle or local tag *)
-
Some { handle = "!"; suffix = String.sub s 1 (len - 1) }
-
-
let to_string t =
-
if t.handle = "!" && t.suffix = "" then "!"
-
else t.handle ^ t.suffix
-
-
let to_uri t =
-
match t.handle with
-
| "!!" -> "tag:yaml.org,2002:" ^ t.suffix
-
| "!" -> "!" ^ t.suffix
-
| h -> h ^ t.suffix
-
-
let pp fmt t =
-
Format.pp_print_string fmt (to_string t)
-
-
let equal a b =
-
String.equal a.handle b.handle && String.equal a.suffix b.suffix
-
-
let compare a b =
-
let c = String.compare a.handle b.handle in
-
if c <> 0 then c else String.compare a.suffix b.suffix
-
-
(** Standard tags *)
-
-
let null = { handle = "!!"; suffix = "null" }
-
let bool = { handle = "!!"; suffix = "bool" }
-
let int = { handle = "!!"; suffix = "int" }
-
let float = { handle = "!!"; suffix = "float" }
-
let str = { handle = "!!"; suffix = "str" }
-
let seq = { handle = "!!"; suffix = "seq" }
-
let map = { handle = "!!"; suffix = "map" }
-
let binary = { handle = "!!"; suffix = "binary" }
-
let timestamp = { handle = "!!"; suffix = "timestamp" }
-
-
(** Check if tag matches a standard type *)
-
-
let is_null t = equal t null || (t.handle = "!" && t.suffix = "")
-
let is_bool t = equal t bool
-
let is_int t = equal t int
-
let is_float t = equal t float
-
let is_str t = equal t str
-
let is_seq t = equal t seq
-
let is_map t = equal t map
-78
yaml/ocaml-yamle/lib/token.ml
···
-
(** YAML token types produced by the scanner *)
-
-
type t =
-
| Stream_start of Encoding.t
-
| Stream_end
-
| Version_directive of { major : int; minor : int }
-
| Tag_directive of { handle : string; prefix : string }
-
| Document_start (** --- *)
-
| Document_end (** ... *)
-
| Block_sequence_start
-
| Block_mapping_start
-
| Block_entry (** - *)
-
| Block_end (** implicit, from dedent *)
-
| Flow_sequence_start (** [ *)
-
| Flow_sequence_end (** ] *)
-
| Flow_mapping_start (** { *)
-
| Flow_mapping_end (** } *)
-
| Flow_entry (** , *)
-
| Key (** ? or implicit key *)
-
| Value (** : *)
-
| Anchor of string (** &name *)
-
| Alias of string (** *name *)
-
| Tag of { handle : string; suffix : string }
-
| Scalar of { style : Scalar_style.t; value : string }
-
-
type spanned = {
-
token : t;
-
span : Span.t;
-
}
-
-
let pp_token fmt = function
-
| Stream_start enc ->
-
Format.fprintf fmt "STREAM-START(%a)" Encoding.pp enc
-
| Stream_end ->
-
Format.fprintf fmt "STREAM-END"
-
| Version_directive { major; minor } ->
-
Format.fprintf fmt "VERSION-DIRECTIVE(%d.%d)" major minor
-
| Tag_directive { handle; prefix } ->
-
Format.fprintf fmt "TAG-DIRECTIVE(%s, %s)" handle prefix
-
| Document_start ->
-
Format.fprintf fmt "DOCUMENT-START"
-
| Document_end ->
-
Format.fprintf fmt "DOCUMENT-END"
-
| Block_sequence_start ->
-
Format.fprintf fmt "BLOCK-SEQUENCE-START"
-
| Block_mapping_start ->
-
Format.fprintf fmt "BLOCK-MAPPING-START"
-
| Block_entry ->
-
Format.fprintf fmt "BLOCK-ENTRY"
-
| Block_end ->
-
Format.fprintf fmt "BLOCK-END"
-
| Flow_sequence_start ->
-
Format.fprintf fmt "FLOW-SEQUENCE-START"
-
| Flow_sequence_end ->
-
Format.fprintf fmt "FLOW-SEQUENCE-END"
-
| Flow_mapping_start ->
-
Format.fprintf fmt "FLOW-MAPPING-START"
-
| Flow_mapping_end ->
-
Format.fprintf fmt "FLOW-MAPPING-END"
-
| Flow_entry ->
-
Format.fprintf fmt "FLOW-ENTRY"
-
| Key ->
-
Format.fprintf fmt "KEY"
-
| Value ->
-
Format.fprintf fmt "VALUE"
-
| Anchor name ->
-
Format.fprintf fmt "ANCHOR(%s)" name
-
| Alias name ->
-
Format.fprintf fmt "ALIAS(%s)" name
-
| Tag { handle; suffix } ->
-
Format.fprintf fmt "TAG(%s, %s)" handle suffix
-
| Scalar { style; value } ->
-
Format.fprintf fmt "SCALAR(%a, %S)" Scalar_style.pp style value
-
-
let pp fmt t = pp_token fmt t
-
-
let pp_spanned fmt { token; span } =
-
Format.fprintf fmt "%a at %a" pp_token token Span.pp span
-182
yaml/ocaml-yamle/lib/value.ml
···
-
(** JSON-compatible YAML value representation *)
-
-
type t = [
-
| `Null
-
| `Bool of bool
-
| `Float of float
-
| `String of string
-
| `A of t list
-
| `O of (string * t) list
-
]
-
-
(** Constructors *)
-
-
let null : t = `Null
-
let bool b : t = `Bool b
-
let int n : t = `Float (Float.of_int n)
-
let float f : t = `Float f
-
let string s : t = `String s
-
-
let list f xs : t = `A (List.map f xs)
-
let obj pairs : t = `O pairs
-
-
(** Type name for error messages *)
-
let type_name : t -> string = function
-
| `Null -> "null"
-
| `Bool _ -> "bool"
-
| `Float _ -> "float"
-
| `String _ -> "string"
-
| `A _ -> "array"
-
| `O _ -> "object"
-
-
(** Safe accessors (return option) *)
-
-
let as_null = function `Null -> Some () | _ -> None
-
let as_bool = function `Bool b -> Some b | _ -> None
-
let as_float = function `Float f -> Some f | _ -> None
-
let as_string = function `String s -> Some s | _ -> None
-
let as_list = function `A l -> Some l | _ -> None
-
let as_assoc = function `O o -> Some o | _ -> None
-
-
let as_int = function
-
| `Float f ->
-
let i = Float.to_int f in
-
if Float.equal (Float.of_int i) f then Some i else None
-
| _ -> None
-
-
(** Unsafe accessors (raise on type mismatch) *)
-
-
let to_null v =
-
match as_null v with
-
| Some () -> ()
-
| None -> Error.raise (Type_mismatch ("null", type_name v))
-
-
let to_bool v =
-
match as_bool v with
-
| Some b -> b
-
| None -> Error.raise (Type_mismatch ("bool", type_name v))
-
-
let to_float v =
-
match as_float v with
-
| Some f -> f
-
| None -> Error.raise (Type_mismatch ("float", type_name v))
-
-
let to_string v =
-
match as_string v with
-
| Some s -> s
-
| None -> Error.raise (Type_mismatch ("string", type_name v))
-
-
let to_list v =
-
match as_list v with
-
| Some l -> l
-
| None -> Error.raise (Type_mismatch ("array", type_name v))
-
-
let to_assoc v =
-
match as_assoc v with
-
| Some o -> o
-
| None -> Error.raise (Type_mismatch ("object", type_name v))
-
-
let to_int v =
-
match as_int v with
-
| Some i -> i
-
| None -> Error.raise (Type_mismatch ("int", type_name v))
-
-
(** Object access *)
-
-
let mem key = function
-
| `O pairs -> List.exists (fun (k, _) -> k = key) pairs
-
| _ -> false
-
-
let find key = function
-
| `O pairs -> List.assoc_opt key pairs
-
| _ -> None
-
-
let get key v =
-
match find key v with
-
| Some v -> v
-
| None -> Error.raise (Key_not_found key)
-
-
let keys = function
-
| `O pairs -> List.map fst pairs
-
| v -> Error.raise (Type_mismatch ("object", type_name v))
-
-
let values = function
-
| `O pairs -> List.map snd pairs
-
| v -> Error.raise (Type_mismatch ("object", type_name v))
-
-
(** Combinators *)
-
-
let combine v1 v2 =
-
match v1, v2 with
-
| `O o1, `O o2 -> `O (o1 @ o2)
-
| v1, _ -> Error.raise (Type_mismatch ("object", type_name v1))
-
-
let map f = function
-
| `A l -> `A (List.map f l)
-
| v -> Error.raise (Type_mismatch ("array", type_name v))
-
-
let filter pred = function
-
| `A l -> `A (List.filter pred l)
-
| v -> Error.raise (Type_mismatch ("array", type_name v))
-
-
(** Pretty printing *)
-
-
let rec pp fmt (v : t) =
-
match v with
-
| `Null -> Format.pp_print_string fmt "null"
-
| `Bool b -> Format.pp_print_bool fmt b
-
| `Float f ->
-
if Float.is_integer f && Float.abs f < 1e15 then
-
Format.fprintf fmt "%.0f" f
-
else
-
Format.fprintf fmt "%g" f
-
| `String s -> Format.fprintf fmt "%S" s
-
| `A [] -> Format.pp_print_string fmt "[]"
-
| `A items ->
-
Format.fprintf fmt "@[<hv 2>[@,%a@]@,]"
-
(Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ",@ ") pp)
-
items
-
| `O [] -> Format.pp_print_string fmt "{}"
-
| `O pairs ->
-
Format.fprintf fmt "@[<hv 2>{@,%a@]@,}"
-
(Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ",@ ")
-
(fun fmt (k, v) -> Format.fprintf fmt "@[<hv 2>%S:@ %a@]" k pp v))
-
pairs
-
-
(** Equality and comparison *)
-
-
let rec equal (a : t) (b : t) =
-
match a, b with
-
| `Null, `Null -> true
-
| `Bool a, `Bool b -> a = b
-
| `Float a, `Float b -> Float.equal a b
-
| `String a, `String b -> String.equal a b
-
| `A a, `A b -> List.equal equal a b
-
| `O a, `O b ->
-
List.length a = List.length b &&
-
List.for_all2 (fun (k1, v1) (k2, v2) -> k1 = k2 && equal v1 v2) a b
-
| _ -> false
-
-
let rec compare (a : t) (b : t) =
-
match a, b with
-
| `Null, `Null -> 0
-
| `Null, _ -> -1
-
| _, `Null -> 1
-
| `Bool a, `Bool b -> Bool.compare a b
-
| `Bool _, _ -> -1
-
| _, `Bool _ -> 1
-
| `Float a, `Float b -> Float.compare a b
-
| `Float _, _ -> -1
-
| _, `Float _ -> 1
-
| `String a, `String b -> String.compare a b
-
| `String _, _ -> -1
-
| _, `String _ -> 1
-
| `A a, `A b -> List.compare compare a b
-
| `A _, _ -> -1
-
| _, `A _ -> 1
-
| `O a, `O b ->
-
let cmp_pair (k1, v1) (k2, v2) =
-
let c = String.compare k1 k2 in
-
if c <> 0 then c else compare v1 v2
-
in
-
List.compare cmp_pair a b
-257
yaml/ocaml-yamle/lib/yaml.ml
···
-
(** Full YAML representation with anchors, tags, and aliases *)
-
-
type t = [
-
| `Scalar of Scalar.t
-
| `Alias of string
-
| `A of t Sequence.t
-
| `O of (t, t) Mapping.t
-
]
-
-
(** Pretty printing *)
-
-
let rec pp fmt (v : t) =
-
match v with
-
| `Scalar s -> Scalar.pp fmt s
-
| `Alias name -> Format.fprintf fmt "*%s" name
-
| `A seq -> Sequence.pp pp fmt seq
-
| `O map -> Mapping.pp pp pp fmt map
-
-
(** Equality *)
-
-
let rec equal (a : t) (b : t) =
-
match a, b with
-
| `Scalar a, `Scalar b -> Scalar.equal a b
-
| `Alias a, `Alias b -> String.equal a b
-
| `A a, `A b -> Sequence.equal equal a b
-
| `O a, `O b -> Mapping.equal equal equal a b
-
| _ -> false
-
-
(** Construct from JSON-compatible Value *)
-
-
let rec of_value (v : Value.t) : t =
-
match v with
-
| `Null -> `Scalar (Scalar.make "null")
-
| `Bool true -> `Scalar (Scalar.make "true")
-
| `Bool false -> `Scalar (Scalar.make "false")
-
| `Float f ->
-
let s =
-
if Float.is_integer f && Float.abs f < 1e15 then
-
Printf.sprintf "%.0f" f
-
else
-
Printf.sprintf "%g" f
-
in
-
`Scalar (Scalar.make s)
-
| `String s ->
-
`Scalar (Scalar.make s ~style:Scalar_style.Double_quoted)
-
| `A items ->
-
`A (Sequence.make (List.map of_value items))
-
| `O pairs ->
-
`O (Mapping.make (List.map (fun (k, v) ->
-
(`Scalar (Scalar.make k), of_value v)
-
) pairs))
-
-
(** Default limits for alias expansion (protection against billion laughs attack) *)
-
let default_max_alias_nodes = 10_000_000
-
let default_max_alias_depth = 100
-
-
(** Resolve aliases by replacing them with referenced nodes.
-
-
@param max_nodes Maximum number of nodes to create during expansion (default 10M)
-
@param max_depth Maximum depth of alias-within-alias resolution (default 100)
-
@raise Alias_expansion_node_limit if max_nodes is exceeded
-
@raise Alias_expansion_depth_limit if max_depth is exceeded
-
*)
-
let resolve_aliases ?(max_nodes = default_max_alias_nodes) ?(max_depth = default_max_alias_depth) (root : t) : t =
-
let anchors = Hashtbl.create 16 in
-
let node_count = ref 0 in
-
-
(* Check node limit *)
-
let check_node_limit () =
-
incr node_count;
-
if !node_count > max_nodes then
-
Error.raise (Alias_expansion_node_limit max_nodes)
-
in
-
-
(* First pass: collect all anchors *)
-
let rec collect (v : t) =
-
match v with
-
| `Scalar s ->
-
(match Scalar.anchor s with
-
| Some name -> Hashtbl.replace anchors name v
-
| None -> ())
-
| `Alias _ -> ()
-
| `A seq ->
-
(match Sequence.anchor seq with
-
| Some name -> Hashtbl.replace anchors name v
-
| None -> ());
-
List.iter collect (Sequence.members seq)
-
| `O map ->
-
(match Mapping.anchor map with
-
| Some name -> Hashtbl.replace anchors name v
-
| None -> ());
-
List.iter (fun (k, v) -> collect k; collect v) (Mapping.members map)
-
in
-
collect root;
-
-
(* Second pass: resolve aliases with depth tracking *)
-
let rec resolve ~depth (v : t) : t =
-
check_node_limit ();
-
match v with
-
| `Scalar _ -> v
-
| `Alias name ->
-
if depth >= max_depth then
-
Error.raise (Alias_expansion_depth_limit max_depth);
-
(match Hashtbl.find_opt anchors name with
-
| Some target -> resolve ~depth:(depth + 1) target
-
| None -> Error.raise (Undefined_alias name))
-
| `A seq ->
-
`A (Sequence.map (resolve ~depth) seq)
-
| `O map ->
-
`O (Mapping.make
-
?anchor:(Mapping.anchor map)
-
?tag:(Mapping.tag map)
-
~implicit:(Mapping.implicit map)
-
~style:(Mapping.style map)
-
(List.map (fun (k, v) -> (resolve ~depth k, resolve ~depth v)) (Mapping.members map)))
-
in
-
resolve ~depth:0 root
-
-
(** Convert scalar to JSON value based on content *)
-
let rec scalar_to_value s =
-
let value = Scalar.value s in
-
let tag = Scalar.tag s in
-
let style = Scalar.style s in
-
-
(* If explicitly tagged, respect the tag *)
-
match tag with
-
| Some "tag:yaml.org,2002:null" | Some "!!null" ->
-
`Null
-
| Some "tag:yaml.org,2002:bool" | Some "!!bool" ->
-
(match String.lowercase_ascii value with
-
| "true" | "yes" | "on" -> `Bool true
-
| "false" | "no" | "off" -> `Bool false
-
| _ -> Error.raise (Invalid_scalar_conversion (value, "bool")))
-
| Some "tag:yaml.org,2002:int" | Some "!!int" ->
-
(try `Float (Float.of_string value)
-
with _ -> Error.raise (Invalid_scalar_conversion (value, "int")))
-
| Some "tag:yaml.org,2002:float" | Some "!!float" ->
-
(try `Float (Float.of_string value)
-
with _ -> Error.raise (Invalid_scalar_conversion (value, "float")))
-
| Some "tag:yaml.org,2002:str" | Some "!!str" ->
-
`String value
-
| Some _ ->
-
(* Unknown tag - treat as string *)
-
`String value
-
| None ->
-
(* Implicit type resolution for plain scalars *)
-
if style <> Scalar_style.Plain then
-
`String value
-
else
-
infer_scalar_type value
-
-
(** Infer type from plain scalar value *)
-
and infer_scalar_type value =
-
let lower = String.lowercase_ascii value in
-
(* Null *)
-
if value = "" || lower = "null" || lower = "~" then
-
`Null
-
(* Boolean *)
-
else if lower = "true" || lower = "yes" || lower = "on" then
-
`Bool true
-
else if lower = "false" || lower = "no" || lower = "off" then
-
`Bool false
-
(* Special floats *)
-
else if lower = ".inf" || lower = "+.inf" then
-
`Float Float.infinity
-
else if lower = "-.inf" then
-
`Float Float.neg_infinity
-
else if lower = ".nan" then
-
`Float Float.nan
-
(* Try numeric *)
-
else
-
try_parse_number value
-
-
(** Try to parse as number *)
-
and try_parse_number value =
-
(* Try integer first *)
-
let try_int () =
-
if String.length value > 0 then
-
let first = value.[0] in
-
if first = '-' || first = '+' || (first >= '0' && first <= '9') then
-
try
-
(* Handle octal: 0o prefix or leading 0 *)
-
if String.length value > 2 && value.[0] = '0' then
-
match value.[1] with
-
| 'x' | 'X' ->
-
(* Hex *)
-
Some (`Float (Float.of_int (int_of_string value)))
-
| 'o' | 'O' ->
-
(* Octal *)
-
Some (`Float (Float.of_int (int_of_string value)))
-
| 'b' | 'B' ->
-
(* Binary *)
-
Some (`Float (Float.of_int (int_of_string value)))
-
| _ ->
-
(* Decimal with leading zero or octal in YAML 1.1 *)
-
Some (`Float (Float.of_string value))
-
else
-
Some (`Float (Float.of_string value))
-
with _ -> None
-
else None
-
else None
-
in
-
match try_int () with
-
| Some v -> v
-
| None ->
-
(* Try float *)
-
try
-
let f = Float.of_string value in
-
`Float f
-
with _ ->
-
(* Not a number - it's a string *)
-
`String value
-
-
(** Convert to JSON-compatible Value.
-
-
@param resolve_aliases_first Whether to resolve aliases before conversion (default true)
-
@param max_nodes Maximum nodes during alias expansion (default 10M)
-
@param max_depth Maximum alias nesting depth (default 100)
-
@raise Unresolved_alias if resolve_aliases_first is false and an alias is encountered
-
*)
-
let to_value
-
?(resolve_aliases_first = true)
-
?(max_nodes = default_max_alias_nodes)
-
?(max_depth = default_max_alias_depth)
-
(v : t) : Value.t =
-
let v = if resolve_aliases_first then resolve_aliases ~max_nodes ~max_depth v else v in
-
let rec convert (v : t) : Value.t =
-
match v with
-
| `Scalar s -> scalar_to_value s
-
| `Alias name -> Error.raise (Unresolved_alias name)
-
| `A seq -> `A (List.map convert (Sequence.members seq))
-
| `O map ->
-
`O (List.map (fun (k, v) ->
-
let key = match k with
-
| `Scalar s -> Scalar.value s
-
| _ -> Error.raise (Type_mismatch ("string key", "complex key"))
-
in
-
(key, convert v)
-
) (Mapping.members map))
-
in
-
convert v
-
-
(** Get anchor from any node *)
-
let anchor (v : t) =
-
match v with
-
| `Scalar s -> Scalar.anchor s
-
| `Alias _ -> None
-
| `A seq -> Sequence.anchor seq
-
| `O map -> Mapping.anchor map
-
-
(** Get tag from any node *)
-
let tag (v : t) =
-
match v with
-
| `Scalar s -> Scalar.tag s
-
| `Alias _ -> None
-
| `A seq -> Sequence.tag seq
-
| `O map -> Mapping.tag map
-185
yaml/ocaml-yamle/lib/yamle.ml
···
-
type value = Value.t
-
type yaml = Yaml.t
-
-
type version = [ `V1_1 | `V1_2 ]
-
-
type encoding = Encoding.t
-
type scalar_style = Scalar_style.t
-
type layout_style = Layout_style.t
-
-
(** {1 Error handling} *)
-
-
type error = Error.t
-
exception Yamle_error = Error.Yamle_error
-
-
(** {1 Alias expansion limits (protection against billion laughs attack)} *)
-
-
let default_max_alias_nodes = Yaml.default_max_alias_nodes
-
let default_max_alias_depth = Yaml.default_max_alias_depth
-
-
(** {1 JSON-compatible parsing} *)
-
-
let of_string
-
?(resolve_aliases = true)
-
?(max_nodes = default_max_alias_nodes)
-
?(max_depth = default_max_alias_depth)
-
s =
-
Loader.value_of_string ~resolve_aliases ~max_nodes ~max_depth s
-
-
let documents_of_string s = Loader.documents_of_string s
-
-
(** {1 JSON-compatible emission} *)
-
-
let to_string
-
?(encoding = Encoding.Utf8)
-
?(scalar_style = Scalar_style.Any)
-
?(layout_style = Layout_style.Any)
-
value =
-
let config = {
-
Emitter.default_config with
-
encoding;
-
scalar_style;
-
layout_style;
-
} in
-
Emitter.value_to_string ~config value
-
-
(** {1 YAML-specific parsing} *)
-
-
let yaml_of_string
-
?(resolve_aliases = false)
-
?(max_nodes = default_max_alias_nodes)
-
?(max_depth = default_max_alias_depth)
-
s =
-
Loader.yaml_of_string ~resolve_aliases ~max_nodes ~max_depth s
-
-
(** {1 YAML-specific emission} *)
-
-
let yaml_to_string
-
?(encoding = Encoding.Utf8)
-
?(scalar_style = Scalar_style.Any)
-
?(layout_style = Layout_style.Any)
-
yaml =
-
let config = {
-
Emitter.default_config with
-
encoding;
-
scalar_style;
-
layout_style;
-
} in
-
Emitter.yaml_to_string ~config yaml
-
-
let documents_to_string
-
?(encoding = Encoding.Utf8)
-
?(scalar_style = Scalar_style.Any)
-
?(layout_style = Layout_style.Any)
-
?(resolve_aliases = true)
-
documents =
-
let config = {
-
Emitter.default_config with
-
encoding;
-
scalar_style;
-
layout_style;
-
} in
-
Emitter.documents_to_string ~config ~resolve_aliases documents
-
-
(** {1 Conversion} *)
-
-
let to_json
-
?(resolve_aliases = true)
-
?(max_nodes = default_max_alias_nodes)
-
?(max_depth = default_max_alias_depth)
-
yaml =
-
Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml
-
-
let of_json value = Yaml.of_value value
-
-
(** {1 Pretty printing} *)
-
-
let pp = Value.pp
-
let pp_yaml = Yaml.pp
-
let equal = Value.equal
-
let equal_yaml = Yaml.equal
-
-
(** {1 Nested modules} *)
-
-
module Error = Error
-
module Position = Position
-
module Span = Span
-
module Encoding = Encoding
-
module Input = Input
-
module Scalar_style = Scalar_style
-
module Layout_style = Layout_style
-
module Chomping = Chomping
-
module Token = Token
-
module Scanner = Scanner
-
module Event = Event
-
module Parser = Parser
-
module Tag = Tag
-
module Value = Value
-
module Scalar = Scalar
-
module Sequence = Sequence
-
module Mapping = Mapping
-
module Yaml = Yaml
-
module Document = Document
-
module Loader = Loader
-
module Emitter = Emitter
-
-
(** {1 Streaming interface} *)
-
-
module Stream = struct
-
type parser = Parser.t
-
type emitter = Emitter.t
-
-
let parser s = Parser.of_string s
-
-
let do_parse p = Parser.next p
-
-
let emitter ?len:_ () = Emitter.create ()
-
-
let emit e ev = Emitter.emit e ev
-
-
let emitter_buf e = Emitter.contents e
-
-
(** Convenience event emitters *)
-
-
let stream_start e enc =
-
Emitter.emit e (Event.Stream_start { encoding = enc })
-
-
let stream_end e =
-
Emitter.emit e Event.Stream_end
-
-
let document_start ?version ?(implicit = true) e =
-
let version = match version with
-
| Some `V1_1 -> Some (1, 1)
-
| Some `V1_2 -> Some (1, 2)
-
| None -> None
-
in
-
Emitter.emit e (Event.Document_start { version; implicit })
-
-
let document_end ?(implicit = true) e =
-
Emitter.emit e (Event.Document_end { implicit })
-
-
let scalar s e =
-
Emitter.emit e (Event.Scalar {
-
anchor = Scalar.anchor s;
-
tag = Scalar.tag s;
-
value = Scalar.value s;
-
plain_implicit = Scalar.plain_implicit s;
-
quoted_implicit = Scalar.quoted_implicit s;
-
style = Scalar.style s;
-
})
-
-
let alias e name =
-
Emitter.emit e (Event.Alias { anchor = name })
-
-
let sequence_start ?anchor ?tag ?(implicit = true) ?(style = Layout_style.Any) e =
-
Emitter.emit e (Event.Sequence_start { anchor; tag; implicit; style })
-
-
let sequence_end e =
-
Emitter.emit e Event.Sequence_end
-
-
let mapping_start ?anchor ?tag ?(implicit = true) ?(style = Layout_style.Any) e =
-
Emitter.emit e (Event.Mapping_start { anchor; tag; implicit; style })
-
-
let mapping_end e =
-
Emitter.emit e Event.Mapping_end
-
end
-375
yaml/ocaml-yamle/tests/cram/anchors.t
···
-
Anchor and Alias Support (currently not supported)
-
-
These tests document anchor (&) and alias (*) support that is not yet
-
implemented. Currently, aliases fail with "unresolved alias" error.
-
-
Test: Simple scalar anchor and alias
-
-
$ echo 'anchor: &anc value
-
> alias: *anc' | yamlcat 2>&1
-
anchor: value
-
alias: value
-
-
Test: Numeric anchor and alias
-
-
$ echo 'original: &num 42
-
> copy: *num' | yamlcat 2>&1
-
original: 42
-
copy: 42
-
-
Test: Sequence anchor and alias
-
-
$ echo 'list: &items
-
> - one
-
> - two
-
> copy: *items' | yamlcat 2>&1
-
list:
-
- one
-
- two
-
copy:
-
- one
-
- two
-
-
Test: Mapping anchor and alias
-
-
$ echo 'person: &p
-
> name: Alice
-
> age: 30
-
> copy: *p' | yamlcat 2>&1
-
person:
-
name: Alice
-
age: 30
-
copy:
-
name: Alice
-
age: 30
-
-
Test: Multiple aliases to same anchor
-
-
$ echo 'value: &v 100
-
> first: *v
-
> second: *v
-
> third: *v' | yamlcat 2>&1
-
value: 100
-
first: 100
-
second: 100
-
third: 100
-
-
Test: Anchor in flow context
-
-
$ echo '[&item apple, *item]' | yamlcat 2>&1
-
- apple
-
- apple
-
-
Test: Anchor with mapping in flow
-
-
$ echo '{original: &cfg {a: 1}, copy: *cfg}' | yamlcat 2>&1
-
original:
-
a: 1
-
copy:
-
a: 1
-
-
Test: Anchors file from test suite
-
-
$ yamlcat ../yaml/anchors_basic.yml 2>&1
-
scalar_anchor: Hello, World!
-
scalar_alias: Hello, World!
-
---
-
original: 42
-
copy: 42
-
another_copy: 42
-
---
-
original_list:
-
- apple
-
- banana
-
- cherry
-
copied_list:
-
- apple
-
- banana
-
- cherry
-
---
-
original_map:
-
name: Alice
-
age: 30
-
city: London
-
copied_map:
-
name: Alice
-
age: 30
-
city: London
-
---
-
defaults:
-
timeout: 30
-
retries: 3
-
colors:
-
- red
-
- green
-
- blue
-
config:
-
settings:
-
timeout: 30
-
retries: 3
-
palette:
-
- red
-
- green
-
- blue
-
---
-
template:
-
metadata:
-
version: 1
-
author: John Doe
-
settings:
-
enabled: true
-
debug: false
-
instance1:
-
metadata:
-
version: 1
-
author: John Doe
-
settings:
-
enabled: true
-
debug: false
-
instance2:
-
metadata:
-
version: 1
-
author: John Doe
-
settings:
-
enabled: true
-
debug: false
-
---
-
items:
-
- id: 1
-
name: First
-
- id: 2
-
name: Second
-
- id: 1
-
name: First
-
---
-
shared_value: 100
-
calculations:
-
base: 100
-
doubled: 200
-
reference: 100
-
another_ref: 100
-
---
-
feature_flag: true
-
features:
-
login: true
-
signup: true
-
export: true
-
---
-
empty: null
-
values:
-
first: null
-
second: null
-
---
-
message: "This is a multi-line\nmessage with some\nspecial content!\n"
-
output1: "This is a multi-line\nmessage with some\nspecial content!\n"
-
output2: "This is a multi-line\nmessage with some\nspecial content!\n"
-
---
-
database:
-
primary:
-
host: localhost
-
port: 5432
-
ssl: true
-
replica:
-
host: localhost
-
port: 5432
-
ssl: true
-
backup:
-
host: localhost
-
port: 5432
-
ssl: true
-
-
$ yamlcat ../yaml/anchors_merge.yml 2>&1
-
defaults:
-
timeout: 30
-
retries: 3
-
verbose: false
-
production:
-
<<:
-
timeout: 30
-
retries: 3
-
verbose: false
-
environment: production
-
---
-
base:
-
color: red
-
size: medium
-
weight: 100
-
custom:
-
<<:
-
color: red
-
size: medium
-
weight: 100
-
color: blue
-
shape: circle
-
---
-
connection:
-
host: localhost
-
port: 8080
-
authentication:
-
username: admin
-
password: secret
-
server:
-
<<:
-
- host: localhost
-
port: 8080
-
- username: admin
-
password: secret
-
ssl: true
-
---
-
defaults:
-
timeout: 30
-
retries: 3
-
advanced:
-
cache: true
-
pool_size: 10
-
config:
-
<<:
-
- timeout: 30
-
retries: 3
-
- cache: true
-
pool_size: 10
-
timeout: 60
-
custom: value
-
---
-
base_style:
-
font: Arial
-
size: 12
-
heading_defaults:
-
<<:
-
font: Arial
-
size: 12
-
weight: bold
-
main_heading:
-
<<:
-
<<:
-
font: Arial
-
size: 12
-
weight: bold
-
size: 18
-
color: navy
-
---
-
common:
-
enabled: true
-
log_level: info
-
services:
-
- name: web
-
<<:
-
enabled: true
-
log_level: info
-
port: 80
-
- name: api
-
<<:
-
enabled: true
-
log_level: info
-
port: 3000
-
- name: worker
-
<<:
-
enabled: true
-
log_level: info
-
threads: 4
-
---
-
empty: {}
-
config:
-
<<: {}
-
key: value
-
---
-
metadata:
-
created: 2023-01-01
-
author: Admin
-
tags:
-
- v1
-
- stable
-
document:
-
<<:
-
created: 2023-01-01
-
author: Admin
-
tags:
-
- v1
-
- stable
-
title: Important Document
-
content: Some content here
-
---
-
level1:
-
a: 1
-
b: 2
-
level2:
-
<<:
-
a: 1
-
b: 2
-
c: 3
-
level3:
-
<<:
-
<<:
-
a: 1
-
b: 2
-
c: 3
-
d: 4
-
---
-
first:
-
name: First
-
value: 100
-
priority: low
-
second:
-
name: Second
-
value: 200
-
category: important
-
combined:
-
<<:
-
- name: First
-
value: 100
-
priority: low
-
- name: Second
-
value: 200
-
category: important
-
name: Combined
-
---
-
numbers:
-
count: 42
-
ratio: 3.14
-
active: true
-
derived:
-
<<:
-
count: 42
-
ratio: 3.14
-
active: true
-
label: Test
-
---
-
db_defaults:
-
pool_size: 5
-
timeout: 30
-
ssl: false
-
cache_defaults:
-
ttl: 3600
-
max_size: 1000
-
development:
-
database:
-
<<:
-
pool_size: 5
-
timeout: 30
-
ssl: false
-
host: localhost
-
name: dev_db
-
cache:
-
<<:
-
ttl: 3600
-
max_size: 1000
-
backend: memory
-
production:
-
database:
-
<<:
-
pool_size: 5
-
timeout: 30
-
ssl: false
-
host: prod.example.com
-
name: prod_db
-
ssl: true
-
pool_size: 20
-
cache:
-
<<:
-
ttl: 3600
-
max_size: 1000
-
backend: redis
-
ttl: 7200
-
-
Note: The anchor test files also use multiple documents, so they fail
-
with multi-document errors before even hitting anchor issues.
-119
yaml/ocaml-yamle/tests/cram/bomb.t
···
-
Billion laughs attack protection tests
-
-
Create a small bomb file for testing:
-
-
$ cat > bomb_small.yml << 'EOF'
-
> # Small "billion laughs" style YAML bomb for testing
-
> # Expands to 9^4 = 6561 nodes when aliases are resolved
-
> a: &a [1, 2, 3, 4, 5, 6, 7, 8, 9]
-
> b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a]
-
> c: &c [*b, *b, *b, *b, *b, *b, *b, *b, *b]
-
> d: &d [*c, *c, *c, *c, *c, *c, *c, *c, *c]
-
> EOF
-
-
Test with a tight node limit (small bomb would expand to ~6561 nodes):
-
-
$ yamlcat --max-nodes 100 --json bomb_small.yml
-
Error: alias expansion exceeded node limit (100 nodes)
-
[1]
-
-
Test with a limit that allows the small bomb:
-
-
$ yamlcat --max-nodes 10000 --json bomb_small.yml | head -c 100
-
{"a": [1, 2, 3, 4, 5, 6, 7, 8, 9], "b": [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9], [
-
-
Test depth limit with a nested alias chain:
-
-
$ cat > depth_bomb.yml << 'EOF'
-
> a: &a [x, y, z]
-
> b: &b [*a, *a]
-
> c: &c [*b, *b]
-
> d: &d [*c, *c]
-
> e: &e [*d, *d]
-
> result: *e
-
> EOF
-
-
$ yamlcat --max-depth 2 --json depth_bomb.yml
-
Error: alias expansion exceeded depth limit (2 levels)
-
[1]
-
-
$ yamlcat --max-depth 10 --json depth_bomb.yml | head -c 50
-
{"a": ["x", "y", "z"], "b": [["x", "y", "z"], ["x"
-
-
Test that --no-resolve-aliases keeps aliases as-is (in debug mode):
-
-
$ cat > simple_alias.yml << 'EOF'
-
> anchor: &anc hello
-
> alias: *anc
-
> EOF
-
-
$ yamlcat --no-resolve-aliases --debug simple_alias.yml
-
Document 1:
-
document(
-
implicit_start=true,
-
implicit_end=true,
-
root=mapping(
-
style=block,
-
members={
-
scalar("anchor", style=plain):
-
scalar("hello", anchor=anc, style=plain),
-
scalar("alias", style=plain): *anc
-
})
-
)
-
-
With resolve (default), aliases are expanded:
-
-
$ yamlcat --json simple_alias.yml
-
{"anchor": "hello", "alias": "hello"}
-
-
Create a full bomb (like the one in ocaml-yaml):
-
-
$ cat > bomb.yml << 'EOF'
-
> a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
-
> b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
-
> c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
-
> d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
-
> e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
-
> f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
-
> g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
-
> h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
-
> i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
-
> EOF
-
-
Test the full bomb is rejected with default limits:
-
-
$ yamlcat --json bomb.yml 2>&1 | head -1
-
Error: alias expansion exceeded node limit (10000000 nodes)
-
-
With a very small limit:
-
-
$ yamlcat --max-nodes 50 --json bomb.yml
-
Error: alias expansion exceeded node limit (50 nodes)
-
[1]
-
-
Test that valid YAML with aliases works:
-
-
$ cat > valid.yml << 'EOF'
-
> defaults: &defaults
-
> timeout: 30
-
> retries: 3
-
> production:
-
> <<: *defaults
-
> port: 8080
-
> EOF
-
-
$ yamlcat --json valid.yml
-
{"defaults": {"timeout": 30, "retries": 3}, "production": {"<<": {"timeout": 30, "retries": 3}, "port": 8080}}
-
-
Test help includes the new options:
-
-
$ yamlcat --help=plain | grep 'max-nodes'
-
--max-nodes=N (absent=10000000)
-
yamlcat --max-nodes 1000 --max-depth 10 untrusted.yaml
-
-
$ yamlcat --help=plain | grep 'max-depth'
-
--max-depth=N (absent=100)
-
yamlcat --max-nodes 1000 --max-depth 10 untrusted.yaml
-
-
$ yamlcat --help=plain | grep 'no-resolve-aliases'
-
--no-resolve-aliases
-6
yaml/ocaml-yamle/tests/cram/bomb_small.yml
···
-
# Small "billion laughs" style YAML bomb for testing
-
# Expands to 9^4 = 6561 nodes when aliases are resolved
-
a: &a [1, 2, 3, 4, 5, 6, 7, 8, 9]
-
b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a]
-
c: &c [*b, *b, *b, *b, *b, *b, *b, *b, *b]
-
d: &d [*c, *c, *c, *c, *c, *c, *c, *c, *c]
-863
yaml/ocaml-yamle/tests/cram/collections.t
···
-
Test collections_block.yml - Block style collections
-
-
$ yamlcat ../yaml/collections_block.yml
-
simple_sequence:
-
- apple
-
- banana
-
- cherry
-
- date
-
simple_mapping:
-
name: John Doe
-
age: 30
-
city: New York
-
country: USA
-
nested_sequences:
-
-
-
- alpha
-
- beta
-
- gamma
-
-
-
- one
-
- two
-
- three
-
-
-
- red
-
- green
-
- blue
-
nested_mappings:
-
person:
-
name: Alice
-
contact:
-
email: alice@example.com
-
phone: 555-1234
-
address:
-
street: 123 Main St
-
city: Boston
-
mapping_with_sequences:
-
colors:
-
- red
-
- green
-
- blue
-
sizes:
-
- small
-
- medium
-
- large
-
numbers:
-
- 1
-
- 2
-
- 3
-
sequence_with_mappings:
-
- name: Alice
-
age: 25
-
role: developer
-
- name: Bob
-
age: 30
-
role: designer
-
- name: Charlie
-
age: 35
-
role: manager
-
deep_nesting:
-
level1:
-
level2:
-
level3:
-
level4:
-
- deeply
-
- nested
-
- values
-
another_key: value
-
items:
-
- item1
-
- item2
-
metadata:
-
created: 2024-01-01
-
modified: 2024-12-04
-
complex_structure:
-
database:
-
connections:
-
- host: db1.example.com
-
port: 5432
-
credentials:
-
username: admin
-
password: secret
-
- host: db2.example.com
-
port: 5432
-
credentials:
-
username: readonly
-
password: public
-
services:
-
- name: api
-
endpoints:
-
- /users
-
- /posts
-
- /comments
-
config:
-
timeout: 30
-
retries: 3
-
- name: worker
-
tasks:
-
- email
-
- reports
-
config:
-
concurrency: 10
-
empty_collections:
-
empty_sequence: []
-
empty_mapping: {}
-
sequence_with_empty:
-
- value1
-
- []
-
- value2
-
mapping_with_empty:
-
key1: value1
-
key2: {}
-
key3: value3
-
-
Test collections_block.yml with JSON output
-
-
$ yamlcat --json ../yaml/collections_block.yml
-
{"simple_sequence": ["apple", "banana", "cherry", "date"], "simple_mapping": {"name": "John Doe", "age": 30, "city": "New York", "country": "USA"}, "nested_sequences": [["alpha", "beta", "gamma"], ["one", "two", "three"], ["red", "green", "blue"]], "nested_mappings": {"person": {"name": "Alice", "contact": {"email": "alice@example.com", "phone": "555-1234"}, "address": {"street": "123 Main St", "city": "Boston"}}}, "mapping_with_sequences": {"colors": ["red", "green", "blue"], "sizes": ["small", "medium", "large"], "numbers": [1, 2, 3]}, "sequence_with_mappings": [{"name": "Alice", "age": 25, "role": "developer"}, {"name": "Bob", "age": 30, "role": "designer"}, {"name": "Charlie", "age": 35, "role": "manager"}], "deep_nesting": {"level1": {"level2": {"level3": {"level4": ["deeply", "nested", "values"], "another_key": "value"}, "items": ["item1", "item2"]}, "metadata": {"created": "2024-01-01", "modified": "2024-12-04"}}}, "complex_structure": {"database": {"connections": [{"host": "db1.example.com", "port": 5432, "credentials": {"username": "admin", "password": "secret"}}, {"host": "db2.example.com", "port": 5432, "credentials": {"username": "readonly", "password": "public"}}]}, "services": [{"name": "api", "endpoints": ["/users", "/posts", "/comments"], "config": {"timeout": 30, "retries": 3}}, {"name": "worker", "tasks": ["email", "reports"], "config": {"concurrency": 10}}]}, "empty_collections": {"empty_sequence": [], "empty_mapping": {}, "sequence_with_empty": ["value1", [], "value2"], "mapping_with_empty": {"key1": "value1", "key2": {}, "key3": "value3"}}}
-
-
Test collections_block.yml with flow output
-
-
$ yamlcat --flow ../yaml/collections_block.yml
-
{simple_sequence: [apple, banana, cherry, date]simple_mapping, {name: John Doe, age: 30, city: New York, country: USA}, nested_sequences: [[alpha, beta, gamma], [one, two, three], [red, green, blue]]nested_mappings, {person: {name: Alice, contact: {email: alice@example.com, phone: 555-1234}address, {street: 123 Main St, city: Boston}}}, mapping_with_sequences: {colors: [red, green, blue]sizes, [small, medium, large], numbers: [1, 2, 3]}sequence_with_mappings, [{name: Alice, age: 25, role: developer}, {name: Bob, age: 30, role: designer}, {name: Charlie, age: 35, role: manager}], deep_nesting: {level1: {level2: {level3: {level4: [deeply, nested, values]another_key, value: }items, [item1, item2]}metadata, {created: 2024-01-01, modified: 2024-12-04}}}complex_structure, {database: {connections: [{host: db1.example.com, port: 5432, credentials: {username: admin, password: secret}}, {host: db2.example.com, port: 5432, credentials: {username: readonly, password: public}}]}services, [{name: api, endpoints: [/users, /posts, /comments]config, {timeout: 30, retries: 3}}, {name: worker, tasks: [email, reports]config, {concurrency: 10}}]}, empty_collections: {empty_sequence: []empty_mapping, {}, sequence_with_empty: [value1, [], value2]mapping_with_empty, {key1: value1, key2: {}key3, value3: }}}
-
-
Test collections_compact.yml - Compact notation
-
-
$ yamlcat ../yaml/collections_compact.yml
-
compact_sequence:
-
- name: Alice
-
age: 25
-
city: Boston
-
- name: Bob
-
age: 30
-
city: Seattle
-
- name: Charlie
-
age: 35
-
city: Portland
-
compact_nested:
-
- id: 1
-
details:
-
type: admin
-
permissions:
-
- read
-
- write
-
- delete
-
- id: 2
-
details:
-
type: user
-
permissions:
-
- read
-
compact_complex:
-
- key1: value1
-
key2: value2
-
nested:
-
sub1: val1
-
sub2: val2
-
- key1: value3
-
key2: value4
-
nested:
-
sub1: val3
-
sub2: val4
-
users:
-
- username: alice
-
email: alice@example.com
-
active: true
-
- username: bob
-
email: bob@example.com
-
active: false
-
compact_with_flow:
-
- name: service1
-
ports:
-
- 8080
-
- 8443
-
env:
-
DEBUG: true
-
MODE: production
-
- name: service2
-
ports:
-
- 3000
-
env:
-
DEBUG: false
-
MODE: development
-
deep_compact:
-
- category: electronics
-
items:
-
- name: laptop
-
specs:
-
cpu: Intel i7
-
ram: 16GB
-
storage: 512GB SSD
-
- name: phone
-
specs:
-
os: Android
-
ram: 8GB
-
storage: 256GB
-
- category: furniture
-
items:
-
- name: desk
-
dimensions:
-
width: 150cm
-
depth: 75cm
-
height: 75cm
-
- name: chair
-
dimensions:
-
width: 60cm
-
depth: 60cm
-
height: 120cm
-
mixed_compact:
-
databases:
-
- type: postgresql
-
connection:
-
host: localhost
-
port: 5432
-
credentials:
-
user: admin
-
password: secret
-
- type: mongodb
-
connection:
-
host: localhost
-
port: 27017
-
credentials:
-
user: root
-
password: root
-
single_line_compact:
-
- name: Alice
-
age: 25
-
role: developer
-
- name: Bob
-
age: 30
-
role: designer
-
- name: Charlie
-
age: 35
-
role: manager
-
sequences_in_compact:
-
- title: Project A
-
members:
-
- Alice
-
- Bob
-
- Charlie
-
tags:
-
- urgent
-
- backend
-
- title: Project B
-
members:
-
- David
-
- Eve
-
tags:
-
- frontend
-
- design
-
compact_with_empty:
-
- id: 1
-
data: []
-
meta: {}
-
- id: 2
-
data:
-
- item1
-
meta:
-
key: value
-
compact_complex_nesting:
-
- level: 1
-
children:
-
- level: 2a
-
children:
-
- level: 3a
-
value: leaf1
-
- level: 3b
-
value: leaf2
-
- level: 2b
-
children:
-
- level: 3c
-
value: leaf3
-
api_endpoints:
-
- path: /users
-
method: GET
-
auth: required
-
params:
-
- name: page
-
type: integer
-
default: 1
-
- name: limit
-
type: integer
-
default: 10
-
- path: '/users/:id'
-
method: GET
-
auth: required
-
params: []
-
- path: /users
-
method: POST
-
auth: required
-
body:
-
username: string
-
email: string
-
password: string
-
compact_types:
-
- string_val: hello
-
number_val: 42
-
float_val: 3.14
-
bool_val: true
-
null_val: null
-
- string_val: world
-
number_val: 100
-
float_val: 2.71
-
bool_val: false
-
null_val: null
-
minimal:
-
- a: 1
-
- b: 2
-
- c: 3
-
-
Test collections_compact.yml with JSON output
-
-
$ yamlcat --json ../yaml/collections_compact.yml
-
{"compact_sequence": [{"name": "Alice", "age": 25, "city": "Boston"}, {"name": "Bob", "age": 30, "city": "Seattle"}, {"name": "Charlie", "age": 35, "city": "Portland"}], "compact_nested": [{"id": 1, "details": {"type": "admin", "permissions": ["read", "write", "delete"]}}, {"id": 2, "details": {"type": "user", "permissions": ["read"]}}], "compact_complex": [{"key1": "value1", "key2": "value2", "nested": {"sub1": "val1", "sub2": "val2"}}, {"key1": "value3", "key2": "value4", "nested": {"sub1": "val3", "sub2": "val4"}}], "users": [{"username": "alice", "email": "alice@example.com", "active": true}, {"username": "bob", "email": "bob@example.com", "active": false}], "compact_with_flow": [{"name": "service1", "ports": [8080, 8443], "env": {"DEBUG": true, "MODE": "production"}}, {"name": "service2", "ports": [3000], "env": {"DEBUG": false, "MODE": "development"}}], "deep_compact": [{"category": "electronics", "items": [{"name": "laptop", "specs": {"cpu": "Intel i7", "ram": "16GB", "storage": "512GB SSD"}}, {"name": "phone", "specs": {"os": "Android", "ram": "8GB", "storage": "256GB"}}]}, {"category": "furniture", "items": [{"name": "desk", "dimensions": {"width": "150cm", "depth": "75cm", "height": "75cm"}}, {"name": "chair", "dimensions": {"width": "60cm", "depth": "60cm", "height": "120cm"}}]}], "mixed_compact": {"databases": [{"type": "postgresql", "connection": {"host": "localhost", "port": 5432}, "credentials": {"user": "admin", "password": "secret"}}, {"type": "mongodb", "connection": {"host": "localhost", "port": 27017}, "credentials": {"user": "root", "password": "root"}}]}, "single_line_compact": [{"name": "Alice", "age": 25, "role": "developer"}, {"name": "Bob", "age": 30, "role": "designer"}, {"name": "Charlie", "age": 35, "role": "manager"}], "sequences_in_compact": [{"title": "Project A", "members": ["Alice", "Bob", "Charlie"], "tags": ["urgent", "backend"]}, {"title": "Project B", "members": ["David", "Eve"], "tags": ["frontend", "design"]}], "compact_with_empty": [{"id": 1, "data": [], "meta": {}}, {"id": 2, "data": ["item1"], "meta": {"key": "value"}}], "compact_complex_nesting": [{"level": 1, "children": [{"level": "2a", "children": [{"level": "3a", "value": "leaf1"}, {"level": "3b", "value": "leaf2"}]}, {"level": "2b", "children": [{"level": "3c", "value": "leaf3"}]}]}], "api_endpoints": [{"path": "/users", "method": "GET", "auth": "required", "params": [{"name": "page", "type": "integer", "default": 1}, {"name": "limit", "type": "integer", "default": 10}]}, {"path": "/users/:id", "method": "GET", "auth": "required", "params": []}, {"path": "/users", "method": "POST", "auth": "required", "body": {"username": "string", "email": "string", "password": "string"}}], "compact_types": [{"string_val": "hello", "number_val": 42, "float_val": 3.14, "bool_val": true, "null_val": null}, {"string_val": "world", "number_val": 100, "float_val": 2.71, "bool_val": false, "null_val": null}], "minimal": [{"a": 1}, {"b": 2}, {"c": 3}]}
-
-
Test collections_compact.yml with flow output
-
-
$ yamlcat --flow ../yaml/collections_compact.yml
-
{compact_sequence: [{name: Alice, age: 25, city: Boston}, {name: Bob, age: 30, city: Seattle}, {name: Charlie, age: 35, city: Portland}]compact_nested, [{id: 1, details: {type: admin, permissions: [read, write, delete]}}, {id: 2, details: {type: user, permissions: [read]}}], compact_complex: [{key1: value1, key2: value2, nested: {sub1: val1, sub2: val2}}, {key1: value3, key2: value4, nested: {sub1: val3, sub2: val4}}]users, [{username: alice, email: alice@example.com, active: true}, {username: bob, email: bob@example.com, active: false}], compact_with_flow: [{name: service1, ports: [8080, 8443]env, {DEBUG: true, MODE: production}}, {name: service2, ports: [3000]env, {DEBUG: false, MODE: development}}]deep_compact, [{category: electronics, items: [{name: laptop, specs: {cpu: Intel i7, ram: 16GB, storage: 512GB SSD}}, {name: phone, specs: {os: Android, ram: 8GB, storage: 256GB}}]}, {category: furniture, items: [{name: desk, dimensions: {width: 150cm, depth: 75cm, height: 75cm}}, {name: chair, dimensions: {width: 60cm, depth: 60cm, height: 120cm}}]}], mixed_compact: {databases: [{type: postgresql, connection: {host: localhost, port: 5432}credentials, {user: admin, password: secret}}, {type: mongodb, connection: {host: localhost, port: 27017}credentials, {user: root, password: root}}]}single_line_compact, [{name: Alice, age: 25, role: developer}, {name: Bob, age: 30, role: designer}, {name: Charlie, age: 35, role: manager}], sequences_in_compact: [{title: Project A, members: [Alice, Bob, Charlie]tags, [urgent, backend]}, {title: Project B, members: [David, Eve]tags, [frontend, design]}]compact_with_empty, [{id: 1, data: []meta, {}}, {id: 2, data: [item1]meta, {key: value}}], compact_complex_nesting: [{level: 1, children: [{level: 2a, children: [{level: 3a, value: leaf1}, {level: 3b, value: leaf2}]}, {level: 2b, children: [{level: 3c, value: leaf3}]}]}]api_endpoints, [{path: /users, method: GET, auth: required, params: [{name: page, type: integer, default: 1}, {name: limit, type: integer, default: 10}]}, {path: '/users/:id', method: GET, auth: required, params: []}, {path: /users, method: POST, auth: required, body: {username: string, email: string, password: string}}], compact_types: [{string_val: hello, number_val: 42, float_val: 3.14, bool_val: true, null_val: null}, {string_val: world, number_val: 100, float_val: 2.71, bool_val: false, null_val: null}]minimal, [{a: 1}, {b: 2}, {c: 3}]}
-
-
Test collections_flow.yml - Flow style collections
-
-
$ yamlcat ../yaml/collections_flow.yml
-
simple_flow_sequence:
-
- apple
-
- banana
-
- cherry
-
- date
-
simple_flow_mapping:
-
name: John
-
age: 30
-
city: New York
-
nested_flow_sequences:
-
-
-
- a
-
- b
-
- c
-
-
-
- 1
-
- 2
-
- 3
-
-
-
- red
-
- green
-
- blue
-
nested_flow_mappings:
-
person:
-
name: Alice
-
age: 25
-
contact:
-
email: alice@example.com
-
phone: 555-1234
-
flow_seq_with_maps:
-
- name: Alice
-
role: dev
-
- name: Bob
-
role: ops
-
- name: Charlie
-
role: qa
-
flow_map_with_seqs:
-
colors:
-
- red
-
- green
-
- blue
-
sizes:
-
- S
-
- M
-
- L
-
numbers:
-
- 1
-
- 2
-
- 3
-
deep_flow_nesting:
-
level1:
-
level2:
-
level3:
-
level4:
-
- a
-
- b
-
- c
-
empty_flow:
-
empty_seq: []
-
empty_map: {}
-
both:
-
- []
-
- {}
-
flow_in_block:
-
sequence:
-
- 1
-
- 2
-
- 3
-
- 4
-
- 5
-
mapping:
-
a: 1
-
b: 2
-
c: 3
-
nested:
-
items:
-
- x
-
- y
-
- z
-
config:
-
timeout: 30
-
retries: 3
-
block_in_flow:
-
users:
-
- name: Alice
-
tags:
-
- dev
-
- senior
-
- name: Bob
-
tags:
-
- ops
-
- junior
-
mixed_structure:
-
services:
-
- name: api
-
ports:
-
- 8080
-
- 8443
-
env:
-
DEBUG: true
-
LOG_LEVEL: info
-
- name: db
-
ports:
-
- 5432
-
env:
-
POSTGRES_DB: mydb
-
POSTGRES_USER: admin
-
config:
-
version: 1
-
enabled: true
-
flow_types:
-
strings:
-
- hello
-
- world
-
- foo
-
- bar
-
numbers:
-
- 1
-
- 2
-
- 3
-
- 42
-
- 100
-
mixed:
-
- string
-
- 123
-
- true
-
- false
-
- null
-
quoted:
-
- with spaces
-
- 'special:chars'
-
- commas, here
-
flow_map_types:
-
string: value
-
number: 42
-
boolean: true
-
null_value: null
-
float: 3.14
-
nested_mixed:
-
- id: 1
-
data:
-
- a
-
- b
-
- c
-
meta:
-
type: first
-
- id: 2
-
data:
-
- d
-
- e
-
- f
-
meta:
-
type: second
-
- id: 3
-
data:
-
- g
-
- h
-
- i
-
meta:
-
type: third
-
multiline_flow:
-
long_sequence:
-
- item1
-
- item2
-
- item3
-
- item4
-
long_mapping:
-
key1: value1
-
key2: value2
-
key3: value3
-
edge_cases:
-
single_item_seq:
-
- alone
-
single_item_map:
-
only: one
-
nested_empty:
-
- []
-
-
-
- {}
-
-
-
- {}
-
- []
-
all_empty:
-
- {}
-
- []
-
- a: []
-
- b: {}
-
-
Test collections_flow.yml with JSON output
-
-
$ yamlcat --json ../yaml/collections_flow.yml
-
{"simple_flow_sequence": ["apple", "banana", "cherry", "date"], "simple_flow_mapping": {"name": "John", "age": 30, "city": "New York"}, "nested_flow_sequences": [["a", "b", "c"], [1, 2, 3], ["red", "green", "blue"]], "nested_flow_mappings": {"person": {"name": "Alice", "age": 25}, "contact": {"email": "alice@example.com", "phone": "555-1234"}}, "flow_seq_with_maps": [{"name": "Alice", "role": "dev"}, {"name": "Bob", "role": "ops"}, {"name": "Charlie", "role": "qa"}], "flow_map_with_seqs": {"colors": ["red", "green", "blue"], "sizes": ["S", "M", "L"], "numbers": [1, 2, 3]}, "deep_flow_nesting": {"level1": {"level2": {"level3": {"level4": ["a", "b", "c"]}}}}, "empty_flow": {"empty_seq": [], "empty_map": {}, "both": [[], {}]}, "flow_in_block": {"sequence": [1, 2, 3, 4, 5], "mapping": {"a": 1, "b": 2, "c": 3}, "nested": {"items": ["x", "y", "z"], "config": {"timeout": 30, "retries": 3}}}, "block_in_flow": {"users": [{"name": "Alice", "tags": ["dev", "senior"]}, {"name": "Bob", "tags": ["ops", "junior"]}]}, "mixed_structure": {"services": [{"name": "api", "ports": [8080, 8443], "env": {"DEBUG": true, "LOG_LEVEL": "info"}}, {"name": "db", "ports": [5432], "env": {"POSTGRES_DB": "mydb", "POSTGRES_USER": "admin"}}], "config": {"version": 1, "enabled": true}}, "flow_types": {"strings": ["hello", "world", "foo", "bar"], "numbers": [1, 2, 3, 42, 100], "mixed": ["string", 123, true, false, null], "quoted": ["with spaces", "special:chars", "commas, here"]}, "flow_map_types": {"string": "value", "number": 42, "boolean": true, "null_value": null, "float": 3.14}, "nested_mixed": [{"id": 1, "data": ["a", "b", "c"], "meta": {"type": "first"}}, {"id": 2, "data": ["d", "e", "f"], "meta": {"type": "second"}}, {"id": 3, "data": ["g", "h", "i"], "meta": {"type": "third"}}], "multiline_flow": {"long_sequence": ["item1", "item2", "item3", "item4"], "long_mapping": {"key1": "value1", "key2": "value2", "key3": "value3"}}, "edge_cases": {"single_item_seq": ["alone"], "single_item_map": {"only": "one"}, "nested_empty": [[], [{}], [{}, []]], "all_empty": [{}, [], {"a": []}, {"b": {}}]}}
-
-
Test collections_flow.yml with flow output
-
-
$ yamlcat --flow ../yaml/collections_flow.yml
-
{simple_flow_sequence: [apple, banana, cherry, date]simple_flow_mapping, {name: John, age: 30, city: New York}, nested_flow_sequences: [[a, b, c], [1, 2, 3], [red, green, blue]]nested_flow_mappings, {person: {name: Alice, age: 25}contact, {email: alice@example.com, phone: 555-1234}}, flow_seq_with_maps: [{name: Alice, role: dev}, {name: Bob, role: ops}, {name: Charlie, role: qa}]flow_map_with_seqs, {colors: [red, green, blue]sizes, [S, M, L], numbers: [1, 2, 3]}, deep_flow_nesting: {level1: {level2: {level3: {level4: [a, b, c]}}}}empty_flow, {empty_seq: []empty_map, {}, both: [[], {}]}, flow_in_block: {sequence: [1, 2, 3, 4, 5]mapping, {a: 1, b: 2, c: 3}, nested: {items: [x, y, z]config, {timeout: 30, retries: 3}}}block_in_flow, {users: [{name: Alice, tags: [dev, senior]}, {name: Bob, tags: [ops, junior]}]}, mixed_structure: {services: [{name: api, ports: [8080, 8443]env, {DEBUG: true, LOG_LEVEL: info}}, {name: db, ports: [5432]env, {POSTGRES_DB: mydb, POSTGRES_USER: admin}}]config, {version: 1, enabled: true}}flow_types, {strings: [hello, world, foo, bar]numbers, [1, 2, 3, 42, 100], mixed: [string, 123, true, false, null]quoted, [with spaces, 'special:chars', commas, here]}, flow_map_types: {string: value, number: 42, boolean: true, null_value: null, float: 3.14}nested_mixed, [{id: 1, data: [a, b, c]meta, {type: first}}, {id: 2, data: [d, e, f]meta, {type: second}}, {id: 3, data: [g, h, i]meta, {type: third}}], multiline_flow: {long_sequence: [item1, item2, item3, item4]long_mapping, {key1: value1, key2: value2, key3: value3}}edge_cases, {single_item_seq: [alone]single_item_map, {only: one}, nested_empty: [[], [{}], [{}, []]]all_empty, [{}, [], {a: []}, {b: {}}]}}
-
-
Inline test: Simple sequence
-
-
$ echo '- a
-
> - b
-
> - c' | yamlcat
-
- a
-
- b
-
- c
-
-
$ echo '- a
-
> - b
-
> - c' | yamlcat --json
-
["a", "b", "c"]
-
-
$ echo '- a
-
> - b
-
> - c' | yamlcat --flow
-
[a, b, c]
-
-
Inline test: Simple mapping
-
-
$ echo 'key1: value1
-
> key2: value2
-
> key3: value3' | yamlcat
-
key1: value1
-
key2: value2
-
key3: value3
-
-
$ echo 'key1: value1
-
> key2: value2
-
> key3: value3' | yamlcat --json
-
{"key1": "value1", "key2": "value2", "key3": "value3"}
-
-
$ echo 'key1: value1
-
> key2: value2
-
> key3: value3' | yamlcat --flow
-
{key1: value1, key2: value2, key3: value3}
-
-
Inline test: Nested sequences
-
-
$ echo 'outer:
-
> - - inner1
-
> - inner2
-
> - - inner3
-
> - inner4' | yamlcat
-
outer:
-
-
-
- inner1
-
- inner2
-
-
-
- inner3
-
- inner4
-
-
$ echo 'outer:
-
> - - inner1
-
> - inner2
-
> - - inner3
-
> - inner4' | yamlcat --json
-
{"outer": [["inner1", "inner2"], ["inner3", "inner4"]]}
-
-
$ echo 'outer:
-
> - - inner1
-
> - inner2
-
> - - inner3
-
> - inner4' | yamlcat --flow
-
{outer: [[inner1, inner2], [inner3, inner4]]}
-
-
Inline test: Nested mappings
-
-
$ echo 'level1:
-
> level2:
-
> level3:
-
> key: value' | yamlcat
-
level1:
-
level2:
-
level3:
-
key: value
-
-
$ echo 'level1:
-
> level2:
-
> level3:
-
> key: value' | yamlcat --json
-
{"level1": {"level2": {"level3": {"key": "value"}}}}
-
-
$ echo 'level1:
-
> level2:
-
> level3:
-
> key: value' | yamlcat --flow
-
{level1: {level2: {level3: {key: value}}}}
-
-
Inline test: Flow sequence
-
-
$ echo '[a, b, c]' | yamlcat
-
- a
-
- b
-
- c
-
-
$ echo '[a, b, c]' | yamlcat --json
-
["a", "b", "c"]
-
-
$ echo '[a, b, c]' | yamlcat --flow
-
[a, b, c]
-
-
Inline test: Flow mapping
-
-
$ echo '{a: 1, b: 2, c: 3}' | yamlcat
-
a: 1
-
b: 2
-
c: 3
-
-
$ echo '{a: 1, b: 2, c: 3}' | yamlcat --json
-
{"a": 1, "b": 2, "c": 3}
-
-
$ echo '{a: 1, b: 2, c: 3}' | yamlcat --flow
-
{a: 1, b: 2, c: 3}
-
-
Inline test: Nested flow collections
-
-
$ echo '[[1, 2], [3, 4], [5, 6]]' | yamlcat
-
-
-
- 1
-
- 2
-
-
-
- 3
-
- 4
-
-
-
- 5
-
- 6
-
-
$ echo '[[1, 2], [3, 4], [5, 6]]' | yamlcat --json
-
[[1, 2], [3, 4], [5, 6]]
-
-
$ echo '[[1, 2], [3, 4], [5, 6]]' | yamlcat --flow
-
[[1, 2], [3, 4], [5, 6]]
-
-
Inline test: Flow mapping with nested mapping
-
-
$ echo '{outer: {inner: value}}' | yamlcat
-
outer:
-
inner: value
-
-
$ echo '{outer: {inner: value}}' | yamlcat --json
-
{"outer": {"inner": "value"}}
-
-
$ echo '{outer: {inner: value}}' | yamlcat --flow
-
{outer: {inner: value}}
-
-
Inline test: Mixed block and flow (flow in block)
-
-
$ echo 'block_key:
-
> flow_seq: [1, 2, 3]
-
> flow_map: {a: 1, b: 2}' | yamlcat
-
block_key:
-
flow_seq:
-
- 1
-
- 2
-
- 3
-
flow_map:
-
a: 1
-
b: 2
-
-
$ echo 'block_key:
-
> flow_seq: [1, 2, 3]
-
> flow_map: {a: 1, b: 2}' | yamlcat --json
-
{"block_key": {"flow_seq": [1, 2, 3], "flow_map": {"a": 1, "b": 2}}}
-
-
$ echo 'block_key:
-
> flow_seq: [1, 2, 3]
-
> flow_map: {a: 1, b: 2}' | yamlcat --flow
-
{block_key: {flow_seq: [1, 2, 3]flow_map, {a: 1, b: 2}}}
-
-
Inline test: Mixed block and flow (block in flow)
-
-
$ echo '{users: [{name: Alice, age: 30}, {name: Bob, age: 25}]}' | yamlcat
-
users:
-
- name: Alice
-
age: 30
-
- name: Bob
-
age: 25
-
-
$ echo '{users: [{name: Alice, age: 30}, {name: Bob, age: 25}]}' | yamlcat --json
-
{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]}
-
-
$ echo '{users: [{name: Alice, age: 30}, {name: Bob, age: 25}]}' | yamlcat --flow
-
{users: [{name: Alice, age: 30}, {name: Bob, age: 25}]}
-
-
Inline test: Compact notation - sequence of mappings
-
-
$ echo '- name: Alice
-
> role: dev
-
> - name: Bob
-
> role: ops' | yamlcat
-
- name: Alice
-
role: dev
-
- name: Bob
-
role: ops
-
-
$ echo '- name: Alice
-
> role: dev
-
> - name: Bob
-
> role: ops' | yamlcat --json
-
[{"name": "Alice", "role": "dev"}, {"name": "Bob", "role": "ops"}]
-
-
$ echo '- name: Alice
-
> role: dev
-
> - name: Bob
-
> role: ops' | yamlcat --flow
-
[{name: Alice, role: dev}, {name: Bob, role: ops}]
-
-
Inline test: Compact with nested collections
-
-
$ echo '- id: 1
-
> tags: [a, b, c]
-
> config:
-
> enabled: true
-
> - id: 2
-
> tags: [x, y, z]
-
> config:
-
> enabled: false' | yamlcat
-
- id: 1
-
tags:
-
- a
-
- b
-
- c
-
config:
-
enabled: true
-
- id: 2
-
tags:
-
- x
-
- y
-
- z
-
config:
-
enabled: false
-
-
$ echo '- id: 1
-
> tags: [a, b, c]
-
> config:
-
> enabled: true
-
> - id: 2
-
> tags: [x, y, z]
-
> config:
-
> enabled: false' | yamlcat --json
-
[{"id": 1, "tags": ["a", "b", "c"], "config": {"enabled": true}}, {"id": 2, "tags": ["x", "y", "z"], "config": {"enabled": false}}]
-
-
$ echo '- id: 1
-
> tags: [a, b, c]
-
> config:
-
> enabled: true
-
> - id: 2
-
> tags: [x, y, z]
-
> config:
-
> enabled: false' | yamlcat --flow
-
[{id: 1, tags: [a, b, c]config, {enabled: true}}, {id: 2, tags: [x, y, z]config, {enabled: false}}]
-
-
Inline test: Empty collections
-
-
$ echo 'empty_seq: []
-
> empty_map: {}' | yamlcat
-
empty_seq: []
-
empty_map: {}
-
-
$ echo 'empty_seq: []
-
> empty_map: {}' | yamlcat --json
-
{"empty_seq": [], "empty_map": {}}
-
-
$ echo 'empty_seq: []
-
> empty_map: {}' | yamlcat --flow
-
{empty_seq: []empty_map, {}}
-
-
Inline test: Sequence with mapping values
-
-
$ echo 'items:
-
> - apple
-
> - banana
-
> config:
-
> mode: fast' | yamlcat
-
items:
-
- apple
-
- banana
-
config:
-
mode: fast
-
-
$ echo 'items:
-
> - apple
-
> - banana
-
> config:
-
> mode: fast' | yamlcat --json
-
{"items": ["apple", "banana"], "config": {"mode": "fast"}}
-
-
$ echo 'items:
-
> - apple
-
> - banana
-
> config:
-
> mode: fast' | yamlcat --flow
-
{items: [apple, banana]config, {mode: fast}}
-
-
Inline test: Complex nested structure
-
-
$ echo 'services:
-
> - name: web
-
> ports:
-
> - 80
-
> - 443
-
> env:
-
> DEBUG: false
-
> MODE: prod' | yamlcat
-
services:
-
- name: web
-
ports:
-
- 80
-
- 443
-
env:
-
DEBUG: false
-
MODE: prod
-
-
$ echo 'services:
-
> - name: web
-
> ports:
-
> - 80
-
> - 443
-
> env:
-
> DEBUG: false
-
> MODE: prod' | yamlcat --json
-
{"services": [{"name": "web", "ports": [80, 443], "env": {"DEBUG": false, "MODE": "prod"}}]}
-
-
$ echo 'services:
-
> - name: web
-
> ports:
-
> - 80
-
> - 443
-
> env:
-
> DEBUG: false
-
> MODE: prod' | yamlcat --flow
-
{services: [{name: web, ports: [80, 443]env, {DEBUG: false, MODE: prod}}]}
-
-
Inline test: Flow sequence with various types
-
-
$ echo '[string, 42, true, false, null, 3.14]' | yamlcat --json
-
["string", 42, true, false, null, 3.14]
-
-
Inline test: Flow mapping with various types
-
-
$ echo '{str: hello, num: 42, bool: true, nil: null, float: 3.14}' | yamlcat --json
-
{"str": "hello", "num": 42, "bool": true, "nil": null, "float": 3.14}
-197
yaml/ocaml-yamle/tests/cram/comments.t
···
-
Test comments.yml file with various comment styles
-
-
$ yamlcat ../yaml/comments.yml
-
name: John Doe
-
age: 30
-
address:
-
street: 123 Main St
-
city: Springfield
-
zip: 12345
-
items:
-
- apple
-
- banana
-
- cherry
-
- date
-
flow_seq:
-
- 1
-
- 2
-
- 3
-
flow_map:
-
key1: value1
-
key2: value2
-
nested:
-
level1:
-
level2:
-
value: deeply nested
-
multi_comment_key: value
-
special: 'value with # hash inside quotes'
-
empty_value: null
-
final_key: final_value
-
-
Test comments.yml roundtrip with JSON to verify parsed values
-
-
$ yamlcat --json ../yaml/comments.yml
-
{"name": "John Doe", "age": 30, "address": {"street": "123 Main St", "city": "Springfield", "zip": 12345}, "items": ["apple", "banana", "cherry", "date"], "flow_seq": [1, 2, 3], "flow_map": {"key1": "value1", "key2": "value2"}, "nested": {"level1": {"level2": {"value": "deeply nested"}}}, "multi_comment_key": "value", "special": "value with # hash inside quotes", "empty_value": null, "final_key": "final_value"}
-
-
Test full line comments are ignored
-
-
$ echo '# This is a full line comment
-
> name: Alice
-
> # Another comment
-
> age: 25' | yamlcat --json
-
{"name": "Alice", "age": 25}
-
-
Test end of line comments after scalars
-
-
$ echo 'name: Bob # This is an end of line comment
-
> age: 35 # Another end of line comment' | yamlcat --json
-
{"name": "Bob", "age": 35}
-
-
Test comments after sequence items
-
-
$ echo 'fruits:
-
> - apple # First fruit
-
> - banana # Second fruit
-
> - cherry # Third fruit' | yamlcat --json
-
{"fruits": ["apple", "banana", "cherry"]}
-
-
Test comments between sequence items
-
-
$ echo 'numbers:
-
> - 1
-
> # Comment between items
-
> - 2
-
> # Another comment
-
> - 3' | yamlcat --json
-
{"numbers": [1, 2, 3]}
-
-
Test comments after flow sequences
-
-
$ echo 'flow: [1, 2, 3] # Comment after flow sequence' | yamlcat --json
-
{"flow": [1, 2, 3]}
-
-
Test comments after flow mappings
-
-
$ echo 'flow: {a: 1, b: 2} # Comment after flow mapping' | yamlcat --json
-
{"flow": {"a": 1, "b": 2}}
-
-
Test comments between mapping entries
-
-
$ echo 'first: value1
-
> # Comment between entries
-
> second: value2
-
> # Another comment
-
> third: value3' | yamlcat --json
-
{"first": "value1", "second": "value2", "third": "value3"}
-
-
Test multiple consecutive comments
-
-
$ echo '# First comment
-
> # Second comment
-
> # Third comment
-
> key: value' | yamlcat --json
-
{"key": "value"}
-
-
Test comments in nested structures
-
-
$ echo 'outer:
-
> # Comment in nested level
-
> inner:
-
> # Comment in deeper level
-
> key: value # End of line comment' | yamlcat --json
-
{"outer": {"inner": {"key": "value"}}}
-
-
Test comments with special characters
-
-
$ echo '# Comment with !@#$%^&*()
-
> key: value' | yamlcat --json
-
{"key": "value"}
-
-
Test that hash in quoted strings is not treated as comment
-
-
$ echo 'text: "This # is not a comment"
-
> other: '"'"'Also # not a comment'"'"'' | yamlcat --json
-
{"text": "This # is not a comment", "other": "Also # not a comment"}
-
-
Test comment before empty value (null)
-
-
$ echo 'key: # Comment, value is null' | yamlcat --json
-
{"key": null}
-
-
Test comments at document start
-
-
$ echo '# Comment at very start
-
> # Another at start
-
> data: value' | yamlcat --json
-
{"data": "value"}
-
-
Test comments at document end
-
-
$ echo 'data: value
-
> # Comment at end
-
> # Another at end' | yamlcat --json
-
{"data": "value"}
-
-
Test comments with various indentation levels
-
-
$ echo 'level1:
-
> # Indented comment
-
> level2:
-
> # More indented comment
-
> value: data' | yamlcat --json
-
{"level1": {"level2": {"value": "data"}}}
-
-
Test empty lines with comments
-
-
$ echo 'first: 1
-
>
-
> # Comment after empty line
-
>
-
> second: 2' | yamlcat --json
-
{"first": 1, "second": 2}
-
-
Test comment after sequence with nested mapping
-
-
$ echo 'items:
-
> - name: item1 # Comment after nested value
-
> value: 10
-
> # Comment between sequence items
-
> - name: item2
-
> value: 20 # Another comment' | yamlcat --json
-
{"items": [{"name": "item1", "value": 10}, {"name": "item2", "value": 20}]}
-
-
Test comment only lines between complex structures
-
-
$ echo 'mapping1:
-
> key: value
-
> # Comment between mappings
-
> mapping2:
-
> key: value' | yamlcat --json
-
{"mapping1": {"key": "value"}, "mapping2": {"key": "value"}}
-
-
Test comments do not affect block scalars
-
-
$ echo 'literal: |
-
> # This looks like a comment
-
> but it is part of the literal text
-
> key: value' | yamlcat --json
-
{"literal": "# This looks like a comment\nbut it is part of the literal text\n", "key": "value"}
-
-
Test comments do not affect folded scalars
-
-
$ echo 'folded: >
-
> # This also looks like a comment
-
> but is part of folded text
-
> key: value' | yamlcat --json
-
{"folded": "# This also looks like a comment but is part of folded text\n", "key": "value"}
-
-
Test whitespace preservation around comments
-
-
$ echo 'key1: value1 # Comment with spaces' | yamlcat --json
-
{"key1": "value1"}
-
-
Test comment after colon but before value
-
-
$ echo 'key: # Comment before value
-
> value' | yamlcat --json
-
{"key": "value"}
-204
yaml/ocaml-yamle/tests/cram/documents.t
···
-
Test YAML directives and single document parsing
-
-
This test suite covers YAML directives (%YAML, %TAG) and various single document formats.
-
Multi-document streams are not yet supported and are not tested here.
-
-
Test 1: Basic YAML 1.2 directive
-
====================================
-
-
$ yamlcat ../yaml/directives.yml
-
version: '1.2'
-
content: This document uses YAML 1.2
-
-
Test 2: YAML 1.1 directive
-
====================================
-
-
$ yamlcat ../yaml/directives_yaml11.yml
-
version: '1.1'
-
content: This document uses YAML 1.1
-
booleans:
-
- true
-
- false
-
- true
-
- false
-
-
Test 3: TAG directive with custom prefix
-
====================================
-
-
$ yamlcat ../yaml/directives_tag.yml
-
shape:
-
radius: 5
-
color: red
-
point:
-
x: 10
-
y: 20
-
-
Test 4: Multiple TAG directives
-
====================================
-
-
$ yamlcat ../yaml/directives_multiple_tags.yml
-
user:
-
name: Alice
-
age: 30
-
location:
-
lat: 40.7128
-
lon: -74.006
-
config:
-
debug: true
-
timeout: 30
-
-
Test 5: Implicit document (no markers)
-
====================================
-
-
$ yamlcat ../yaml/documents_single.yml
-
key1: value1
-
key2: value2
-
nested:
-
inner: data
-
list:
-
- item1
-
- item2
-
- item3
-
-
Test 6: Explicit start marker (---)
-
====================================
-
-
$ yamlcat ../yaml/documents_single_explicit_start.yml
-
key1: value1
-
key2: value2
-
nested:
-
inner: data
-
list:
-
- item1
-
- item2
-
- item3
-
-
Test 7: Explicit start and end markers (--- ... )
-
====================================
-
-
$ yamlcat ../yaml/documents_single_explicit_both.yml
-
key1: value1
-
key2: value2
-
nested:
-
inner: data
-
list:
-
- item1
-
- item2
-
- item3
-
-
Test 8: Document with YAML directive
-
====================================
-
-
$ yamlcat ../yaml/documents_single_with_directive.yml
-
key1: value1
-
key2: value2
-
nested:
-
inner: data
-
list:
-
- item1
-
- item2
-
- item3
-
-
Test 9: Inline - implicit document (no markers)
-
====================================
-
-
$ echo 'name: John
-
> age: 30
-
> city: New York' | yamlcat
-
name: John
-
age: 30
-
city: New York
-
-
Test 10: Inline - explicit start marker
-
====================================
-
-
$ echo '---
-
> name: Jane
-
> age: 25' | yamlcat
-
name: Jane
-
age: 25
-
-
Test 11: Inline - explicit start and end markers
-
====================================
-
-
$ echo '---
-
> title: Example
-
> content: data
-
> ...' | yamlcat
-
title: Example
-
content: data
-
-
Test 12: Inline - document with %YAML 1.2 directive
-
====================================
-
-
$ echo '%YAML 1.2
-
> ---
-
> version: 1.2
-
> enabled: true' | yamlcat
-
version: 1.2
-
enabled: true
-
-
Test 13: Inline - document with comment before content
-
====================================
-
-
$ echo '# This is a comment
-
> name: Alice
-
> # Another comment
-
> value: 42' | yamlcat
-
name: Alice
-
value: 42
-
-
Test 14: Inline - document with comment after directive
-
====================================
-
-
$ echo '%YAML 1.2
-
> # Comment after directive
-
> ---
-
> key: value' | yamlcat
-
key: value
-
-
Test 15: Inline - explicit markers with comments
-
====================================
-
-
$ echo '--- # Document start
-
> data: content
-
> # Mid-document comment
-
> more: values
-
> ... # Document end' | yamlcat
-
data: content
-
more: values
-
-
Test 16: Verify JSON roundtrip for directive file
-
====================================
-
-
$ yamlcat --json ../yaml/directives.yml
-
{"version": "1.2", "content": "This document uses YAML 1.2"}
-
-
Test 17: Verify JSON roundtrip for explicit markers
-
====================================
-
-
$ yamlcat --json ../yaml/documents_single_explicit_both.yml
-
{"key1": "value1", "key2": "value2", "nested": {"inner": "data"}, "list": ["item1", "item2", "item3"]}
-
-
Test 18: Empty document with explicit markers
-
====================================
-
-
$ echo '---
-
> ...' | yamlcat
-
null
-
-
Test 19: Document with only whitespace and markers
-
====================================
-
-
$ echo '---
-
>
-
> ...' | yamlcat
-
null
-
-
Test 20: Directive followed by content without explicit start
-
====================================
-
-
$ echo '%YAML 1.2
-
> simple: document' | yamlcat
-
Error: expected document start '---' at line 2, columns 1-1
-
[1]
-4
yaml/ocaml-yamle/tests/cram/dune
···
-
(cram
-
(deps
-
(package yamle)
-
(glob_files ../yaml/*.yml)))
-49
yaml/ocaml-yamle/tests/cram/empty.t
···
-
Empty Collection YAML Emission
-
-
These tests verify that empty sequences and mappings are correctly emitted
-
as [] and {} in YAML output.
-
-
Test: Empty sequence
-
-
$ echo 'empty_seq: []' | yamlcat
-
empty_seq: []
-
-
Test: Empty mapping
-
-
$ echo 'empty_map: {}' | yamlcat
-
empty_map: {}
-
-
Test: Multiple empty collections
-
-
$ echo 'seq: []
-
> map: {}
-
> data: value' | yamlcat
-
seq: []
-
map: {}
-
data: value
-
-
Test: Nested empty collections
-
-
$ echo 'outer:
-
> inner_seq: []
-
> inner_map: {}' | yamlcat
-
outer:
-
inner_seq: []
-
inner_map: {}
-
-
Test: Empty collection in sequence
-
-
$ echo 'items:
-
> - first
-
> - []
-
> - third' | yamlcat
-
items:
-
- first
-
- []
-
- third
-
-
Test: Verify JSON output is correct (for comparison)
-
-
$ echo 'empty_seq: []
-
> empty_map: {}' | yamlcat --json
-
{"empty_seq": [], "empty_map": {}}
-45
yaml/ocaml-yamle/tests/cram/failing_escapes.t
···
-
Escape Sequence Issues (documentation of known edge cases)
-
-
These tests document escape sequence handling edge cases.
-
-
The primary issue is with \U (capital U) in double-quoted strings.
-
In YAML, \U is a 32-bit unicode escape that expects 8 hex digits.
-
When users write paths like "C:\Users" the \U is interpreted as
-
a unicode escape but "sers" are not valid hex digits.
-
-
Test: Capital U interpreted as unicode escape
-
-
$ echo 'path: "C:\\Users\\Name"' | yamlcat --json 2>&1
-
Error: invalid hex escape: at line 1, columns 12-12
-
[1]
-
-
This fails because:
-
- Shell: echo 'C:\\Users\\Name' produces C:\Users\Name
-
- YAML sees: "C:\Users\Name"
-
- \U is a 32-bit unicode escape (expects \UHHHHHHHH)
-
- "sers" are not 8 hex digits, so it fails
-
-
Test: Lowercase u unicode escape works
-
-
$ echo 'unicode: "\\u0041"' | yamlcat --json
-
{"unicode": "A"}
-
-
Test: Uppercase U requires 8 hex digits
-
-
$ echo 'unicode: "\\U00000041"' | yamlcat --json
-
{"unicode": "A"}
-
-
Test: Backslash escaping works for non-unicode
-
-
$ echo 'escaped: "one\\\\two\\\\three"' | yamlcat --json
-
{"escaped": "one\\two\\three"}
-
-
Test: Mixed valid escapes
-
-
$ echo 'text: "Tab:\\t Newline:\\n Quote:\\\\"' | yamlcat --json
-
{"text": "Tab:\t Newline:\n Quote:\\"}
-
-
Test: Backslash a is bell character
-
-
$ echo 'text: "test\\a"' | yamlcat --json
-
{"text": "test\007"}
-407
yaml/ocaml-yamle/tests/cram/multidoc.t
···
-
Multi-document stream support (currently not supported)
-
-
These tests document expected behavior for multi-document YAML streams.
-
They currently fail with "multiple documents found when single expected".
-
-
Test: Two documents separated by ---
-
-
$ echo '---
-
> first: document
-
> ---
-
> second: document' | yamlcat 2>&1
-
first: document
-
---
-
second: document
-
-
Test: Three documents with different types
-
-
$ echo '---
-
> mapping: value
-
> ---
-
> - sequence
-
> - items
-
> ---
-
> scalar value' | yamlcat 2>&1
-
mapping: value
-
---
-
- sequence
-
- items
-
---
-
scalar value
-
-
Test: Documents with explicit end markers
-
-
$ echo '---
-
> doc1: value
-
> ...
-
> ---
-
> doc2: value
-
> ...' | yamlcat 2>&1
-
doc1: value
-
---
-
doc2: value
-
-
Test: Empty documents
-
-
$ echo '---
-
> ---
-
> content: here
-
> ---' | yamlcat 2>&1
-
null
-
---
-
content: here
-
---
-
null
-
-
Test: Multi-document file
-
-
$ yamlcat ../yaml/documents_multi.yml 2>&1
-
document: first
-
type: mapping
-
data:
-
key1: value1
-
key2: value2
-
---
-
document: second
-
type: mapping
-
data:
-
key3: value3
-
key4: value4
-
-
$ yamlcat ../yaml/documents_multi_three.yml 2>&1
-
name: John Doe
-
age: 30
-
city: New York
-
---
-
- apple
-
- banana
-
- orange
-
- grape
-
---
-
This is a plain scalar document
-
-
$ yamlcat ../yaml/documents_multi_with_end.yml 2>&1
-
first:
-
document: data1
-
value: 100
-
---
-
second:
-
document: data2
-
value: 200
-
---
-
third:
-
document: data3
-
value: 300
-
-
$ yamlcat ../yaml/documents_multi_empty.yml 2>&1
-
null
-
---
-
key: value
-
---
-
null
-
---
-
- item1
-
- item2
-
-
Test: Anchors file (uses multiple documents)
-
-
$ yamlcat ../yaml/anchors_basic.yml 2>&1
-
scalar_anchor: Hello, World!
-
scalar_alias: Hello, World!
-
---
-
original: 42
-
copy: 42
-
another_copy: 42
-
---
-
original_list:
-
- apple
-
- banana
-
- cherry
-
copied_list:
-
- apple
-
- banana
-
- cherry
-
---
-
original_map:
-
name: Alice
-
age: 30
-
city: London
-
copied_map:
-
name: Alice
-
age: 30
-
city: London
-
---
-
defaults:
-
timeout: 30
-
retries: 3
-
colors:
-
- red
-
- green
-
- blue
-
config:
-
settings:
-
timeout: 30
-
retries: 3
-
palette:
-
- red
-
- green
-
- blue
-
---
-
template:
-
metadata:
-
version: 1
-
author: John Doe
-
settings:
-
enabled: true
-
debug: false
-
instance1:
-
metadata:
-
version: 1
-
author: John Doe
-
settings:
-
enabled: true
-
debug: false
-
instance2:
-
metadata:
-
version: 1
-
author: John Doe
-
settings:
-
enabled: true
-
debug: false
-
---
-
items:
-
- id: 1
-
name: First
-
- id: 2
-
name: Second
-
- id: 1
-
name: First
-
---
-
shared_value: 100
-
calculations:
-
base: 100
-
doubled: 200
-
reference: 100
-
another_ref: 100
-
---
-
feature_flag: true
-
features:
-
login: true
-
signup: true
-
export: true
-
---
-
empty: null
-
values:
-
first: null
-
second: null
-
---
-
message: "This is a multi-line\nmessage with some\nspecial content!\n"
-
output1: "This is a multi-line\nmessage with some\nspecial content!\n"
-
output2: "This is a multi-line\nmessage with some\nspecial content!\n"
-
---
-
database:
-
primary:
-
host: localhost
-
port: 5432
-
ssl: true
-
replica:
-
host: localhost
-
port: 5432
-
ssl: true
-
backup:
-
host: localhost
-
port: 5432
-
ssl: true
-
-
$ yamlcat ../yaml/anchors_merge.yml 2>&1
-
defaults:
-
timeout: 30
-
retries: 3
-
verbose: false
-
production:
-
<<:
-
timeout: 30
-
retries: 3
-
verbose: false
-
environment: production
-
---
-
base:
-
color: red
-
size: medium
-
weight: 100
-
custom:
-
<<:
-
color: red
-
size: medium
-
weight: 100
-
color: blue
-
shape: circle
-
---
-
connection:
-
host: localhost
-
port: 8080
-
authentication:
-
username: admin
-
password: secret
-
server:
-
<<:
-
- host: localhost
-
port: 8080
-
- username: admin
-
password: secret
-
ssl: true
-
---
-
defaults:
-
timeout: 30
-
retries: 3
-
advanced:
-
cache: true
-
pool_size: 10
-
config:
-
<<:
-
- timeout: 30
-
retries: 3
-
- cache: true
-
pool_size: 10
-
timeout: 60
-
custom: value
-
---
-
base_style:
-
font: Arial
-
size: 12
-
heading_defaults:
-
<<:
-
font: Arial
-
size: 12
-
weight: bold
-
main_heading:
-
<<:
-
<<:
-
font: Arial
-
size: 12
-
weight: bold
-
size: 18
-
color: navy
-
---
-
common:
-
enabled: true
-
log_level: info
-
services:
-
- name: web
-
<<:
-
enabled: true
-
log_level: info
-
port: 80
-
- name: api
-
<<:
-
enabled: true
-
log_level: info
-
port: 3000
-
- name: worker
-
<<:
-
enabled: true
-
log_level: info
-
threads: 4
-
---
-
empty: {}
-
config:
-
<<: {}
-
key: value
-
---
-
metadata:
-
created: 2023-01-01
-
author: Admin
-
tags:
-
- v1
-
- stable
-
document:
-
<<:
-
created: 2023-01-01
-
author: Admin
-
tags:
-
- v1
-
- stable
-
title: Important Document
-
content: Some content here
-
---
-
level1:
-
a: 1
-
b: 2
-
level2:
-
<<:
-
a: 1
-
b: 2
-
c: 3
-
level3:
-
<<:
-
<<:
-
a: 1
-
b: 2
-
c: 3
-
d: 4
-
---
-
first:
-
name: First
-
value: 100
-
priority: low
-
second:
-
name: Second
-
value: 200
-
category: important
-
combined:
-
<<:
-
- name: First
-
value: 100
-
priority: low
-
- name: Second
-
value: 200
-
category: important
-
name: Combined
-
---
-
numbers:
-
count: 42
-
ratio: 3.14
-
active: true
-
derived:
-
<<:
-
count: 42
-
ratio: 3.14
-
active: true
-
label: Test
-
---
-
db_defaults:
-
pool_size: 5
-
timeout: 30
-
ssl: false
-
cache_defaults:
-
ttl: 3600
-
max_size: 1000
-
development:
-
database:
-
<<:
-
pool_size: 5
-
timeout: 30
-
ssl: false
-
host: localhost
-
name: dev_db
-
cache:
-
<<:
-
ttl: 3600
-
max_size: 1000
-
backend: memory
-
production:
-
database:
-
<<:
-
pool_size: 5
-
timeout: 30
-
ssl: false
-
host: prod.example.com
-
name: prod_db
-
ssl: true
-
pool_size: 20
-
cache:
-
<<:
-
ttl: 3600
-
max_size: 1000
-
backend: redis
-
ttl: 7200
-471
yaml/ocaml-yamle/tests/cram/scalars.t
···
-
YAML Scalar Parsing Tests
-
-
This file tests various forms of YAML scalar values including plain, quoted, and block scalars.
-
-
================================================================================
-
PLAIN SCALARS
-
================================================================================
-
-
Simple plain scalars
-
-
$ echo 'key: value' | yamlcat
-
key: value
-
-
$ echo 'name: Alice
-
> age: 30
-
> active: true' | yamlcat
-
name: Alice
-
age: 30
-
active: true
-
-
Plain scalars with special values
-
-
$ echo 'null_val: null
-
> bool_true: true
-
> bool_false: false
-
> number: 42
-
> float: 3.14' | yamlcat --json
-
{"null_val": null, "bool_true": true, "bool_false": false, "number": 42, "float": 3.14}
-
-
================================================================================
-
QUOTED SCALARS - SINGLE QUOTES
-
================================================================================
-
-
Single-quoted strings preserve literal text
-
-
$ echo "single: 'hello world'" | yamlcat
-
single: hello world
-
-
Single-quoted strings with embedded double quotes
-
-
$ echo "quote: 'He said \"hello\"'" | yamlcat
-
quote: "He said \"hello\""
-
-
Single-quoted strings with escaped single quotes (doubled)
-
-
$ echo "escaped: 'It''s a test'" | yamlcat
-
escaped: It's a test
-
-
Single-quoted multiline (newlines become spaces)
-
-
$ echo "text: 'This is a
-
> multi-line
-
> string'" | yamlcat --json
-
{"text": "This is a multi-line string"}
-
-
Empty single-quoted string
-
-
$ echo "empty: ''" | yamlcat
-
empty: ''
-
-
================================================================================
-
QUOTED SCALARS - DOUBLE QUOTES
-
================================================================================
-
-
Simple double-quoted strings
-
-
$ echo 'double: "hello world"' | yamlcat
-
double: hello world
-
-
Double-quoted with escaped newline
-
-
$ echo 'text: "Line one\nLine two"' | yamlcat --json
-
{"text": "Line one Line two"}
-
-
Double-quoted with escaped tab
-
-
$ echo 'text: "Col1\tCol2\tCol3"' | yamlcat --json
-
{"text": "Col1\tCol2\tCol3"}
-
-
Double-quoted with backslash escape
-
-
$ echo 'path: "C:\\Users\\Name"' | yamlcat --json
-
Error: invalid hex escape: at line 1, columns 12-12
-
[1]
-
-
Double-quoted with escaped quote
-
-
$ echo 'text: "She said \"hello\""' | yamlcat --json
-
{"text": "She said \"hello\""}
-
-
Double-quoted with multiple escape sequences
-
-
$ echo 'text: "Tab:\t Newline:\n Quote:\" Backslash:\\\\"' | yamlcat --json
-
{"text": "Tab:\t Newline: Quote:\" Backslash:\\"}
-
-
Empty double-quoted string
-
-
$ echo 'empty: ""' | yamlcat
-
empty: ''
-
-
================================================================================
-
BLOCK SCALARS - LITERAL STYLE (|)
-
================================================================================
-
-
Basic literal block scalar (preserves newlines)
-
-
$ echo 'text: |
-
> line one
-
> line two
-
> line three' | yamlcat --json
-
{"text": "line one\nline two\nline three\n"}
-
-
Literal with indentation
-
-
$ echo 'text: |
-
> First line
-
> Indented line
-
> Back to first' | yamlcat --json
-
{"text": "First line\n Indented line\nBack to first\n"}
-
-
Literal with blank lines
-
-
$ echo 'text: |
-
> First paragraph
-
>
-
> Second paragraph' | yamlcat --json
-
{"text": "First paragraph\n\nSecond paragraph\n"}
-
-
================================================================================
-
BLOCK SCALARS - FOLDED STYLE (>)
-
================================================================================
-
-
Basic folded block scalar (newlines become spaces)
-
-
$ echo 'text: >
-
> This is a long paragraph
-
> that will be folded into
-
> a single line.' | yamlcat --json
-
{"text": "This is a long paragraph that will be folded into a single line.\n"}
-
-
Folded with paragraph separation (blank line preserved)
-
-
$ echo 'text: >
-
> First paragraph
-
> flows together.
-
>
-
> Second paragraph
-
> also flows.' | yamlcat --json
-
{"text": "First paragraph flows together.\nSecond paragraph also flows.\n"}
-
-
================================================================================
-
CHOMPING INDICATORS
-
================================================================================
-
-
Strip chomping (-) removes trailing newlines
-
-
$ echo 'text: |-
-
> No trailing newline' | yamlcat --json
-
{"text": "No trailing newline"}
-
-
$ echo 'text: |-
-
> Text here
-
>
-
> ' | yamlcat --json
-
{"text": "Text here"}
-
-
Folded with strip
-
-
$ echo 'text: >-
-
> Folded text
-
> with stripped
-
> trailing newlines
-
>
-
> ' | yamlcat --json
-
{"text": "Folded text with stripped trailing newlines"}
-
-
Clip chomping (default) keeps single trailing newline
-
-
$ echo 'text: |
-
> One trailing newline
-
>
-
> ' | yamlcat --json
-
{"text": "One trailing newline\n"}
-
-
$ echo 'text: >
-
> Folded with one
-
> trailing newline
-
>
-
> ' | yamlcat --json
-
{"text": "Folded with one trailing newline\n"}
-
-
Keep chomping (+) preserves all trailing newlines
-
-
$ echo 'text: |+
-
> Keeps trailing newlines
-
>
-
>
-
> ' | yamlcat --json
-
{"text": "Keeps trailing newlines\n\n\n\n"}
-
-
$ echo 'text: >+
-
> Folded text
-
> keeps trailing
-
>
-
>
-
> ' | yamlcat --json
-
{"text": "Folded text keeps trailing\n\n\n\n"}
-
-
================================================================================
-
EXPLICIT INDENTATION INDICATORS
-
================================================================================
-
-
Literal with explicit 2-space indentation
-
-
$ echo 'text: |2
-
> Two space base
-
> Second line
-
> Extra indent' | yamlcat --json
-
{"text": " Two space base\n Second line\n Extra indent\n"}
-
-
Folded with explicit indentation
-
-
$ echo 'text: >2
-
> Text with two space
-
> base indentation that
-
> will be folded.' | yamlcat --json
-
{"text": "Text with two space base indentation that will be folded.\n"}
-
-
Combined indentation and chomping indicators
-
-
$ echo 'text: |2-
-
> Indented by 2
-
> No trailing newlines
-
>
-
> ' | yamlcat --json
-
{"text": " Indented by 2\n No trailing newlines"}
-
-
$ echo 'text: |2+
-
> Indented by 2
-
> Keeps trailing newlines
-
>
-
>
-
> ' | yamlcat --json
-
{"text": " Indented by 2\n Keeps trailing newlines\n\n\n\n"}
-
-
================================================================================
-
FILE TESTS - QUOTED SCALARS
-
================================================================================
-
-
Test parsing scalars_quoted.yml file
-
-
$ yamlcat ../yaml/scalars_quoted.yml | head -20
-
single_simple: hello world
-
single_with_double: "He said \"hello\""
-
single_escaped_quote: 'It''s a single quote: ''example'''
-
single_multiline: This is a multi-line single quoted string
-
double_simple: hello world
-
double_with_single: It's easy
-
double_escaped_quote: "She said \"hello\""
-
escaped_newline: "Line one\nLine two\nLine three"
-
escaped_tab: "Column1\tColumn2\tColumn3"
-
escaped_backslash: "Path: C:\\Users\\Name"
-
escaped_carriage: "Before\rAfter"
-
escaped_bell: "Bell\x07"
-
escaped_backspace: "Back\x08"
-
escaped_formfeed: "Form\x0c"
-
escaped_vertical: "Vertical\x0btab"
-
unicode_16bit: 'Snowman: ☃'
-
unicode_32bit: 'Emoji: 😀'
-
unicode_hex: "Null byte: \x00"
-
empty_single: ''
-
empty_double: ''
-
-
Test JSON output for quoted scalars
-
-
$ yamlcat --json ../yaml/scalars_quoted.yml | head -c 500
-
{"single_simple": "hello world", "single_with_double": "He said \"hello\"", "single_escaped_quote": "It's a single quote: 'example'", "single_multiline": "This is a multi-line single quoted string", "double_simple": "hello world", "double_with_single": "It's easy", "double_escaped_quote": "She said \"hello\"", "escaped_newline": "Line one\nLine two\nLine three", "escaped_tab": "Column1\tColumn2\tColumn3", "escaped_backslash": "Path: C:\\Users\\Name", "escaped_carriage": "Before\rAfter", "escaped
-
-
Verify specific escape handling in JSON
-
-
$ yamlcat --json ../yaml/scalars_quoted.yml | grep -o '"escaped_newline": "[^"]*"'
-
"escaped_newline": "Line one\nLine two\nLine three"
-
-
$ yamlcat --json ../yaml/scalars_quoted.yml | grep -o '"escaped_tab": "[^"]*"'
-
"escaped_tab": "Column1\tColumn2\tColumn3"
-
-
Verify Unicode handling
-
-
$ yamlcat --json ../yaml/scalars_quoted.yml | grep -o '"unicode_16bit": "[^"]*"'
-
"unicode_16bit": "Snowman: \226\152\131"
-
-
$ yamlcat --json ../yaml/scalars_quoted.yml | grep -o '"unicode_32bit": "[^"]*"'
-
"unicode_32bit": "Emoji: \240\159\152\128"
-
-
Verify quoted strings preserve type indicators
-
-
$ yamlcat --json ../yaml/scalars_quoted.yml | grep -o '"string_true": "[^"]*"'
-
"string_true": "true"
-
-
$ yamlcat --json ../yaml/scalars_quoted.yml | grep -o '"string_null": "[^"]*"'
-
"string_null": "null"
-
-
$ yamlcat --json ../yaml/scalars_quoted.yml | grep -o '"string_number": "[^"]*"'
-
"string_number": "123"
-
-
================================================================================
-
FILE TESTS - BLOCK SCALARS
-
================================================================================
-
-
Test parsing scalars_block.yml file
-
-
$ yamlcat ../yaml/scalars_block.yml | head -30
-
literal_basic: "Line one\nLine two\nLine three\n"
-
literal_with_indent: "First line\n Indented line\n More indented\n Back to second level\nBack to first level\n"
-
folded_basic: "This is a long paragraph that will be folded into a single line with the newlines converted to spaces.\n"
-
folded_paragraph: "First paragraph flows together into a single line.\nSecond paragraph after blank line also flows together.\n"
-
literal_strip: No trailing newline
-
literal_strip_multiple: Text here
-
folded_strip: Folded text with stripped trailing newlines
-
literal_clip: "One trailing newline\n"
-
literal_clip_explicit: "This is the default behavior\n"
-
folded_clip: "Folded with one trailing newline\n"
-
literal_keep: "Keeps trailing newlines\n\n\n"
-
literal_keep_multiple: "Text here\n\n\n"
-
folded_keep: "Folded text keeps trailing\n\n\n"
-
literal_indent_2: " Two space indentation\n is preserved here\n Extra indent\n Back to two\n"
-
literal_indent_4: " Four space base indent\n Second line\n Extra indent\n Back to base\n"
-
folded_indent_2: "Text with two space base indentation that will be folded.\n"
-
folded_indent_3: "Three space indent for this folded text block.\n"
-
literal_indent_strip: " Indented by 2\n No trailing newlines"
-
folded_indent_strip: Folded with indent and stripped end
-
literal_indent_keep: " Indented by 2\n Keeps trailing newlines\n\n\n"
-
folded_indent_keep: "Folded indent 4 keeps all trailing\n\n\n"
-
empty_literal: "\nempty_folded: >\n\n# Block scalar with only newlines\nonly_newlines_literal: |\n\n\nonly_newlines_folded: >\n\n\n# Complex indentation patterns\ncomplex_literal: |\nFirst level\n Second level\n Third level\n Back to second\nBack to first\n\nNew paragraph\n With indent\n\nFinal paragraph\n"
-
complex_folded: "This paragraph flows together.\nThis is separate. This line starts more indented and continues.\nFinal thoughts here.\n"
-
special_chars_literal: "Special: @#$%^&*()\nQuotes: \"double\" 'single'\nBrackets: [array] {object}\nSymbols: | > & * ? : -\n"
-
special_chars_folded: "All special chars are literal in block scalars: []{}|>*&\n"
-
sequence_with_blocks:
-
- "First item\nliteral block\n"
-
- "Second item folded block\n"
-
- "Third item\nstripped"
-
- "Fourth item\nkept\n\n\n"
-
-
Test JSON output for block scalars
-
-
$ yamlcat --json ../yaml/scalars_block.yml | grep -o '"literal_basic": "[^"]*"'
-
"literal_basic": "Line one\nLine two\nLine three\n"
-
-
$ yamlcat --json ../yaml/scalars_block.yml | grep -o '"folded_basic": "[^"]*"' | head -c 100
-
"folded_basic": "This is a long paragraph that will be folded into a single line with the newlines c
-
-
Verify strip chomping
-
-
$ yamlcat --json ../yaml/scalars_block.yml | grep -o '"literal_strip": "[^"]*"'
-
"literal_strip": "No trailing newline"
-
-
$ yamlcat --json ../yaml/scalars_block.yml | grep -o '"folded_strip": "[^"]*"'
-
"folded_strip": "Folded text with stripped trailing newlines"
-
-
Verify clip chomping (single newline)
-
-
$ yamlcat --json ../yaml/scalars_block.yml | grep -o '"literal_clip": "[^"]*"'
-
"literal_clip": "One trailing newline\n"
-
-
Verify keep chomping (all newlines)
-
-
$ yamlcat --json ../yaml/scalars_block.yml | grep -o '"literal_keep": "[^"]*"'
-
"literal_keep": "Keeps trailing newlines\n\n\n"
-
-
$ yamlcat --json ../yaml/scalars_block.yml | grep -o '"folded_keep": "[^"]*"'
-
"folded_keep": "Folded text keeps trailing\n\n\n"
-
-
Verify indentation handling
-
-
$ yamlcat --json ../yaml/scalars_block.yml | grep -o '"literal_indent_2": "[^"]*"'
-
"literal_indent_2": " Two space indentation\n is preserved here\n Extra indent\n Back to two\n"
-
-
Verify nested structures with block scalars
-
-
$ yamlcat ../yaml/scalars_block.yml | tail -10
-
special_chars_folded: "All special chars are literal in block scalars: []{}|>*&\n"
-
sequence_with_blocks:
-
- "First item\nliteral block\n"
-
- "Second item folded block\n"
-
- "Third item\nstripped"
-
- "Fourth item\nkept\n\n\n"
-
nested:
-
description: "This is a folded description that spans multiple lines.\n"
-
code: "def hello():\n print(\"Hello, World!\")\n return True\n"
-
notes: "Final notes\nwith stripped end"
-
-
================================================================================
-
SPECIAL CASES AND EDGE CASES
-
================================================================================
-
-
Empty block scalars
-
-
$ echo 'empty_literal: |' | yamlcat --json
-
{"empty_literal": ""}
-
-
$ echo 'empty_folded: >' | yamlcat --json
-
{"empty_folded": ""}
-
-
Block scalars with special characters (no escaping needed)
-
-
$ echo 'code: |
-
> Special: @#$%^&*()
-
> Quotes: "double" '"'"'single'"'"'
-
> Brackets: [array] {object}' | yamlcat --json
-
{"code": "Special: @#$%^&*()\nQuotes: \"double\" 'single'\nBrackets: [array] {object}\n"}
-
-
Plain scalar vs quoted string for special values
-
-
$ echo 'unquoted_true: true
-
> quoted_true: "true"' | yamlcat --json
-
{"unquoted_true": true, "quoted_true": "true"}
-
-
$ echo 'unquoted_null: null
-
> quoted_null: "null"' | yamlcat --json
-
{"unquoted_null": null, "quoted_null": "null"}
-
-
Strings that need quoting to preserve leading/trailing spaces
-
-
$ echo 'leading: " spaces"
-
> trailing: "spaces "
-
> both: " spaces "' | yamlcat --json
-
{"leading": " spaces", "trailing": "spaces ", "both": " spaces "}
-
-
Block scalars in sequences
-
-
$ echo 'items:
-
> - |
-
> First item
-
> multiline
-
> - >
-
> Second item
-
> folded' | yamlcat --json
-
{"items": ["First item\nmultiline\n", "Second item folded\n"]}
-
-
Block scalars in nested mappings
-
-
$ echo 'outer:
-
> inner:
-
> description: >
-
> This is a folded
-
> description.
-
> code: |
-
> def test():
-
> return True' | yamlcat --json
-
{"outer": {"inner": {"description": "This is a folded description.\n", "code": "def test():\n return True\n"}}}
-
-
Preserving indentation in literal blocks
-
-
$ echo 'code: |
-
> def hello():
-
> print("Hello")
-
> if True:
-
> return 42' | yamlcat --json
-
{"code": "def hello():\n print(\"Hello\")\n if True:\n return 42\n"}
-
-
Folded scalars preserve more-indented lines
-
-
$ echo 'text: >
-
> Normal paragraph
-
> continues here.
-
>
-
> Indented block
-
> preserved.
-
>
-
> Back to normal.' | yamlcat --json
-
{"text": "Normal paragraph continues here.\nIndented block preserved.\nBack to normal.\n"}
-60
yaml/ocaml-yamle/tests/cram/tags.t
···
-
Tag Support Tests
-
-
These tests verify YAML tag support including type coercion and
-
different tag formats.
-
-
Test: String tag shorthand
-
-
$ printf '!!str 123' | yamlcat
-
'123'
-
-
The !!str tag forces the value to be treated as a string.
-
-
Test: Integer tag shorthand
-
-
$ printf '!!int "42"' | yamlcat
-
42
-
-
The !!int tag coerces the quoted string to an integer.
-
-
Test: Boolean tag shorthand
-
-
$ printf '!!bool "yes"' | yamlcat
-
true
-
-
The !!bool tag coerces the string to a boolean.
-
-
Test: Null tag shorthand
-
-
$ printf '!!null ""' | yamlcat
-
null
-
-
The !!null tag coerces the value to null.
-
-
Test: Float tag shorthand
-
-
$ printf '!!float 3.14' | yamlcat
-
3.14
-
-
The !!float tag specifies a floating-point number.
-
-
Test: Tag shorthand in mapping value
-
-
$ printf 'value: !!str 42' | yamlcat
-
value: '42'
-
-
Tags work in mapping values and force type coercion.
-
-
Test: Local tags
-
-
$ printf '!local_tag value' | yamlcat
-
value
-
-
Local tags (single !) are treated as unknown and default to string type.
-
-
Test: Verbatim tags
-
-
$ printf '!<tag:example.com:type> value' | yamlcat
-
value
-
-
Verbatim tags (!<...>) are treated as unknown and default to string type.
-444
yaml/ocaml-yamle/tests/cram/values.t
···
-
Test YAML null values from values_null.yml
-
-
$ yamlcat ../yaml/values_null.yml
-
explicit_null: null
-
tilde_null: null
-
empty_null: null
-
flow_null:
-
- null
-
- null
-
- null
-
sequence_nulls:
-
- null
-
- null
-
- null
-
- explicit: null
-
- tilde: null
-
- empty: null
-
mapping_nulls:
-
key1: null
-
key2: null
-
key3: null
-
"null": null key with string value
-
"~": tilde key with string value
-
nested:
-
level1:
-
null_value: null
-
tilde_value: null
-
empty_value: null
-
list:
-
- null
-
- null
-
- null
-
- some_value
-
map:
-
a: null
-
b: null
-
c: null
-
string_nulls:
-
quoted_null: 'null'
-
quoted_tilde: '~'
-
null_in_string: this is null
-
word_null: 'null'
-
-
$ yamlcat --json ../yaml/values_null.yml
-
{"explicit_null": null, "tilde_null": null, "empty_null": null, "flow_null": [null, null, null], "sequence_nulls": [null, null, null, {"explicit": null}, {"tilde": null}, {"empty": null}], "mapping_nulls": {"key1": null, "key2": null, "key3": null}, "null": "null key with string value", "~": "tilde key with string value", "nested": {"level1": {"null_value": null, "tilde_value": null, "empty_value": null, "list": [null, null, null, "some_value"], "map": {"a": null, "b": null, "c": null}}}, "string_nulls": {"quoted_null": "null", "quoted_tilde": "~", "null_in_string": "this is null", "word_null": "null"}}
-
-
Test YAML boolean values from values_bool.yml
-
-
$ yamlcat ../yaml/values_bool.yml
-
bool_true: true
-
bool_false: false
-
capitalized_true: true
-
capitalized_false: false
-
yes_value: true
-
no_value: false
-
Yes_value: true
-
No_value: false
-
YES_value: true
-
NO_value: false
-
on_value: true
-
off_value: false
-
On_value: true
-
Off_value: false
-
ON_value: true
-
OFF_value: false
-
bool_sequence:
-
- true
-
- false
-
- true
-
- false
-
- true
-
- false
-
flow_bools:
-
- true
-
- false
-
- true
-
- false
-
bool_mapping:
-
active: true
-
disabled: false
-
enabled: true
-
stopped: false
-
quoted_bools:
-
quoted_true: 'true'
-
quoted_false: 'false'
-
quoted_yes: 'yes'
-
quoted_no: 'no'
-
single_true: 'true'
-
single_false: 'false'
-
nested_bools:
-
settings:
-
debug: true
-
verbose: false
-
legacy_yes: true
-
legacy_no: false
-
flags:
-
- true
-
- false
-
- true
-
- false
-
mixed_case:
-
"TRUE": true
-
"FALSE": false
-
"TrUe": true
-
"FaLsE": false
-
bool_like_strings:
-
truthy: truely
-
falsy: falsetto
-
yes_sir: yessir
-
no_way: noway
-
-
$ yamlcat --json ../yaml/values_bool.yml
-
{"bool_true": true, "bool_false": false, "capitalized_true": true, "capitalized_false": false, "yes_value": true, "no_value": false, "Yes_value": true, "No_value": false, "YES_value": true, "NO_value": false, "on_value": true, "off_value": false, "On_value": true, "Off_value": false, "ON_value": true, "OFF_value": false, "bool_sequence": [true, false, true, false, true, false], "flow_bools": [true, false, true, false], "bool_mapping": {"active": true, "disabled": false, "enabled": true, "stopped": false}, "quoted_bools": {"quoted_true": "true", "quoted_false": "false", "quoted_yes": "yes", "quoted_no": "no", "single_true": "true", "single_false": "false"}, "nested_bools": {"settings": {"debug": true, "verbose": false, "legacy_yes": true, "legacy_no": false}, "flags": [true, false, true, false]}, "mixed_case": {"TRUE": true, "FALSE": false, "TrUe": true, "FaLsE": false}, "bool_like_strings": {"truthy": "truely", "falsy": "falsetto", "yes_sir": "yessir", "no_way": "noway"}}
-
-
Test YAML number values from values_numbers.yml
-
-
$ yamlcat ../yaml/values_numbers.yml
-
int_zero: 0
-
int_positive: 42
-
int_negative: -17
-
int_large: 1000000
-
int_with_underscores: 1000000
-
octal_value: 12
-
octal_zero: 0
-
octal_large: 511
-
hex_lowercase: 26
-
hex_uppercase: 26
-
hex_mixed: 3735928559
-
hex_zero: 0
-
float_simple: 3.14
-
float_negative: -0.5
-
float_zero: 0
-
float_leading_dot: 0.5
-
float_trailing_zero: 1
-
scientific_positive: 10000000000
-
scientific_negative: 0.0015
-
scientific_uppercase: 250
-
scientific_no_sign: 300000
-
positive_infinity: .inf
-
negative_infinity: -.inf
-
not_a_number: .nan
-
infinity_upper: .inf
-
infinity_caps: .inf
-
nan_upper: .nan
-
nan_caps: .nan
-
number_sequence:
-
- 0
-
- 42
-
- -17
-
- 3.14
-
- 10000000000
-
- .inf
-
- .nan
-
flow_numbers:
-
- 0
-
- 42
-
- -17
-
- 3.14
-
- 26
-
- 12
-
number_mapping:
-
count: 100
-
price: 19.99
-
discount: -5
-
hex_color: 16734003
-
octal_perms: 493
-
scientific: 6.022e+23
-
quoted_numbers:
-
string_int: '42'
-
string_float: '3.14'
-
string_hex: '0x1A'
-
string_octal: 0o14
-
string_inf: '.inf'
-
string_nan: '.nan'
-
numeric_strings:
-
phone: 555-1234
-
version: 1.2.3
-
code: 123
-
leading_zero: 7
-
plus_sign: 123
-
edge_cases:
-
min_int: -9.22337e+18
-
max_int: 9.22337e+18
-
very_small: 1e-100
-
very_large: 1e+100
-
negative_zero: -0
-
positive_zero: 0
-
nested_numbers:
-
coordinates:
-
x: 10.5
-
y: -20.3
-
z: 0
-
measurements:
-
- 1.1
-
- 2.2
-
- 3.3
-
stats:
-
count: 1000
-
average: 45.67
-
max: .inf
-
min: -.inf
-
legacy_octal: 14
-
binary_like: 10
-
format_tests:
-
no_decimal: 42
-
with_decimal: 42
-
leading_zero_decimal: 0.42
-
no_leading_digit: 0.42
-
trailing_decimal: 42
-
-
$ yamlcat --json ../yaml/values_numbers.yml
-
{"int_zero": 0, "int_positive": 42, "int_negative": -17, "int_large": 1000000, "int_with_underscores": 1000000, "octal_value": 12, "octal_zero": 0, "octal_large": 511, "hex_lowercase": 26, "hex_uppercase": 26, "hex_mixed": 3735928559, "hex_zero": 0, "float_simple": 3.14, "float_negative": -0.5, "float_zero": 0, "float_leading_dot": 0.5, "float_trailing_zero": 1, "scientific_positive": 10000000000, "scientific_negative": 0.0015, "scientific_uppercase": 250, "scientific_no_sign": 300000, "positive_infinity": inf, "negative_infinity": -inf, "not_a_number": nan, "infinity_upper": inf, "infinity_caps": inf, "nan_upper": nan, "nan_caps": nan, "number_sequence": [0, 42, -17, 3.14, 10000000000, inf, nan], "flow_numbers": [0, 42, -17, 3.14, 26, 12], "number_mapping": {"count": 100, "price": 19.99, "discount": -5, "hex_color": 16734003, "octal_perms": 493, "scientific": 6.022e+23}, "quoted_numbers": {"string_int": "42", "string_float": "3.14", "string_hex": "0x1A", "string_octal": "0o14", "string_inf": ".inf", "string_nan": ".nan"}, "numeric_strings": {"phone": "555-1234", "version": "1.2.3", "code": 123, "leading_zero": 7, "plus_sign": 123}, "edge_cases": {"min_int": -9.22337e+18, "max_int": 9.22337e+18, "very_small": 1e-100, "very_large": 1e+100, "negative_zero": -0, "positive_zero": 0}, "nested_numbers": {"coordinates": {"x": 10.5, "y": -20.3, "z": 0}, "measurements": [1.1, 2.2, 3.3], "stats": {"count": 1000, "average": 45.67, "max": inf, "min": -inf}}, "legacy_octal": 14, "binary_like": 10, "format_tests": {"no_decimal": 42, "with_decimal": 42, "leading_zero_decimal": 0.42, "no_leading_digit": 0.42, "trailing_decimal": 42}}
-
-
Test YAML timestamp values from values_timestamps.yml
-
-
$ yamlcat ../yaml/values_timestamps.yml
-
date_simple: 2001-12-15
-
date_earliest: 1970-01-01
-
date_leap_year: 2020-02-29
-
date_current: 2025-12-04
-
datetime_utc: '2001-12-15T02:59:43.1Z'
-
datetime_utc_full: '2001-12-15T02:59:43.123456Z'
-
datetime_utc_no_frac: '2001-12-15T02:59:43Z'
-
datetime_offset_pos: '2001-12-15T02:59:43.1+05:30'
-
datetime_offset_neg: '2001-12-15T02:59:43.1-05:00'
-
datetime_offset_hours: '2001-12-15T02:59:43+05'
-
datetime_spaced: '2001-12-14 21:59:43.10 -5'
-
datetime_spaced_utc: '2001-12-15 02:59:43.1 Z'
-
datetime_spaced_offset: '2001-12-14 21:59:43.10 -05:00'
-
datetime_no_frac: '2001-12-15T14:30:00Z'
-
date_only: 2001-12-15
-
timestamp_formats:
-
iso_date: 2001-12-15
-
iso_datetime_z: '2001-12-15T02:59:43Z'
-
iso_datetime_offset: '2001-12-15T02:59:43+00:00'
-
spaced_datetime: '2001-12-14 21:59:43.10 -5'
-
canonical: '2001-12-15T02:59:43.1Z'
-
timestamp_sequence:
-
- 2001-12-15
-
- '2001-12-15T02:59:43.1Z'
-
- '2001-12-14 21:59:43.10 -5'
-
- '2025-01-01T00:00:00Z'
-
events:
-
created: '2001-12-15T02:59:43.1Z'
-
modified: '2001-12-16T10:30:00Z'
-
published: '2001-12-14 21:59:43.10 -5'
-
quoted_timestamps:
-
string_date: 2001-12-15
-
string_datetime: '2001-12-15T02:59:43.1Z'
-
string_spaced: '2001-12-14 21:59:43.10 -5'
-
edge_cases:
-
midnight: '2001-12-15T00:00:00Z'
-
end_of_day: '2001-12-15T23:59:59Z'
-
microseconds: '2001-12-15T02:59:43.123456Z'
-
no_seconds: '2001-12-15T02:59Z'
-
hour_only: 2001-12-15T02Z
-
nested_timestamps:
-
project:
-
start_date: 2001-12-15
-
milestones:
-
- date: 2001-12-20
-
time: '2001-12-20T14:00:00Z'
-
- date: 2002-01-15
-
time: '2002-01-15T09:30:00-05:00'
-
metadata:
-
created: '2001-12-14 21:59:43.10 -5'
-
updated: '2001-12-15T02:59:43.1Z'
-
invalid_timestamps:
-
bad_date: 2001-13-45
-
bad_time: '2001-12-15T25:99:99Z'
-
incomplete: 2001-12
-
no_leading_zero: 2001-1-5
-
timezones:
-
utc_z: '2001-12-15T02:59:43Z'
-
utc_offset: '2001-12-15T02:59:43+00:00'
-
est: '2001-12-14T21:59:43-05:00'
-
ist: '2001-12-15T08:29:43+05:30'
-
jst: '2001-12-15T11:59:43+09:00'
-
date_range:
-
past: 1900-01-01
-
unix_epoch: '1970-01-01T00:00:00Z'
-
y2k: '2000-01-01T00:00:00Z'
-
present: 2025-12-04
-
future: '2099-12-31T23:59:59Z'
-
-
$ yamlcat --json ../yaml/values_timestamps.yml
-
{"date_simple": "2001-12-15", "date_earliest": "1970-01-01", "date_leap_year": "2020-02-29", "date_current": "2025-12-04", "datetime_utc": "2001-12-15T02:59:43.1Z", "datetime_utc_full": "2001-12-15T02:59:43.123456Z", "datetime_utc_no_frac": "2001-12-15T02:59:43Z", "datetime_offset_pos": "2001-12-15T02:59:43.1+05:30", "datetime_offset_neg": "2001-12-15T02:59:43.1-05:00", "datetime_offset_hours": "2001-12-15T02:59:43+05", "datetime_spaced": "2001-12-14 21:59:43.10 -5", "datetime_spaced_utc": "2001-12-15 02:59:43.1 Z", "datetime_spaced_offset": "2001-12-14 21:59:43.10 -05:00", "datetime_no_frac": "2001-12-15T14:30:00Z", "date_only": "2001-12-15", "timestamp_formats": {"iso_date": "2001-12-15", "iso_datetime_z": "2001-12-15T02:59:43Z", "iso_datetime_offset": "2001-12-15T02:59:43+00:00", "spaced_datetime": "2001-12-14 21:59:43.10 -5", "canonical": "2001-12-15T02:59:43.1Z"}, "timestamp_sequence": ["2001-12-15", "2001-12-15T02:59:43.1Z", "2001-12-14 21:59:43.10 -5", "2025-01-01T00:00:00Z"], "events": {"created": "2001-12-15T02:59:43.1Z", "modified": "2001-12-16T10:30:00Z", "published": "2001-12-14 21:59:43.10 -5"}, "quoted_timestamps": {"string_date": "2001-12-15", "string_datetime": "2001-12-15T02:59:43.1Z", "string_spaced": "2001-12-14 21:59:43.10 -5"}, "edge_cases": {"midnight": "2001-12-15T00:00:00Z", "end_of_day": "2001-12-15T23:59:59Z", "microseconds": "2001-12-15T02:59:43.123456Z", "no_seconds": "2001-12-15T02:59Z", "hour_only": "2001-12-15T02Z"}, "nested_timestamps": {"project": {"start_date": "2001-12-15", "milestones": [{"date": "2001-12-20", "time": "2001-12-20T14:00:00Z"}, {"date": "2002-01-15", "time": "2002-01-15T09:30:00-05:00"}], "metadata": {"created": "2001-12-14 21:59:43.10 -5", "updated": "2001-12-15T02:59:43.1Z"}}}, "invalid_timestamps": {"bad_date": "2001-13-45", "bad_time": "2001-12-15T25:99:99Z", "incomplete": "2001-12", "no_leading_zero": "2001-1-5"}, "timezones": {"utc_z": "2001-12-15T02:59:43Z", "utc_offset": "2001-12-15T02:59:43+00:00", "est": "2001-12-14T21:59:43-05:00", "ist": "2001-12-15T08:29:43+05:30", "jst": "2001-12-15T11:59:43+09:00"}, "date_range": {"past": "1900-01-01", "unix_epoch": "1970-01-01T00:00:00Z", "y2k": "2000-01-01T00:00:00Z", "present": "2025-12-04", "future": "2099-12-31T23:59:59Z"}}
-
-
Test inline null values
-
-
$ echo 'explicit: null' | yamlcat --json
-
{"explicit": null}
-
-
$ echo 'tilde: ~' | yamlcat --json
-
{"tilde": null}
-
-
$ echo 'empty:' | yamlcat --json
-
{"empty": null}
-
-
$ echo '[null, ~, ]' | yamlcat --json
-
[null, null, null]
-
-
Test inline boolean values
-
-
$ echo 'bool: true' | yamlcat --json
-
{"bool": true}
-
-
$ echo 'bool: false' | yamlcat --json
-
{"bool": false}
-
-
$ echo '[true, false]' | yamlcat --json
-
[true, false]
-
-
Test inline integer values
-
-
$ echo 'positive: 42' | yamlcat --json
-
{"positive": 42}
-
-
$ echo 'negative: -17' | yamlcat --json
-
{"negative": -17}
-
-
$ echo 'zero: 0' | yamlcat --json
-
{"zero": 0}
-
-
$ echo '[0, 42, -17]' | yamlcat --json
-
[0, 42, -17]
-
-
Test inline float values
-
-
$ echo 'simple: 3.14' | yamlcat --json
-
{"simple": 3.14}
-
-
$ echo 'negative: -0.5' | yamlcat --json
-
{"negative": -0.5}
-
-
$ echo 'leading_dot: .5' | yamlcat --json
-
{"leading_dot": 0.5}
-
-
$ echo '[3.14, -0.5, 0.0]' | yamlcat --json
-
[3.14, -0.5, 0]
-
-
Test scientific notation
-
-
$ echo 'positive_exp: 1.5e10' | yamlcat --json
-
{"positive_exp": 15000000000}
-
-
$ echo 'negative_exp: 1.5e-3' | yamlcat --json
-
{"negative_exp": 0.0015}
-
-
$ echo 'uppercase: 2.5E+2' | yamlcat --json
-
{"uppercase": 250}
-
-
Test special float values
-
-
$ echo 'pos_inf: .inf' | yamlcat --json
-
{"pos_inf": inf}
-
-
$ echo 'neg_inf: -.inf' | yamlcat --json
-
{"neg_inf": -inf}
-
-
$ echo 'not_a_num: .nan' | yamlcat --json
-
{"not_a_num": nan}
-
-
$ echo '[.inf, -.inf, .nan]' | yamlcat --json
-
[inf, -inf, nan]
-
-
Test hexadecimal numbers
-
-
$ echo 'hex_lower: 0x1a' | yamlcat --json
-
{"hex_lower": 26}
-
-
$ echo 'hex_upper: 0x1A' | yamlcat --json
-
{"hex_upper": 26}
-
-
$ echo 'hex_mixed: 0xDeadBeef' | yamlcat --json
-
{"hex_mixed": 3735928559}
-
-
Test octal numbers
-
-
$ echo 'octal: 0o17' | yamlcat --json
-
{"octal": 15}
-
-
$ echo 'octal_zero: 0o0' | yamlcat --json
-
{"octal_zero": 0}
-
-
$ echo 'octal_large: 0o755' | yamlcat --json
-
{"octal_large": 493}
-
-
Test mixed special values
-
-
$ echo 'null: null
-
> bool: true
-
> int: 42
-
> float: 3.14
-
> sci: 1.5e10' | yamlcat --json
-
{"null": null, "bool": true, "int": 42, "float": 3.14, "sci": 15000000000}
-
-
Test quoted vs unquoted special values
-
-
$ echo 'unquoted_null: null
-
> quoted_null: "null"' | yamlcat --json
-
{"unquoted_null": null, "quoted_null": "null"}
-
-
$ echo 'unquoted_bool: true
-
> quoted_bool: "true"' | yamlcat --json
-
{"unquoted_bool": true, "quoted_bool": "true"}
-
-
$ echo 'unquoted_num: 42
-
> quoted_num: "42"' | yamlcat --json
-
{"unquoted_num": 42, "quoted_num": "42"}
-
-
Test edge case numbers
-
-
$ echo 'positive_zero: +0.0' | yamlcat --json
-
{"positive_zero": 0}
-
-
$ echo 'negative_zero: -0.0' | yamlcat --json
-
{"negative_zero": -0}
-
-
$ echo 'very_large: 1.0e100' | yamlcat --json
-
{"very_large": 1e+100}
-
-
$ echo 'very_small: 1.0e-100' | yamlcat --json
-
{"very_small": 1e-100}
-
-
Test YAML 1.2 boolean strictness (only true/false are booleans)
-
-
$ echo 'yes: yes' | yamlcat --json
-
{"yes": true}
-
-
$ echo 'no: no' | yamlcat --json
-
{"no": false}
-
-
$ echo 'on: on' | yamlcat --json
-
{"on": true}
-
-
$ echo 'off: off' | yamlcat --json
-
{"off": false}
-
-
$ echo 'True: True' | yamlcat --json
-
{"True": true}
-
-
$ echo 'FALSE: FALSE' | yamlcat --json
-
{"FALSE": false}
-161
yaml/ocaml-yamle/tests/cram/yamlcat.t
···
-
Test yamlcat with simple YAML
-
-
$ echo 'hello: world' | yamlcat
-
hello: world
-
-
$ echo 'name: Alice
-
> age: 30' | yamlcat
-
name: Alice
-
age: 30
-
-
Test nested mappings
-
-
$ echo 'server:
-
> host: localhost
-
> port: 8080
-
> database:
-
> name: mydb' | yamlcat
-
server:
-
host: localhost
-
port: 8080
-
database:
-
name: mydb
-
-
Test sequences
-
-
$ echo '- apple
-
> - banana
-
> - cherry' | yamlcat
-
- apple
-
- banana
-
- cherry
-
-
Test mapping with sequence value
-
-
$ echo 'fruits:
-
> - apple
-
> - banana' | yamlcat
-
fruits:
-
- apple
-
- banana
-
-
Test flow style output
-
-
$ echo 'name: Alice
-
> hobbies:
-
> - reading
-
> - coding' | yamlcat --flow
-
{name: Alice, hobbies: [reading, coding]}
-
-
Test JSON output
-
-
$ echo 'name: Alice
-
> age: 30' | yamlcat --json
-
{"name": "Alice", "age": 30}
-
-
Test seq.yml file (multiline plain scalar)
-
-
$ yamlcat ../yaml/seq.yml
-
- hello - whats - up
-
- foo
-
- bar
-
-
Test seq.yml roundtrip preserves data
-
-
$ yamlcat --json ../yaml/seq.yml
-
["hello - whats - up", "foo", "bar"]
-
-
Test cohttp.yml
-
-
$ yamlcat ../yaml/cohttp.yml
-
language: c
-
sudo: false
-
services:
-
- docker
-
install: 'wget https://raw.githubusercontent.com/ocaml/ocaml-travisci-skeleton/master/.travis-docker.sh'
-
script: bash -ex ./.travis-docker.sh
-
env:
-
global:
-
- "EXTRA_REMOTES=\"https://github.com/mirage/mirage-dev.git\""
-
- "PINS=\"cohttp-top:. cohttp-async:. cohttp-lwt-unix:. cohttp-lwt-jsoo:. cohttp-lwt:. cohttp-mirage:. cohttp:.\""
-
matrix:
-
- "PACKAGE=\"cohttp\" DISTRO=\"alpine-3.5\" OCAML_VERSION=\"4.06.0\""
-
- "PACKAGE=\"cohttp-async\" DISTRO=\"alpine\" OCAML_VERSION=\"4.06.0\""
-
- "PACKAGE=\"cohttp-lwt\" DISTRO=\"debian-unstable\" OCAML_VERSION=\"4.03.0\""
-
- "PACKAGE=\"cohttp-mirage\" DISTRO=\"debian-unstable\" OCAML_VERSION=\"4.03.0\""
-
notifications:
-
webhooks:
-
urls:
-
- 'https://webhooks.gitter.im/e/6ee5059c7420709f4ad1'
-
on_success: change
-
on_failure: always
-
on_start: false
-
-
Test cohttp.yml roundtrip with JSON
-
-
$ yamlcat --json ../yaml/cohttp.yml
-
{"language": "c", "sudo": false, "services": ["docker"], "install": "wget https://raw.githubusercontent.com/ocaml/ocaml-travisci-skeleton/master/.travis-docker.sh", "script": "bash -ex ./.travis-docker.sh", "env": {"global": ["EXTRA_REMOTES=\"https://github.com/mirage/mirage-dev.git\"", "PINS=\"cohttp-top:. cohttp-async:. cohttp-lwt-unix:. cohttp-lwt-jsoo:. cohttp-lwt:. cohttp-mirage:. cohttp:.\""], "matrix": ["PACKAGE=\"cohttp\" DISTRO=\"alpine-3.5\" OCAML_VERSION=\"4.06.0\"", "PACKAGE=\"cohttp-async\" DISTRO=\"alpine\" OCAML_VERSION=\"4.06.0\"", "PACKAGE=\"cohttp-lwt\" DISTRO=\"debian-unstable\" OCAML_VERSION=\"4.03.0\"", "PACKAGE=\"cohttp-mirage\" DISTRO=\"debian-unstable\" OCAML_VERSION=\"4.03.0\""]}, "notifications": {"webhooks": {"urls": ["https://webhooks.gitter.im/e/6ee5059c7420709f4ad1"], "on_success": "change", "on_failure": "always", "on_start": false}}}
-
-
Test special values
-
-
$ echo 'null_val: null
-
> bool_true: true
-
> bool_false: false
-
> number: 42
-
> float: 3.14' | yamlcat --json
-
{"null_val": null, "bool_true": true, "bool_false": false, "number": 42, "float": 3.14}
-
-
Test quoted strings
-
-
$ echo 'single: '"'"'hello world'"'"'
-
> double: "hello world"' | yamlcat
-
single: hello world
-
double: hello world
-
-
Test literal block scalar
-
-
$ echo 'text: |
-
> line one
-
> line two' | yamlcat --json
-
{"text": "line one\nline two\n"}
-
-
Test folded block scalar
-
-
$ echo 'text: >
-
> line one
-
> line two' | yamlcat --json
-
{"text": "line one line two\n"}
-
-
Test linuxkit.yml (sequences of mappings)
-
-
$ yamlcat ../yaml/linuxkit.yml | head -30
-
kernel:
-
image: 'linuxkit/kernel:4.9.40'
-
cmdline: console=tty0 console=ttyS0
-
init:
-
- 'linuxkit/init:906e174b3f2e07f97d6fd693a2e8518e98dafa58'
-
- 'linuxkit/runc:90e45f13e1d0a0983f36ef854621e3eac91cf541'
-
- 'linuxkit/containerd:7c986fb7df33bea73b5c8097b46989e46f49d875'
-
- 'linuxkit/ca-certificates:e44b0a66df5a102c0e220f0066b0d904710dcb10'
-
onboot:
-
- name: sysctl
-
image: 'linuxkit/sysctl:184c914d23a017062d7b53d7fc1dfaf47764bef6'
-
- name: dhcpcd
-
image: 'linuxkit/dhcpcd:f3f5413abb78fae9020e35bd4788fa93df4530b7'
-
command:
-
- /sbin/dhcpcd
-
- '--nobackground'
-
- '-f'
-
- /dhcpcd.conf
-
- '-1'
-
onshutdown:
-
- name: shutdown
-
image: 'busybox:latest'
-
command:
-
- /bin/echo
-
- so long and thanks for all the fish
-
services:
-
- name: getty
-
image: 'linuxkit/getty:2c841cdc34396e3fa8f25b62d112808f63f16df6'
-
env:
-
- INSECURE=true
-9
yaml/ocaml-yamle/tests/dune
···
-
(test
-
(name test_yamle)
-
(modules test_yamle)
-
(libraries yamle alcotest))
-
-
(executable
-
(name run_all_tests)
-
(modules run_all_tests)
-
(libraries yamle test_suite_lib))
-389
yaml/ocaml-yamle/tests/run_all_tests.ml
···
-
(* Run all yaml-test-suite tests with optional HTML output *)
-
open Yamle
-
module TL = Test_suite_lib.Test_suite_loader
-
module TF = Test_suite_lib.Tree_format
-
-
(* Configuration - single variable for test suite path *)
-
let test_suite_path = "../yaml-test-suite"
-
-
(* HTML escape function *)
-
let html_escape s =
-
let buf = Buffer.create (String.length s) in
-
String.iter (function
-
| '<' -> Buffer.add_string buf "&lt;"
-
| '>' -> Buffer.add_string buf "&gt;"
-
| '&' -> Buffer.add_string buf "&amp;"
-
| '"' -> Buffer.add_string buf "&quot;"
-
| c -> Buffer.add_char buf c
-
) s;
-
Buffer.contents buf
-
-
let normalize_tree s =
-
let lines = String.split_on_char '\n' s in
-
let lines = List.filter (fun l -> String.trim l <> "") lines in
-
String.concat "\n" lines
-
-
type test_result = {
-
id : string;
-
name : string;
-
yaml : string;
-
expected_tree : string option;
-
is_error_test : bool;
-
status : [`Pass | `Fail of string | `Skip];
-
output : string;
-
}
-
-
let run_test (test : TL.test_case) : test_result =
-
let base = {
-
id = test.id;
-
name = test.name;
-
yaml = test.yaml;
-
expected_tree = test.tree;
-
is_error_test = test.fail;
-
status = `Skip;
-
output = "";
-
} in
-
if test.fail then begin
-
try
-
let parser = Parser.of_string test.yaml in
-
let events = Parser.to_list parser in
-
let tree = TF.of_spanned_events events in
-
{ base with
-
status = `Fail "Expected parsing to fail";
-
output = tree;
-
}
-
with
-
| Yamle_error e ->
-
{ base with
-
status = `Pass;
-
output = Format.asprintf "%a" Error.pp e;
-
}
-
| exn ->
-
{ base with
-
status = `Pass;
-
output = Printexc.to_string exn;
-
}
-
end
-
else begin
-
match test.tree with
-
| None ->
-
(* No expected tree - check if json indicates expected success *)
-
(match test.json with
-
| Some _ ->
-
(* Has json output, so should parse successfully *)
-
(try
-
let parser = Parser.of_string test.yaml in
-
let events = Parser.to_list parser in
-
let tree = TF.of_spanned_events events in
-
{ base with status = `Pass; output = tree }
-
with exn ->
-
{ base with
-
status = `Fail (Printf.sprintf "Should parse but got: %s" (Printexc.to_string exn));
-
output = Printexc.to_string exn;
-
})
-
| None ->
-
(* No tree, no json, no fail - ambiguous edge case, skip *)
-
{ base with status = `Skip; output = "(no expected tree or json)" })
-
| Some expected ->
-
try
-
let parser = Parser.of_string test.yaml in
-
let events = Parser.to_list parser in
-
let actual = TF.of_spanned_events events in
-
let expected_norm = normalize_tree expected in
-
let actual_norm = normalize_tree actual in
-
if expected_norm = actual_norm then
-
{ base with status = `Pass; output = actual }
-
else
-
{ base with
-
status = `Fail (Printf.sprintf "Tree mismatch");
-
output = Printf.sprintf "Expected:\n%s\n\nActual:\n%s" expected_norm actual_norm;
-
}
-
with exn ->
-
{ base with
-
status = `Fail (Printf.sprintf "Exception: %s" (Printexc.to_string exn));
-
output = Printexc.to_string exn;
-
}
-
end
-
-
let status_class = function
-
| `Pass -> "pass"
-
| `Fail _ -> "fail"
-
| `Skip -> "skip"
-
-
let status_text = function
-
| `Pass -> "PASS"
-
| `Fail _ -> "FAIL"
-
| `Skip -> "SKIP"
-
-
let generate_html results output_file =
-
let oc = open_out output_file in
-
let pass_count = List.length (List.filter (fun r -> r.status = `Pass) results) in
-
let fail_count = List.length (List.filter (fun r -> match r.status with `Fail _ -> true | _ -> false) results) in
-
let skip_count = List.length (List.filter (fun r -> r.status = `Skip) results) in
-
let total = List.length results in
-
-
Printf.fprintf oc {|<!DOCTYPE html>
-
<html lang="en">
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>Yamle Test Results</title>
-
<style>
-
:root {
-
--pass-color: #22c55e;
-
--fail-color: #ef4444;
-
--skip-color: #f59e0b;
-
--bg-color: #1a1a2e;
-
--card-bg: #16213e;
-
--text-color: #e2e8f0;
-
--border-color: #334155;
-
}
-
* { box-sizing: border-box; margin: 0; padding: 0; }
-
body {
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-
background: var(--bg-color);
-
color: var(--text-color);
-
line-height: 1.6;
-
padding: 2rem;
-
}
-
.container { max-width: 1400px; margin: 0 auto; }
-
h1 { margin-bottom: 1.5rem; font-size: 2rem; }
-
.summary {
-
display: flex;
-
gap: 1rem;
-
margin-bottom: 2rem;
-
flex-wrap: wrap;
-
}
-
.stat {
-
background: var(--card-bg);
-
padding: 1rem 1.5rem;
-
border-radius: 8px;
-
border-left: 4px solid var(--border-color);
-
}
-
.stat.pass { border-left-color: var(--pass-color); }
-
.stat.fail { border-left-color: var(--fail-color); }
-
.stat.skip { border-left-color: var(--skip-color); }
-
.stat-value { font-size: 2rem; font-weight: bold; }
-
.stat-label { font-size: 0.875rem; opacity: 0.8; }
-
.filters {
-
margin-bottom: 1.5rem;
-
display: flex;
-
gap: 0.5rem;
-
flex-wrap: wrap;
-
}
-
.filter-btn {
-
padding: 0.5rem 1rem;
-
border: 1px solid var(--border-color);
-
background: var(--card-bg);
-
color: var(--text-color);
-
border-radius: 4px;
-
cursor: pointer;
-
transition: all 0.2s;
-
}
-
.filter-btn:hover { border-color: var(--text-color); }
-
.filter-btn.active { background: var(--text-color); color: var(--bg-color); }
-
.search {
-
padding: 0.5rem 1rem;
-
border: 1px solid var(--border-color);
-
background: var(--card-bg);
-
color: var(--text-color);
-
border-radius: 4px;
-
width: 200px;
-
}
-
.tests { display: flex; flex-direction: column; gap: 1rem; }
-
.test {
-
background: var(--card-bg);
-
border-radius: 8px;
-
border: 1px solid var(--border-color);
-
overflow: hidden;
-
}
-
.test-header {
-
padding: 1rem;
-
display: flex;
-
align-items: center;
-
gap: 1rem;
-
cursor: pointer;
-
border-bottom: 1px solid var(--border-color);
-
}
-
.test-header:hover { background: rgba(255,255,255,0.05); }
-
.badge {
-
padding: 0.25rem 0.5rem;
-
border-radius: 4px;
-
font-size: 0.75rem;
-
font-weight: bold;
-
text-transform: uppercase;
-
}
-
.badge.pass { background: var(--pass-color); color: #000; }
-
.badge.fail { background: var(--fail-color); color: #fff; }
-
.badge.skip { background: var(--skip-color); color: #000; }
-
.badge.error-test { background: #8b5cf6; color: #fff; margin-left: auto; }
-
.test-id { font-family: monospace; font-weight: bold; }
-
.test-name { opacity: 0.8; flex: 1; }
-
.test-content { display: none; padding: 1rem; }
-
.test.expanded .test-content { display: block; }
-
.section { margin-bottom: 1rem; }
-
.section-title {
-
font-size: 0.875rem;
-
text-transform: uppercase;
-
opacity: 0.6;
-
margin-bottom: 0.5rem;
-
letter-spacing: 0.05em;
-
}
-
pre {
-
background: #0f172a;
-
padding: 1rem;
-
border-radius: 4px;
-
overflow-x: auto;
-
font-size: 0.875rem;
-
white-space: pre-wrap;
-
word-break: break-all;
-
}
-
.expand-icon { transition: transform 0.2s; }
-
.test.expanded .expand-icon { transform: rotate(90deg); }
-
</style>
-
</head>
-
<body>
-
<div class="container">
-
<h1>Yamle Test Results</h1>
-
<div class="summary">
-
<div class="stat pass">
-
<div class="stat-value">%d</div>
-
<div class="stat-label">Passed</div>
-
</div>
-
<div class="stat fail">
-
<div class="stat-value">%d</div>
-
<div class="stat-label">Failed</div>
-
</div>
-
<div class="stat skip">
-
<div class="stat-value">%d</div>
-
<div class="stat-label">Skipped</div>
-
</div>
-
<div class="stat">
-
<div class="stat-value">%d</div>
-
<div class="stat-label">Total</div>
-
</div>
-
</div>
-
<div class="filters">
-
<button class="filter-btn active" data-filter="all">All</button>
-
<button class="filter-btn" data-filter="pass">Pass</button>
-
<button class="filter-btn" data-filter="fail">Fail</button>
-
<button class="filter-btn" data-filter="skip">Skip</button>
-
<input type="text" class="search" placeholder="Search by ID or name...">
-
</div>
-
<div class="tests">
-
|} pass_count fail_count skip_count total;
-
-
List.iter (fun result ->
-
let error_badge = if result.is_error_test then
-
{|<span class="badge error-test">Error Test</span>|}
-
else "" in
-
Printf.fprintf oc {| <div class="test" data-status="%s" data-id="%s" data-name="%s">
-
<div class="test-header" onclick="this.parentElement.classList.toggle('expanded')">
-
<span class="expand-icon">▶</span>
-
<span class="badge %s">%s</span>
-
<span class="test-id">%s</span>
-
<span class="test-name">%s</span>
-
%s
-
</div>
-
<div class="test-content">
-
<div class="section">
-
<div class="section-title">YAML Input</div>
-
<pre>%s</pre>
-
</div>
-
<div class="section">
-
<div class="section-title">Yamle Output</div>
-
<pre>%s</pre>
-
</div>
-
</div>
-
</div>
-
|}
-
(status_class result.status)
-
(html_escape result.id)
-
(html_escape (String.lowercase_ascii result.name))
-
(status_class result.status)
-
(status_text result.status)
-
(html_escape result.id)
-
(html_escape result.name)
-
error_badge
-
(html_escape result.yaml)
-
(html_escape result.output)
-
) results;
-
-
Printf.fprintf oc {| </div>
-
</div>
-
<script>
-
document.querySelectorAll('.filter-btn').forEach(btn => {
-
btn.addEventListener('click', () => {
-
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
-
btn.classList.add('active');
-
filterTests();
-
});
-
});
-
document.querySelector('.search').addEventListener('input', filterTests);
-
function filterTests() {
-
const filter = document.querySelector('.filter-btn.active').dataset.filter;
-
const search = document.querySelector('.search').value.toLowerCase();
-
document.querySelectorAll('.test').forEach(test => {
-
const status = test.dataset.status;
-
const id = test.dataset.id.toLowerCase();
-
const name = test.dataset.name;
-
const matchesFilter = filter === 'all' || status === filter;
-
const matchesSearch = !search || id.includes(search) || name.includes(search);
-
test.style.display = matchesFilter && matchesSearch ? '' : 'none';
-
});
-
}
-
</script>
-
</body>
-
</html>
-
|};
-
close_out oc
-
-
let () =
-
let html_output = ref None in
-
let show_skipped = ref false in
-
let args = [
-
"--html", Arg.String (fun s -> html_output := Some s),
-
"<file> Generate HTML report to file";
-
"--show-skipped", Arg.Set show_skipped,
-
" Show details of skipped tests";
-
] in
-
Arg.parse args (fun _ -> ()) "Usage: run_all_tests [--html <file>] [--show-skipped]";
-
-
let all_tests = TL.load_directory test_suite_path in
-
Printf.printf "Total tests loaded: %d\n%!" (List.length all_tests);
-
-
let results = List.map run_test all_tests in
-
-
let pass_count = List.length (List.filter (fun r -> r.status = `Pass) results) in
-
let fail_count = List.length (List.filter (fun r -> match r.status with `Fail _ -> true | _ -> false) results) in
-
let skip_count = List.length (List.filter (fun r -> r.status = `Skip) results) in
-
-
Printf.printf "\nResults: %d pass, %d fail, %d skip (total: %d)\n%!"
-
pass_count fail_count skip_count (pass_count + fail_count + skip_count);
-
-
if fail_count > 0 then begin
-
Printf.printf "\nFailing tests:\n";
-
List.iter (fun r ->
-
match r.status with
-
| `Fail msg -> Printf.printf " %s: %s - %s\n" r.id r.name msg
-
| _ -> ()
-
) results
-
end;
-
-
if !show_skipped && skip_count > 0 then begin
-
Printf.printf "\nSkipped tests (no expected tree):\n";
-
List.iter (fun r ->
-
if r.status = `Skip then begin
-
Printf.printf " %s: %s\n" r.id r.name;
-
Printf.printf " YAML (%d chars): %S\n" (String.length r.yaml)
-
(if String.length r.yaml <= 60 then r.yaml
-
else String.sub r.yaml 0 60 ^ "...")
-
end
-
) results
-
end;
-
-
match !html_output with
-
| Some file ->
-
generate_html results file;
-
Printf.printf "\nHTML report generated: %s\n" file
-
| None -> ()
-4
yaml/ocaml-yamle/tests/test_suite_lib/dune
···
-
(library
-
(name test_suite_lib)
-
(modules test_suite_loader tree_format)
-
(libraries yamle))
-97
yaml/ocaml-yamle/tests/test_suite_lib/test_suite_loader.ml
···
-
(* Load yaml-test-suite test cases from data branch format *)
-
-
type test_case = {
-
id : string;
-
name : string;
-
yaml : string;
-
tree : string option;
-
json : string option;
-
fail : bool;
-
}
-
-
let read_file path =
-
try
-
let ic = open_in path in
-
let n = in_channel_length ic in
-
let s = really_input_string ic n in
-
close_in ic;
-
Some s
-
with _ -> None
-
-
let read_file_required path =
-
match read_file path with
-
| Some s -> s
-
| None -> ""
-
-
let file_exists path =
-
Sys.file_exists path
-
-
let is_directory path =
-
Sys.file_exists path && Sys.is_directory path
-
-
(* Load a single test from a directory *)
-
let load_test_dir base_id dir_path =
-
let name_file = Filename.concat dir_path "===" in
-
let yaml_file = Filename.concat dir_path "in.yaml" in
-
let tree_file = Filename.concat dir_path "test.event" in
-
let json_file = Filename.concat dir_path "in.json" in
-
let error_file = Filename.concat dir_path "error" in
-
-
(* Must have in.yaml to be a valid test *)
-
if not (file_exists yaml_file) then None
-
else
-
let name = match read_file name_file with
-
| Some s -> String.trim s
-
| None -> base_id
-
in
-
let yaml = read_file_required yaml_file in
-
let tree = read_file tree_file in
-
let json = read_file json_file in
-
let fail = file_exists error_file in
-
Some { id = base_id; name; yaml; tree; json; fail }
-
-
(* Load tests from a test ID directory (may have subdirectories for variants) *)
-
let load_test_id test_suite_path test_id =
-
let dir_path = Filename.concat test_suite_path test_id in
-
if not (is_directory dir_path) then []
-
else
-
(* Check if this directory has variant subdirectories (00, 01, etc.) *)
-
let entries = Sys.readdir dir_path in
-
let has_variants = Array.exists (fun e ->
-
let subdir = Filename.concat dir_path e in
-
is_directory subdir &&
-
String.length e >= 2 &&
-
e.[0] >= '0' && e.[0] <= '9'
-
) entries in
-
-
if has_variants then
-
(* Load each variant subdirectory *)
-
let variants = Array.to_list entries
-
|> List.filter (fun e ->
-
let subdir = Filename.concat dir_path e in
-
is_directory subdir && String.length e >= 2 && e.[0] >= '0' && e.[0] <= '9')
-
|> List.sort String.compare
-
in
-
List.filter_map (fun variant ->
-
let variant_path = Filename.concat dir_path variant in
-
let variant_id = Printf.sprintf "%s:%s" test_id variant in
-
load_test_dir variant_id variant_path
-
) variants
-
else
-
(* Single test in this directory *)
-
match load_test_dir test_id dir_path with
-
| Some t -> [t]
-
| None -> []
-
-
let load_directory test_suite_path =
-
if not (is_directory test_suite_path) then []
-
else
-
let entries = Sys.readdir test_suite_path in
-
let test_ids = Array.to_list entries
-
|> List.filter (fun e ->
-
is_directory (Filename.concat test_suite_path e) &&
-
String.length e >= 4 && (* Test IDs are 4 chars *)
-
e.[0] >= '0' && e.[0] <= 'Z') (* Start with alphanumeric *)
-
|> List.sort String.compare
-
in
-
List.concat_map (load_test_id test_suite_path) test_ids
-69
yaml/ocaml-yamle/tests/test_suite_lib/tree_format.ml
···
-
(* Format parser events as tree notation compatible with yaml-test-suite *)
-
-
open Yamle
-
-
let escape_string s =
-
let buf = Buffer.create (String.length s * 2) in
-
String.iter (fun c ->
-
match c with
-
| '\n' -> Buffer.add_string buf "\\n"
-
| '\t' -> Buffer.add_string buf "\\t"
-
| '\r' -> Buffer.add_string buf "\\r"
-
| '\\' -> Buffer.add_string buf "\\\\"
-
| '\x00' -> Buffer.add_string buf "\\0"
-
| '\x07' -> Buffer.add_string buf "\\a"
-
| '\x08' -> Buffer.add_string buf "\\b"
-
| '\x0b' -> Buffer.add_string buf "\\v"
-
| '\x0c' -> Buffer.add_string buf "\\f"
-
| '\x1b' -> Buffer.add_string buf "\\e"
-
| '\xa0' -> Buffer.add_string buf "\\_"
-
| c -> Buffer.add_char buf c
-
) s;
-
Buffer.contents buf
-
-
let style_char = function
-
| Scalar_style.Plain -> ':'
-
| Scalar_style.Single_quoted -> '\''
-
| Scalar_style.Double_quoted -> '"'
-
| Scalar_style.Literal -> '|'
-
| Scalar_style.Folded -> '>'
-
| Scalar_style.Any -> ':'
-
-
let format_event { Event.event; span = _span } =
-
match event with
-
| Event.Stream_start _ -> "+STR"
-
| Event.Stream_end -> "-STR"
-
| Event.Document_start { implicit; _ } ->
-
if implicit then "+DOC"
-
else "+DOC ---"
-
| Event.Document_end { implicit } ->
-
if implicit then "-DOC"
-
else "-DOC ..."
-
| Event.Mapping_start { anchor; tag; style; _ } ->
-
let anchor_str = match anchor with Some a -> " &" ^ a | None -> "" in
-
let tag_str = match tag with Some t -> " <" ^ t ^ ">" | None -> "" in
-
let flow_str = match style with Layout_style.Flow -> " {}" | _ -> "" in
-
Printf.sprintf "+MAP%s%s%s" flow_str anchor_str tag_str
-
| Event.Mapping_end -> "-MAP"
-
| Event.Sequence_start { anchor; tag; style; _ } ->
-
let anchor_str = match anchor with Some a -> " &" ^ a | None -> "" in
-
let tag_str = match tag with Some t -> " <" ^ t ^ ">" | None -> "" in
-
let flow_str = match style with Layout_style.Flow -> " []" | _ -> "" in
-
Printf.sprintf "+SEQ%s%s%s" flow_str anchor_str tag_str
-
| Event.Sequence_end -> "-SEQ"
-
| Event.Scalar { anchor; tag; value; style; _ } ->
-
let anchor_str = match anchor with Some a -> " &" ^ a | None -> "" in
-
let tag_str = match tag with Some t -> " <" ^ t ^ ">" | None -> "" in
-
let style_c = style_char style in
-
Printf.sprintf "=VAL%s%s %c%s" anchor_str tag_str style_c (escape_string value)
-
| Event.Alias { anchor } ->
-
Printf.sprintf "=ALI *%s" anchor
-
-
let of_spanned_events events =
-
let buf = Buffer.create 256 in
-
List.iter (fun (e : Event.spanned) ->
-
let line = format_event e in
-
Buffer.add_string buf line;
-
Buffer.add_char buf '\n'
-
) events;
-
Buffer.contents buf
-353
yaml/ocaml-yamle/tests/test_yamle.ml
···
-
(** Tests for the Yamle library *)
-
-
open Yamle
-
-
(** Test helpers *)
-
-
let check_value msg expected actual =
-
Alcotest.(check bool) msg true (Value.equal expected actual)
-
-
let _check_string msg expected actual =
-
Alcotest.(check string) msg expected actual
-
-
(** Scanner tests *)
-
-
let test_scanner_simple () =
-
let scanner = Scanner.of_string "hello: world" in
-
let tokens = Scanner.to_list scanner in
-
let token_types = List.map (fun (t : Token.spanned) -> t.token) tokens in
-
Alcotest.(check int) "token count" 8 (List.length token_types);
-
(* Stream_start, Block_mapping_start, Key, Scalar, Value, Scalar, Block_end, Stream_end *)
-
match token_types with
-
| Token.Stream_start _ :: Token.Block_mapping_start :: Token.Key ::
-
Token.Scalar { value = "hello"; _ } :: Token.Value ::
-
Token.Scalar { value = "world"; _ } :: Token.Block_end :: Token.Stream_end :: [] ->
-
()
-
| _ ->
-
Alcotest.fail "unexpected token sequence"
-
-
let test_scanner_sequence () =
-
let scanner = Scanner.of_string "- one\n- two\n- three" in
-
let tokens = Scanner.to_list scanner in
-
Alcotest.(check bool) "has tokens" true (List.length tokens > 0)
-
-
let test_scanner_flow () =
-
let scanner = Scanner.of_string "[1, 2, 3]" in
-
let tokens = Scanner.to_list scanner in
-
let has_flow_start = List.exists (fun (t : Token.spanned) ->
-
match t.token with Token.Flow_sequence_start -> true | _ -> false
-
) tokens in
-
Alcotest.(check bool) "has flow sequence start" true has_flow_start
-
-
let scanner_tests = [
-
"simple mapping", `Quick, test_scanner_simple;
-
"sequence", `Quick, test_scanner_sequence;
-
"flow sequence", `Quick, test_scanner_flow;
-
]
-
-
(** Parser tests *)
-
-
let test_parser_events () =
-
let parser = Parser.of_string "key: value" in
-
let events = Parser.to_list parser in
-
Alcotest.(check bool) "has events" true (List.length events > 0);
-
let has_stream_start = List.exists (fun (e : Event.spanned) ->
-
match e.event with Event.Stream_start _ -> true | _ -> false
-
) events in
-
Alcotest.(check bool) "has stream start" true has_stream_start
-
-
let test_parser_sequence_events () =
-
let parser = Parser.of_string "- a\n- b" in
-
let events = Parser.to_list parser in
-
let has_seq_start = List.exists (fun (e : Event.spanned) ->
-
match e.event with Event.Sequence_start _ -> true | _ -> false
-
) events in
-
Alcotest.(check bool) "has sequence start" true has_seq_start
-
-
let parser_tests = [
-
"parse events", `Quick, test_parser_events;
-
"sequence events", `Quick, test_parser_sequence_events;
-
]
-
-
(** Value parsing tests *)
-
-
let test_parse_null () =
-
check_value "null" `Null (of_string "null");
-
check_value "~" `Null (of_string "~");
-
check_value "empty" `Null (of_string "")
-
-
let test_parse_bool () =
-
check_value "true" (`Bool true) (of_string "true");
-
check_value "false" (`Bool false) (of_string "false");
-
check_value "yes" (`Bool true) (of_string "yes");
-
check_value "no" (`Bool false) (of_string "no")
-
-
let test_parse_number () =
-
check_value "integer" (`Float 42.0) (of_string "42");
-
check_value "negative" (`Float (-17.0)) (of_string "-17");
-
check_value "float" (`Float 3.14) (of_string "3.14")
-
-
let test_parse_string () =
-
check_value "plain" (`String "hello") (of_string "hello world" |> function `String s -> `String (String.sub s 0 5) | v -> v);
-
check_value "quoted" (`String "hello") (of_string {|"hello"|})
-
-
let test_parse_sequence () =
-
let result = of_string "- one\n- two\n- three" in
-
match result with
-
| `A [_; _; _] -> ()
-
| _ -> Alcotest.fail "expected sequence with 3 elements"
-
-
let test_parse_mapping () =
-
let result = of_string "name: Alice\nage: 30" in
-
match result with
-
| `O pairs when List.length pairs = 2 -> ()
-
| _ -> Alcotest.fail "expected mapping with 2 pairs"
-
-
let test_parse_nested () =
-
let yaml = {|
-
person:
-
name: Bob
-
hobbies:
-
- reading
-
- coding
-
|} in
-
let result = of_string yaml in
-
match result with
-
| `O [("person", `O _)] -> ()
-
| _ -> Alcotest.fail "expected nested structure"
-
-
let test_parse_flow_sequence () =
-
let result = of_string "[1, 2, 3]" in
-
match result with
-
| `A [`Float 1.0; `Float 2.0; `Float 3.0] -> ()
-
| _ -> Alcotest.fail "expected flow sequence [1, 2, 3]"
-
-
let test_parse_flow_mapping () =
-
let result = of_string "{a: 1, b: 2}" in
-
match result with
-
| `O [("a", `Float 1.0); ("b", `Float 2.0)] -> ()
-
| _ -> Alcotest.fail "expected flow mapping {a: 1, b: 2}"
-
-
let test_parse_flow_mapping_trailing_comma () =
-
let result = of_string "{ a: 1, }" in
-
match result with
-
| `O [("a", `Float 1.0)] -> ()
-
| `O pairs ->
-
Alcotest.failf "expected 1 pair but got %d pairs (trailing comma should not create empty entry)"
-
(List.length pairs)
-
| _ -> Alcotest.fail "expected flow mapping with 1 pair"
-
-
let value_tests = [
-
"parse null", `Quick, test_parse_null;
-
"parse bool", `Quick, test_parse_bool;
-
"parse number", `Quick, test_parse_number;
-
"parse string", `Quick, test_parse_string;
-
"parse sequence", `Quick, test_parse_sequence;
-
"parse mapping", `Quick, test_parse_mapping;
-
"parse nested", `Quick, test_parse_nested;
-
"parse flow sequence", `Quick, test_parse_flow_sequence;
-
"parse flow mapping", `Quick, test_parse_flow_mapping;
-
"flow mapping trailing comma", `Quick, test_parse_flow_mapping_trailing_comma;
-
]
-
-
(** Emitter tests *)
-
-
let test_emit_null () =
-
let result = to_string `Null in
-
Alcotest.(check bool) "contains null" true (String.length result > 0)
-
-
let starts_with prefix s =
-
String.length s >= String.length prefix &&
-
String.sub s 0 (String.length prefix) = prefix
-
-
let test_emit_mapping () =
-
let value = `O [("name", `String "Alice"); ("age", `Float 30.0)] in
-
let result = to_string value in
-
let trimmed = String.trim result in
-
Alcotest.(check bool) "contains name" true (starts_with "name" trimmed || starts_with "\"name\"" trimmed)
-
-
let test_roundtrip_simple () =
-
let yaml = "name: Alice" in
-
let value = of_string yaml in
-
let _ = to_string value in
-
(* Just check it doesn't crash *)
-
()
-
-
let test_roundtrip_sequence () =
-
let yaml = "- one\n- two\n- three" in
-
let value = of_string yaml in
-
match value with
-
| `A items when List.length items = 3 ->
-
let _ = to_string value in
-
()
-
| _ -> Alcotest.fail "roundtrip failed"
-
-
let emitter_tests = [
-
"emit null", `Quick, test_emit_null;
-
"emit mapping", `Quick, test_emit_mapping;
-
"roundtrip simple", `Quick, test_roundtrip_simple;
-
"roundtrip sequence", `Quick, test_roundtrip_sequence;
-
]
-
-
(** YAML-specific tests *)
-
-
let test_yaml_anchor () =
-
let yaml = "&anchor hello" in
-
let result = yaml_of_string yaml in
-
match result with
-
| `Scalar s when Scalar.anchor s = Some "anchor" -> ()
-
| _ -> Alcotest.fail "expected scalar with anchor"
-
-
let test_yaml_alias () =
-
let yaml = {|
-
defaults: &defaults
-
timeout: 30
-
production:
-
<<: *defaults
-
port: 8080
-
|} in
-
(* Just check it parses without error *)
-
let _ = yaml_of_string yaml in
-
()
-
-
let yaml_tests = [
-
"yaml anchor", `Quick, test_yaml_anchor;
-
"yaml alias", `Quick, test_yaml_alias;
-
]
-
-
(** Multiline scalar tests *)
-
-
let test_literal_block () =
-
let yaml = {|description: |
-
This is a
-
multi-line
-
description
-
|} in
-
let result = of_string yaml in
-
match result with
-
| `O [("description", `String _)] -> ()
-
| _ -> Alcotest.fail "expected mapping with literal block"
-
-
let test_folded_block () =
-
let yaml = {|description: >
-
This is a
-
folded
-
description
-
|} in
-
let result = of_string yaml in
-
match result with
-
| `O [("description", `String _)] -> ()
-
| _ -> Alcotest.fail "expected mapping with folded block"
-
-
let multiline_tests = [
-
"literal block", `Quick, test_literal_block;
-
"folded block", `Quick, test_folded_block;
-
]
-
-
(** Error handling tests *)
-
-
let test_error_position () =
-
try
-
let _ = of_string "key: [unclosed" in
-
Alcotest.fail "expected error"
-
with
-
| Yamle_error e ->
-
Alcotest.(check bool) "has span" true (e.span <> None)
-
-
let error_tests = [
-
"error position", `Quick, test_error_position;
-
]
-
-
(** Alias expansion limit tests (billion laughs protection) *)
-
-
let test_node_limit () =
-
(* Small bomb that would expand to 9^4 = 6561 nodes *)
-
let yaml = {|
-
a: &a [1,2,3,4,5,6,7,8,9]
-
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
-
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
-
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
-
|} in
-
(* Should fail with a small node limit *)
-
try
-
let _ = of_string ~max_nodes:100 yaml in
-
Alcotest.fail "expected node limit error"
-
with
-
| Yamle_error e ->
-
(match e.Error.kind with
-
| Error.Alias_expansion_node_limit _ -> ()
-
| _ -> Alcotest.fail "expected Alias_expansion_node_limit error")
-
-
let test_depth_limit () =
-
(* Create deeply nested alias chain:
-
*e -> [*d,*d] -> [*c,*c] -> [*b,*b] -> [*a,*a] -> [x,y,z]
-
Each alias resolution increases depth by 1 *)
-
let yaml = {|
-
a: &a [x, y, z]
-
b: &b [*a, *a]
-
c: &c [*b, *b]
-
d: &d [*c, *c]
-
e: &e [*d, *d]
-
result: *e
-
|} in
-
(* Should fail with a small depth limit (depth 3 means max 3 alias hops) *)
-
try
-
let _ = of_string ~max_depth:3 yaml in
-
Alcotest.fail "expected depth limit error"
-
with
-
| Yamle_error e ->
-
(match e.Error.kind with
-
| Error.Alias_expansion_depth_limit _ -> ()
-
| _ -> Alcotest.fail ("expected Alias_expansion_depth_limit error, got: " ^ Error.kind_to_string e.Error.kind))
-
-
let test_normal_aliases_work () =
-
(* Normal alias usage should work fine *)
-
let yaml = {|
-
defaults: &defaults
-
timeout: 30
-
retries: 3
-
production:
-
<<: *defaults
-
port: 8080
-
|} in
-
let result = of_string yaml in
-
match result with
-
| `O _ -> ()
-
| _ -> Alcotest.fail "expected mapping"
-
-
let test_resolve_aliases_false () =
-
(* With resolve_aliases=false, aliases should remain unresolved *)
-
let yaml = {|
-
a: &anchor value
-
b: *anchor
-
|} in
-
let result = yaml_of_string ~resolve_aliases:false yaml in
-
(* Check that alias is preserved *)
-
match result with
-
| `O map ->
-
let pairs = Mapping.members map in
-
(match List.assoc_opt (`Scalar (Scalar.make "b")) pairs with
-
| Some (`Alias "anchor") -> ()
-
| _ -> Alcotest.fail "expected alias to be preserved")
-
| _ -> Alcotest.fail "expected mapping"
-
-
let alias_limit_tests = [
-
"node limit", `Quick, test_node_limit;
-
"depth limit", `Quick, test_depth_limit;
-
"normal aliases work", `Quick, test_normal_aliases_work;
-
"resolve_aliases false", `Quick, test_resolve_aliases_false;
-
]
-
-
(** Run all tests *)
-
-
let () =
-
Alcotest.run "yamle" [
-
"scanner", scanner_tests;
-
"parser", parser_tests;
-
"value", value_tests;
-
"emitter", emitter_tests;
-
"yaml", yaml_tests;
-
"multiline", multiline_tests;
-
"errors", error_tests;
-
"alias_limits", alias_limit_tests;
-
]
-24
yaml/ocaml-yamle/tests/yaml/anchor.yml
···
-
datetime: 2001-12-15T02:59:43.1Z
-
datetime_with_spaces: 2001-12-14 21:59:43.10 -5
-
date: 2002-12-14
-
-
# The !!binary tag indicates that a string is actually a base64-encoded
-
# representation of a binary blob.
-
gif_file: !!binary |
-
R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5
-
OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+
-
+f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC
-
AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=
-
-
# YAML also has a set type, which looks like this:
-
set:
-
? item1
-
? item2
-
? item3
-
-
# Like Python, sets are just maps with null values; the above is equivalent to:
-
set2:
-
item1: null
-
item2: null
-
item3: null
-
-125
yaml/ocaml-yamle/tests/yaml/anchors_basic.yml
···
-
# Basic Anchor and Alias Test Cases
-
# Tests fundamental anchor (&) and alias (*) functionality
-
-
# Test 1: Simple scalar anchor and alias
-
---
-
scalar_anchor: &simple_scalar "Hello, World!"
-
scalar_alias: *simple_scalar
-
# Expected: both should have the value "Hello, World!"
-
-
# Test 2: Numeric scalar anchor
-
---
-
original: &num 42
-
copy: *num
-
another_copy: *num
-
# Expected: all three should have the value 42
-
-
# Test 3: Sequence anchor and alias
-
---
-
original_list: &my_list
-
- apple
-
- banana
-
- cherry
-
-
copied_list: *my_list
-
# Expected: both lists should be identical
-
-
# Test 4: Mapping anchor and alias
-
---
-
original_map: &person
-
name: Alice
-
age: 30
-
city: London
-
-
copied_map: *person
-
# Expected: both maps should be identical
-
-
# Test 5: Multiple anchors in same document
-
---
-
defaults: &defaults
-
timeout: 30
-
retries: 3
-
-
colors: &colors
-
- red
-
- green
-
- blue
-
-
config:
-
settings: *defaults
-
palette: *colors
-
# Expected: config.settings should have timeout and retries, config.palette should have the color list
-
-
# Test 6: Nested structure with anchor
-
---
-
template: &template
-
metadata:
-
version: 1.0
-
author: John Doe
-
settings:
-
enabled: true
-
debug: false
-
-
instance1: *template
-
instance2: *template
-
# Expected: both instances should be identical copies of template
-
-
# Test 7: Anchor in sequence
-
---
-
items:
-
- &first_item
-
id: 1
-
name: First
-
- id: 2
-
name: Second
-
- *first_item
-
# Expected: first and third items should be identical
-
-
# Test 8: Multiple uses of same alias
-
---
-
shared_value: &shared 100
-
calculations:
-
base: *shared
-
doubled: 200 # Just a value, not calculated
-
reference: *shared
-
another_ref: *shared
-
# Expected: base, reference, and another_ref should all be 100
-
-
# Test 9: Boolean anchor
-
---
-
feature_flag: &enabled true
-
features:
-
login: *enabled
-
signup: *enabled
-
export: *enabled
-
# Expected: all features should be true
-
-
# Test 10: Null anchor
-
---
-
empty: &null_value ~
-
values:
-
first: *null_value
-
second: *null_value
-
# Expected: all should be null
-
-
# Test 11: String with special characters
-
---
-
message: &msg |
-
This is a multi-line
-
message with some
-
special content!
-
-
output1: *msg
-
output2: *msg
-
# Expected: both outputs should have the same multi-line string
-
-
# Test 12: Anchor in mapping value
-
---
-
database:
-
primary: &db_config
-
host: localhost
-
port: 5432
-
ssl: true
-
replica: *db_config
-
backup: *db_config
-
# Expected: primary, replica, and backup should all have identical configuration
-194
yaml/ocaml-yamle/tests/yaml/anchors_merge.yml
···
-
# Merge Key Test Cases
-
# Tests YAML 1.1 merge key (<<) functionality
-
# Note: Merge keys are a YAML 1.1 feature and may not be supported in YAML 1.2
-
-
# Test 1: Basic merge key
-
---
-
defaults: &defaults
-
timeout: 30
-
retries: 3
-
verbose: false
-
-
production:
-
<<: *defaults
-
environment: production
-
# Expected: production should have timeout, retries, verbose from defaults, plus environment
-
-
# Test 2: Override after merge
-
---
-
base: &base
-
color: red
-
size: medium
-
weight: 100
-
-
custom:
-
<<: *base
-
color: blue
-
shape: circle
-
# Expected: custom should have size and weight from base, but color should be blue, and add shape
-
-
# Test 3: Merging multiple anchors
-
---
-
connection: &connection
-
host: localhost
-
port: 8080
-
-
authentication: &auth
-
username: admin
-
password: secret
-
-
server:
-
<<: [*connection, *auth]
-
ssl: true
-
# Expected: server should have host, port, username, password, and ssl
-
-
# Test 4: Multiple merges with override
-
---
-
defaults: &defaults
-
timeout: 30
-
retries: 3
-
-
advanced: &advanced
-
cache: true
-
pool_size: 10
-
-
config:
-
<<: [*defaults, *advanced]
-
timeout: 60
-
custom: value
-
# Expected: config should have all fields from both anchors, with timeout overridden to 60
-
-
# Test 5: Nested merge
-
---
-
base_style: &base_style
-
font: Arial
-
size: 12
-
-
heading_defaults: &heading
-
<<: *base_style
-
weight: bold
-
-
main_heading:
-
<<: *heading
-
size: 18
-
color: navy
-
# Expected: main_heading should inherit from heading (which inherits from base_style) with overrides
-
-
# Test 6: Merge in sequence context
-
---
-
common: &common
-
enabled: true
-
log_level: info
-
-
services:
-
- name: web
-
<<: *common
-
port: 80
-
- name: api
-
<<: *common
-
port: 3000
-
- name: worker
-
<<: *common
-
threads: 4
-
# Expected: each service should have enabled and log_level, plus their specific fields
-
-
# Test 7: Empty merge (edge case)
-
---
-
empty: &empty {}
-
-
config:
-
<<: *empty
-
key: value
-
# Expected: config should just have key: value
-
-
# Test 8: Merge with nested structures
-
---
-
metadata: &metadata
-
created: 2023-01-01
-
author: Admin
-
tags:
-
- v1
-
- stable
-
-
document:
-
<<: *metadata
-
title: Important Document
-
content: Some content here
-
# Expected: document should have all metadata fields plus title and content
-
-
# Test 9: Chain of merges
-
---
-
level1: &l1
-
a: 1
-
b: 2
-
-
level2: &l2
-
<<: *l1
-
c: 3
-
-
level3:
-
<<: *l2
-
d: 4
-
# Expected: level3 should have a, b, c, and d
-
-
# Test 10: Merge with conflicting keys
-
---
-
first: &first
-
name: First
-
value: 100
-
priority: low
-
-
second: &second
-
name: Second
-
value: 200
-
category: important
-
-
combined:
-
<<: [*first, *second]
-
name: Combined
-
# Expected: later merges and direct assignments take precedence
-
-
# Test 11: Merge preserving types
-
---
-
numbers: &numbers
-
count: 42
-
ratio: 3.14
-
active: true
-
-
derived:
-
<<: *numbers
-
label: Test
-
# Expected: types should be preserved (int, float, bool)
-
-
# Test 12: Complex real-world example
-
---
-
db_defaults: &db_defaults
-
pool_size: 5
-
timeout: 30
-
ssl: false
-
-
cache_defaults: &cache_defaults
-
ttl: 3600
-
max_size: 1000
-
-
development:
-
database:
-
<<: *db_defaults
-
host: localhost
-
name: dev_db
-
cache:
-
<<: *cache_defaults
-
backend: memory
-
-
production:
-
database:
-
<<: *db_defaults
-
host: prod.example.com
-
name: prod_db
-
ssl: true
-
pool_size: 20
-
cache:
-
<<: *cache_defaults
-
backend: redis
-
ttl: 7200
-
# Expected: each environment should inherit defaults with environment-specific overrides
-23
yaml/ocaml-yamle/tests/yaml/cohttp.yml
···
-
language: c
-
sudo: false
-
services:
-
- docker
-
install: wget https://raw.githubusercontent.com/ocaml/ocaml-travisci-skeleton/master/.travis-docker.sh
-
script: bash -ex ./.travis-docker.sh
-
env:
-
global:
-
- EXTRA_REMOTES="https://github.com/mirage/mirage-dev.git"
-
- PINS="cohttp-top:. cohttp-async:. cohttp-lwt-unix:. cohttp-lwt-jsoo:. cohttp-lwt:. cohttp-mirage:. cohttp:."
-
matrix:
-
- PACKAGE="cohttp" DISTRO="alpine-3.5" OCAML_VERSION="4.06.0"
-
- PACKAGE="cohttp-async" DISTRO="alpine" OCAML_VERSION="4.06.0"
-
- PACKAGE="cohttp-lwt" DISTRO="debian-unstable" OCAML_VERSION="4.03.0"
-
- PACKAGE="cohttp-mirage" DISTRO="debian-unstable" OCAML_VERSION="4.03.0"
-
notifications:
-
webhooks:
-
urls:
-
- https://webhooks.gitter.im/e/6ee5059c7420709f4ad1
-
on_success: change
-
on_failure: always
-
on_start: false
-
-126
yaml/ocaml-yamle/tests/yaml/collections_block.yml
···
-
# Block Style Collections Test File
-
# Testing various block-style collection structures
-
-
# Simple sequence
-
simple_sequence:
-
- apple
-
- banana
-
- cherry
-
- date
-
-
# Simple mapping
-
simple_mapping:
-
name: John Doe
-
age: 30
-
city: New York
-
country: USA
-
-
# Nested sequences
-
nested_sequences:
-
- - alpha
-
- beta
-
- gamma
-
- - one
-
- two
-
- three
-
- - red
-
- green
-
- blue
-
-
# Nested mappings
-
nested_mappings:
-
person:
-
name: Alice
-
contact:
-
email: alice@example.com
-
phone: 555-1234
-
address:
-
street: 123 Main St
-
city: Boston
-
-
# Mapping containing sequences
-
mapping_with_sequences:
-
colors:
-
- red
-
- green
-
- blue
-
sizes:
-
- small
-
- medium
-
- large
-
numbers:
-
- 1
-
- 2
-
- 3
-
-
# Sequence containing mappings
-
sequence_with_mappings:
-
- name: Alice
-
age: 25
-
role: developer
-
- name: Bob
-
age: 30
-
role: designer
-
- name: Charlie
-
age: 35
-
role: manager
-
-
# Deep nesting (4 levels)
-
deep_nesting:
-
level1:
-
level2:
-
level3:
-
level4:
-
- deeply
-
- nested
-
- values
-
another_key: value
-
items:
-
- item1
-
- item2
-
metadata:
-
created: 2024-01-01
-
modified: 2024-12-04
-
-
# Mixed complex structure
-
complex_structure:
-
database:
-
connections:
-
- host: db1.example.com
-
port: 5432
-
credentials:
-
username: admin
-
password: secret
-
- host: db2.example.com
-
port: 5432
-
credentials:
-
username: readonly
-
password: public
-
services:
-
- name: api
-
endpoints:
-
- /users
-
- /posts
-
- /comments
-
config:
-
timeout: 30
-
retries: 3
-
- name: worker
-
tasks:
-
- email
-
- reports
-
config:
-
concurrency: 10
-
-
# Empty sequences and mappings in block style
-
empty_collections:
-
empty_sequence: []
-
empty_mapping: {}
-
sequence_with_empty:
-
- value1
-
- []
-
- value2
-
mapping_with_empty:
-
key1: value1
-
key2: {}
-
key3: value3
-198
yaml/ocaml-yamle/tests/yaml/collections_compact.yml
···
-
# Compact Notation Collections Test File
-
# Testing compact block notation and mixed styles
-
-
# Compact nested mapping in sequence (most common form)
-
compact_sequence:
-
- name: Alice
-
age: 25
-
city: Boston
-
- name: Bob
-
age: 30
-
city: Seattle
-
- name: Charlie
-
age: 35
-
city: Portland
-
-
# Compact with nested structures
-
compact_nested:
-
- id: 1
-
details:
-
type: admin
-
permissions:
-
- read
-
- write
-
- delete
-
- id: 2
-
details:
-
type: user
-
permissions:
-
- read
-
-
# Multiple keys in same sequence entry with sub-structures
-
compact_complex:
-
- key1: value1
-
key2: value2
-
nested:
-
sub1: val1
-
sub2: val2
-
- key1: value3
-
key2: value4
-
nested:
-
sub1: val3
-
sub2: val4
-
-
# Compact block mappings with inline values
-
users:
-
- username: alice
-
email: alice@example.com
-
active: true
-
- username: bob
-
email: bob@example.com
-
active: false
-
-
# Compact with flow collections
-
compact_with_flow:
-
- name: service1
-
ports: [8080, 8443]
-
env: {DEBUG: true, MODE: production}
-
- name: service2
-
ports: [3000]
-
env: {DEBUG: false, MODE: development}
-
-
# Deeply nested compact notation
-
deep_compact:
-
- category: electronics
-
items:
-
- name: laptop
-
specs:
-
cpu: Intel i7
-
ram: 16GB
-
storage: 512GB SSD
-
- name: phone
-
specs:
-
os: Android
-
ram: 8GB
-
storage: 256GB
-
- category: furniture
-
items:
-
- name: desk
-
dimensions:
-
width: 150cm
-
depth: 75cm
-
height: 75cm
-
- name: chair
-
dimensions:
-
width: 60cm
-
depth: 60cm
-
height: 120cm
-
-
# Compact with mixed indentation styles
-
mixed_compact:
-
databases:
-
- type: postgresql
-
connection:
-
host: localhost
-
port: 5432
-
credentials:
-
user: admin
-
password: secret
-
- type: mongodb
-
connection:
-
host: localhost
-
port: 27017
-
credentials:
-
user: root
-
password: root
-
-
# Single-line compact entries
-
single_line_compact:
-
- {name: Alice, age: 25, role: developer}
-
- {name: Bob, age: 30, role: designer}
-
- {name: Charlie, age: 35, role: manager}
-
-
# Compact notation with sequences as values
-
sequences_in_compact:
-
- title: Project A
-
members:
-
- Alice
-
- Bob
-
- Charlie
-
tags:
-
- urgent
-
- backend
-
- title: Project B
-
members:
-
- David
-
- Eve
-
tags:
-
- frontend
-
- design
-
-
# Compact with empty values
-
compact_with_empty:
-
- id: 1
-
data: []
-
meta: {}
-
- id: 2
-
data:
-
- item1
-
meta:
-
key: value
-
-
# Compact notation with complex nesting
-
compact_complex_nesting:
-
- level: 1
-
children:
-
- level: 2a
-
children:
-
- level: 3a
-
value: leaf1
-
- level: 3b
-
value: leaf2
-
- level: 2b
-
children:
-
- level: 3c
-
value: leaf3
-
-
# Real-world example: API endpoints
-
api_endpoints:
-
- path: /users
-
method: GET
-
auth: required
-
params:
-
- name: page
-
type: integer
-
default: 1
-
- name: limit
-
type: integer
-
default: 10
-
- path: /users/:id
-
method: GET
-
auth: required
-
params: []
-
- path: /users
-
method: POST
-
auth: required
-
body:
-
username: string
-
email: string
-
password: string
-
-
# Compact with various data types
-
compact_types:
-
- string_val: hello
-
number_val: 42
-
float_val: 3.14
-
bool_val: true
-
null_val: null
-
- string_val: world
-
number_val: 100
-
float_val: 2.71
-
bool_val: false
-
null_val: ~
-
-
# Edge case: minimal compact notation
-
minimal:
-
- a: 1
-
- b: 2
-
- c: 3
-96
yaml/ocaml-yamle/tests/yaml/collections_flow.yml
···
-
# Flow Style Collections Test File
-
# Testing various flow-style collection structures
-
-
# Simple flow sequence
-
simple_flow_sequence: [apple, banana, cherry, date]
-
-
# Simple flow mapping
-
simple_flow_mapping: {name: John, age: 30, city: New York}
-
-
# Nested flow sequences
-
nested_flow_sequences: [[a, b, c], [1, 2, 3], [red, green, blue]]
-
-
# Nested flow mappings
-
nested_flow_mappings: {person: {name: Alice, age: 25}, contact: {email: alice@example.com, phone: 555-1234}}
-
-
# Flow sequence with mappings
-
flow_seq_with_maps: [{name: Alice, role: dev}, {name: Bob, role: ops}, {name: Charlie, role: qa}]
-
-
# Flow mapping with sequences
-
flow_map_with_seqs: {colors: [red, green, blue], sizes: [S, M, L], numbers: [1, 2, 3]}
-
-
# Deeply nested flow collections
-
deep_flow_nesting: {level1: {level2: {level3: {level4: [a, b, c]}}}}
-
-
# Empty flow collections
-
empty_flow: {empty_seq: [], empty_map: {}, both: [[], {}]}
-
-
# Mixed flow and block - flow in block
-
flow_in_block:
-
sequence: [1, 2, 3, 4, 5]
-
mapping: {a: 1, b: 2, c: 3}
-
nested:
-
items: [x, y, z]
-
config: {timeout: 30, retries: 3}
-
-
# Mixed flow and block - block in flow
-
block_in_flow: {
-
users: [
-
{name: Alice, tags: [dev, senior]},
-
{name: Bob, tags: [ops, junior]}
-
]
-
}
-
-
# Complex mixed structure
-
mixed_structure:
-
services:
-
- name: api
-
ports: [8080, 8443]
-
env: {DEBUG: true, LOG_LEVEL: info}
-
- name: db
-
ports: [5432]
-
env: {POSTGRES_DB: mydb, POSTGRES_USER: admin}
-
config: {version: 1.0, enabled: true}
-
-
# Flow sequences with various types
-
flow_types:
-
strings: [hello, world, foo, bar]
-
numbers: [1, 2, 3, 42, 100]
-
mixed: [string, 123, true, false, null]
-
quoted: ["with spaces", "special:chars", "commas, here"]
-
-
# Flow mappings with various types
-
flow_map_types: {
-
string: value,
-
number: 42,
-
boolean: true,
-
null_value: null,
-
float: 3.14
-
}
-
-
# Nested mixed collections
-
nested_mixed:
-
- {id: 1, data: [a, b, c], meta: {type: first}}
-
- {id: 2, data: [d, e, f], meta: {type: second}}
-
- {id: 3, data: [g, h, i], meta: {type: third}}
-
-
# Flow with multiline (should still be valid)
-
multiline_flow:
-
long_sequence: [
-
item1,
-
item2,
-
item3,
-
item4
-
]
-
long_mapping: {
-
key1: value1,
-
key2: value2,
-
key3: value3
-
}
-
-
# Edge cases
-
edge_cases:
-
single_item_seq: [alone]
-
single_item_map: {only: one}
-
nested_empty: [[], [{}], [{}, []]]
-
all_empty: [{}, [], {a: []}, {b: {}}]
-53
yaml/ocaml-yamle/tests/yaml/comments.yml
···
-
# Full line comment at the beginning
-
# This is a YAML file testing comment handling
-
-
# Comment before a mapping
-
name: John Doe # End of line comment after a scalar value
-
age: 30 # Another end of line comment
-
-
# Comment between mapping entries
-
address:
-
# Comment inside nested mapping
-
street: 123 Main St # End of line comment in nested value
-
city: Springfield
-
# Comment between nested entries
-
zip: 12345
-
-
# Comment before sequence
-
items:
-
- apple # Comment after sequence item
-
- banana
-
# Comment between sequence items
-
- cherry
-
- date # Last item comment
-
-
# Comment before flow sequence
-
flow_seq: [1, 2, 3] # Comment after flow sequence
-
-
# Comment before flow mapping
-
flow_map: {key1: value1, key2: value2} # Comment after flow mapping
-
-
# Comments with various indentation levels
-
nested:
-
# Indented comment level 1
-
level1:
-
# Indented comment level 2
-
level2:
-
# Indented comment level 3
-
value: deeply nested # End comment at depth
-
-
# Multiple consecutive comments
-
# Line 1
-
# Line 2
-
# Line 3
-
multi_comment_key: value
-
-
# Comment with special characters: !@#$%^&*()
-
special: "value with # hash inside quotes"
-
-
# Empty value with comment
-
empty_value: # This key has no value (null)
-
-
# Comment before document end
-
final_key: final_value
-
# Final comment at end of file
-8
yaml/ocaml-yamle/tests/yaml/directives.yml
···
-
# YAML directive tests
-
-
# Test 1: %YAML 1.2 directive
-
%YAML 1.2
-
---
-
version: "1.2"
-
content: This document uses YAML 1.2
-
...
-15
yaml/ocaml-yamle/tests/yaml/directives_multiple_tags.yml
···
-
# Test 4: Multiple TAG directives
-
%YAML 1.2
-
%TAG !e! tag:example.com,2025:
-
%TAG !app! tag:myapp.org,2025:types:
-
%TAG !geo! tag:geography.net,2025:shapes:
-
---
-
user: !e!person
-
name: Alice
-
age: 30
-
location: !geo!coordinates
-
lat: 40.7128
-
lon: -74.0060
-
config: !app!settings
-
debug: true
-
timeout: 30
-10
yaml/ocaml-yamle/tests/yaml/directives_tag.yml
···
-
# Test 3: %TAG directive with custom prefix
-
%YAML 1.2
-
%TAG !custom! tag:example.com,2025:
-
---
-
shape: !custom!circle
-
radius: 5
-
color: red
-
point: !custom!point
-
x: 10
-
y: 20
-10
yaml/ocaml-yamle/tests/yaml/directives_yaml11.yml
···
-
# Test 2: %YAML 1.1 directive
-
%YAML 1.1
-
---
-
version: "1.1"
-
content: This document uses YAML 1.1
-
booleans:
-
- yes
-
- no
-
- on
-
- off
-15
yaml/ocaml-yamle/tests/yaml/documents_multi.yml
···
-
# Multiple document variations
-
-
# Test 1: Two documents separated by ---
-
---
-
document: first
-
type: mapping
-
data:
-
key1: value1
-
key2: value2
-
---
-
document: second
-
type: mapping
-
data:
-
key3: value3
-
key4: value4
-10
yaml/ocaml-yamle/tests/yaml/documents_multi_empty.yml
···
-
# Test 4: Empty documents
-
---
-
# Empty document (implicitly null)
-
---
-
key: value
-
---
-
# Another empty document
-
---
-
- item1
-
- item2
-15
yaml/ocaml-yamle/tests/yaml/documents_multi_three.yml
···
-
# Test 2: Three documents with different content types
-
---
-
# First document: mapping
-
name: John Doe
-
age: 30
-
city: New York
-
---
-
# Second document: sequence
-
- apple
-
- banana
-
- orange
-
- grape
-
---
-
# Third document: scalar
-
This is a plain scalar document
-16
yaml/ocaml-yamle/tests/yaml/documents_multi_with_end.yml
···
-
# Test 3: Documents with explicit end markers
-
---
-
first:
-
document: data1
-
value: 100
-
...
-
---
-
second:
-
document: data2
-
value: 200
-
...
-
---
-
third:
-
document: data3
-
value: 300
-
...
-11
yaml/ocaml-yamle/tests/yaml/documents_single.yml
···
-
# Single document variations
-
-
# Test 1: Implicit document (no markers)
-
key1: value1
-
key2: value2
-
nested:
-
inner: data
-
list:
-
- item1
-
- item2
-
- item3
-11
yaml/ocaml-yamle/tests/yaml/documents_single_explicit_both.yml
···
-
# Test 3: Explicit start and end (--- ... )
-
---
-
key1: value1
-
key2: value2
-
nested:
-
inner: data
-
list:
-
- item1
-
- item2
-
- item3
-
...
-10
yaml/ocaml-yamle/tests/yaml/documents_single_explicit_start.yml
···
-
# Test 2: Explicit start (---)
-
---
-
key1: value1
-
key2: value2
-
nested:
-
inner: data
-
list:
-
- item1
-
- item2
-
- item3
-11
yaml/ocaml-yamle/tests/yaml/documents_single_with_directive.yml
···
-
# Test 4: With %YAML directive
-
%YAML 1.2
-
---
-
key1: value1
-
key2: value2
-
nested:
-
inner: data
-
list:
-
- item1
-
- item2
-
- item3
-155
yaml/ocaml-yamle/tests/yaml/edge_cases.yml
···
-
# Edge cases test file for YAML parsing
-
-
# Case 1: Keys with colons (must be quoted)
-
"key:with:colons": value
-
"http://example.com": url_as_key
-
"time:12:30": time_value
-
-
# Case 2: Values starting with indicators (must be quoted or escaped)
-
indicator_square: "[this starts with bracket]"
-
indicator_curly: "{this starts with brace}"
-
indicator_star: "*this starts with star"
-
indicator_amp: "&this starts with ampersand"
-
indicator_question: "?this starts with question"
-
indicator_pipe: "|this starts with pipe"
-
indicator_gt: ">this starts with gt"
-
indicator_dash: "-this starts with dash"
-
indicator_hash: "#this starts with hash"
-
-
# Case 3: Special string values that look like other types
-
string_true: "true"
-
string_false: "false"
-
string_null: "null"
-
string_number: "123"
-
string_float: "45.67"
-
string_yes: "yes"
-
string_no: "no"
-
-
# Case 4: Actual special values
-
bool_true: true
-
bool_false: false
-
null_value: null
-
null_tilde: ~
-
number_int: 123
-
number_float: 45.67
-
number_exp: 1.23e4
-
number_hex: 0x1F
-
number_oct: 0o17
-
-
# Case 5: Empty values
-
empty_string: ""
-
empty_list: []
-
empty_map: {}
-
null_implicit:
-
-
# Case 6: Very long lines
-
very_long_key: "This is a very long value that contains a lot of text to test how the parser handles long lines. It should be able to handle lines that are much longer than typical lines in most YAML files. This continues for quite a while to make sure we test the boundaries of reasonable line lengths. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
-
-
very_long_literal: |
-
This is a very long literal block that should preserve all the whitespace and newlines exactly as written. It can contain very long lines that go on and on and on without breaking. This tests whether the parser can handle long content in literal blocks properly. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
-
-
# Case 7: Unicode and special characters
-
unicode_emoji: "Hello 🌍 World 🚀"
-
unicode_chars: "Héllo Wörld 你好 مرحبا"
-
unicode_key_🔑: unicode_value
-
escaped_chars: "Line1\nLine2\tTabbed"
-
-
# Case 8: Nested empty structures
-
nested_empty:
-
level1: {}
-
level2:
-
inner: []
-
level3:
-
inner:
-
deep: null
-
-
# Case 9: Complex keys (flow collections as keys)
-
? [complex, key]
-
: complex_value
-
? {nested: key}
-
: another_value
-
-
# Case 10: Multi-line keys and values
-
? |
-
This is a multi-line
-
key using literal block
-
: |
-
This is a multi-line
-
value using literal block
-
-
# Case 11: Quoted strings with escape sequences
-
single_quoted: 'It''s a single-quoted string with doubled quotes'
-
double_quoted: "It's a \"double-quoted\" string with escapes"
-
backslash: "Path\\to\\file"
-
newline_escape: "First line\nSecond line"
-
-
# Case 12: Anchors and aliases at edge positions
-
anchor_list: &anchor_ref
-
- item1
-
- item2
-
- item3
-
-
alias_usage: *anchor_ref
-
-
nested_anchor:
-
data: &nested_ref
-
key: value
-
reference: *nested_ref
-
-
# Case 13: Mixed flow and block styles
-
mixed_style:
-
block_key:
-
- flow_in_block: [1, 2, 3]
-
- another: {a: 1, b: 2}
-
flow_key: {block_in_flow:
-
- item1
-
- item2}
-
-
# Case 14: Trailing commas in flow (typically invalid in YAML)
-
# flow_trailing: [1, 2, 3,] # This would be invalid
-
-
# Case 15: Strings that need quoting
-
needs_quote_1: "value with # in it"
-
needs_quote_2: "value with: colon"
-
needs_quote_3: "value with @ at sign"
-
needs_quote_4: "value with ` backtick"
-
-
# Case 16: Multiple documents separator (not starting a new document)
-
not_doc_separator: "--- this is just a string value"
-
-
# Case 17: Extremely nested structures
-
deeply_nested:
-
l1:
-
l2:
-
l3:
-
l4:
-
l5:
-
l6:
-
l7:
-
l8:
-
l9:
-
l10: "deep value"
-
-
# Case 18: Large sequence
-
large_sequence:
-
- item_001
-
- item_002
-
- item_003
-
- item_004
-
- item_005
-
- item_006
-
- item_007
-
- item_008
-
- item_009
-
- item_010
-
-
# Case 19: Keys and values with only whitespace differences
-
" key": "value with leading space in key"
-
"key ": "value with trailing space in key"
-
" spaced ": " spaced "
-
-
# Case 20: Binary-looking values
-
binary_string: "0b101010"
-
hex_string: "0xDEADBEEF"
-
-
# End of edge cases test file
-59
yaml/ocaml-yamle/tests/yaml/linuxkit.yml
···
-
kernel:
-
image: linuxkit/kernel:4.9.40
-
cmdline: "console=tty0 console=ttyS0"
-
init:
-
- linuxkit/init:906e174b3f2e07f97d6fd693a2e8518e98dafa58
-
- linuxkit/runc:90e45f13e1d0a0983f36ef854621e3eac91cf541
-
- linuxkit/containerd:7c986fb7df33bea73b5c8097b46989e46f49d875
-
- linuxkit/ca-certificates:e44b0a66df5a102c0e220f0066b0d904710dcb10
-
onboot:
-
- name: sysctl
-
image: linuxkit/sysctl:184c914d23a017062d7b53d7fc1dfaf47764bef6
-
- name: dhcpcd
-
image: linuxkit/dhcpcd:f3f5413abb78fae9020e35bd4788fa93df4530b7
-
command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
-
onshutdown:
-
- name: shutdown
-
image: busybox:latest
-
command: ["/bin/echo", "so long and thanks for all the fish"]
-
services:
-
- name: getty
-
image: linuxkit/getty:2c841cdc34396e3fa8f25b62d112808f63f16df6
-
env:
-
- INSECURE=true
-
- name: rngd
-
image: linuxkit/rngd:b2f4bdcb55aa88a25c86733e294628614504f383
-
- name: nginx
-
image: nginx:alpine
-
capabilities:
-
- CAP_NET_BIND_SERVICE
-
- CAP_CHOWN
-
- CAP_SETUID
-
- CAP_SETGID
-
- CAP_DAC_OVERRIDE
-
files:
-
- path: etc/containerd/config.toml
-
contents: |
-
state = "/run/containerd"
-
root = "/var/lib/containerd"
-
snapshotter = "io.containerd.snapshotter.v1.overlayfs"
-
differ = "io.containerd.differ.v1.base-diff"
-
subreaper = false
-
-
[grpc]
-
address = "/run/containerd/containerd.sock"
-
uid = 0
-
gid = 0
-
-
[debug]
-
address = "/run/containerd/debug.sock"
-
level = "info"
-
-
[metrics]
-
address = ":13337"
-
- path: etc/linuxkit-config
-
metadata: yaml
-
trust:
-
org:
-
- linuxkit
-
- library
-192
yaml/ocaml-yamle/tests/yaml/scalars_block.yml
···
-
# Block scalars - literal and folded styles
-
---
-
# Literal style (|) - preserves newlines
-
literal_basic: |
-
Line one
-
Line two
-
Line three
-
-
literal_with_indent: |
-
First line
-
Indented line
-
More indented
-
Back to second level
-
Back to first level
-
-
# Folded style (>) - converts newlines to spaces
-
folded_basic: >
-
This is a long paragraph
-
that will be folded into
-
a single line with the
-
newlines converted to spaces.
-
-
folded_paragraph: >
-
First paragraph flows together
-
into a single line.
-
-
Second paragraph after blank line
-
also flows together.
-
-
# Chomping indicators
-
# Strip (-) - removes trailing newlines
-
literal_strip: |-
-
No trailing newline
-
-
-
literal_strip_multiple: |-
-
Text here
-
-
-
folded_strip: >-
-
Folded text
-
with stripped
-
trailing newlines
-
-
-
# Clip (default) - keeps single trailing newline
-
literal_clip: |
-
One trailing newline
-
-
-
literal_clip_explicit: |
-
This is the default behavior
-
-
-
folded_clip: >
-
Folded with one
-
trailing newline
-
-
-
# Keep (+) - preserves all trailing newlines
-
literal_keep: |+
-
Keeps trailing newlines
-
-
-
literal_keep_multiple: |+
-
Text here
-
-
-
folded_keep: >+
-
Folded text
-
keeps trailing
-
-
-
# Explicit indentation indicators
-
literal_indent_2: |2
-
Two space indentation
-
is preserved here
-
Extra indent
-
Back to two
-
-
literal_indent_4: |4
-
Four space base indent
-
Second line
-
Extra indent
-
Back to base
-
-
folded_indent_2: >2
-
Text with two space
-
base indentation that
-
will be folded.
-
-
folded_indent_3: >3
-
Three space indent
-
for this folded
-
text block.
-
-
# Combinations of indicators
-
literal_indent_strip: |2-
-
Indented by 2
-
No trailing newlines
-
-
-
folded_indent_strip: >3-
-
Folded with indent
-
and stripped end
-
-
-
literal_indent_keep: |2+
-
Indented by 2
-
Keeps trailing newlines
-
-
-
folded_indent_keep: >4+
-
Folded indent 4
-
keeps all trailing
-
-
-
# Empty block scalars
-
empty_literal: |
-
-
empty_folded: >
-
-
# Block scalar with only newlines
-
only_newlines_literal: |
-
-
-
only_newlines_folded: >
-
-
-
# Complex indentation patterns
-
complex_literal: |
-
First level
-
Second level
-
Third level
-
Back to second
-
Back to first
-
-
New paragraph
-
With indent
-
-
Final paragraph
-
-
complex_folded: >
-
This paragraph
-
flows together.
-
-
This is separate.
-
This line starts more indented
-
and continues.
-
-
Final thoughts here.
-
-
# Special characters in block scalars
-
special_chars_literal: |
-
Special: @#$%^&*()
-
Quotes: "double" 'single'
-
Brackets: [array] {object}
-
Symbols: | > & * ? : -
-
-
special_chars_folded: >
-
All special chars are literal
-
in block scalars: []{}|>*&
-
-
# Block scalars in sequences
-
sequence_with_blocks:
-
- |
-
First item
-
literal block
-
- >
-
Second item
-
folded block
-
- |-
-
Third item
-
stripped
-
- |+
-
Fourth item
-
kept
-
-
-
# Block scalars in nested mappings
-
nested:
-
description: >
-
This is a folded
-
description that spans
-
multiple lines.
-
code: |
-
def hello():
-
print("Hello, World!")
-
return True
-
notes: |-
-
Final notes
-
with stripped end
-60
yaml/ocaml-yamle/tests/yaml/scalars_plain.yml
···
-
# Plain scalars - no quotes needed
-
---
-
# Simple words
-
simple_word: hello
-
single_character: x
-
number_like: 123
-
boolean_like: true
-
null_like: null
-
-
# Multi-word values (no special meaning characters)
-
sentence: this is a plain scalar
-
phrase: plain scalars can have spaces
-
-
# Numbers and special values that remain strings in context
-
age: 42
-
pi: 3.14159
-
negative: -273
-
scientific: 1.23e-4
-
hex_like: 0x1A2B
-
octal_like: 0o755
-
-
# Special characters that are valid in plain scalars
-
with_colon: "value: with colon needs quotes in value"
-
with_comma: "commas, need quotes in flow context"
-
with_hash: "# needs quotes if starting with hash"
-
hyphen_start: "- needs quotes if starting like list"
-
question_start: "? needs quotes if starting like mapping key"
-
-
# Plain scalars with valid special characters
-
email: user@example.com
-
url: http://example.com/path
-
path: /usr/local/bin
-
ratio: 16:9
-
version: v1.2.3
-
-
# Multi-line plain scalars (line folding)
-
# Newlines become spaces, blank lines become newlines
-
folded_plain: This is a long
-
plain scalar that spans
-
multiple lines and will
-
be folded into a single line
-
with spaces.
-
-
another_folded: First paragraph
-
continues here and here.
-
-
Second paragraph after blank line.
-
Also continues.
-
-
# Trailing and leading spaces are trimmed in plain scalars
-
spaces_trimmed: value with spaces
-
-
# Plain scalars can contain most punctuation
-
punctuation: Hello, world! How are you? I'm fine.
-
symbols: $100 & 50% off @ store #1
-
math: 2+2=4 and 3*3=9
-
-
# Empty plain scalar (becomes null)
-
empty_implicit:
-
explicit_empty: ""
-81
yaml/ocaml-yamle/tests/yaml/scalars_quoted.yml
···
-
# Quoted scalars - single and double quoted strings
-
---
-
# Single-quoted strings
-
single_simple: 'hello world'
-
single_with_double: 'He said "hello"'
-
single_escaped_quote: 'It''s a single quote: ''example'''
-
single_multiline: 'This is a
-
multi-line single
-
quoted string'
-
-
# Double-quoted strings
-
double_simple: "hello world"
-
double_with_single: "It's easy"
-
double_escaped_quote: "She said \"hello\""
-
-
# Escape sequences in double-quoted strings
-
escaped_newline: "Line one\nLine two\nLine three"
-
escaped_tab: "Column1\tColumn2\tColumn3"
-
escaped_backslash: "Path: C:\\Users\\Name"
-
escaped_carriage: "Before\rAfter"
-
escaped_bell: "Bell\a"
-
escaped_backspace: "Back\b"
-
escaped_formfeed: "Form\f"
-
escaped_vertical: "Vertical\vtab"
-
-
# Unicode escapes
-
unicode_16bit: "Snowman: \u2603"
-
unicode_32bit: "Emoji: \U0001F600"
-
unicode_hex: "Null byte: \x00"
-
-
# Empty strings
-
empty_single: ''
-
empty_double: ""
-
-
# Strings that would be interpreted as other types if unquoted
-
string_true: "true"
-
string_false: "false"
-
string_null: "null"
-
string_number: "123"
-
string_float: "45.67"
-
string_octal: "0o755"
-
string_hex: "0xFF"
-
-
# Special YAML characters that need quoting
-
starts_with_at: "@username"
-
starts_with_backtick: "`command`"
-
starts_with_ampersand: "&reference"
-
starts_with_asterisk: "*alias"
-
starts_with_exclamation: "!tag"
-
starts_with_pipe: "|literal"
-
starts_with_gt: ">folded"
-
starts_with_percent: "%directive"
-
-
# Flow indicators that need quoting
-
with_brackets: "[not a list]"
-
with_braces: "{not: a map}"
-
with_comma: "a, b, c"
-
with_colon_space: "key: value"
-
-
# Quoted strings preserve leading/trailing whitespace
-
leading_space: " spaces before"
-
trailing_space: "spaces after "
-
both_spaces: " spaces both "
-
-
# Multi-line quoted strings
-
double_multiline: "This is a string
-
that spans multiple
-
lines with escaped newlines."
-
-
single_fold: 'This single quoted
-
string will fold
-
lines into spaces.'
-
-
# Complex escape sequences
-
complex_escapes: "Tab:\t Newline:\n Quote:\" Backslash:\\ Unicode:\u0041"
-
-
# Edge cases
-
only_spaces_single: ' '
-
only_spaces_double: " "
-
only_newlines: "\n\n\n"
-
mixed_quotes: "She said 'it''s a beautiful day'"
-5
yaml/ocaml-yamle/tests/yaml/seq.yml
···
-
- hello
-
- whats
-
- up
-
- foo
-
- bar
-82
yaml/ocaml-yamle/tests/yaml/values_bool.yml
···
-
# Boolean value test cases for YAML 1.2
-
# Note: YAML 1.2 only recognizes 'true' and 'false' as booleans
-
# Other values like yes/no, on/off are treated as strings in 1.2
-
-
# Standard YAML 1.2 booleans (lowercase)
-
bool_true: true
-
bool_false: false
-
-
# Capitalized forms (should be strings in YAML 1.2)
-
capitalized_true: True
-
capitalized_false: False
-
-
# YAML 1.1 style booleans (should be strings in YAML 1.2)
-
yes_value: yes
-
no_value: no
-
Yes_value: Yes
-
No_value: No
-
YES_value: YES
-
NO_value: NO
-
-
# On/Off style (should be strings in YAML 1.2)
-
on_value: on
-
off_value: off
-
On_value: On
-
Off_value: Off
-
ON_value: ON
-
OFF_value: OFF
-
-
# Booleans in sequences
-
bool_sequence:
-
- true
-
- false
-
- yes
-
- no
-
- on
-
- off
-
-
# Booleans in flow style
-
flow_bools: [true, false, yes, no]
-
-
# Booleans in mappings
-
bool_mapping:
-
active: true
-
disabled: false
-
enabled: yes
-
stopped: no
-
-
# String literals that should NOT be parsed as booleans
-
quoted_bools:
-
quoted_true: "true"
-
quoted_false: "false"
-
quoted_yes: "yes"
-
quoted_no: "no"
-
single_true: 'true'
-
single_false: 'false'
-
-
# Nested boolean values
-
nested_bools:
-
settings:
-
debug: true
-
verbose: false
-
legacy_yes: yes
-
legacy_no: no
-
flags:
-
- true
-
- false
-
- on
-
- off
-
-
# Mixed case variations
-
mixed_case:
-
TRUE: TRUE
-
FALSE: FALSE
-
TrUe: TrUe
-
FaLsE: FaLsE
-
-
# Boolean-like strings that should remain strings
-
bool_like_strings:
-
truthy: truely
-
falsy: falsetto
-
yes_sir: yessir
-
no_way: noway
-55
yaml/ocaml-yamle/tests/yaml/values_null.yml
···
-
# Null value test cases for YAML 1.2
-
-
# Explicit null keyword
-
explicit_null: null
-
-
# Tilde shorthand for null
-
tilde_null: ~
-
-
# Empty value (implicit null)
-
empty_null:
-
-
# Null in flow style
-
flow_null: [null, ~, ]
-
-
# Null in sequences
-
sequence_nulls:
-
- null
-
- ~
-
-
-
- explicit: null
-
- tilde: ~
-
- empty:
-
-
# Null in mappings
-
mapping_nulls:
-
key1: null
-
key2: ~
-
key3:
-
-
# Null as key
-
null: "null key with string value"
-
~: "tilde key with string value"
-
-
# Mixed null values in nested structures
-
nested:
-
level1:
-
null_value: null
-
tilde_value: ~
-
empty_value:
-
list:
-
- null
-
- ~
-
-
-
- some_value
-
map:
-
a: null
-
b: ~
-
c:
-
-
# String literals that contain "null" (should NOT be parsed as null)
-
string_nulls:
-
quoted_null: "null"
-
quoted_tilde: "~"
-
null_in_string: "this is null"
-
word_null: 'null'
-120
yaml/ocaml-yamle/tests/yaml/values_numbers.yml
···
-
# Numeric value test cases for YAML 1.2
-
-
# Integers
-
int_zero: 0
-
int_positive: 42
-
int_negative: -17
-
int_large: 1000000
-
int_with_underscores: 1_000_000
-
-
# Octal notation (YAML 1.2 style with 0o prefix)
-
octal_value: 0o14
-
octal_zero: 0o0
-
octal_large: 0o777
-
-
# Hexadecimal notation
-
hex_lowercase: 0x1a
-
hex_uppercase: 0x1A
-
hex_mixed: 0xDeadBeef
-
hex_zero: 0x0
-
-
# Floating point numbers
-
float_simple: 3.14
-
float_negative: -0.5
-
float_zero: 0.0
-
float_leading_dot: .5
-
float_trailing_zero: 1.0
-
-
# Scientific notation
-
scientific_positive: 1.0e10
-
scientific_negative: 1.5e-3
-
scientific_uppercase: 2.5E+2
-
scientific_no_sign: 3.0e5
-
-
# Special floating point values
-
positive_infinity: .inf
-
negative_infinity: -.inf
-
not_a_number: .nan
-
infinity_upper: .Inf
-
infinity_caps: .INF
-
nan_upper: .NaN
-
nan_caps: .NAN
-
-
# Numbers in sequences
-
number_sequence:
-
- 0
-
- 42
-
- -17
-
- 3.14
-
- 1.0e10
-
- .inf
-
- .nan
-
-
# Numbers in flow style
-
flow_numbers: [0, 42, -17, 3.14, 0x1A, 0o14]
-
-
# Numbers in mappings
-
number_mapping:
-
count: 100
-
price: 19.99
-
discount: -5.0
-
hex_color: 0xFF5733
-
octal_perms: 0o755
-
scientific: 6.022e23
-
-
# String literals that look like numbers (quoted)
-
quoted_numbers:
-
string_int: "42"
-
string_float: "3.14"
-
string_hex: "0x1A"
-
string_octal: "0o14"
-
string_inf: ".inf"
-
string_nan: ".nan"
-
-
# Numeric strings that should remain strings
-
numeric_strings:
-
phone: 555-1234
-
version: 1.2.3
-
code: 00123
-
leading_zero: 007
-
plus_sign: +123
-
-
# Edge cases
-
edge_cases:
-
min_int: -9223372036854775808
-
max_int: 9223372036854775807
-
very_small: 1.0e-100
-
very_large: 1.0e100
-
negative_zero: -0.0
-
positive_zero: +0.0
-
-
# Nested numeric values
-
nested_numbers:
-
coordinates:
-
x: 10.5
-
y: -20.3
-
z: 0.0
-
measurements:
-
- 1.1
-
- 2.2
-
- 3.3
-
stats:
-
count: 1000
-
average: 45.67
-
max: .inf
-
min: -.inf
-
-
# YAML 1.1 style octals (no 0o prefix) - should be strings in YAML 1.2
-
legacy_octal: 014
-
-
# Binary notation (not part of YAML 1.2 core, but sometimes supported)
-
# These should be treated as strings in strict YAML 1.2
-
binary_like: 0b1010
-
-
# Numbers with various formats
-
format_tests:
-
no_decimal: 42
-
with_decimal: 42.0
-
leading_zero_decimal: 0.42
-
no_leading_digit: .42
-
trailing_decimal: 42.
-101
yaml/ocaml-yamle/tests/yaml/values_timestamps.yml
···
-
# Timestamp value test cases for YAML 1.1
-
# Note: YAML 1.2 does not have a timestamp type in the core schema
-
# These are recognized in YAML 1.1 and some extended schemas
-
-
# ISO 8601 date format (YYYY-MM-DD)
-
date_simple: 2001-12-15
-
date_earliest: 1970-01-01
-
date_leap_year: 2020-02-29
-
date_current: 2025-12-04
-
-
# ISO 8601 datetime with timezone (UTC)
-
datetime_utc: 2001-12-15T02:59:43.1Z
-
datetime_utc_full: 2001-12-15T02:59:43.123456Z
-
datetime_utc_no_frac: 2001-12-15T02:59:43Z
-
-
# ISO 8601 datetime with timezone offset
-
datetime_offset_pos: 2001-12-15T02:59:43.1+05:30
-
datetime_offset_neg: 2001-12-15T02:59:43.1-05:00
-
datetime_offset_hours: 2001-12-15T02:59:43+05
-
-
# Spaced datetime format (YAML 1.1 style)
-
datetime_spaced: 2001-12-14 21:59:43.10 -5
-
datetime_spaced_utc: 2001-12-15 02:59:43.1 Z
-
datetime_spaced_offset: 2001-12-14 21:59:43.10 -05:00
-
-
# Datetime without fractional seconds
-
datetime_no_frac: 2001-12-15T14:30:00Z
-
-
# Date only (no time component)
-
date_only: 2001-12-15
-
-
# Various formats
-
timestamp_formats:
-
iso_date: 2001-12-15
-
iso_datetime_z: 2001-12-15T02:59:43Z
-
iso_datetime_offset: 2001-12-15T02:59:43+00:00
-
spaced_datetime: 2001-12-14 21:59:43.10 -5
-
canonical: 2001-12-15T02:59:43.1Z
-
-
# Timestamps in sequences
-
timestamp_sequence:
-
- 2001-12-15
-
- 2001-12-15T02:59:43.1Z
-
- 2001-12-14 21:59:43.10 -5
-
- 2025-01-01T00:00:00Z
-
-
# Timestamps in mappings
-
events:
-
created: 2001-12-15T02:59:43.1Z
-
modified: 2001-12-16T10:30:00Z
-
published: 2001-12-14 21:59:43.10 -5
-
-
# String literals that look like timestamps (quoted)
-
quoted_timestamps:
-
string_date: "2001-12-15"
-
string_datetime: "2001-12-15T02:59:43.1Z"
-
string_spaced: "2001-12-14 21:59:43.10 -5"
-
-
# Edge cases and variations
-
edge_cases:
-
midnight: 2001-12-15T00:00:00Z
-
end_of_day: 2001-12-15T23:59:59Z
-
microseconds: 2001-12-15T02:59:43.123456Z
-
no_seconds: 2001-12-15T02:59Z
-
hour_only: 2001-12-15T02Z
-
-
# Nested timestamp values
-
nested_timestamps:
-
project:
-
start_date: 2001-12-15
-
milestones:
-
- date: 2001-12-20
-
time: 2001-12-20T14:00:00Z
-
- date: 2002-01-15
-
time: 2002-01-15T09:30:00-05:00
-
metadata:
-
created: 2001-12-14 21:59:43.10 -5
-
updated: 2001-12-15T02:59:43.1Z
-
-
# Invalid timestamp formats (should be treated as strings)
-
invalid_timestamps:
-
bad_date: 2001-13-45
-
bad_time: 2001-12-15T25:99:99Z
-
incomplete: 2001-12
-
no_leading_zero: 2001-1-5
-
-
# Different timezone representations
-
timezones:
-
utc_z: 2001-12-15T02:59:43Z
-
utc_offset: 2001-12-15T02:59:43+00:00
-
est: 2001-12-14T21:59:43-05:00
-
ist: 2001-12-15T08:29:43+05:30
-
jst: 2001-12-15T11:59:43+09:00
-
-
# Historical and future dates
-
date_range:
-
past: 1900-01-01
-
unix_epoch: 1970-01-01T00:00:00Z
-
y2k: 2000-01-01T00:00:00Z
-
present: 2025-12-04
-
future: 2099-12-31T23:59:59Z
-105
yaml/ocaml-yamle/tests/yaml/whitespace.yml
···
-
# Whitespace handling test file
-
-
# Section 1: Different indentation levels (2 spaces)
-
two_space_indent:
-
level1:
-
level2:
-
level3: value
-
-
# Section 2: Four space indentation
-
four_space_indent:
-
level1:
-
level2:
-
level3: value
-
-
# Section 3: Mixed content with blank lines
-
-
first_key: first_value
-
-
-
second_key: second_value
-
-
-
-
third_key: third_value
-
-
# Section 4: Sequences with varying indentation
-
sequence_2space:
-
- item1
-
- item2
-
- nested:
-
- nested_item1
-
- nested_item2
-
-
sequence_4space:
-
- item1
-
- item2
-
- nested:
-
- nested_item1
-
- nested_item2
-
-
# Section 5: Trailing whitespace (spaces after values - invisible but present)
-
trailing_spaces: value
-
another_key: another_value
-
-
# Section 6: Leading whitespace preservation in literals
-
literal_block: |
-
This is a literal block
-
with preserved indentation
-
including extra spaces
-
and blank lines
-
-
like this one above
-
-
folded_block: >
-
This is a folded block
-
that will be folded into
-
a single line but preserves
-
-
paragraph breaks like above
-
-
# Section 7: Whitespace in flow collections
-
flow_with_spaces: [ item1 , item2 , item3 ]
-
flow_tight: [item1,item2,item3]
-
flow_map_spaces: { key1: value1 , key2: value2 }
-
flow_map_tight: {key1:value1,key2:value2}
-
-
# Section 8: Multiple consecutive blank lines between top-level keys
-
key_before_blanks: value1
-
-
-
-
-
key_after_blanks: value2
-
-
# Section 9: Indentation in mappings
-
mapping_indent:
-
key1: value1
-
key2: value2
-
nested:
-
nested_key1: nested_value1
-
nested_key2: nested_value2
-
deep_nested:
-
deep_key: deep_value
-
-
# Section 10: Whitespace around colons and hyphens
-
no_space_colon:value
-
space_after_colon: value
-
spaces_around: value
-
- sequence_item_no_space
-
- nested_sequence
-
-
# Section 11: Empty lines in sequences
-
sequence_with_blanks:
-
- item1
-
-
- item2
-
-
- item3
-
-
# Section 12: Whitespace-only mapping values (implicit null)
-
explicit_null: null
-
implicit_null:
-
space_only:
-
-
# End of whitespace test file
-3
yaml/ocaml-yamle/tests/yaml/yaml-1.2.yml
···
-
- {"when the key is quoted":"space after colon can be omitted."}
-
- "quoted slashes \/ are allowed."
-
- {?"a key can be looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooger": "than 1024 when parsing is unambiguous before seeing the colon."}
-32
yaml/ocaml-yamle/yamle.opam
···
-
# This file is generated by dune, edit dune-project instead
-
opam-version: "2.0"
-
version: "0.1.0"
-
synopsis: "Pure OCaml YAML 1.2 parser and emitter"
-
description:
-
"A pure OCaml implementation of YAML 1.2 parsing and emission, with no C dependencies."
-
maintainer: ["yamle@example.com"]
-
authors: ["Yamle Authors"]
-
license: "ISC"
-
homepage: "https://github.com/ocaml/yamle"
-
bug-reports: "https://github.com/ocaml/yamle/issues"
-
depends: [
-
"ocaml" {>= "4.14.0"}
-
"dune" {>= "3.0" & >= "3.0"}
-
"alcotest" {with-test}
-
"odoc" {with-doc}
-
]
-
build: [
-
["dune" "subst"] {dev}
-
[
-
"dune"
-
"build"
-
"-p"
-
name
-
"-j"
-
jobs
-
"@install"
-
"@runtest" {with-test}
-
"@doc" {with-doc}
-
]
-
]
-
dev-repo: "git+https://github.com/ocaml/yamle.git"