Module: Echoes::SvgPathParser

Defined in:
lib/echoes/svg_path_parser.rb

Overview

Tokenizer / parser for the ‘<path d=“…”>` mini-language. Returns an ordered list of `[cmd_symbol, [args…]]` tuples.

Uppercase cmd_symbols (:M, :L, :C, …) are absolute; lowercase (:m, :l, :c, …) are relative — the renderer applies the absolute-vs-relative interpretation since both forms have the same operand counts.

Returns nil on:

- empty / nil input
- first command isn't M or m (spec violation; no current point)
- unknown command letter
- operand-count mismatch (e.g. trailing partial group)
- any tokenizer leftover (unrecognized character)

Constant Summary collapse

OPERAND_COUNT =

Operand count per command, per spec.

{
  M: 2, m: 2,
  L: 2, l: 2,
  H: 1, h: 1,
  V: 1, v: 1,
  C: 6, c: 6,
  S: 4, s: 4,
  Q: 4, q: 4,
  T: 2, t: 2,
  A: 7, a: 7,
  Z: 0, z: 0,
}.freeze
IMPLICIT_CONTINUATION =

Implicit continuation: after consuming a command’s operands, if there are more numbers without a new command letter, reuse the last command — except M/m, where the implicit continuation is L/l per spec (“Subsequent pairs are treated as implicit lineto commands”).

{M: :L, m: :l}.freeze
TOKEN_RE =

Token regex: matches a command letter (excluding E/e which are reserved for scientific-notation exponents) OR a number. Number form: optional sign, then digits-and-optional-decimal or leading-decimal, with optional scientific notation. Whitespace and commas between tokens are silently skipped by the scanner.

/
  ([MmLlHhVvCcSsQqTtAaZz]) |
  ([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)
/x

Class Method Summary collapse

Class Method Details

.parse(d) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/echoes/svg_path_parser.rb', line 52

def parse(d)
  return nil if d.nil?
  tokens = tokenize(d)
  return nil if tokens.nil? || tokens.empty?

  # First token must be M or m.
  first = tokens.first
  return nil unless first.is_a?(Symbol) && (first == :M || first == :m)

  ops = []
  i = 0
  last_cmd = nil
  while i < tokens.size
    tok = tokens[i]
    if tok.is_a?(Symbol)
      cmd = tok
      i += 1
    else
      return nil unless last_cmd
      cmd = IMPLICIT_CONTINUATION[last_cmd] || last_cmd
    end

    arity = OPERAND_COUNT[cmd]
    return nil unless arity

    if arity.zero?
      ops << [cmd, []]
      last_cmd = cmd
      next
    end

    args = tokens[i, arity]
    return nil if args.size < arity
    return nil if args.any? { |t| t.is_a?(Symbol) }

    ops << [cmd, args]
    i += arity
    last_cmd = cmd
  end

  ops
end

.tokenize(d) ⇒ Object

Scan the string for tokens, ensuring we account for every non-whitespace, non-comma character. If any junk is left, returns nil so the caller can bail.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/echoes/svg_path_parser.rb', line 98

def tokenize(d)
  tokens = []
  pos = 0
  bytes = d.b
  while pos < bytes.length
    # Skip separators (whitespace, comma).
    if (m = /\G[\s,]+/.match(bytes, pos))
      pos = m.end(0)
      next
    end
    m = TOKEN_RE.match(bytes, pos)
    return nil unless m && m.begin(0) == pos
    if m[1]
      tokens << m[1].to_sym
    else
      tokens << m[2].to_f
    end
    pos = m.end(0)
  end
  tokens
end