chiark / gitweb /
Initial commit
authorJeff Ober <jober@ziprecruiter.com>
Sun, 22 Dec 2019 20:11:15 +0000 (15:11 -0500)
committerJeff Ober <jober@ziprecruiter.com>
Fri, 3 Jan 2020 15:30:09 +0000 (10:30 -0500)
25 files changed:
.appveyor.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
Changes [new file with mode: 0644]
README.pod [new file with mode: 0644]
TODO.md [new file with mode: 0644]
cpanfile [new file with mode: 0644]
dist.ini [new file with mode: 0644]
lib/TOML/Tiny.pm [new file with mode: 0644]
lib/TOML/Tiny/Grammar.pm [new file with mode: 0644]
lib/TOML/Tiny/Parser.pm [new file with mode: 0644]
lib/TOML/Tiny/Tokenizer.pm [new file with mode: 0644]
t/parity.t [new file with mode: 0644]
t/tokens/array-of-tables.t [new file with mode: 0644]
t/tokens/array.t [new file with mode: 0644]
t/tokens/boolean.t [new file with mode: 0644]
t/tokens/datetime.t [new file with mode: 0644]
t/tokens/float.t [new file with mode: 0644]
t/tokens/inline-table.t [new file with mode: 0644]
t/tokens/integer.t [new file with mode: 0644]
t/tokens/key-value-pair.t [new file with mode: 0644]
t/tokens/key.t [new file with mode: 0644]
t/tokens/string.t [new file with mode: 0644]
t/tokens/table.t [new file with mode: 0644]
toml.abnf [new file with mode: 0644]

diff --git a/.appveyor.yml b/.appveyor.yml
new file mode 100644 (file)
index 0000000..7fc1e16
--- /dev/null
@@ -0,0 +1,21 @@
+skip_tags: true
+
+build: off
+
+cache:
+  - C:\strawberry
+
+platform:
+  - x86
+  - x64
+
+install:
+  - if not exist "C:\strawberry" cinst strawberryperl
+  - set PATH=C:\strawberry\perl\bin;C:\strawberry\perl\site\bin;C:\strawberry\c\bin;%PATH%
+  - cd C:\projects\%APPVEYOR_PROJECT_NAME%
+  - cpanm -n Dist::Zilla
+  - dzil authordeps --missing | cpanm -nq
+  - dzil listdeps --missing | cpanm -nq
+
+test_script:
+  - dzil test
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..7f5bacd
--- /dev/null
@@ -0,0 +1,2 @@
+.build
+scratch.pl
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..2fe7c27
--- /dev/null
@@ -0,0 +1,26 @@
+language: perl
+
+sudo: false
+
+notifications:
+  email: false
+
+cache:
+  directories:
+    - ~/perl5
+
+perl:
+  - "5.30"
+  - "5.28"
+  - "5.26"
+  - "5.24"
+  - "5.22"
+  - "5.20"
+  - "5.18"
+  - "5.16"
+  - "5.14"
+  - "5.12"
+  - "5.10"
+
+before_install:
+  - eval $(curl https://travis-perl.github.io/init) --auto --always-upgrade-modules
diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..aec11e9
--- /dev/null
+++ b/Changes
@@ -0,0 +1,2 @@
+{{$NEXT}}
+-Initial release
diff --git a/README.pod b/README.pod
new file mode 100644 (file)
index 0000000..d09e5ea
--- /dev/null
@@ -0,0 +1,24 @@
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+TOML::Tiny - a minimal TOML parser and serializer
+
+=head1 VERSION
+
+version 0.01
+
+=head1 AUTHOR
+
+Jeff Ober <sysread@fastmail.fm>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2020 by Jeff Ober.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
diff --git a/TODO.md b/TODO.md
new file mode 100644 (file)
index 0000000..b7cd55f
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,4 @@
+* Serialization
+* Line numbers
+* Optimization
+* More complete unit tests
diff --git a/cpanfile b/cpanfile
new file mode 100644 (file)
index 0000000..7ec73c8
--- /dev/null
+++ b/cpanfile
@@ -0,0 +1,9 @@
+requires 'perl'     => '>= 5.014';
+requires 'JSON::PP' => '0';
+
+recommends 'Types::Serialiser' => 0;
+
+on test => sub{
+  requires 'Test::Pod' => '0';
+  requires 'Test2::V0' => '0';
+};
diff --git a/dist.ini b/dist.ini
new file mode 100644 (file)
index 0000000..b62f9d6
--- /dev/null
+++ b/dist.ini
@@ -0,0 +1,7 @@
+name = TOML-Tiny
+author = Jeff Ober <sysread@fastmail.fm>
+license = Perl_5
+copyright_holder = Jeff Ober
+
+[@Author::BLUEFEET]
+[PodWeaver]
diff --git a/lib/TOML/Tiny.pm b/lib/TOML/Tiny.pm
new file mode 100644 (file)
index 0000000..32627fc
--- /dev/null
@@ -0,0 +1,11 @@
+package TOML::Tiny;
+# ABSTRACT: a minimal TOML parser and serializer
+
+use strict;
+use warnings;
+
+use TOML::Tiny::Grammar;
+
+our $GRAMMAR_V5 = $TOML::Tiny::Grammar::GRAMMAR_V5;
+
+1;
diff --git a/lib/TOML/Tiny/Grammar.pm b/lib/TOML/Tiny/Grammar.pm
new file mode 100644 (file)
index 0000000..fc9c1cb
--- /dev/null
@@ -0,0 +1,248 @@
+package TOML::Tiny::Grammar;
+
+use strict;
+use warnings;
+
+our $GRAMMAR_V5 = qr{
+
+(?(DEFINE)
+  #-----------------------------------------------------------------------------
+  # Misc
+  #-----------------------------------------------------------------------------
+  (?<Value>
+      (?&Boolean)
+    | (?&DateTime)
+    | (?&Float)
+    | (?&Integer)
+    | (?&String)
+    | (?&Array)
+    | (?&InlineTable)
+  )
+
+  (?<NLSeq> (?: \x0A) | (?: \x0D \x0A))
+  (?<NL> (?&NLSeq) | (?&Comment))
+
+  (?<WSChar> [ \x20 \x09 ])       # (space, tab)
+  (?<WS> (?&WSChar)*)
+
+  (?<Comment> \x23 .* (?&NLSeq))
+
+  #-----------------------------------------------------------------------------
+  # Array of tables
+  #-----------------------------------------------------------------------------
+  (?<ArrayOfTables>
+    (?m)
+    (?s)
+
+    \[\[ (?&Key) \]\] \n
+    (?:
+        (?: (?&KeyValuePair) \n )
+      | (?&ArrayOfTables)
+      | (?&Table)
+    )*
+
+    (?-s)
+    (?-m)
+  )
+
+  #-----------------------------------------------------------------------------
+  # Table
+  #-----------------------------------------------------------------------------
+  (?<KeyValuePair> (?&Key) (?&WS) = (?&WS) (?&Value))
+  (?<KeyValuePairDecl> (?&Key) (?&WS) = (?&WS) (?&Value) (?&WS) (?&NL))
+
+  (?<InlineTableSep>
+    (?&WS)
+    [,]
+    (?&WS)
+    (?&NLSeq)?
+    (?&WS)
+  )
+
+  (?<KeyValuePairList>
+      (?&KeyValuePair) (?&InlineTableSep) (?&KeyValuePairList)?
+    | (?&KeyValuePair)
+  )
+
+  (?<InlineTable>
+    (?s)
+
+    {
+      (?&WS) (?&NLSeq)? (?&WS)
+
+      (?&KeyValuePairList)
+
+      (?&WS) (?&NLSeq)? (?&WS)
+    }
+
+    (?-s)
+  )
+
+  (?<TableDecl>
+    \[ (?&Key) \] \n
+  )
+
+  (?<Table>
+    (?&TableDecl)
+    (?:
+        (?&KeyValuePairDecl)
+      | (?&ArrayOfTables)
+    )*
+  )
+
+  #-----------------------------------------------------------------------------
+  # Array
+  #-----------------------------------------------------------------------------
+  (?<ListSep>
+    (?&WS)
+    [,]
+    (?&WS)
+    (?&NLSeq)?
+    (?&WS)
+  )
+
+  (?<List>
+      (?&Value) (?&ListSep) (?&List)?
+    | (?&Value)
+  )
+
+  (?<Array>
+    \[
+
+    (?&WS) (?&NLSeq)? (?&WS)
+
+    (?&List)
+
+    (?&WS) (?&NLSeq)? (?&WS)
+
+    \]
+  )
+
+  #-----------------------------------------------------------------------------
+  # Key
+  #-----------------------------------------------------------------------------
+  (?<BareKey> [-_a-zA-Z0-9]+)
+  (?<QuotedKey> (?&BasicString) | (?&StringLiteral))
+  (?<DottedKey>
+    (?: (?&BareKey) | (?&QuotedKey) )
+    (?: (?&WS) [.] (?&WS) (?: (?&BareKey) | (?&QuotedKey) ) )+
+  )
+  (?<Key> (?&DottedKey) | (?&BareKey) | (?&QuotedKey) )
+
+  #-----------------------------------------------------------------------------
+  # Boolean
+  #-----------------------------------------------------------------------------
+  (?<Boolean> \b(?:true)|(?:false))\b
+
+  #-----------------------------------------------------------------------------
+  # Integer
+  #-----------------------------------------------------------------------------
+  (?<Dec> [-+]? [_0-9]+)
+  (?<Hex> 0x[_0-9a-fA-F]+)
+  (?<Oct> 0o[_0-7]+)
+  (?<Bin> 0b[_01]+)
+  (?<Integer> (?&Hex) | (?&Oct) | (?&Bin) | (?&Dec))
+
+  #-----------------------------------------------------------------------------
+  # Float
+  #-----------------------------------------------------------------------------
+  (?<Exponent> [eE] (?&Dec))
+  (?<SpecialFloat> [-+]?  (?:inf) | (?:nan))
+  (?<Fraction> [.] [_0-9]+)
+
+  (?<Float>
+    (?:
+        (?: (?&Dec) (?&Fraction) (?&Exponent) )
+      | (?: (?&Dec) (?&Exponent) )
+      | (?: (?&Dec) (?&Fraction) )
+    )
+    |
+    (?&SpecialFloat)
+  )
+
+  #-----------------------------------------------------------------------------
+  # String
+  #-----------------------------------------------------------------------------
+  (?<EscapeChar>
+    \\                        # leading \
+    (?:
+        [\\/"btnfr]           # escapes: \\ \/ \b \t \n \f \r
+      | (?: u \d{4} )         # unicode (4 bytes)
+      | (?: U \d{8} )         # unicode (8 bytes)
+    )
+  )
+
+  (?<StringLiteral>
+    (?: ' ([^']*) ')          # single quoted string (no escaped chars allowed)
+  )
+
+  (?<MultiLineStringLiteral>
+    (?m)
+    (?s)
+    '''                       # opening triple-quote
+    (.)*?                     # capture
+    '''                       # closing triple-quote
+    (?-s)
+    (?-m)
+  )
+
+  (?<BasicString>
+    (?:
+      "                       # opening quote
+      (?:                     # capture escape sequences or any char except " or \
+          (?: (?&EscapeChar) )
+        | [^"\\]
+      )*
+      "                       # closing quote
+    )
+  )
+
+  (?<MultiLineString>
+    (?s)
+    """                       # opening triple-quote
+    (                         # capture:
+      (?: (?&EscapeChar) )    # escaped char
+      | .
+    )*?
+    """                       # closing triple-quote
+    (?-s)
+  )
+
+  (?<String>
+      (?&MultiLineString)
+    | (?&BasicString)
+    | (?&MultiLineStringLiteral)
+    | (?&StringLiteral)
+  )
+
+  #-----------------------------------------------------------------------------
+  # Dates (RFC 3339)
+  #   1985-04-12T23:20:50.52Z
+  #-----------------------------------------------------------------------------
+  (?<Date> \d{4}-\d{2}-\d{2})
+
+  (?<Offset>
+      (?: [-+] \d{2}:\d{2} )
+    | [Z]
+  )
+
+  (?<SimpleTime>
+    \d{2}:\d{2}:\d{2}
+    (?: [.] \d+ )?
+  )
+
+  (?<Time>
+    (?&SimpleTime)
+    (?&Offset)?
+  )
+
+  (?<DateTime>
+      (?: (?&Date) [T ] (?&Time) )
+    | (?&Date)
+    | (?&Time)
+  )
+)
+
+}x;
+
+1;
diff --git a/lib/TOML/Tiny/Parser.pm b/lib/TOML/Tiny/Parser.pm
new file mode 100644 (file)
index 0000000..bec0543
--- /dev/null
@@ -0,0 +1,203 @@
+package TOML::Tiny::Parser;
+
+use strict;
+use warnings;
+use feature qw(switch);
+no warnings qw(experimental);
+
+use Carp;
+use DDP;
+use Data::Dumper;
+use TOML::Tiny::Tokenizer;
+
+our $TOML  = $TOML::Tiny::Grammar::GRAMMAR_V5;
+our $TRUE  = 1;
+our $FALSE = 0;
+
+BEGIN{
+  eval{
+    require Types::Serialiser;
+    $TRUE = Types::Serialiser::true();
+    $FALSE = Types::Serialiser::false();
+  };
+}
+
+sub new {
+  my ($class, %param) = @_;
+  return bless{
+    inflate_datetime => $param{inflate_datetime},
+    inflate_boolean  => $param{inflate_boolean},
+  }, $class;
+}
+
+sub parse {
+  my $self   = shift;
+  my $tokens = TOML::Tiny::Tokenizer::tokenize(@_);
+  my $root   = {};
+  my $acc    = { root => $root, node => $root };
+  $self->parse_token($_, $acc) for @$tokens;
+  return $root;
+}
+
+sub parse_token {
+  my $self   = shift;
+  my $token  = shift;
+  my $acc    = shift;
+  my $type   = shift @$token;
+
+  for ($type) {
+    # Table
+    when ('table') {
+      my $keys = $self->parse_key(shift @$token);
+      $acc->{node} = $self->mkpath($keys, $acc->{root});
+    }
+
+    # Array of tables
+    when ('array-of-tables') {
+      my $keys = $self->parse_key(shift @$token);
+      my $last = pop @$keys;
+      $acc->{node} = $self->mkpath($keys, $acc->{root});
+      $acc->{node}{$last} ||= [];
+      push @{ $acc->{node}{$last} } => $acc->{node} = {};
+    }
+
+    # Key-value pair
+    when ('assignment') {
+      my $keys = $self->parse_key(shift @$token);
+      my $last = pop @$keys;
+      my $value = $self->parse_value(shift @$token);
+      my $node = $self->mkpath($keys, $acc->{node});
+      $node->{$last} = $value;
+    }
+  }
+
+  return $acc;
+}
+
+sub mkpath {
+  my $self = shift;
+  my $keys = shift;
+  my $node = shift;
+
+  for my $key (@$keys) {
+    if (exists $node->{$key}) {
+      for (ref $node->{$key}) {
+        $node = $node->{$key}[-1] when 'ARRAY';
+        $node = $node->{$key}     when 'HASH';
+      }
+    }
+    else {
+      $node = $node->{$key} ||= {};
+    }
+  }
+
+  return $node;
+}
+
+sub parse_key {
+  my ($self, $key) = @_;
+  my $type = shift @$key;
+  for ($type) {
+    return $self->dotted_key(shift @$key) when 'dotted-key';
+    return $self->quoted_key(shift @$key) when 'quoted-key';
+    return $self->bare_key(shift @$key)   when 'bare-key';
+  }
+}
+
+sub parse_value {
+  my ($self, $token) = @_;
+  my $type = shift @$token;
+
+  for ($type) {
+    return $self->datetime(@$token) when 'datetime';
+    return $self->boolean(@$token)  when 'boolean';
+
+    when ('array') {
+      my $contents = shift @$token;
+      return [ map{ $self->parse_value($_) } @$contents ];
+    }
+
+    when ('inline-table') {
+      my $tokens = shift @$token;
+      my $root   = {};
+      my $acc    = {root => $root, node => $root};
+      $self->parse_token($_, $acc) for @$tokens;
+      return $root;
+    }
+
+    default{
+      return shift @$token;
+    }
+  }
+}
+
+sub bare_key {
+  my ($self, $key) = @_;
+  return [$key];
+}
+
+sub quoted_key {
+  my ($self, $key) = @_;
+  $key =~ s/^"//;
+  $key =~ s/"$//;
+  return [$key];
+}
+
+sub dotted_key {
+  my ($self, $key) = @_;
+  my @parts = split /\./, $key;
+  return \@parts;
+}
+
+sub number {
+  my ($self, $n) = @_;
+  defined $n ? 0 + $n : $n;
+}
+
+sub datetime {
+  my ($self, $dt) = @_;
+
+  if ($self->{inflate_datetime}) {
+    my ($year, $month, $day, $hour, $minute, $second, $fractional, $offset) = $dt =~ qr{
+        (?:
+          (\d\d\d\d) - (\d\d) - (\d\d)  # yyyy-mm-dd
+        )?
+
+        (?:
+          [T ]
+          (\d\d) : (\d\d) : (\d\d)      # hh:mm:ss.fractional
+          (?:[.] (\d+) )?
+
+          ((?&Offset)?)
+        )?
+
+        $TOML::Tiny::Tokenizer::TOML
+    }x;
+
+    return {
+      original   => $dt,
+      year       => $self->number($year),
+      month      => $self->number($month),
+      day        => $self->number($day),
+      hour       => $self->number($hour),
+      minute     => $self->number($minute),
+      second     => $self->number($second),
+      fractional => $self->number($fractional),
+      offset     => $offset,
+    };
+  } else {
+    return $dt;
+  }
+}
+
+sub boolean {
+  my ($self, $bool) = @_;
+  if ($self->{inflate_boolean}) {
+    return $TRUE if $bool eq 'true';
+    return $FALSE;
+  } else {
+    return $bool;
+  }
+}
+
+1;
diff --git a/lib/TOML/Tiny/Tokenizer.pm b/lib/TOML/Tiny/Tokenizer.pm
new file mode 100644 (file)
index 0000000..06e62bd
--- /dev/null
@@ -0,0 +1,221 @@
+package TOML::Tiny::Tokenizer;
+
+use strict;
+use warnings;
+use feature qw(switch);
+no warnings qw(experimental);
+
+use JSON::PP;
+use TOML::Tiny::Grammar;
+
+our $TOML = $TOML::Tiny::Grammar::GRAMMAR_V5;
+
+sub tokenize {
+  my $toml = shift;
+  my @tokens;
+
+  TOKEN: while ((pos($toml) // 0) < length($toml)) {
+    for ($toml) {
+      when (/\G ((?&Boolean)) $TOML/xgc) {
+        push @tokens, ['boolean', $1];
+      }
+
+      when (/\G ((?&DateTime)) $TOML/xgc) {
+        push @tokens, ['datetime', $1];
+      }
+
+      when (/\G ((?&Float)) $TOML/xgc) {
+        push @tokens, ['float', tokenize_float($1)];
+      }
+
+      when (/\G ((?&Integer)) $TOML/xgc) {
+        push @tokens, ['integer', tokenize_integer($1)];
+      }
+
+      when (/\G ((?&String)) $TOML/xgc) {
+        push @tokens, ['string', tokenize_string($1)];
+      }
+
+      when (/\G ((?&KeyValuePairDecl)) $TOML/xgc) {
+        push @tokens, tokenize_assignment($1);
+      }
+
+      when (/\G ((?&Array)) $TOML/xgc) {
+        push @tokens, tokenize_array($1);
+      }
+
+      when (/\G ((?&InlineTable)) $TOML/xgc) {
+        push @tokens, tokenize_inline_table($1);
+      }
+
+      when (/\G \[ (?&WS) ((?&Key)) (?&WS) \] (?&WS) (?&NL) $TOML/xgc) {
+        push @tokens, ['table', tokenize_key($1)];
+      }
+
+      when (/\G \[\[ (?&WS) ((?&Key)) (?&WS) \]\] (?&WS) (?&NL) $TOML/xgc) {
+        push @tokens, ['array-of-tables', tokenize_key($1)];
+      }
+
+      when (/\G (?: (?&WSChar) | (?&NLSeq) | (?&Comment) )+ $TOML/xgc) {
+        next TOKEN;
+      }
+
+      default{
+        my $prev = JSON::PP->new->pretty->encode($tokens[ scalar(@tokens) - 1 ]);
+        my $substr = substr($toml, pos($toml), 30) // 'undef';
+        die "syntax error at:\n-->$substr\n\nprevious token was: $prev\n";
+      }
+    }
+  }
+
+  return \@tokens;
+}
+
+sub tokenize_inline_table {
+  my $toml = shift;
+  my @items;
+
+  $toml =~ s/^\s*\{\s*//;
+  $toml =~ s/\s*\}\s*$//;
+
+  ITEM: while ((pos($toml) // 0) < length($toml)) {
+    for ($toml) {
+      next ITEM when /\G\s*/gc;
+      next ITEM when /\G\{/gc;
+      next ITEM when /\G\}/gc;
+
+      when (/\G ((?&KeyValuePair)) (?&WS) ,? $TOML/xgc) {
+        push @items, tokenize_assignment($1);
+      }
+
+      default{
+        die "invalid inline table syntax: $toml";
+      }
+    }
+  }
+
+  return ['inline-table', \@items];
+}
+
+sub tokenize_array {
+  my $toml = shift;
+  my @items;
+
+  $toml =~ s/^\s*\[\s*//;
+  $toml =~ s/\s*\]\s*$//;
+
+  ITEM: while ((pos($toml) // 0) < length($toml)) {
+    my $pos = pos($toml) // 0;
+    for ($toml) {
+      when (/\G ((?&Value)) (?&WS) [,]? $TOML/xgc) {
+        push @items, @{ tokenize($1) };
+      }
+
+      next ITEM when /\G\s*/gc;
+      next ITEM when /\G\[/gc;
+      next ITEM when /\G\]/gc;
+
+      default{
+        die "invalid array syntax: $toml";
+      }
+    }
+  }
+
+  return ['array', \@items];
+}
+
+sub tokenize_assignment {
+  my $toml = shift;
+
+  for ($toml) {
+    when (/\G(?&WS) ((?&Key)) (?&WS) = (?&WS) ((?&Value)) (?&NL)? $TOML/xgc) {
+      my $key = tokenize_key($1);
+      my $val = tokenize($2);
+      return ['assignment', $key, @$val];
+    }
+
+    default{
+      die "invalid assignment syntax: $toml";
+    }
+  }
+}
+
+sub tokenize_key {
+  my $toml = shift;
+
+  for ($toml) {
+    return ['dotted-key', $1] when /^ ((?&DottedKey)) $TOML/x;
+    return ['quoted-key', $1] when /^ ((?&QuotedKey)) $TOML/x;
+    return ['bare-key', $1]   when /^ ((?&BareKey)) $TOML/x;
+
+    default{
+      die "invalid key: syntax $toml";
+    }
+  }
+}
+
+sub tokenize_string {
+  my $toml = shift;
+  my $str = '';
+
+  for ($toml) {
+    when (/^ ((?&MultiLineString)) $TOML/x) {
+      $str = substr $1, 3, length($1) - 6;
+      $str =~ s/^(?&WS) (?&NL) $TOML//x;
+    }
+
+    when (/^ ((?&BasicString)) $TOML/x) {
+      $str = substr($1, 1, length($1) - 2);
+    }
+
+    when (/^ ((?&MultiLineStringLiteral)) $TOML/x) {
+      $str = substr $1, 3, length($1) - 6;
+      $str =~ s/^(?&WS) (?&NL) $TOML//x;
+    }
+
+    when (/^ ((?&StringLiteral)) $TOML/x) {
+      $str = substr($1, 1, length($1) - 2);
+    }
+
+    default{
+      die "invalid string syntax: $toml";
+    }
+  }
+
+  return ''.$str;
+}
+
+sub tokenize_integer {
+  my $toml = shift;
+
+  for ($toml) {
+    when (/(?&Oct) $TOML/x) {
+      $toml =~ s/^0o/0/; # convert to perl's octal format
+      return oct $toml;
+    }
+
+    when (/(?&Bin) $TOML/x) {
+      return oct $toml;
+    }
+
+    when (/(?&Hex) $TOML/x) {
+      return hex $toml;
+    }
+
+    when (/(?&Dec) $TOML/x) {
+    }
+
+    default{
+      die "invalid datetime syntax: $toml";
+    }
+  }
+
+  return 0 + $toml;
+}
+
+sub tokenize_float {
+  my $toml = shift;
+  return 0 + $toml;
+}
+
+1;
diff --git a/t/parity.t b/t/parity.t
new file mode 100644 (file)
index 0000000..2dc52a6
--- /dev/null
@@ -0,0 +1,69 @@
+#-------------------------------------------------------------------------------
+# Tests the results of parsing the example TOML from
+# https://github.com/toml-lang/toml against the de facto standard TOML module
+# on CPAN. Includes the array-of-tables example as well since that is not
+# represented in the synopsis example
+#-------------------------------------------------------------------------------
+use Test2::V0;
+use TOML::Tiny::Parser;
+use TOML;
+
+my $toml = do{ local $/; <DATA> };
+
+my $parser = TOML::Tiny::Parser->new(
+  inflate_datetime => 0,
+  inflate_boolean  => 0,
+);
+
+my $got = $parser->parse($toml);
+my $exp = from_toml($toml);
+
+is $got, $exp, 'parity with TOML module';
+done_testing;
+
+__DATA__
+# This is a TOML document.
+
+title = "TOML Example"
+
+[owner]
+name = "Tom Preston-Werner"
+dob = 1979-05-27T07:32:00-08:00 # First class dates
+
+[database]
+server = "192.168.1.1"
+ports = [ 8001, 8001, 8002 ]
+connection_max = 5000
+enabled = true
+options = {"quote-keys"=false}
+
+[servers]
+
+  # Indentation (tabs and/or spaces) is allowed but not required
+  [servers.alpha]
+  ip = "10.0.0.1"
+  dc = "eqdc10"
+
+  [servers.beta]
+  ip = "10.0.0.2"
+  dc = "eqdc10"
+
+[clients]
+data = [ ["gamma", "delta"], [1, 2] ]
+
+# Line breaks are OK when inside arrays
+hosts = [
+  "alpha",
+  "omega"
+]
+
+[[products]]
+name = "Hammer"
+sku = 738594937
+
+[[products]]
+
+[[products]]
+name = "Nail"
+sku = 284758393
+color = "gray"
diff --git a/t/tokens/array-of-tables.t b/t/tokens/array-of-tables.t
new file mode 100644 (file)
index 0000000..42cffe0
--- /dev/null
@@ -0,0 +1,29 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&ArrayOfTables)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = (
+  qq{[[foo]]\n},
+
+  qq{[[foo]]
+bar = 1234},
+
+  qq{[[foo]]
+bar = 1234
+baz = 5678},
+
+  qq{[[foo]]
+bar = 1234
+[[baz]]
+bat = 5678},
+
+  qq{[[foo]]
+bar = 1234
+[baz]
+bat = 5678},
+);
+
+ok($_ =~ /$re/, $_) for @valid;
+
+done_testing;
diff --git a/t/tokens/array.t b/t/tokens/array.t
new file mode 100644 (file)
index 0000000..30d6dd3
--- /dev/null
@@ -0,0 +1,28 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&Array)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = (
+  q{[ 1, 2, 3 ]},
+  q{[ "red", "yellow", "green" ]},
+  q{[ [ 1, 2 ], [3, 4, 5] ]},
+  q{[ [ 1, 2 ], ["a", "b", "c"] ]},
+  q{[ "all", 'strings', """are the same""", '''type''' ]},
+  q{[ 0.1, 0.2, 0.5, 1, 2, 5 ]},
+  q{[ "Foo Bar <foo@example.com>", { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" } ]},
+  q{[ 1, 2, 3 ]},
+  q{[ 1, 2, ]},
+  q{[
+    1,
+    2,
+  ]},
+  q{[
+    1,
+    2
+  ]},
+);
+
+ok($_ =~ /$re/, $_) for @valid;
+
+done_testing;
diff --git a/t/tokens/boolean.t b/t/tokens/boolean.t
new file mode 100644 (file)
index 0000000..8178513
--- /dev/null
@@ -0,0 +1,10 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&Boolean)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+like 'true', $re, 'true';
+like 'false', $re, 'false';
+unlike 'invalid', $re, 'invalid';
+
+done_testing;
diff --git a/t/tokens/datetime.t b/t/tokens/datetime.t
new file mode 100644 (file)
index 0000000..25ee426
--- /dev/null
@@ -0,0 +1,20 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&DateTime)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = (
+  '1979-05-27T07:32:00Z',
+  '1979-05-27T00:32:00-07:00',
+  '1979-05-27T00:32:00.999999-07:00',
+  '1979-05-27 07:32:00Z',
+  '1979-05-27T07:32:00',
+  '1979-05-27T00:32:00.999999',
+  '1979-05-27',
+  '07:32:00',
+  '00:32:00.999999',
+);
+
+ok($_ =~ /$re/, $_) for @valid;
+
+done_testing;
diff --git a/t/tokens/float.t b/t/tokens/float.t
new file mode 100644 (file)
index 0000000..6c978e7
--- /dev/null
@@ -0,0 +1,25 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&Float)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = qw(
+  +1.0
+  3.1415
+  -0.01
+  5e+22
+  1e06
+  -2E-2
+  6.626e-34
+  224_617.445_991_228
+  inf
+  +inf
+  -inf
+  nan
+  +nan
+  -nan
+);
+
+ok($_ =~ /$re/, $_) for @valid;
+
+done_testing;
diff --git a/t/tokens/inline-table.t b/t/tokens/inline-table.t
new file mode 100644 (file)
index 0000000..ff90e5a
--- /dev/null
@@ -0,0 +1,28 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&InlineTable)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = (
+  q|{ first = "Tom", last = "Preston-Werner" }|,
+  q|{ x = 1, y = 2 }|,
+  q|{ type.name = "pug" }|,
+  q|{
+    x = 1
+  }|,
+  q|{
+    x = 1,
+  }|,
+  q|{
+    x = 1,
+    y = 2
+  }|,
+  q|{
+    x = 1,
+    y = 2,
+  }|
+);
+
+ok($_ =~ /$re/, $_) for @valid;
+
+done_testing;
diff --git a/t/tokens/integer.t b/t/tokens/integer.t
new file mode 100644 (file)
index 0000000..cbf548f
--- /dev/null
@@ -0,0 +1,24 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&Integer)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = qw(
+  +99
+  42
+  0
+  -17
+  1_000
+  5_349_221
+  1_2_3_4_5
+  0xDEADBEEF
+  0xdeadbeef
+  0xdead_beef
+  0o01234567
+  0o755
+  0b110101101
+);
+
+like($_, $re, $_) for @valid;
+
+done_testing;
diff --git a/t/tokens/key-value-pair.t b/t/tokens/key-value-pair.t
new file mode 100644 (file)
index 0000000..ab69262
--- /dev/null
@@ -0,0 +1,21 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&KeyValuePair)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = (
+  q{foo= "bar"},
+  q{foo ="bar"},
+  q{foo="bar"},
+  q{foo = "bar"},
+  q{foo = 1234},
+  q{foo = 12.34},
+  q{foo = true},
+  q{foo = [1,2,3]},
+  q{foo = [1, 2, 3]},
+  q{foo = [ 1, 2, 3 ]},
+);
+
+ok($_ =~ /$re/, $_) for @valid;
+
+done_testing;
diff --git a/t/tokens/key.t b/t/tokens/key.t
new file mode 100644 (file)
index 0000000..f2d5975
--- /dev/null
@@ -0,0 +1,23 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ ((?&Key)) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = qw(
+  key
+  bare_key
+  bare-key
+  1234
+  "127.0.0.1"
+  "character encoding"
+  "ʎǝʞ"
+  'key2'
+  'quoted "value"'
+  physical.color
+  physical.shape
+  site."google.com"
+);
+
+ok($_ =~ /$re/, $_) for @valid;
+
+done_testing;
diff --git a/t/tokens/string.t b/t/tokens/string.t
new file mode 100644 (file)
index 0000000..f328a59
--- /dev/null
@@ -0,0 +1,108 @@
+use Test2::V0;
+use TOML::Tiny;
+
+sub test_simple_matches {
+  my ($re, @tests) = @_;
+  for (@tests) {
+    my ($toml, $expected, $label) = @$_;
+    my ($match) = $toml =~ $re;
+    is $match, $expected, $label;
+  }
+}
+
+subtest 'escaped characters' => sub{
+  my $re = qr{
+    ((?&EscapeChar))
+    $TOML::Tiny::GRAMMAR_V5
+  }x;
+
+  test_simple_matches($re,
+    ['\\\\', '\\\\', 'slash'],
+    ['\\b', '\\b', 'backspace'],
+    ['\\t', '\\t', 'tab'],
+    ['\\n', '\\n', 'line feed'],
+    ['\\f', '\\f', 'form feed'],
+    ['\\r', '\\r', 'carriage return'],
+    ['\\"', '\\"', 'quote'],
+    ['\\\\', '\\\\', 'backslash'],
+    ['\\u1234', '\\u1234', 'unicode (4 bytes)'],
+    ['\\U12345678', '\\U12345678', 'unicode (8 bytes)'],
+    ['\\x', undef, 'invalid'],
+  );
+};
+
+subtest 'string literals' => sub{
+  my $re = qr{
+    ((?&StringLiteral))
+    $TOML::Tiny::GRAMMAR_V5
+  }x;
+
+  test_simple_matches($re,
+    [q{'abc'}, q{'abc'}, 'single-quoted'],
+    [q{''}, q{''}, 'empty single-quoted'],
+  );
+};
+
+subtest 'basic strings' => sub{
+  my $re = qr{
+    ((?&BasicString))
+    $TOML::Tiny::GRAMMAR_V5
+  }x;
+
+  test_simple_matches($re,
+    ['""', '""', "empty string"],
+    ['"abc"', '"abc"', 'simple'],
+    ['"\\tfoo"', '"\\tfoo"', 'escaped chars'],
+    ['1234', undef, 'invalid'],
+  );
+};
+
+subtest 'multi-line strings' => sub{
+  my $re = qr{
+    ((?&MultiLineString))
+    $TOML::Tiny::GRAMMAR_V5
+  }x;
+
+  test_simple_matches($re,
+    [
+      qq{"""\nabc"""},
+      qq{"""\nabc"""},
+      'simple',
+    ],
+
+    [
+      qq{"""a\n"b"\nc"""},
+      qq{"""a\n"b"\nc"""},
+      'individual quotes within ml string',
+    ],
+
+    [
+      qq{"""foo"""bar"""},
+      qq{"""foo"""},
+      'invalid: triple-quotes appear within ml string',
+    ],
+  );
+};
+
+subtest 'multi-line string literals' => sub{
+  my $re = qr{
+    ((?&MultiLineStringLiteral))
+    $TOML::Tiny::GRAMMAR_V5
+  }x;
+
+  test_simple_matches($re,
+    [
+      qq{'''\nabc'''},
+      qq{'''\nabc'''},
+      'simple',
+    ],
+
+    [
+      qq{'''foo'''bar'''},
+      qq{'''foo'''},
+      'invalid: triple-quotes appear within ml string',
+    ],
+  );
+};
+
+done_testing;
diff --git a/t/tokens/table.t b/t/tokens/table.t
new file mode 100644 (file)
index 0000000..41cb8fc
--- /dev/null
@@ -0,0 +1,24 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $re = qr{ (?&Table) $TOML::Tiny::GRAMMAR_V5 }x;
+
+my @valid = (
+  qq{[foo]\n},
+
+  qq{[foo]
+bar = 1234},
+
+  qq{[foo]
+bar = 1234
+baz = 5678},
+
+  qq{[foo]
+bar = 1234
+[baz]
+bat = 5678},
+);
+
+ok($_ =~ /$re/, $_) for @valid;
+
+done_testing;
diff --git a/toml.abnf b/toml.abnf
new file mode 100644 (file)
index 0000000..b142e6c
--- /dev/null
+++ b/toml.abnf
@@ -0,0 +1,238 @@
+;; WARNING: This document is a work-in-progress and should not be considered
+;; authoritative until further notice.
+
+;; This is an attempt to define TOML in ABNF according to the grammar defined
+;; in RFC 5234 (http://www.ietf.org/rfc/rfc5234.txt).
+
+;; You can try out this grammar using http://instaparse.mojombo.com/
+;; To do so, in the lower right, click on Options and change `:input-format` to
+;; ':abnf'. Then paste this entire ABNF document into the grammar entry box
+;; (above the options). Then you can type or paste a sample TOML document into
+;; the beige box on the left. Tada!
+
+;; Overall Structure
+
+toml = expression *( newline expression )
+
+expression =  ws [ comment ]
+expression =/ ws keyval ws [ comment ]
+expression =/ ws table ws [ comment ]
+
+;; Whitespace
+
+ws = *wschar
+wschar =  %x20  ; Space
+wschar =/ %x09  ; Horizontal tab
+
+;; Newline
+
+newline =  %x0A     ; LF
+newline =/ %x0D.0A  ; CRLF
+
+;; Comment
+
+comment-start-symbol = %x23 ; #
+non-ascii = %x80-D7FF / %xE000-10FFFF
+non-eol = %x09 / %x20-7F / non-ascii
+
+comment = comment-start-symbol *non-eol
+
+;; Key-Value pairs
+
+keyval = key keyval-sep val
+
+key = simple-key / dotted-key
+simple-key = quoted-key / unquoted-key
+
+unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
+quoted-key = basic-string / literal-string
+dotted-key = simple-key 1*( dot-sep simple-key )
+
+dot-sep   = ws %x2E ws  ; . Period
+keyval-sep = ws %x3D ws ; =
+
+val = string / boolean / array / inline-table / date-time / float / integer
+
+;; String
+
+string = ml-basic-string / basic-string / ml-literal-string / literal-string
+
+;; Basic String
+
+basic-string = quotation-mark *basic-char quotation-mark
+
+quotation-mark = %x22            ; "
+
+basic-char = basic-unescaped / escaped
+basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
+escaped = escape escape-seq-char
+
+escape = %x5C                   ; \
+escape-seq-char =  %x22         ; "    quotation mark  U+0022
+escape-seq-char =/ %x5C         ; \    reverse solidus U+005C
+escape-seq-char =/ %x62         ; b    backspace       U+0008
+escape-seq-char =/ %x66         ; f    form feed       U+000C
+escape-seq-char =/ %x6E         ; n    line feed       U+000A
+escape-seq-char =/ %x72         ; r    carriage return U+000D
+escape-seq-char =/ %x74         ; t    tab             U+0009
+escape-seq-char =/ %x75 4HEXDIG ; uXXXX                U+XXXX
+escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX            U+XXXXXXXX
+
+;; Multiline Basic String
+
+ml-basic-string = ml-basic-string-delim ml-basic-body ml-basic-string-delim
+ml-basic-string-delim = 3quotation-mark
+ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
+
+mlb-content = mlb-char / newline / mlb-escaped-nl
+mlb-char = mlb-unescaped / escaped
+mlb-quotes = 1*2quotation-mark
+mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
+mlb-escaped-nl = escape ws newline *( wschar / newline )
+
+;; Literal String
+
+literal-string = apostrophe *literal-char apostrophe
+
+apostrophe = %x27 ; ' apostrophe
+
+literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
+
+;; Multiline Literal String
+
+ml-literal-string = ml-literal-string-delim ml-literal-body ml-literal-string-delim
+ml-literal-string-delim = 3apostrophe
+ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
+
+mll-content = mll-char / newline
+mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
+mll-quotes = 1*2apostrophe
+
+;; Integer
+
+integer = dec-int / hex-int / oct-int / bin-int
+
+minus = %x2D                       ; -
+plus = %x2B                        ; +
+underscore = %x5F                  ; _
+digit1-9 = %x31-39                 ; 1-9
+digit0-7 = %x30-37                 ; 0-7
+digit0-1 = %x30-31                 ; 0-1
+
+hex-prefix = %x30.78               ; 0x
+oct-prefix = %x30.6f               ; 0o
+bin-prefix = %x30.62               ; 0b
+
+dec-int = [ minus / plus ] unsigned-dec-int
+unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT )
+
+hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG )
+oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 )
+bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 )
+
+;; Float
+
+float = float-int-part ( exp / frac [ exp ] )
+float =/ special-float
+
+float-int-part = dec-int
+frac = decimal-point zero-prefixable-int
+decimal-point = %x2E               ; .
+zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
+
+exp = "e" float-exp-part
+float-exp-part = [ minus / plus ] zero-prefixable-int
+
+special-float = [ minus / plus ] ( inf / nan )
+inf = %x69.6e.66  ; inf
+nan = %x6e.61.6e  ; nan
+
+;; Boolean
+
+boolean = true / false
+
+true    = %x74.72.75.65     ; true
+false   = %x66.61.6C.73.65  ; false
+
+;; Date and Time (as defined in RFC 3339)
+
+date-time      = offset-date-time / local-date-time / local-date / local-time
+
+date-fullyear  = 4DIGIT
+date-month     = 2DIGIT  ; 01-12
+date-mday      = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on month/year
+time-delim     = "T" / %x20 ; T, t, or space
+time-hour      = 2DIGIT  ; 00-23
+time-minute    = 2DIGIT  ; 00-59
+time-second    = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second rules
+time-secfrac   = "." 1*DIGIT
+time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
+time-offset    = "Z" / time-numoffset
+
+partial-time   = time-hour ":" time-minute ":" time-second [ time-secfrac ]
+full-date      = date-fullyear "-" date-month "-" date-mday
+full-time      = partial-time time-offset
+
+;; Offset Date-Time
+
+offset-date-time = full-date time-delim full-time
+
+;; Local Date-Time
+
+local-date-time = full-date time-delim partial-time
+
+;; Local Date
+
+local-date = full-date
+
+;; Local Time
+
+local-time = partial-time
+
+;; Array
+
+array = array-open [ array-values ] ws-comment-newline array-close
+
+array-open =  %x5B ; [
+array-close = %x5D ; ]
+
+array-values =  ws-comment-newline val ws array-sep array-values
+array-values =/ ws-comment-newline val ws [ array-sep ]
+
+array-sep = %x2C  ; , Comma
+
+ws-comment-newline = *( wschar / [ comment ] newline )
+
+;; Table
+
+table = std-table / array-table
+
+;; Standard Table
+
+std-table = std-table-open key std-table-close
+
+std-table-open  = %x5B ws     ; [ Left square bracket
+std-table-close = ws %x5D     ; ] Right square bracket
+
+;; Inline Table
+
+inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
+
+inline-table-open  = %x7B ws     ; {
+inline-table-close = ws %x7D     ; }
+inline-table-sep   = ws %x2C ws  ; , Comma
+
+inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
+
+;; Array Table
+
+array-table = array-table-open key array-table-close
+
+array-table-open  = %x5B.5B ws  ; [[ Double left square bracket
+array-table-close = ws %x5D.5D  ; ]] Double right square bracket
+
+;; Built-in ABNF terms, reproduced here for clarity
+
+ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
+DIGIT = %x30-39 ; 0-9
+HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"