1
0
Fork 0
mirror of https://github.com/Oreolek/ink-instead.git synced 2024-04-28 15:19:25 +03:00

Merge pull request #4 from premek/flatparse

Flat parser refactor - parsed table is now flat, not nested - e.g. knot content is not inside the knot node but following the knot node. the same for options. Simpler parser, runtime not much more complicated
This commit is contained in:
Přemek Vyhnal 2017-08-12 23:39:56 +02:00 committed by GitHub
commit faedb6a3be
14 changed files with 305 additions and 262 deletions

View file

@ -4,11 +4,6 @@ local S,C,Ct,Cc,Cg,Cb,Cf,Cmt,P,V =
lpeg.S, lpeg.C, lpeg.Ct, lpeg.Cc, lpeg.Cg, lpeg.Cb, lpeg.Cf, lpeg.Cmt,
lpeg.P, lpeg.V
local concat = function (p)
return Cf(p, function (a,b) return a..b end)
end
local parserLogger = print
local eof = -1
local sp = S" \t" ^0 + eof
local wh = S" \t\r\n" ^0 + eof
@ -16,68 +11,52 @@ local nl = S"\r\n" ^1 + eof
local id = (lpeg.alpha + '_') * (lpeg.alnum + '_')^0
local addr = C(id) * ('.' * C(id))^-1
local todo = sp * 'TODO:' * sp * (1-nl)^0 / parserLogger * wh -- TODO log location
local commOL = sp * '//' * sp * (1-nl)^0 * wh
local commML = sp * '/*' * wh * (P(1)-'*/')^0 * '*/' * wh
local todo = Ct(sp * 'TODO:'/"todo" * sp * C((1-nl)^0)) * wh
local commOL = Ct(sp * '//'/"comment" * sp * C((1-nl)^0)) * wh
local commML = Ct(sp * '/*'/"comment" * wh * C((P(1)-'*/')^0)) * '*/' * wh
local comm = commOL + commML + todo
local glue = P'<>'/'glue' *wh -- FIXME do not consume spaces after glue
local glue = Ct(P'<>'/'glue') *wh -- FIXME do not consume spaces after glue
local divertSym = '->' *wh
local divertEndSym = C('END') *wh
local divertEnd = divertSym * divertEndSym
local divertEnd = Ct(divertSym/'end' * 'END' * wh)
local divertJump = Ct(divertSym/'divert' * addr * wh)
local divert = divertEnd + divertJump
local knotHead = P('=')^2/'knot' * wh * C(id) * wh * P('=')^0 * wh
local stitchHead = P('=')^1/'stitch' * wh * C(id) * wh * P('=')^0 * wh
local knot = Ct(P('=')^2/'knot' * wh * C(id) * wh * P('=')^0) * wh
local stitch = Ct(P('=')^1/'stitch' * wh * C(id) * wh * P('=')^0) * wh
local optDiv = '[' * C((P(1) - ']')^0) * ']'
local optStars = concat(wh * C(P'*') * (sp * C'*')^0)
local optStarsSameIndent = Cmt(Cb("indent") * optStars,
function (s, i, a, b) return a == b end)
local optStarsLEIndent = Cmt(Cb("indent") * optStars,
function (s, i, backtrack, this)
return string.len(this) <= string.len(backtrack)
end)
local tag = Ct(wh * P('#')/'tag' * wh * V'text' * wh)
local optStars = wh * Ct(C'*' * (sp * C'*')^0)/table.getn
local hash = P('#')
local tag = hash * wh * V'text'
local tagGlobal = Ct(Cc'tag' * Cc'global' * tag * wh)
local tagAbove = Ct(Cc'tag' * Cc'above' * tag * wh)
local tagEnd = Ct(Cc'tag' * Cc'end' * tag * sp)
local ink = P({
"lines",
knotKnot = Ct(knotHead * (V'line'-knotHead)^0 * wh),
knotStitch = Ct(stitchHead * (V'line'-stitchHead)^0 * wh),
knot = V'knotKnot' + V'knotStitch',
stmt = glue + divert + knot + stitch + V'option' + optDiv + comm + V'include',
text = C((1-nl-V'stmt'-hash)^1),
textEmptyCapt = C((1-nl-V'stmt'-hash)^0),
stmt = glue + divert + V'knot' + optDiv + comm + V'include' + tag,
text = C((1-nl-V'stmt')^1) *wh,
textE = C((1-nl-V'stmt')^0) *wh,
optAnsWithDiv = V'textE' * optDiv * V'textE' * wh,
optAnsWithoutDiv = V'textE' * Cc ''* Cc ''* wh, -- huh?
optAnsWithDiv = V'textEmptyCapt' * sp * optDiv * V'text'^0 * wh,
optAnsWithoutDiv = V'textEmptyCapt' * sp * Cc '' * Cc '' * wh, -- huh?
optAns = V'optAnsWithDiv' + V'optAnsWithoutDiv',
-- TODO clean this
opt = Cg(optStars,'indent') *
Ct(Cc'option' * sp * V'optAns' * (V'line'-V'optLEIndent'-V'knot')^0 * wh), --TODO which can by toplevel only?
optSameIndent = Ct(Cc'option' * optStarsSameIndent * sp * V'optAns' * (V'line'-V'optLEIndent'-V'knot')^0 * wh),
optLEIndent = Ct(Cc'option' * optStarsLEIndent * sp * V'optAns' * (V'line'-V'optLEIndent'-V'knot')^0 * wh),
option = Ct(Cc'option' * optStars * sp * V'optAns'),
opts = (V'opt'*V'optSameIndent'^0),
choice = Ct(Cc'choice' * V'opts')/function(t) t.indent=nil; return t end,
include = Ct(P('INCLUDE')/'include' * wh * V'text' * wh),
para = Ct(Cc'para' * V'text'),
para = tagAbove^0 * Ct(Cc'para' * V'text') * tagEnd^0 * wh + tagGlobal,
line = V'stmt' + V'choice' + V'para',
line = V'stmt' + V'para',
lines = Ct(V'line'^0)
})

View file

@ -1,104 +1,158 @@
local debug = function(x) print( require('test/luaunit').prettystr(x) ) end
local is = function (what, node)
return node ~= nil
and (type(node) == "table" and node[1] == what)
or (type(node) == "string" and node == what)
end
local getPara = function (node)
if is('para', node) then return node[2] end
end
return function (tree)
local s = {}
local pointer = nil
local tab = {}
local knots = {}
-- TODO state should contain tab/pointer to be able to save / load
s.state = {
visitCount = {}
local s = {
globalTags = {},
state = {
visitCount = {},
}
}
local process = function ()
for _, v in ipairs(tree) do
if is('knot', v) then
knots[v[2]] = v
local pointer = 1
local knots = {}
local tags = {} -- maps (pointer to para) -> (list of tags)
local tagsForContentAtPath = {}
local currentChoicesPointers = {}
-- TODO state should contain tree/pointer to be able to save / load
local preProcess = function ()
local aboveTags = {}
local lastPara = {}
local lastKnotName
for p, n in ipairs(tree) do
if is('knot', n) then -- FIXME stitches
knots[n[2]] = p
--print(v[2],k)
end
if is('tag', n) then
if n[2] == 'global' then
table.insert(s.globalTags, n[3])
end
if n[2] == 'above' then
if lastKnotName then table.insert(tagsForContentAtPath[lastKnotName], n[3]) end
table.insert(aboveTags, n[3])
end
if n[2] == 'end' then
table.insert(tags[lastPara], n[3])
end
end
if is('knot', n) or is('stitch', n) then
lastKnotName = n[2]
tagsForContentAtPath[lastKnotName] = {}
end
if is('para', n) then
tags[p] = aboveTags
aboveTags = {}
lastPara = p
end
end
end
local goToKnot = function(knotName)
if knots[knotName] then
s.state.visitCount[knotName] = (s.state.visitCount[knotName] or 0) + 1
tab = knots[knotName]
pointer = 3
return tab[pointer]
else
s.state.visitCount[knotName] = s.state.visitCountAtPathString(knotName) + 1
pointer = knots[knotName] + 1 -- go to the line after the knot
else
print('unknown knot', knotName)
end
end
local update = function ()
local next = tab[pointer]
local isNext = function (what)
return is(what, tree[pointer])
end
if is('knot', next) then
local getPara = function ()
if isNext('para') then return tree[pointer][2] end
end
local update
update = function ()
if isNext('knot') then
-- FIXME: we shouldn't continue to next knot automatically probably - how about stitches?
--next = goToKnot(next[2])
end
if is('divert', next) then next = goToKnot(next[2]) end
if isNext('divert') then
goToKnot(tree[pointer][2])
update()
return
end
s.canContinue = is('para', next)
if isNext('tag') then
pointer = pointer + 1
update()
return
end
s.canContinue = isNext('para')
s.currentChoices = {}
if is('choice', next) then
for i=2, #next do
--print(to_string(next[i]))
table.insert(s.currentChoices, {
text = (next[i][2] or '') .. (next[i][3] or ''),
choiceText = next[i][2] .. (next[i][4] or ''),
})
currentChoicesPointers = {}
if isNext('option') then
local choiceDepth = tree[pointer][2]
-- find all choices on the same level in the same knot and in the same super-choice
for p=pointer, #tree do
local n = tree[p]
--print('looking for options', choiceDepth, n[1], n[2])
if is('knot', n) or is('stitch', n) or (is('option', n) and n[2] < choiceDepth) then
--print('stop looking for options');
break
end
if is('option', n) and n[2] == choiceDepth then
-- print('adding', p, n[3])
table.insert(currentChoicesPointers, p)
table.insert(s.currentChoices, {
text = (n[3] or '') .. (n[4] or ''),
choiceText = n[3] .. (n[5] or ''),
})
end
end
end
end
local step = function ()
pointer = pointer + 1
update()
return tab[pointer]
end
local stepTo = function (table, pos)
tab = table
pointer = pos
update()
return tab[pointer]
end
s.canContinue = nil
s.continue = function()
local res = getPara(tab[pointer])
local next = step()
if is('glue', next) then
step()
local res = getPara()
s.currentTags = tags[pointer] or {}
pointer = pointer + 1
update()
if isNext('glue') then
pointer = pointer + 1
update()
res = res .. s.continue()
end
return res;
end
s.currentChoices = nil
s.currentChoices = {}
s.chooseChoiceIndex = function(index)
s.currentChoices = {}
local choice = tab[pointer]
local option = choice[1 + index]
stepTo(option, 5)
pointer = currentChoicesPointers[index]+1
update()
end
s.choosePathString = function(knotName)
@ -110,14 +164,21 @@ return function (tree)
return s.state.visitCount[knotName] or 0
end
s.tagsForContentAtPath = function(knotName)
return tagsForContentAtPath[knotName] or {}
end
s.currentTags = {}
s.variablesState = {}
-- s.state.ToJson();s.state.LoadJson(savedJson);
stepTo(tree, 1)
process()
preProcess()
--debug(tree)
--debug(tags)
-- debug
s._tree = tree
return s
end

View file

@ -2,21 +2,10 @@ return {
ink=[[
Hello, world!
Hello?
//TODO: this is a test todo-item
"What do you make of this?" she asked.
// Something unprintable...
"I couldn't possibly comment," I replied.
/*
... or an unlimited block of text
*/
we <> /* he
asdf
*/ hurr ied-> to_savile_row // comm
we <>
hurr ied-> to_savile_row
=== to_savile_row ===
@ -33,14 +22,14 @@ expected= {
{"para", '"What do you make of this?" she asked.'},
{"para", "\"I couldn't possibly comment,\" I replied."},
{"para", "we "},
"glue",
{"glue"},
{"para", "hurr ied"},
{"divert", "to_savile_row"},
{
"knot",
"to_savile_row",
{"para", "to Savile Row"},
{"stitch", "st", {"para", "stiiii"}},
{"stitch", "st2", {"para", "222stiiii "}, "END"}
}
{"knot", "to_savile_row"},
{"para", "to Savile Row"},
{"stitch", "st"},
{"para", "stiiii"},
{"stitch", "st2"},
{"para", "222stiiii "},
{"end"}
}}

View file

@ -27,46 +27,27 @@ He insisted that we hurried home to Savile Row
<> as fast as we could.
]],
expected= {
{"knot", "back_in_london"},
{"para", "We arrived into London at 9.45pm exactly."},
{"option", 1, '"There is not a moment to lose!"', "", " I declared."},
{"divert", "hurry_outside"},
{"option", 1, '"Monsieur, let us savour this moment!"', "", " I declared."},
{
"knot",
"back_in_london",
{"para", "We arrived into London at 9.45pm exactly."},
{
"choice",
{
"option",
'"There is not a moment to lose!"',
"",
" I declared.",
{"divert", "hurry_outside"}
},
{
"option",
'"Monsieur, let us savour this moment!"',
"",
" I declared.",
{
"para",
"My master clouted me firmly around the head and dragged me out of the door."
},
'glue',
{"divert", "dragged_outside"}
},
{"option", "", "We hurried home", " ", {"divert", "hurry_outside"}}
}
"para",
"My master clouted me firmly around the head and dragged me out of the door."
},
{
"knot",
"hurry_outside",
{"para", "We hurried home to Savile Row "},
{"divert", "as_fast_as_we_could"}
},
{
"knot",
"dragged_outside",
{"para", "He insisted that we hurried home to Savile Row"},
{"divert", "as_fast_as_we_could"}
},
{"knot", "as_fast_as_we_could", "glue", {"para", "as fast as we could."}}
{"glue"},
{"divert", "dragged_outside"},
{"option", 1, "", "We hurried home", " "},
{"divert", "hurry_outside"},
{"knot", "hurry_outside"},
{"para", "We hurried home to Savile Row "},
{"divert", "as_fast_as_we_could"},
{"knot", "dragged_outside"},
{"para", "He insisted that we hurried home to Savile Row"},
{"divert", "as_fast_as_we_could"},
{"knot", "as_fast_as_we_could"},
{"glue"},
{"para", "as fast as we could."}
}
}

View file

@ -12,30 +12,16 @@ ink=[[
== finale ==
]],
expected= {
{
"knot",
"start",
{
"choice",
{"option", "I dont know", "", ""},
{
"option",
'"I am somewhat tired',
'."',
'," I repeated.',
{"para", '"Really," he responded.'},
{"para", '"How deleterious."'}
},
{
"option",
'"Nothing, Monsieur!"',
"",
" I replied.",
{"para", '"Very good, *then."'}
},
{"option", "I said no more", "", "", {"para", '"Ah,". "I see you"'}}
}
},
{"knot", "start"},
{"option", 1, "I dont know", "", ""},
{"option", 1, '"I am somewhat tired', '."', '," I repeated.'},
{"para", '"Really," he responded.'},
{"para", '"How deleterious."'},
{"option", 1, '"Nothing, Monsieur!"', "", " I replied."},
{"para", '"Very good,'},
{"option", 1, 'then."', "", ""},
{"option", 1, "I said no more", "", ""},
{"para", '"Ah,". "I see you"'},
{"knot", "finale"}
}
}

34
test/parser/comments.lua Normal file
View file

@ -0,0 +1,34 @@
return {
ink=[[
Hello?
TODO: this is a test todo-item
"What do you make of this?" she asked.
// Something unprintable...
"I couldn't possibly comment," I replied.
/*
... or an unlimited block of text
*/
we <> /* he
asdf
*/ hurr ied-> to_savile_row // comm
]],
expected= {
{"para", "Hello?"},
{"todo", "this is a test todo-item"},
{"para", '"What do you make of this?" she asked.'},
{"comment", "Something unprintable..."},
{"para", "\"I couldn't possibly comment,\" I replied."},
{"comment", "... or an unlimited block of text\n"},
{"para", "we "},
{"glue"},
{"comment", "he\nasdf\n"},
{"para", "hurr ied"},
{"divert", "to_savile_row"},
{"comment", "comm"},
}}

View file

@ -9,22 +9,14 @@ ink=[[
]],
expected={
{
"choice",
{
"option",
'"Monsieur, let us savour this moment!"',
"",
" I declared.",
{
"para",
"My master clouted me firmly around the head and dragged me out of the door. "
},
"glue",
{"divert", "dragged_outside"}
},
{"option", "", "We hurried home", " ", {"divert", "hurry_outside"}}
},
{"knot", "as_fast_as_we_could", "glue", {"para", "as fast as we could."}} -- TODO should be space before 'as'
{"option", 1, '"Monsieur, let us savour this moment!"', "", " I declared."},
{"para", "My master clouted me firmly around the head and dragged me out of the door. " },
{"glue"},
{"divert", "dragged_outside"},
{"option", 1, "", "We hurried home", " "},
{"divert", "hurry_outside"},
{"knot", "as_fast_as_we_could"},
{"glue"},
{"para", "as fast as we could."} -- TODO should be space before 'as'
}
}

View file

@ -16,15 +16,18 @@ ink=[[
]],
expected={
{
"knot",
"the_orient_express",
{"stitch", "in_first_class", {"para", "..."}},
{"stitch", "in_third_class", {"para", "..."}},
{"stitch", "in_the_guards_van", {"para", "..."}, {"para", "..."}},
{"stitch", "missed_the_train", {"para", "..."}}
},
{"knot", "the_orient_express"},
{"knot", "the_orient_express", {"stitch", "stitch"}}
{ "knot", "the_orient_express"},
{"stitch", "in_first_class"},
{"para", "..."},
{"stitch", "in_third_class"},
{"para", "..."},
{"stitch", "in_the_guards_van"},
{"para", "..."},
{"para", "..."},
{"stitch", "missed_the_train"},
{"para", "..."},
{"knot", "the_orient_express"},
{"knot", "the_orient_express"},
{"stitch", "stitch"}
}
}

View file

@ -2,18 +2,14 @@ return {
ink=[[
* "Murder!"
** A
* * A
* "Suicide!"
]], expected= {
{
"choice",
{
"option",
'"Murder!"',
"",
"",
{"choice", {"option", "A", "", ""}}
},
{"option", '"Suicide!"', "", ""}
}
{"option", 1, '"Murder!"', "", ""},
{"option", 2, "A", "", ""},
{"option", 2, "A", "", ""},
{"option", 1, '"Suicide!"', "", ""}
}
}

View file

@ -10,22 +10,11 @@ ink=[[
]],
expected = {
{"para", '"Well, Poirot? Murder or suicide?"'},
{
"choice",
{
"option",
'"Murder"',
"",
"",
{"para", '"And who did it?"'},
{
"choice",
{"option", '"Detective-Inspector Japp!"', "", ""},
{"option", '"Captain Hastings!"', "", ""},
{"option", '"Myself!"', "", ""}
}
},
{"option", '"Suicide"', "", ""}
}
{"option", 1, '"Murder"', "", ""},
{"para", '"And who did it?"'},
{"option", 2, '"Detective-Inspector Japp!"', "", ""},
{"option", 2, '"Captain Hastings!"', "", ""},
{"option", 2, '"Myself!"', "", ""},
{"option", 1, '"Suicide"', "", ""}
}
}

View file

@ -9,10 +9,21 @@ ink=[[
# require: Train ticket
This is the line of content. # the third tag # really_monsieur.ogg
#tag
aaa
]],
expected={
{"tag", "global", "author: Joseph Humfrey"},
{"tag", "global", "title: My Wonderful Ink Story"},
{"knot", "content"},
{"tag", "above", "location: Germany"},
{"tag", "above", "overview: munich.ogg"},
{"tag", "above", "require: Train ticket"},
{"para", "This is the line of content. "},
{"tag", "end", "the third tag "},
{"tag", "end", "really_monsieur.ogg"},
{"tag", "above", "tag"},
{"para", "aaa"}
}
}

View file

@ -1,14 +1,17 @@
=== back_in_london ===
We arrived into London at 9.45pm exactly.
We arrived into London at 9.45pm
exactly
* "There is not a moment to lose!"[] I declared.
-> hurry_outside
* "Monsieur, let us savour this moment!"[] I declared.
My master clouted me firmly around the head and dragged me out of the door.<>
-> dragged_outside
My master clouted me firmly around the head
** ugh?
no thanks
** hug
huhu
* [We hurried home] -> hurry_outside

View file

@ -3,15 +3,11 @@
->content
=== content
A line of normal game-text. # colour it blue
Passepartout: Really, Monsieur. # surly
Passepartout: Really, Monsieur. # surly # really_monsieur.ogg
# the first tag
# the second tag
This is the line of content. # the third tag
#not this one
that to this
=== Munich ==
# location: Germany

View file

@ -15,13 +15,10 @@ function testText() doTestS(
{{"para", "Hello world"}}
) end
function testOpt1() doTestS(
'* "I am somewhat tired[."]," I repeated.',
{{'choice', {"option", '"I am somewhat tired', '."', '," I repeated.'}}}
) end
function testBasic() doTest('basic') end
function testComments() doTest('comments') end
function testChoices() doTest('choices') end
function testNest() doTest('nested') end
function testNest2() doTest('nested2') end
@ -29,12 +26,34 @@ function testKnot() doTest('knot') end
function testBranching() doTest('branching') end
function testGlue() doTest('glue') end
function testInclude() doTest('include') end
function testInclude() doTest('tags') end
function testTags() doTest('tags') end
--- runtime ---
function testVisitCount()
function testRBasic()
local story = pink.getStory('test/runtime/hello.ink')
luaunit.assertEquals(story.continue(), 'hello world')
luaunit.assertFalse(story.canContinue)
end
function testRChoices()
local story = pink.getStory('test/runtime/branching.ink')
story.choosePathString('back_in_london');
story.continue()
luaunit.assertEquals(story.continue(), 'exactly')
luaunit.assertFalse(story.canContinue)
luaunit.assertEquals(#story.currentChoices, 3)
story.chooseChoiceIndex(2)
luaunit.assertEquals(story.continue(), 'My master clouted me firmly around the head')
luaunit.assertEquals(#story.currentChoices, 2)
story.chooseChoiceIndex(2)
luaunit.assertEquals(story.continue(), 'huhu')
end
function testRVisitCount()
local story = pink.getStory('test/runtime/branching.ink')
story.choosePathString('hurry_outside');
luaunit.assertEquals(story.state.visitCountAtPathString('as_fast_as_we_could'), 0)
@ -46,22 +65,26 @@ function testVisitCount()
luaunit.assertEquals(story.state.visitCountAtPathString('as_fast_as_we_could'), 2)
end
function testIncludeR()
function testRInclude()
local story = pink.getStory('test/runtime/include.ink')
luaunit.assertEquals(story.continue(), 'hello world')
luaunit.assertEquals(story.continue(), 'hello again')
luaunit.assertFalse(story.canContinue)
end
function testTags()
function testRTags()
local story = pink.getStory('test/runtime/tags.ink')
luaunit.assertEquals(story.continue(), '')
luaunit.assertEquals(story.continue(), '')
luaunit.assertEquals(story.globalTags, {""})
luaunit.assertEquals(story.globalTags, {"author: Joseph Humfrey", "title: My Wonderful Ink Story"})
story.choosePathString('content');
luaunit.assertEquals(story.continue(), 'This is the line of content. ')
luaunit.assertEquals(story.currentTags, {"the first tag", "the second tag", "the third tag"})
story.continue()
luaunit.assertEquals(story.currentTags, {"not this one"})
luaunit.assertFalse(story.canContinue)
luaunit.assertEquals(story.tagsForContentAtPath('Munich'), {"location: Germany", "overview: munich.ogg", "require: Train ticket"})
end
function testInvisibleDiverts()
function testRInvisibleDiverts()
local story = pink.getStory('test/runtime/branching.ink')
story.choosePathString('hurry_outside')
luaunit.assertEquals(story.continue(), "We hurried home to Savile Row as fast as we could.")