Login
Artifact [ffb502ca12]
Login

Artifact ffb502ca127965976651786439bc18f8e08796e3be81e549469ff4a70e9873e9:


#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU General Public License as published the Free
#### Software Foundation, either version 3 of the License, or (at your option)
#### any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE.See the GNU General Public License for
#### more details.
####
#### You should have received a copy of the GNU General Public License along
#### with this program.If not, see<http:####www.gnu.org/licenses/.>
require "../../libremiliacr"

module RemiLib::RSConf
  # Base class for all RSConf errors.
  class RSConfError < Exception
  end

  # Represents a stream encoding error.
  class RSConfEncodingError < RSConfError
  end

  # Raised when a Byte-Order-Mark is detected.
  class RSConfBOMError < RSConfEncodingError
  end

  # Raised when an error occurs during parsing and is parsing-related.
  class RSConfParseError < RSConfEncodingError
    getter line : UInt64 = 1
    getter column : UInt64 = 1

    def initialize(@line : UInt64, @column : UInt64, msg : String)
      super(msg)
    end
  end

  # Raised when an error occurs specifically because of an issue related to
  # serialization or deserialization.
  class RSConfSerializationError < RSConfError
  end

  # :nodoc:
  ILLEGAL_WHITESPACE = [
    8203, # Zero width space
    160,  # No-break space
    8199, # Figure space
    5760, # Ogham space mark
    8192, # En quad
    8193, # Em quad
    8194, # En space
    8195, # Em space
    8196, # Three-per-em space
    8197, # Four-per-em space
    8198, # Six-per-em space
    8200, # Punctuation space
    8201, # Thin space
    8202, # Hair space
    8239, # Narrow no-break space
    8287, # Medium mathematical space
    12288 # Ideographic space
  ] of UInt16

  # How integers are stored in memory.  This effectively puts a maximum
  # supported value on integers in RSConf data for this library.
  alias RSInteger = Int128

  # `RSScalar` wraps a scalar value in RSConf.  Scalar values are integers,
  # floats, booleans, strings, or the null value.
  struct RSScalar
    # Returns the value of this scalar.
    getter val : RSInteger|Float64|Bool|String|Nil

    # Creates a new `RSScalar` instance.  The value must fit into an
    # `RSInteger`.
    def initialize(value : Int)
      @val = value.to_i128
    end

    # :ditto:
    def initialize(value : Float32|Float64)
      @val = value.to_f64
    end

    # Creates a new `RSScalar` instance.
    def initialize(value : Bool)
      @val = value
    end

    # Creates a new `RSScalar` instance.
    def initialize(value : String)
      @val = value
    end

    # Creates a new `RSScalar` instance.
    def initialize(value : Nil)
      @val = value
    end

    {% begin %}
      {% for pair in [[:RSInteger, :int, :int?, :isInt?],
                      [:Float64, :float, :float?, :isFloat?],
                      [:Bool, :bool, :bool?, :isBool?],
                      [:String, :string, :string?, :isString?]] %}
        # Returns the scalar value that has the type `{{pair[0].id}}`.  If
        # this instance does not hold the correct type, an exception is raised.
        def {{pair[1].id}} : {{pair[0].id}}
          @val.as({{pair[0].id}})
        end

        # Returns the scalar value that has the type `{{pair[0].id}}|Nil`.  If
        # this instance does not hold the correct type, an exception is raised.
        def {{pair[2].id}} : {{pair[0].id}}|Nil
          @val.as?({{pair[0].id}})
        end

        # Returns `true` if this instance contains a scalar of the type
        # `{{pair[0].id}}`, or `false` otherwise.
        def {{pair[3].id}} : Bool
          @val.is_a?({{pair[0].id}})
        end
      {% end %}
    {% end %}

    # Returns the scalar value that has the type `RSInteger`.  If this instance
    # does not hold either an integer or float, an exception is raised.  If this
    # holds a float, it is converted to `RSInteger` using `to_128!`.
    def int! : RSInteger
      @val.as(RSInteger) || @val.as(Float64).to_i128!
    end

    # Attempts to return the scalar value that has the type `RSInteger`.  If
    # this instance does not hold either an integer, this returns `nil`.
    #
    # This is not the same as a scalar that holds an integer or a null value.
    # For that, use `#int?`.
    def tryInt : RSInteger?
      @val.as?(RSInteger)
    end

    # Returns `true` if this scalar holds an integer or float value, or `false`
    # otherwise.
    def isIntable? : Bool
      @val.is_a?(RSInteger) || @val.is_a?(Float64)
    end

    # Returns the scalar value that has the type `Float64`.  If this instance
    # does not hold either an integer or float, an exception is raised.  If this
    # holds an integer, it is converted to `Float64` using `to_f64!`.
    def float! : Float64
      @val.as(Float64) || @val.as(RSInteger).to_f64!
    end

    # Attempts to return the scalar value that has the type `Float64`.  If this
    # instance does not hold a float, this returns `nil`.
    #
    # This is not the same as a scalar that holds a float or a null value.  For
    # that, use `#float?`.
    def tryFloat : Float64?
      @val.as?(Float64)
    end

    # Returns `true` if this scalar holds an integer or float value, or `false`
    # otherwise.
    def isFloatable? : Bool
      @val.is_a?(Float64) || @val.is_a?(RSInteger)
    end

    # Returns `true` if this scalar holds the null value, or `false` otherwise.
    def isNil? : Bool
      @val.nil?
    end

    # Returns this scalar value as a string.  Non-string values will be
    # converted to string using `#to_s`.
    def string! : String
      @val.to_s
    end

    # Converts this instance to RSConf data by writing data using *builder*.
    def toRsconf(builder : ::RemiLib::RSConf::Builder)
      if @val.nil?
        builder.writeNil
      else
        @val.toRsconf(builder)
      end
    end
  end

  # The `RSObject` class holds an object value in RSConf.  Object values are
  # like hash tables, where they map a string to an RSConf value.
  class RSObject
    @val : Hash(String, RSScalar|RSObject|RSArray)
    delegate :each, to: @val
    delegate :has_key?, to: @val
    delegate :size, to: @val

    # Creates a new `RSObject` instance.
    def initialize(@val : Hash(String, RSScalar|RSObject|RSArray))
    end

    # Creates a new `RSObject` instance by yielding each key and its index to
    # the block, then storing the result of the block as the value for that key.
    def initialize(keys : Array(String), &)
      @val = {} of String => RSScalar|RSObject|RSArray
      keys.each_with_index do |k, idx|
        newVal = (yield k, idx)
        @val[k] = RSScalar.new(newVal)
      end
    end

    # Creates a new `RSObject` instance with no mappings.
    def initialize
      @val = {} of String => RSScalar|RSObject|RSArray
    end

    # Same as `Hash#[]`.
    def [](key : String) : RSScalar|RSObject|RSArray
      @val[key]
    end

    # Same as `Hash#[]=`.
    def []=(key : String, val : RSScalar|RSObject|RSArray) : self
      @val[key] = val
      self
    end

    # :ditto:
    def []=(key : String, val : Int) : self
      @val[key] = RSScalar.new(val)
      self
    end

    # :ditto:
    def []=(key : String, val : Float32|Float64) : self
      @val[key] = RSScalar.new(val)
      self
    end

    # :ditto:
    def []=(key : String, val : Bool) : self
      @val[key] = RSScalar.new(val)
      self
    end

    # :ditto:
    def []=(key : String, val : String) : self
      @val[key] = RSScalar.new(val)
      self
    end

    # :ditto:
    def []=(key : String, val : Nil) : self
      @val[key] = RSScalar.new(val)
      self
    end

    # :ditto:
    def []?(key : String) : Tuple(RSScalar|RSObject|RSArray, Bool)
      if @val.has_key?(key)
        {@val[key], true}
      else
        {@val[key], false}
      end
    end

    # Converts this instance to RSConf data by writing data using *builder*.
    def toRsconf(builder : ::RemiLib::RSConf::Builder)
      builder.writeObject do
        @val.each do |k, v|
          builder.writePair(k, v)
        end
      end
    end
  end

  # The `RSArray` class holds an array of RSConf values.
  class RSArray
    @val : Array(RSScalar|RSObject|RSArray)
    delegate :[], to: @val
    delegate :[]=, to: @val
    delegate :<<, to: @val
    delegate :each, to: @val
    delegate :size, to: @val

    # Creates a new `RSArray` instance.
    def initialize(@val : Array(RSScalar|RSObject|RSArray))
    end

    # Creates a new `RSArray` instance with no elements.
    def initialize
      @val = Array(RSScalar|RSObject|RSArray).new
    end

    # Converts this instance to RSConf data by writing data using *builder*.
    def toRsconf(builder : ::RemiLib::RSConf::Builder)
      builder.writeArray do
        @val.each(&.toRsconf(builder))
      end
    end
  end

  # Any valid RSConf value.
  alias RSValue = RSScalar|RSObject|RSArray

  # The type of RSConf values that can appear at the toplevel.
  #
  # Note that in RSConf, if the root value is an object, it does not require the
  # `{` and `}` characters.
  alias RSTopLevel = RSObject|RSArray
end