1# 2# YAML parser for Tcl. 3# 4# See http://www.yaml.org/spec/1.1/ 5# 6# yaml.tcl,v 0.3.5 2009-05-24 11:52:34 KATO Kanryu(kanryu6@users.sourceforge.net) 7# 8# It is published with the terms of tcllib's BSD-style license. 9# See the file named license.terms. 10# 11# It currently supports a very limited subsection of the YAML spec. 12# 13# 14 15if {$::tcl_version < 8.5} { 16 package require dict 17} 18 19package provide yaml 0.3.5 20package require cmdline 21package require huddle 22 23 24namespace eval ::yaml { 25 namespace export load setOptions dict2dump list2dump 26 variable data 27 array set data {} 28 29 # fixed value groups for some yaml-types. 30 variable fixed 31 32 # a plane scalar is worked for matching and converting to the specific type. 33 # proc some_command {value} { 34 # return [list !!type $treatmented-value] 35 # or 36 # return "" 37 # } 38 variable parsers 39 40 # scalar/collection treatment for matched specific yaml-tag 41 # proc some_composer {type value} { 42 # return [list 1 $result-type $treatmented-value] 43 # or 44 # return "" 45 # } 46 variable composer 47 48 variable defaults 49 array set defaults { 50 isfile 0 51 validate 0 52 types {timestamp int float null true false} 53 composer { 54 !!binary ::yaml::_composeBinary 55 } 56 parsers { 57 timestamp ::yaml::_parseTimestamp 58 } 59 shorthands { 60 !! {tag:yaml.org,2002:} 61 } 62 fixed { 63 null:Value "" 64 null:Group {null "" ~} 65 true:Value 1 66 true:Group {true on + yes y} 67 false:Value 0 68 false:Group {false off - no n} 69 } 70 } 71 72 variable _dumpIndent 2 73 variable _dumpWordWrap 40 74 75 variable opts [lrange [::cmdline::GetOptionDefaults { 76 {file {input is filename}} 77 {stream {input is stream}} 78 {m.arg "" {fixed-modifiers bulk setting(null/true/false)}} 79 {m:null.arg "" {null modifier setting(default {"" {null "" ~}})}} 80 {m:true.arg "" {true modifier setting(default {1 {true on + yes y}})}} 81 {m:false.arg "" {false modifier setting(default {0 {false off - no n}})}} 82 {types.arg "" {modifier list setting(default {nop timestamp integer null true false})}} 83 {validate {to validate the input(not dumped tcl content)}} 84 } result] 2 end] ;# Remove ? and help. 85 86 variable errors 87 array set errors { 88 TAB_IN_PLAIN {Tabs can be used only in comments, and in quoted "..." '...'.} 89 AT_IN_PLAIN {Reserved indicators {@} can't start a plain scalar.} 90 BT_IN_PLAIN {Reserved indicators {`} can't start a plain scalar.} 91 SEQEND_NOT_IN_SEQ {There is a flow-sequence end '\]' not in flow-sequence [v, ...].} 92 MAPEND_NOT_IN_MAP {There is a flow-mapping end '\}' not in flow-mapping {k: v, ...}.} 93 ANCHOR_NOT_FOUND {Could not find the anchor-name(current-version, "after refering" is not supported)} 94 MALFORM_D_QUOTE {Double quote "..." parsing error. end of quote is missing?} 95 MALFORM_S_QUOTE {Single quote '...' parsing error. end of quote is missing?} 96 TAG_NOT_FOUND {The "$p1" handle wasn't declared.} 97 INVALID_MERGE_KEY {merge-key "<<" is not impremented in not mapping scope(e.g. in sequence).} 98 MALFORMED_MERGE_KEY {malformed merge-key "<<" using.} 99 } 100} 101 102 103#################### 104# Public APIs 105#################### 106 107proc ::yaml::yaml2dict {args} { 108 _getOption $args 109 110 set result [_parseBlockNode] 111 if {$yaml::data(validate)} { 112 set result [string map "{\n} {\\n}" $result] 113 } 114 return [huddle strip $result] 115} 116 117proc ::yaml::yaml2huddle {args} { 118 _getOption $args 119 120 set result [_parseBlockNode] 121 if {$yaml::data(validate)} { 122 set result [string map "{\n} {\\n}" $result] 123 } 124 return $result 125} 126 127proc ::yaml::setOptions {argv} { 128 variable defaults 129 array set options [_imp_getOptions argv] 130 array set defaults [array get options] 131} 132 133# Dump TCL List to YAML 134# 135 136proc ::yaml::list2yaml {list {indent 2} {wordwrap 40}} { 137 return [huddle2yaml [eval huddle list $list] $indent $wordwrap] 138} 139 140proc ::yaml::dict2yaml {dict {indent 2} {wordwrap 40}} { 141 return [huddle2yaml [eval huddle create $dict] $indent $wordwrap] 142} 143 144proc ::yaml::huddle2yaml {huddle {indent 2} {wordwrap 40}} { 145 set yaml::_dumpIndent $indent 146 set yaml::_dumpWordWrap $wordwrap 147 148 # Start at the base of the array and move through it. 149 set out [join [list "---\n" [_imp_huddle2yaml $huddle] "\n"] ""] 150 return $out 151} 152 153 154#################### 155# Option Setting 156#################### 157 158proc ::yaml::_getOption {argv} { 159 variable data 160 variable parsers 161 variable fixed 162 variable composer 163 164 # default setting 165 array set options [_imp_getOptions argv] 166 167 array set fixed $options(fixed) 168 array set parsers $options(parsers) 169 array set composer $options(composer) 170 array set data [list validate $options(validate) types $options(types)] 171 set isfile $options(isfile) 172 173 foreach {buffer} $argv break 174 if {$isfile} { 175 set fd [open $buffer r] 176 set buffer [read $fd] 177 close $fd 178 } 179 set data(buffer) $buffer 180 set data(start) 0 181 set data(length) [string length $buffer] 182 set data(current) 0 183 set data(finished) 0 184} 185 186proc ::yaml::_imp_getOptions {{argvvar argv}} { 187 upvar 1 $argvvar argv 188 189 variable defaults 190 variable opts 191 array set options [array get defaults] 192 193 # default setting 194 array set fixed $options(fixed) 195 196 # parse argv 197 set argc [llength $argv] 198 while {[set err [::cmdline::getopt argv $opts opt arg]]} { 199 if {$err eq -1} break 200 switch -- $opt { 201 "file" { 202 set options(isfile) 1 203 } 204 "stream" { 205 set options(isfile) 0 206 } 207 "m" { 208 array set options(fixed) $arg 209 } 210 "validate" { 211 set options(validate) 1 212 } 213 "types" { 214 set options(types) $arg 215 } 216 default { 217 if {[regexp {m:(\w+)} $opt nop type]} { 218 if {$arg eq ""} { 219 set fixed(${type}:Group) "" 220 } else { 221 foreach {value group} $arg { 222 set fixed(${type}:Value) $value 223 set fixed(${type}:Group) $group 224 } 225 } 226 } 227 } 228 } 229 } 230 set options(fixed) [array get fixed] 231 return [array get options] 232} 233 234######################### 235# Scalar/Block Composers 236######################### 237proc ::yaml::_composeTags {tag value} { 238 if {$tag eq ""} {return $value} 239 set value [huddle strip $value] 240 if {$tag eq "!!str"} { 241 set pair [list $tag $value] 242 } elseif {[info exists yaml::composer($tag)]} { 243 set pair [$yaml::composer($tag) $value] 244 } else { 245 error [_getErrorMessage TAG_NOT_FOUND $tag] 246 } 247 return [eval huddle wrap $pair] 248} 249 250proc ::yaml::_composeBinary {value} { 251 package require base64 252 return [list !!binary [::base64::decode $value]] 253} 254 255proc ::yaml::_composePlain {value} { 256 if {[huddle type $value] ne "plain"} {return $value} 257 set value [huddle strip $value] 258 set pair [_toType $value] 259 return [eval huddle wrap $pair] 260} 261 262proc ::yaml::_toType {value} { 263 if {$value eq ""} {return [list !!str ""]} 264 265 set lowerval [string tolower $value] 266 foreach {type} $yaml::data(types) { 267 if {[info exists yaml::parsers($type)]} { 268 set pair [$yaml::parsers($type) $value] 269 if {$pair ne ""} {return $pair} 270 continue 271 } 272 switch -- $type { 273 int { 274 # YAML 1.1 275 if {[regexp {^-?\d[\d,]*\d$|^\d$} $value]} { 276 regsub -all "," $value "" integer 277 return [list !!int $integer] 278 } 279 } 280 float { 281 # don't run before "integer" 282 regsub -all "," $value "" val 283 if {[string is double $val]} { 284 return [list !!float $val] 285 } 286 } 287 default { 288 # !!null !!true !!false 289 if {[info exists yaml::fixed($type:Group)] \ 290 && [lsearch $yaml::fixed($type:Group) $lowerval] >= 0} { 291 set value $yaml::fixed($type:Value) 292 return [list !!$type $value] 293 } 294 } 295 } 296 } 297 298 # the others 299 return [list !!str $value] 300} 301 302#################### 303# Block Node parser 304#################### 305proc ::yaml::_parseBlockNode {{status ""} {indent -1}} { 306 variable data 307 set prev {} 308 set result {} 309 set scalar 0 310 set pos 0 311 set tag "" 312 while {1} { 313 if {$data(finished) == 1} { 314 break 315 } 316 _skipSpaces 1 317 set type [_getc] 318 set current [_getCurrent] 319 if {$type eq "-"} { 320 set cc "[_getc][_getc]" 321 if {"$type$cc" eq "---" && $current == 0} { 322 set result {} 323 continue 324 } else { 325 _ungetc 2 326 327 # [Spec] 328 # Since people perceive the�g-�hindicator as indentation, 329 # nested block sequences may be indented by one less space 330 # to compensate, except, of course, 331 # if nested inside another block sequence. 332 incr current 333 } 334 } 335 if {$type eq "."} { 336 set cc "[_getc][_getc]" 337 if {"$type$cc" eq "..." && $current == 0} { 338 set data(finished) 1 339 break 340 } else { 341 _ungetc 2 342 343# # [Spec] 344# # Since people perceive the�g-�hindicator as indentation, 345# # nested block sequences may be indented by one less space 346# # to compensate, except, of course, 347# # if nested inside another block sequence. 348# incr current 349 } 350 } 351 if {$type eq "" || $current <= $indent} { ; # end document 352 _ungetc 353 break 354 } 355 switch -- $type { 356 "-" { ; # block sequence entry 357 set pos $current 358 # [196] l-block-seq-entry(n,c) 359 foreach {scalar value} [_parseSubBlock $pos "SEQUENCE"] break 360 } 361 "?" { ; # mapping key 362 foreach {scalar nop} [_parseSubBlock $pos ""] break 363 } 364 ":" { ; # mapping value 365 if {$current < $pos} {set pos [expr {$current+1}]} 366 foreach {scalar value} [_parseSubBlock $pos "MAPPING"] break 367 } 368 "|" { ; # literal block scalar 369 set value [_parseBlockScalar $indent "\n"] 370 } 371 ">" { ; # folded block scalar 372 set value [_parseBlockScalar $indent " "] 373 } 374 "<" { ; # mergeing 375 set c [_getc] 376 if {"$type$c" eq "<<"} { 377 set pos [_getCurrent] 378 _skipSpaces 1 379 set c [_getc] 380 if {$c ne ":"} {error [_getErrorMessage INVALID_MERGE_KEY]} 381 if {$status ne "" && $status ne "MAPPING"} {error [_getErrorMessage INVALID_MERGE_KEY]} 382 set status "MAPPING" 383 foreach {result prev} [_mergeExpandedAliases $result $pos $prev] break 384 } else { 385 _ungetc 386 set scalar 1 387 } 388 } 389 "&" { ; # node's anchor property 390 set anchor [_getToken] 391 } 392 "*" { ; # alias node 393 set alias [_getToken] 394 if {$yaml::data(validate)} { 395 set status "ALIAS" 396 set value *$alias 397 } else { 398 set value [_getAnchor $alias] 399 } 400 } 401 "!" { ; # node's tag 402 _ungetc 403 set tag [_getToken] 404 } 405 "%" { ; # directive line 406 _getLine 407 } 408 default { 409 if {[regexp {^[\[\]\{\}\"']$} $type]} { 410 set pos [expr {1 + $current}] 411 _ungetc 412 set value [_parseFlowNode] 413 } else { 414 set scalar 1 415 } 416 } 417 } 418 if {$scalar} { 419 set pos [_getCurrent] 420 _ungetc 421 set value [_parseScalarNode $type "BLOCK" $pos] 422 set value [_composeTags $tag $value] 423 set tag "" 424 set scalar 0 425 } 426 if {[info exists value]} { 427 if {$status eq "NODE"} {return $value} 428 foreach {result prev} [_pushValue $result $prev $status $value "BLOCK"] break 429 unset value 430 } 431 } 432 if {$status eq "SEQUENCE"} { 433 set result [eval huddle sequence $result] 434 } elseif {$status eq "MAPPING"} { 435 if {[llength $prev] == 2} { 436 set result [_set_huddle_mapping $result $prev] 437 } 438 } else { 439 if {[info exists prev]} { 440 set result $prev 441 } 442 set result [lindex $result 0] 443 set result [_composePlain $result] 444 if {![huddle isHuddle $result]} { 445 set result [huddle wrap !!str $result] 446 } 447 } 448 if {$tag ne ""} { 449 set result [_composeTags $tag $result] 450 unset tag 451 } 452 if {[info exists anchor]} { 453 _setAnchor $anchor $result 454 unset anchor 455 } 456 return $result 457} 458 459proc ::yaml::_mergeExpandedAliases {result pos prev} { 460 if {$result eq ""} {set result [huddle mapping]} 461 if {$prev ne ""} { 462 if {[llength $prev] < 2} {error [_getErrorMessage MALFORMED_MERGE_KEY]} 463 set result [_set_huddle_mapping $result $prev] 464 set prev {} 465 } 466 467 set value [_parseBlockNode "" $pos] 468 if {[huddle type $value] eq "list"} { 469 set len [huddle llength $value] 470 for {set i 0} {$i < $len} {incr i} { 471 set sub [huddle get $value $i] 472 set result [huddle combine $result $sub] 473 } 474 unset sub len 475 } else { 476 set result [huddle combine $result $value] 477 } 478 return [list $result $prev] 479} 480 481 482proc ::yaml::_parseSubBlock {pos statusnew} { 483 upvar 1 status status 484 set scalar 0 485 set value "" 486 if {[_next_is_blank]} { 487 if {$statusnew ne ""} { 488 set status $statusnew 489 set value [_parseBlockNode "" $pos] 490 } 491 } else { 492 _ungetc 493 set scalar 1 494 } 495 return [list $scalar $value] 496} 497 498proc ::yaml::_set_huddle_mapping {result prev} { 499 foreach {key val} $prev break 500 set val [_composePlain $val] 501 if {[huddle isHuddle $key]} { 502 set key [huddle strip $key] 503 } 504 if {$result eq ""} { 505 set result [huddle mapping $key $val] 506 } else { 507 huddle append result $key $val 508 } 509 return $result 510} 511 512 513# remove duplications with saving key order 514proc ::yaml::_remove_duplication {dict} { 515 array set tmp $dict 516 array set tmp2 {} 517 foreach {key nop} $dict { 518 if {[info exists tmp2($key)]} continue 519 lappend result $key $tmp($key) 520 set tmp2($key) 1 521 } 522 return $result 523} 524 525 526# literal "|" (line separator is "\n") 527# folding ">" (line separator is " ") 528proc ::yaml::_parseBlockScalar {base separator} { 529 foreach {explicit chomping} [_parseBlockIndicator] break 530 531 set idch [string repeat " " $explicit] 532 set sep $separator 533 foreach {indent c line} [_getLine] break 534 if {$indent < $base} {return ""} 535 # the first line, NOT ignored comment (as a normal-string) 536 set first $indent 537 set value $line 538 set stop 0 539 540 while {![_eof]} { 541 set pos [_getpos] 542 foreach {indent c line} [_getLine] break 543 if {$line eq ""} { 544 regsub " " $sep "" sep 545 append sep "\n" 546 continue 547 } 548 if {$c eq "#"} { 549 # skip comments 550 continue 551 } 552 if {$indent <= $base} { 553 set stop 1 554 break 555 } 556 append value $sep[string repeat " " [expr {$indent - $first}]]$line 557 set sep $separator 558 } 559 if {[info exists pos] && $stop} {_setpos $pos} 560 switch -- $chomping { 561 "strip" { 562 } 563 "keep" { 564 append value $sep 565 } 566 "clip" { 567 append value "\n" 568 } 569 } 570 return [huddle wrap !!str $value] 571} 572 573# in {> |} 574proc ::yaml::_parseBlockIndicator {} { 575 set chomping "clip" 576 set explicit 0 577 while {1} { 578 set type [_getc] 579 if {[regexp {[1-9]} $type digit]} { ; # block indentation 580 set explicit $digit 581 } elseif {$type eq "-"} { ; # strip chomping 582 set chomping "strip" 583 } elseif {$type eq "+"} { ; # keep chomping 584 set chomping "keep" 585 } else { 586 _ungetc 587 break 588 } 589 } 590 # Note: skipped after the indicator 591 _getLine 592 return [list $explicit $chomping] 593} 594 595# [162] ns-plain-multi(n,c) 596proc ::yaml::_parsePlainScalarInBlock {base {loop 0}} { 597 if {$loop == 5} { return } 598 variable data 599 set start $data(start) 600 set reStr {(?:[^:#\t \n]*(?::[^\t \n]+)*(?:#[^\t \n]+)* *)*[^:#\t \n]*} 601 set result [_getFoldedString $reStr] 602 603 set result [string trim $result] 604 set c [_getc 0] 605 if {$c eq "\n" || $c eq "#"} { ; # multi-line 606 set lb "" 607 while {1} { 608 set fpos [_getpos] 609 foreach {indent nop line} [_getLine] break 610 if {[_eof]} {break} 611 612 if {$line ne "" && [string index $line 0] ne "#"} { 613 break 614 } 615 append lb "\n" 616 } 617 set lb [string range $lb 1 end] 618 if {!$yaml::data(finished)} { 619 _setpos $fpos 620 } 621 if {$start == $data(start)} { 622 return $result 623 } 624 if {$base <= $indent} { 625 if {$lb eq ""} { 626 set lb " " 627 } 628 set subs [_parsePlainScalarInBlock $base [expr {$loop+1}]] 629 if {$subs ne ""} { 630 append result "$lb$subs" 631 } 632 } 633 } 634 return $result 635} 636 637#################### 638# Flow Node parser 639#################### 640proc ::yaml::_parseFlowNode {{status ""}} { 641 set scalar 0 642 set result {} 643 set tag "" 644 set prev {} 645 while {1} { 646 _skipSpaces 1 647 set type [_getc] 648 switch -- $type { 649 "" { 650 break 651 } 652 "?" - 653 ":" { ; # mapping value 654 if {[_next_is_blank]} { 655 set value [_parseFlowNode "NODE"] 656 } else { 657 set scalar 1 658 } 659 } 660 "," { ; # ends a flow collection entry 661 if {$status eq"NODE"} { 662 _ungetc 663 return $value 664 } 665 } 666 "\{" { ; # starts a flow mapping 667 set value [_parseFlowNode "MAPPING"] 668 } 669 "\}" { ; # ends a flow mapping 670 if {$status ne "MAPPING"} {error [_getErrorMessage MAPEND_NOT_IN_MAP] } 671 return $result 672 } 673 "\[" { ; # starts a flow sequence 674 set value [_parseFlowNode "SEQUENCE"] 675 } 676 "\]" { ; # ends a flow sequence 677 if {$status ne "SEQUENCE"} {error [_getErrorMessage SEQEND_NOT_IN_SEQ] } 678 set result [eval huddle sequence $result] 679 return $result 680 } 681 "&" { ; # node's anchor property 682 set anchor [_getToken] 683 } 684 "*" { ; # alias node 685 set alias [_getToken] 686 set value [_getAnchor $alias] 687 } 688 "!" { ; # node's tag 689 _ungetc 690 set tag [_getToken] 691 } 692 "%" { ; # directive line 693 _ungetc 694 _parseDirective 695 } 696 default { 697 set scalar 1 698 } 699 } 700 if {$scalar} { 701 _ungetc 702 set value [_parseScalarNode $type "FLOW"] 703 set value [_composeTags $tag $value] 704 set tag "" 705 set scalar 0 706 } 707 if {[info exists value]} { 708 if {[info exists anchor]} { 709 _setAnchor $anchor $value 710 unset anchor 711 } 712 if {$status eq "" || $status eq "NODE"} {return $value} 713 foreach {result prev} [_pushValue $result $prev $status $value "FLOW"] break 714 unset value 715 } 716 } 717 return $result 718} 719 720proc ::yaml::_pushValue {result prev status value scope} { 721 switch -- $status { 722 "SEQUENCE" { 723 lappend result [_composePlain $value] 724 } 725 "MAPPING" { 726 if {$scope eq "BLOCK"} { 727 if {[llength $prev] == 2} { 728 set result [_set_huddle_mapping $result $prev] 729 set prev [list $value] 730 } else { 731 lappend prev $value 732 } 733 } else { 734 lappend prev $value 735 if {[llength $prev] == 2} { 736 set result [_set_huddle_mapping $result $prev] 737 set prev "" 738 } 739 } 740 } 741 default { 742 if {$scope eq "BLOCK"} {lappend prev $value} 743 } 744 } 745 return [list $result $prev] 746} 747 748proc ::yaml::_parseScalarNode {type scope {pos 0}} { 749 set tag !!str 750 switch -- $type { 751 \" { ; # surrounds a double-quoted flow scalar 752 set value [_parseDoubleQuoted] 753 } 754 {'} { ; # surrounds a single-quoted flow scalar 755 set value [_parseSingleQuoted] 756 } 757 "\t" {error [_getErrorMessage TAB_IN_PLAIN] } 758 "@" {error [_getErrorMessage AT_IN_PLAIN] } 759 "`" {error [_getErrorMessage BT_IN_PLAIN] } 760 default { 761 # Plane Scalar 762 if {$scope eq "FLOW"} { 763 set value [_parsePlainScalarInFlow] 764 } elseif {$scope eq "BLOCK"} { 765 set value [_parsePlainScalarInBlock $pos] 766 } 767 set tag !!plain 768 } 769 } 770 return [huddle wrap $tag $value] 771} 772 773# [time scanning at JST] 774# 2001-12-15T02:59:43.1Z => 1008385183 775# 2001-12-14t21:59:43.10-05:00 => 1008385183 776# 2001-12-14 21:59:43.10 -5 => 1008385183 777# 2001-12-15 2:59:43.10 => 1008352783 778# 2002-12-14 => 1039791600 779proc ::yaml::_parseTimestamp {scalar} { 780 if {![regexp {^\d\d\d\d-\d\d-\d\d} $scalar]} {return ""} 781 set datestr {\d\d\d\d-\d\d-\d\d} 782 set timestr {\d\d?:\d\d:\d\d} 783 set timezone {Z|[-+]\d\d?(?::\d\d)?} 784 785 set canonical [subst -nobackslashes -nocommands {^($datestr)[Tt ]($timestr)\.\d+ ?($timezone)?$}] 786 set dttm [subst -nobackslashes -nocommands {^($datestr)(?:[Tt ]($timestr))?$}] 787 if {$::tcl_version < 8.5} { 788 if {[regexp $canonical $scalar nop dt tm zone]} { 789 # Canonical 790 if {$zone eq ""} { 791 return [list !!timestamp [clock scan "$dt $tm"]] 792 } elseif {$zone eq "Z"} { 793 return [list !!timestamp [clock scan "$dt $tm" -gmt 1]] 794 } 795 if {[regexp {^([-+])(\d\d?)$} $zone nop sign d]} {set zone [format "$sign%02d:00" $d]} 796 regexp {^([-+]\d\d):(\d\d)} $zone nop h m 797 set m [expr {$h > 0 ? $h*60 + $m : $h*60 - $m}] 798 return [list !!timestamp [clock scan "[expr {-$m}] minutes" -base [clock scan "$dt $tm" -gmt 1]]] 799 } elseif {[regexp $dttm $scalar nop dt tm]} { 800 if {$tm ne ""} { 801 return [list !!timestamp [clock scan "$dt $tm"]] 802 } else { 803 return [list !!timestamp [clock scan $dt]] 804 } 805 } 806 } else { 807 if {[regexp $canonical $scalar nop dt tm zone]} { 808 # Canonical 809 if {$zone ne ""} { 810 if {[regexp {^([-+])(\d\d?)$} $zone nop sign d]} {set zone [format "$sign%02d:00" $d]} 811 return [list !!timestamp [clock scan "$dt $tm $zone" -format {%Y-%m-%d %k:%M:%S %Z}]] 812 } else { 813 return [list !!timestamp [clock scan "$dt $tm" -format {%Y-%m-%d %k:%M:%S}]] 814 } 815 } elseif {[regexp $dttm $scalar nop dt tm]} { 816 if {$tm ne ""} { 817 return [list !!timestamp [clock scan "$dt $tm" -format {%Y-%m-%d %k:%M:%S}]] 818 } else { 819 return [list !!timestamp [clock scan $dt -format {%Y-%m-%d}]] 820 } 821 } 822 } 823 return "" 824} 825 826 827proc ::yaml::_parseDirective {} { 828 variable data 829 variable shorthands 830 831 set directive [_getToken] 832 833 if {[regexp {^%YAML} $directive]} { 834 # YAML directive 835 _skipSpaces 836 set version [_getToken] 837 set data(YAMLVersion) $version 838 if {![regexp {^\d\.\d$} $version]} { error [_getErrorMessage ILLEGAL_YAML_DIRECTIVE] } 839 } elseif {[regexp {^%TAG} $directive]} { 840 # TAG directive 841 _skipSpaces 842 set handle [_getToken] 843 if {![regexp {^!$|^!\w*!$} $handle]} { error [_getErrorMessage ILLEGAL_YAML_DIRECTIVE] } 844 845 _skipSpaces 846 set prefix [_getToken] 847 if {![regexp {^!$|^!\w*!$} $prefix]} { error [_getErrorMessage ILLEGAL_YAML_DIRECTIVE] } 848 set shorthands(handle) $prefix 849 } 850} 851 852proc ::yaml::_parseTagHandle {} { 853 set token [_getToken] 854 855 if {[regexp {^(!|!\w*!)(.*)} $token nop handle named]} { 856 # shorthand or non-specific Tags 857 switch -- $handle { 858 ! { ; # local or non-specific Tags 859 } 860 !! { ; # yaml Tags 861 } 862 default { ; # shorthand Tags 863 864 } 865 } 866 if {![info exists prefix($handle)]} { error [_getErrorMessage TAG_NOT_FOUND] } 867 } elseif {[regexp {^!<(.+)>} $token nop uri]} { 868 # Verbatim Tags 869 if {![regexp {^[\w:/]$} $token nop uri]} { error [_getErrorMessage ILLEGAL_TAG_HANDLE] } 870 } else { 871 error [_getErrorMessage ILLEGAL_TAG_HANDLE] 872 } 873 874 return "!<$prefix($handle)$named>" 875} 876 877 878proc ::yaml::_parseDoubleQuoted {} { 879 # capture quoted string with backslash sequences 880 set reStr {(?:(?:\")(?:[^\\\"]*(?:\\.[^\\\"]*)*)(?:\"))} 881 set result [_getFoldedString $reStr] 882 if {$result eq ""} { error [_getErrorMessage MALFORM_D_QUOTE] } 883 884 # [116] nb-double-multi-line 885 regsub -all {[ \t]*\n[\t ]*} $result "\r" result 886 regsub -all {([^\r])\r} $result {\1 } result 887 regsub -all { ?\r} $result "\n" result 888 # [112] s-s-double-escaped(n) 889 # is not impremented.(specification ???) 890 891 # chop off outer ""s and substitute backslashes 892 # This does more than the RFC-specified backslash sequences, 893 # but it does cover them all 894 set chopped [subst -nocommands -novariables \ 895 [string range $result 1 end-1]] 896 return $chopped 897} 898 899proc ::yaml::_parseSingleQuoted {} { 900 set reStr {(?:(?:')(?:[^']*(?:''[^']*)*)(?:'))} 901 set result [_getFoldedString $reStr] 902 if {$result eq ""} { error [_getErrorMessage MALFORM_S_QUOTE] } 903 904 # [126] nb-single-multi-line 905 regsub -all {[ \t]*\n[\t ]*} $result "\r" result 906 regsub -all {([^\r])\r} $result {\1 } result 907 regsub -all { ?\r} $result "\n" result 908 909 regsub -all {''} [string range $result 1 end-1] {'} chopped 910 911 return $chopped 912} 913 914 915# [155] nb-plain-char-in 916proc ::yaml::_parsePlainScalarInFlow {} { 917 set sep {\t \n,\[\]\{\}} 918 set reStr {(?:[^$sep:#]*(?::[^$sep]+)*(?:#[^$sep]+)* *)*[^$sep:#]*} 919 set reStr [subst -nobackslashes -nocommands $reStr] 920 set result [_getFoldedString $reStr] 921 set result [string trim $result] 922 923 if {[_getc 0] eq "#"} { 924 _getLine 925 set result "$result [_parsePlainScalarInFlow]" 926 } 927 return $result 928} 929 930#################### 931# Generic parser 932#################### 933proc ::yaml::_getFoldedString {reStr} { 934 variable data 935 936 set buff [string range $data(buffer) $data(start) end] 937 regexp $reStr $buff token 938 if {![info exists token]} {return} 939 940 set len [string length $token] 941 if {[string first "\n" $token] >= 0} { ; # multi-line 942 set data(current) [expr {$len - [string last "\n" $token]}] 943 } else { 944 incr data(current) $len 945 } 946 incr data(start) $len 947 948 return $token 949} 950 951# get a space separated token 952proc ::yaml::_getToken {} { 953 variable data 954 955 set reStr {^[^ \t\n,]+} 956 set result [_getFoldedString $reStr] 957 return $result 958} 959 960proc ::yaml::_skipSpaces {{commentSkip 0}} { 961 variable data 962 963 while {1} { 964 set ch [string index $data(buffer) $data(start)] 965 incr data(start) 966 switch -- $ch { 967 " " { 968 incr data(current) 969 continue 970 } 971 "\n" { 972 set data(current) 0 973 continue 974 } 975 "\#" { 976 if {$commentSkip} { 977 _getLine 978 continue 979 } 980 } 981 } 982 break 983 } 984 incr data(start) -1 985} 986 987# get a line of stream(line-end trimed) 988# (cannot _ungetc) 989proc ::yaml::_getLine {{scrolled 1}} { 990 variable data 991 992 set pos [string first "\n" $data(buffer) $data(start)] 993 if {$pos == -1} { 994 set pos $data(length) 995 } 996 set line [string range $data(buffer) $data(start) [expr {$pos-1}]] 997 if {$line eq "..." && $data(current) == 0} { 998 set data(finished) 1 999 } 1000 regexp {^( *)(.*)} $line nop space result 1001 if {$scrolled} { 1002 set data(start) [expr {$pos + 1}] 1003 set data(current) 0 1004 } 1005 if {$line == "" && $data(start) == $data(length)} { 1006 set data(finished) 1 1007 } 1008 return [list [string length $space] [string index $result 0] $result] 1009} 1010 1011proc ::yaml::_getCurrent {} { 1012 variable data 1013 return [expr {$data(current) ? $data(current)-1 : 0}] 1014} 1015 1016proc ::yaml::_getLineNum {} { 1017 variable data 1018 set prev [string range $data(buffer) 0 $data(start)] 1019 return [llength [split $prev "\n"]] 1020} 1021 1022proc ::yaml::_getc {{scrolled 1}} { 1023 variable data 1024 1025 set result [string index $data(buffer) $data(start)] 1026 if {$scrolled} { 1027 incr data(start) 1028 if {$result eq "\n"} { 1029 set data(current) 0 1030 } else { 1031 incr data(current) 1032 } 1033 } 1034 return $result 1035} 1036 1037proc ::yaml::_eof {} { 1038 variable data 1039 return [expr {$data(finished) || $data(start) == $data(length)}] 1040} 1041 1042 1043proc ::yaml::_getpos {} { 1044 variable data 1045 return $data(start) 1046} 1047 1048proc ::yaml::_setpos {pos} { 1049 variable data 1050 set data(start) $pos 1051} 1052 1053proc ::yaml::_ungetc {{len 1}} { 1054 variable data 1055 incr data(start) [expr {-$len}] 1056 incr data(current) [expr {-$len}] 1057 if {$data(current) < 0} { 1058 set prev [string range $data(buffer) 0 $data(start)] 1059 if {[string index $prev end] eq "\n"} {set prev [string replace $prev end end a]} 1060 set data(current) [expr {$data(start) - [string last "\n" $prev] - 1}] 1061 } 1062} 1063 1064proc ::yaml::_next_is_blank {} { 1065 set c [_getc 0] 1066 if {$c eq " " || $c eq "\n"} { 1067 return 1 1068 } else { 1069 return 0 1070 } 1071} 1072 1073proc ::yaml::_setAnchor {anchor value} { 1074 variable data 1075 set data(anchor:$anchor) $value 1076} 1077 1078proc ::yaml::_getAnchor {anchor} { 1079 variable data 1080 if {![info exists data(anchor:$anchor)]} {error [_getErrorMessage ANCHOR_NOT_FOUND]} 1081 return $data(anchor:$anchor) 1082} 1083 1084proc ::yaml::_getErrorMessage {ID {p1 ""}} { 1085 set num [_getLineNum] 1086 if {$p1 != ""} { 1087 return "line($num): [subst -nobackslashes -nocommands $yaml::errors($ID)]" 1088 } else { 1089 return "line($num): $yaml::errors($ID)" 1090 } 1091} 1092 1093# Finds and returns the indentation of a YAML line 1094proc ::yaml::_getIndent {line} { 1095 set match [regexp -inline -- {^\s{1,}} " $line"] 1096 return [expr {[string length $match] - 3}] 1097} 1098 1099 1100################ 1101## Dumpers ## 1102################ 1103 1104proc ::yaml::_imp_huddle2yaml {data {offset ""}} { 1105 set nextoff "$offset[string repeat { } $yaml::_dumpIndent]" 1106 switch -- [huddle type $data] { 1107 "string" { 1108 set data [huddle strip $data] 1109 return [_dumpScalar $data $offset] 1110 } 1111 "list" { 1112 set inner {} 1113 set len [huddle llength $data] 1114 for {set i 0} {$i < $len} {incr i} { 1115 set sub [huddle get $data $i] 1116 set sep [expr {[huddle type $sub] eq "string" ? " " : "\n"}] 1117 lappend inner [join [list $offset - $sep [_imp_huddle2yaml $sub $nextoff]] ""] 1118 } 1119 return [join $inner "\n"] 1120 } 1121 "dict" { 1122 set inner {} 1123 foreach {key} [huddle keys $data] { 1124 set sub [huddle get $data $key] 1125 set sep [expr {[huddle type $sub] eq "string" ? " " : "\n"}] 1126 lappend inner [join [list $offset $key: $sep [_imp_huddle2yaml $sub $nextoff]] ""] 1127 } 1128 return [join $inner "\n"] 1129 } 1130 default { 1131 return $data 1132 } 1133 } 1134} 1135 1136proc ::yaml::_dumpScalar {value offset} { 1137 if { [string first "\n" $value] >= 0 1138 || [string first ": " $value] >= 0 1139 || [string first "- " $value] >= 0} { 1140 return [_doLiteralBlock $value $offset] 1141 } else { 1142 return [_doFolding $value $offset] 1143 } 1144} 1145 1146# Creates a literal block for dumping 1147proc ::yaml::_doLiteralBlock {value offset} { 1148 if {[string index $value end] eq "\n"} { 1149 set newValue "|" 1150 set value [string range $value 0 end-1] 1151 } else { 1152 set newValue "|-" 1153 } 1154 set exploded [split $value "\n"] 1155 1156 set value [string trimright $value] 1157 foreach {line} $exploded { 1158 set newValue "$newValue\n$offset[string trim $line]" 1159 } 1160 return $newValue 1161} 1162 1163# Folds a string of text, if necessary 1164proc ::yaml::_doFolding {value offset} { 1165 variable _dumpWordWrap 1166 # Don't do anything if wordwrap is set to 0 1167 if {$_dumpWordWrap == 0} { 1168 return $value 1169 } 1170 1171 if {[string length $value] > $_dumpWordWrap} { 1172 set wrapped [_simple_justify $value $_dumpWordWrap "\n$offset"] 1173 set value ">\n$offset$wrapped" 1174 } 1175 return $value 1176} 1177 1178# http://wiki.tcl.tk/1774 1179proc ::yaml::_simple_justify {text width {wrap \n} {cut 0}} { 1180 set brk "" 1181 for {set result {}} {[string length $text] > $width} { 1182 set text [string range $text [expr {$brk+1}] end] 1183 } { 1184 set brk [string last " " $text $width] 1185 if { $brk < 0 } { 1186 if {$cut == 0} { 1187 append result $text 1188 return $result 1189 } else { 1190 set brk $width 1191 } 1192 } 1193 append result [string range $text 0 $brk] $wrap 1194 } 1195 return $result$text 1196} 1197 1198######################## 1199## Huddle Settings ## 1200######################## 1201 1202 1203proc ::yaml::_huddle_mapping {command args} { 1204 switch -- $command { 1205 setting { ; # type definition 1206 return { 1207 type dict 1208 method {mapping} 1209 tag {!!map parent} 1210 constructor mapping 1211 str !!str 1212 } 1213 } 1214 mapping { ; # $args: all arguments after "huddle mapping" 1215 if {[llength $args] % 2} {error {wrong # args: should be "huddle mapping ?key value ...?"}} 1216 set resultL {} 1217 foreach {key value} $args { 1218 lappend resultL $key [huddle to_node $value !!str] 1219 } 1220 return [huddle wrap !!map $resultL] 1221 } 1222 default { ; # devolving to default dict-callback 1223 return [huddle call D $command $args] 1224 } 1225 } 1226} 1227 1228proc ::yaml::_huddle_sequence {command args} { 1229 switch -- $command { 1230 setting { ; # type definition 1231 return { 1232 type list 1233 method {sequence} 1234 tag {!!seq parent} 1235 constructor sequence 1236 str !!str 1237 } 1238 } 1239 sequence { 1240 set resultL {} 1241 foreach {value} $args { 1242 lappend resultL [huddle to_node $value !!str] 1243 } 1244 return [huddle wrap !!seq $resultL] 1245 } 1246 default { 1247 return [huddle call L $command $args] 1248 } 1249 } 1250} 1251 1252proc ::yaml::_makeChildType {type tag} { 1253 set procname ::yaml::_huddle_$type 1254 proc $procname {command args} [string map "@TYPE@ $type @TAG@ $tag" { 1255 switch -- $command { 1256 setting { ; # type definition 1257 return { 1258 type @TYPE@ 1259 method {} 1260 tag {@TAG@ child} 1261 constructor "" 1262 str @TAG@ 1263 } 1264 } 1265 default { 1266 return [huddle call s $command $args] 1267 } 1268 } 1269 }] 1270 return $procname 1271} 1272 1273huddle addType ::yaml::_huddle_mapping 1274huddle addType ::yaml::_huddle_sequence 1275huddle addType [::yaml::_makeChildType string !!str] 1276huddle addType [::yaml::_makeChildType string !!timestamp] 1277huddle addType [::yaml::_makeChildType string !!float] 1278huddle addType [::yaml::_makeChildType string !!int] 1279huddle addType [::yaml::_makeChildType string !!null] 1280huddle addType [::yaml::_makeChildType string !!true] 1281huddle addType [::yaml::_makeChildType string !!false] 1282huddle addType [::yaml::_makeChildType string !!binary] 1283huddle addType [::yaml::_makeChildType plain !!plain] 1284 1285 1286