I haven't made the Show HN post yet, but using the parser combinator library that I've been building[1], here's an answer to your exercise:

module Joker_vDChallengeParser
include Parsby::Combinators
extend self

def parse(s)
(expr < eof).parse(s)
end

define_combinator(:exp_op) {|left, right| group(left, spaced(lit("^")), right) }

define_combinator(:pos_op) {|subexpr| group(lit("+") < ws, subexpr) }
define_combinator(:neg_op) {|subexpr| group(lit("-") < ws, subexpr) }

define_combinator(:div_op) {|left, right| group(left, spaced(lit("/")), right) }
define_combinator(:mul_op) {|left, right| group(left, spaced(lit("*")), right) }

define_combinator(:add_op) {|left, right| group(left, spaced(lit("+")), right) }
define_combinator(:sub_op) {|left, right| group(left, spaced(lit("-")), right) }

define_combinator(:call_op) {|left, right| group(left, ws_1, right) }

define_combinator :identifier do
first_char = char_in([*('a'..'z'), *('A'..'Z'), '_'].join)
rest_char = first_char | char_in([*('0'..'9')].join)
first_char + join(many(rest_char))
end

define_combinator :lambda_expr do
group(
sep_by_1(ws_1, identifier) < spaced(lit(".")),
expr,
)
end

def scope(x, &b)
b.call x
end

define_combinator :expr do
lazy do
e = choice(
decimal,
lambda_expr,
identifier,
between(lit("("), lit(")"), expr),
)

# Each e = scope ... block is a precedence level. You can switch
# them around to play with the precedence of the operators.
#
# hpe -- higher precedence level expression
# spe -- same precedence level expression

# Basing myself on Haskell and making function calls the highest
# precedence operators.
e = scope e do |hpe|
reduce hpe do |left_result|
choice(
call_op(pure(left_result), hpe),
)
end
end

# Our only right-associative precedence level.
e = scope e do |hpe|
recursive do |spe|
choice(
exp_op(hpe, spe),
hpe,
)
end
end

e = scope e do |hpe|
recursive do |spe|
choice(
neg_op(spe),
pos_op(spe),
hpe,
)
end
end

# Left-associativity done by parsing left operand bottom-up and
# right operands top-down.
e = scope e do |hpe|
reduce hpe do |left_result|
choice(
mul_op(pure(left_result), hpe),
div_op(pure(left_result), hpe),
)
end
end

e = scope e do |hpe|
reduce hpe do |left_result|
choice(
add_op(pure(left_result), hpe),
sub_op(pure(left_result), hpe),
)
end
end
end
end
end

That was a nice exercise. Here's some example parses:

pry(main)> Joker_vDChallengeParser.parse "- 3 - foo bar . foo - bar - 5"
=> [["-", 3], "-", [["foo", "bar"], [["foo", "-", "bar"], "-", 5]]]
pry(main)> Joker_vDChallengeParser.parse "- 3 - (foo bar . foo - bar - 5) - 4 * -2 + 5 / 2"
=> [[[["-", 3], "-", [["foo", "bar"], [["foo", "-", "bar"], "-", 5]]], "-", [4, "*", ["-", 2]]], "+", [5, "/", 2]]
pry(main)> Joker_vDChallengeParser.parse "- 3 - (foo bar . foo 5 6 - bar - 5) - 4 * -2 + 5 / 2"
=> [[[["-", 3], "-", [["foo", "bar"], [[[["foo", " ", 5], " ", 6], "-", "bar"], "-", 5]]], "-", [4, "*", ["-", 2]]], "+", [5, "/", 2]]

pry(main)> Joker_vDChallengeParser.parse "foo bar . foo bar baz qux . baz qux"
=> [["foo", "bar"], [["foo", "bar", "baz", "qux"], ["baz", " ", "qux"]]]
pry(main)> Joker_vDChallengeParser.parse "foo bar . foo bar + baz qux . baz qux"
=> [["foo", "bar"], [["foo", " ", "bar"], "+", [["baz", "qux"], ["baz", " ", "qux"]]]]

You can try it by cloning my repo, running `./bin/console`, and pasting the module, or putting it in a file in `lib/`. If you put it in a file, you can reload changes with `reload!` in the console.

[1] https://github.com/jolmg/parsby

EDIT: Fixed the syntax for newer versions of Ruby.