Commit 21f0a9c8 authored by Leon Arnott's avatar Leon Arnott
Browse files

Finished implementing (cycling-link:) and VarBind.

I'm still not 100% confident on the "bind" keyword, but I guess this is what I'm sticking with.

You may notice that a VarBind stores a VarRef, which contains a variable name and property chain, but afterward one can then alter the variable from under it, so that a property chain for a datamap is now applied to an array. canSet() in varref.js has some provisions to make these not much of a hassle, but I changed it so that internally, VarRefs' property chains now store array indexes as numbers instead of strings, so that they might be distinguished from datamap names. This isn't ideal at all, but for now it produces user-facing errors when it should.

Working on this implementation leaves me with a few additional questions:

 * Whether (cycling-link:) rejecting an empty link text should be universalised to the other link macros.

 * Whether (t8n:) attached to a (cycling-link:) causing it to transition in on passage start is a problem. (Should it follow how (link:) interacts with t8n?)
parent ae0835bab5ff
......@@ -16,6 +16,7 @@ Rough documentation is at http://twine2.neocities.org/. See below for compilatio
* The `(text:)` macro now has another alias, `(str:)`. This alias will now be the preferred name for this macro in the documentation, mainly due to the arrival of other string macros that begin with "str-", and additionally to avoid semantic conflict with the various "text-" changer macros like `(text-style:)`.
* To more clearly separate the concepts of "printing data" and "running commands" in Harlowe, the `(print:)` macro will no longer run commands passed to it (that is, `(print:(go-to:"Foo"))` and `(go-to:"Foo")` will no longer do the same thing - the former will just print out a descriptive string, as if printing out a changer). Commands can now only be run by placing them directly in the passage (either as plain calls, inside variables, or wrapped in strings that (print:) receives).
* Passage links no longer have a `passage-name` attribute indicating which passage they lead to, which the player could inspect using developer tools.
* If links' text contains an error message (for instance, in the case of `(link-replace:"(print:2+true"))[]`), then the link can no longer be clicked (so that the error message can be expanded).
####Additions
......@@ -23,12 +24,14 @@ Rough documentation is at http://twine2.neocities.org/. See below for compilatio
* Added `is a` and `is an` operators, which can be used to determine what datatype a variable or piece of data is - `$message is a string` is true if the variable is a string. The datatype names are `number` or `num`, `string` or `str`, `boolean`, `array`, `datamap` or `dm`, `dataset` or `ds`, `command`, `changer`, and `color` or `colour`.
* Added a `matches` operator, which functions similarly to `is a`, but can also be used to check if a data structure's shape resembles a pattern - a similar data structure with datatype names as "holes" in it. `(a: 2, 3) matches (a: num, num)` checks that the first array contains exactly two numbers. `(dm: "Faction", str) matches (dm: "Faction", "Slugbikers")` checks that the second datamap contains only one name with a string value. Nested patterns - `(a: (a: num), num, num)` - are also usable.
* Added a `bind` operator, which is used to "bind" variables to certain interaction macros, like (cycling-link:), described below.
#####Macros
* Various command macros that print visible elements into the page, such as `(print:)`, `(display:)`, `(link-goto:)`, and standard passage links, have been updated so that you can attach changers to them. These commands are now "HookCommands", in that changers can be "hooked" onto them, just like hooks.
* Added `(transition-depart:)` and `(transition-arrive:)` (aliases `(t8n-depart:)` and `(t8n-arrive:)`), macros which allow you to finally change the passage transition used by links, by just attaching them to the front: `(t8n-depart:"dissolve")[[Think it over]]` will create a link that, when clicked, goes to the "Think it over" passage and fades out the current passage using a dissolve transition. These can be used in tandem for a number of interesting effects: `(t8n-depart:"dissolve")(t8n-arrive:"pulse")[[That memory...]]` will work as expected. You can also use these with `(link-goto:)`, `(link-undo:)`, and work with `(enchant: ?Link)` too.
* The "instant" transition has been added, which makes the transitioning entity instantly appear. (Try placing `(enchant:?Link, (t8n-arrive:"instant"))` in your header passages.)
* Added `(cycling-link:)`, a popular interaction macro from Twine 1. Clicking a cycling link changes its text to the next string in the given list, and can optionally set a variable to that same string. An example usage is `(cycling-link: bind $flower, "rose", "violet", "daffodil")`. This is a HookCommand and can use an attached `(t8n:)` to transition each string in as it's clicked. (I'd meant to include for a very long time, but it needed several other features in this update to fully meet the UX standards of Harlowe.)
* Added a string-specific shorthand of `(repeated:)` called `(str-repeated:)` (and aliased as `(string-repeated:)`). `(str: ...(repeated: 14, "-+*+"))` is the same as `(str-repeated: 14, "-+*+")`.
* Added `(reversed:)`, a macro which constructs an array with the given enements in reverse order, and `(str-reversed:)`, a shorthand that reverses a single string's characters. (Prior to now, you could accomplish this with `(folded: _e making _a via (a: _e) + _a, (a:), ...$arr)`, but this offers a far easier formulation.)
* Added `(click-goto:)`, `(mouseover-goto:)` and `(mouseout-goto:)`, which are combinations of `(click:)`, `(mouseover:)` and `(mouseout:)` with `(goto:)`, similar to `(link-goto:)`.
......
......@@ -10,11 +10,22 @@ define(['utils', 'utils/operationutils', 'internaltypes/varref', 'internaltypes/
/*d:
Bound Variable data
TBW
A few macros that produce interactive elements, like (cycling-link:), have the ability to automatically update a variable whenever the
player interacts with them. There needs to be a way to specify which variable these will update: simply giving the macro the variable
itself, such as in `(cycling-link: $hat, "Poker visor", "Beret")`, won't work - the value that's currently inside `$hat` will be given
instead, as one would expect for every other kind of macro. So, the `bind` keyword is needed to make your intent unambiguous:
`bind $hat` produces a "bound variable".
One can bind any kind of variable: story-wide variables like `$morality`, temp variables like `_glance`, and data values and positions
inside them, like `$diary's 1st's event`. Once bound, the macro's element will set data to it automatically, as if by a series of
unseen (set:)s or (move:)s.
Note that bound variables can't be (set:) into variables themselves, because there's no real point to doing so (and it could lead to
a lot of undue confusion).
| Operator | Purpose | Example
|---
| `bind` | Binds the named variable on the right. | `bind $weapon`, `bind _hat`
| `bind` | Binds the named variable on the right. | `bind $weapon`, `bind _hat`, `bind $profile's age`
*/
const VarBind = Object.freeze({
/*
......@@ -36,7 +47,12 @@ define(['utils', 'utils/operationutils', 'internaltypes/varref', 'internaltypes/
Produce a user-facing error if a non-varRef was given.
Since "bind" is just another operator, this can't be picked up in compilation until now.
*/
if (!VarRef.isPrototypeOf(varRef)) {
if (!VarRef.isPrototypeOf(varRef)
/*
There's another kind of varRef-like object though: wrapped TwineErrors that have been given get() and set()
methods. These don't have the VarRef prototype (because it's frozen and thus non-overridable) but
*/
&& !varRef.varref) {
return TwineError.create("operation", "I can only 'bind' a variable, not " + objectName(varRef) + ".");
}
......
......@@ -47,7 +47,7 @@ define(['state', 'internaltypes/twineerror', 'utils', 'utils/operationutils', 'd
Sequentials have special sugar string property indices:
length: this falls back to JS's length property for Arrays and Strings.
1st, 2nd etc.: indices.
1st, 2nd etc.: 1-based indices.
last: antonym of 1st.
2ndlast, 3rdlast: reverse indices.
any, all: produce special "determiner" objects used for comparison operations.
......@@ -85,10 +85,15 @@ define(['state', 'internaltypes/twineerror', 'utils', 'utils/operationutils', 'd
and let Operations.get() do the work of offsetting it after it
deals with the astral characters.
*/
prop = -match[1] + "";
prop = -match[1];
}
else if (typeof prop === "string" && (match = /(\d+)(?:st|[nr]d|th)/i.exec(prop))) {
prop = match[1] - 1 + "";
/*
It's actually important that prop remains a number and not a string:
a few operations (such as canSet()) use the type of the props to
check if the VarRef's property chain remains legal.
*/
prop = match[1] - 1;
}
else if (prop === "last") {
prop = -1;
......@@ -302,10 +307,14 @@ define(['state', 'internaltypes/twineerror', 'utils', 'utils/operationutils', 'd
Neither can colours.
*/
if (Colour.isPrototypeOf(obj)) {
return TwineError.create('operation', "I can't modify the components of " + objectName(obj));
return TwineError.create('operation', "I can't modify the components of " + objectName(obj) + ".");
}
if (obj instanceof Map) {
if(typeof prop !== "string") {
return TwineError.create('operation',
objectName(obj) + " can only have string data names, not " + objectName(prop) + ".");
}
return true;
}
/*
......
"use strict";
define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine', 'internaltypes/twineerror', 'datatypes/hookset', 'utils/operationutils'],
(requestAnimationFrame, Macros, {toJSLiteral, unescape}, State, Passages, Engine, TwineError, HookSet, {printBuiltinValue}) => {
define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine', 'internaltypes/twineerror', 'datatypes/hookset', 'datatypes/varbind', 'utils/operationutils'],
(requestAnimationFrame, Macros, {toJSLiteral, unescape}, State, Passages, Engine, TwineError, HookSet, VarBind, {printBuiltinValue}) => {
/*d:
Command data
......@@ -26,7 +26,7 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
Note that HookCommands only have similarities to anonymous hooks: they can't be referred to by (click:) or
(replace:) to change them after the fact (unless the (hook:) macro is used to give them a name).
*/
const {Any, rest, optional} = Macros.TypeSignature;
const {Any, rest, either, optional} = Macros.TypeSignature;
const {assign} = Object;
const hasStorage = !!localStorage
......@@ -275,18 +275,20 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
[])
/*d:
(cycling-link: Bind, ...String) -> Command
(cycling-link: [Bind], ...String) -> HookCommand
A command that, when evaluated, creates a cycling link - a link which does not go anywhere, but changes its own text
to the next in a looping sequence of strings, and sets the bound variable to match the string value of the text.
to the next in a looping sequence of strings, and sets the optional bound variable to match the string value of the text.
Example usage:
`(cycling-link: bind $head's hair, "Black", "Brown", "Blonde", "Red", "White")`
* `(cycling-link: bind $head's hair, "Black", "Brown", "Blonde", "Red", "White")` binds the "hair" value in the $head datamap to the
current link text.
* `(cycling-link: "Mew", "Miao", "Mrr", "Mlem")` has no bound variable.
Rationale:
The cycling link was an interaction idiom popularised in Twine 1 which combined the utility of a dial input element with
the discovery and visual consistency of a link: the player can only discover that this is a cycling link by clicking it,
and can then only discover the full set of labels by clicking through them. This affords a number of subtle dramatic and humourous
The cycling link is an interaction idiom popularised in Twine 1 which combined the utility of a dial input element with
the discovery and visual consistency of a link: the player can typically only discover that this is a cycling link by clicking it,
and can then discover the full set of labels by clicking through them. This affords a number of subtle dramatic and humourous
possibilities, and moreover allows the link to sit comfortably among passage prose without standing out as an interface element.
The addition of a variable bound to the link, changing to match whichever text the player finally dialed the link to, allows
......@@ -294,17 +296,111 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
other, even though no hooks and (set:)s can be attached to them.
Details:
TBW
If one of the strings is empty, such as `(cycling-link: "Two eggs", "One egg", "")`, then upon reaching the empty string, the link
will disappear permanently. If the *first* string is empty, an error will be produced (because then the link can never appear at all).
If attempting to render one string produces an error, such as `(cycling-link: "Goose", "(print: 2 + 'foo')")`, then the error will only appear
once the link cycles to that string.
The bound variable will be set to the first value as soon as the cycling link is displayed - so, even if the player doesn't
interact with the link at all, the variable will still have the intended value.
If you want to include the current value of the bound variable in the link's series of strings, simply refer to it directly after
the bind: `(cycling-link: bind $hat, $hat, "Beret", "Poker visor", "Tricorn")` will place $hat's current value in the series,
and, as the first value, it will harmlessly overwrite $hat with itself (and thus leave it unchanged). Additionally, if you have
an array of strings that you'd like to reuse for the same cycling link in multiple passages, say as
`(set: $allHats to (a: "Helmet", "Beret", "Poker visor", "Tricorn"))`, and you'd like each cycling link to start on
whatever matching value is currently in $hat, simply first provide $hat as the first value,
then follow it with $allHats with $hat removed: `(cycling-link: bind $hat, $hat, ...($allHats - (a:$hat)))`. (Note that this doesn't
preserve the order of $allHats relative to $hat, however.)
If only one string was given to this macro, an error will be produced.
#input
*/
("cycling-link",
() => {},
(cd, _, bind, ...labels) => {
// NEED evaluateTwineMarkup() here...
return assign(cd, { source: '<tw-link>' + labels[0] + '</tw-link>' });
(...labels) => {
if (labels[0] === "") {
return TwineError.create("macrocall", "The first string in a (cycling-link:) can't be empty.");
}
if (labels.length <= (VarBind.isPrototypeOf(labels[0]) ? 2 : 1)) {
return TwineError.create("macrocall",
"I need two or more strings to cycle through, not just '"
+ labels[labels.length - 1]
+ "'."
);
}
},
(cd, _, ...labels) => {
/*
Often, all the params are labels. But if the first one is actually the optional VarBind,
we need to extract it from the labels array.
*/
let bind;
if (VarBind.isPrototypeOf(labels[0])) {
bind = labels.shift();
}
let index = 0;
/*
An operation that should be performed when this is initially printed, and
every time it is clicked: set the bound variable to match the current label.
*/
const setBoundVariable = (labelValue) => {
const result = bind.varRef.set(labelValue);
let error;
if ((error = TwineError.containsError(result))) {
return error;
}
};
cd.data.clickEvent = (link) => {
/*
Rotate to the next label, cycling around if it's past the end.
*/
index = (index + 1) % labels.length;
let source = (labels[index] === "" ? "" : '<tw-link>' + labels[index] + '</tw-link>');
/*
If there's a bound variable, set it to the value of the next string.
(If it returns an error, let that replace the source.)
*/
if (bind) {
const result = setBoundVariable(labels[index]);
if (TwineError.containsError(result)) {
/*
As this clickEvent occurs when the interface element has already been rendered,
we need to explicitly replace the link with the rendered error rather than return
the error (to nobody).
*/
link.replaceWith(result.render(labels[index]));
return;
}
}
/*
Display the next label, reusing the ChangeDescriptor (and thus the transitions, style changes, etc)
that the original run received.
*/
const cd2 = assign({}, cd, { source, });
/*
Since cd2's target SHOULD equal link, passing anything as the second argument won't do anything useful
(much like how the first argument is overwritten by cd2's source). So, null is given.
*/
cd.section.renderInto("", null, cd2);
};
/*
As above, the bound variable, if present, is set to the first label. Errors resulting
from this operation can be returned immediately.
*/
let source = '<tw-link>' + labels[0] + '</tw-link>';
if (bind) {
const result = setBoundVariable(labels[index]);
if (TwineError.containsError(result)) {
return result;
}
}
assign(cd, { source, append: "replace", });
return cd;
},
[String]);
[either(VarBind, String), rest(String)]);
Macros.addCommand
/*d:
......
......@@ -44,6 +44,14 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
event = link.closest('tw-expression, tw-hook').data('clickEvent');
if (event) {
/*
If a link's body contains a <tw-error>, then don't
allow it to be clicked anymore, so that (for instance), the error message
can be expanded to see the line of additional advice.
*/
if (link.find('tw-error').length > 0) {
return;
}
event(link);
return;
}
......@@ -198,8 +206,8 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
if (arr[0] === "link-reveal") {
link.contents().unwrap();
}
desc.source = desc.innerSource;
desc.section.renderInto(desc.innerSource + "", null, desc);
desc.source = desc.innerSource + "";
desc.section.renderInto("", null, desc);
};
},
[String]
......@@ -453,12 +461,13 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
was displayed by the innerSource.
*/
link.contents().unwrap();
const normalExit = desc.section.renderInto(desc.innerSource + "", null, desc);
desc.section.renderInto(desc.innerSource + "", null, desc);
/*
Having revealed, we now go-to, UNLESS an early exit was invoked, which signifies a different (go-to:) was
activated.
Much as in doExpressions() in section.renderInto(), we can check for an early exit via the DOM.
*/
if (!normalExit) {
if (!desc.target.find('[earlyexit]').length) {
Engine.goToPassage(passageName, { transitionOut: desc.data.t8nDepart, transitionIn: desc.data.t8nArrive });
}
};
......
......@@ -318,9 +318,17 @@ define([
from running the command (such as running (undo:) on the first turn).
*/
if (TwineError.containsError(result)) {
expr.replaceWith(result.render(expr.attr('title'), expr));
expr.replaceWith(result.render(expr.attr('title')));
}
if (ChangeDescriptor.isPrototypeOf(result)) {
/*
We need to update the ChangeDescriptor to have these fields, so
that certain interaction macros that want to reuse it (such as (cycling-link:))
can pass it to renderInto().
*/
result.section = this;
result.target = nextElem;
this.renderInto('', nextElem, result);
/*
If TwineScript_Run returns a ChangeDescriptor with earlyExit,
......@@ -331,14 +339,6 @@ define([
return "earlyexit";
}
}
/*
This should be refactored out soon (May 2018)...
The only commands whose Run() would produce a string are
links, and all those should be HookCommands.
*/
if (typeof result === "string") {
this.renderInto(result, nextElem);
}
}
/*
Print the expression if it's a string, number, data structure,
......@@ -376,7 +376,7 @@ define([
if (result instanceof Error) {
result = TwineError.fromError(result);
}
expr.replaceWith(result.render(expr.attr('title'), expr));
expr.replaceWith(result.render(expr.attr('title')));
}
else {
/*
......@@ -900,7 +900,7 @@ define([
dom.attr('earlyexit', true);
return false;
}
return result;
return;
}
}
}
......
......@@ -131,7 +131,7 @@ define(['utils'], ({toJSLiteral, impossible}) => {
[
["error"],
["comma"],
["spread"],
["spread", "bind"],
["to"],
["into"],
{rightAssociative: ["where", "when", "via"]},
......
......@@ -7,6 +7,7 @@ define([
'datatypes/hookset',
'datatypes/lambda',
'datatypes/datatype',
'datatypes/varbind',
'internaltypes/varref',
'internaltypes/twineerror',
'twinescript/operations'
......@@ -14,7 +15,7 @@ define([
/*
To keep the eval scope very clean in compiled code, no destructuring is done here.
*/
(Macros, State, Utils, Colour, HookSet, Lambda, Datatype, VarRef, TwineError, OperationsProto) => {
(Macros, State, Utils, Colour, HookSet, Lambda, Datatype, VarBind, VarRef, TwineError, OperationsProto) => {
/*
Creates a new script execution environment. This accepts and
decorates a Section object (see Engine.showPassage) with the
......
describe("interface macros", function(){
'use strict';
describe("the (cycling-link:) macro", function(){
it("accepts one optional bound variable, and two or more strings", function() {
expect("(print:(cycling-link:))").markupToError();
expect("(print:(cycling-link:''))").markupToError();
expect("(print:(cycling-link:'baz'))").markupToError();
expect("(print:(cycling-link:2))").markupToError();
expect("(print:(cycling-link:false))").markupToError();
expect("(print:(cycling-link:bind $foo))").markupToError();
expect("(print:(cycling-link:'baz', 'baz'))").not.markupToError();
expect("(print:(cycling-link:'baz', 'baz', 'qux'))").not.markupToError();
expect("(print:(cycling-link:bind $foo, 'baz'))").markupToError();
expect("(print:(cycling-link:bind $foo, 'baz', 'qux'))").not.markupToError();
});
it("when clicked, it cycles to the next string and remains clickable", function() {
var p = runPassage("(cycling-link:'bar','baz','qux')");
expect(p.find('tw-link').text()).toBe('bar');
p.find('tw-link').click();
expect(p.find('tw-link').text()).toBe('baz');
p.find('tw-link').click();
expect(p.find('tw-link').text()).toBe('qux');
p.find('tw-link').click();
expect(p.find('tw-link').text()).toBe('bar');
p.find('tw-link').click();
expect(p.find('tw-link').text()).toBe('baz');
p.find('tw-link').click();
expect(p.find('tw-link').text()).toBe('qux');
});
it("works with (transition:)", function() {
runPassage("foo","foo");
var p = runPassage("(t8n:'pulse')(cycling-link:'bar','baz','qux')");
expect(p.find('tw-link').text()).toBe('bar');
p.find('tw-link').click();
expect(p.find('tw-transition-container[data-t8n="pulse"]').length).toBe(1);
});
it("executes each label every time it appears", function() {
var p = runPassage("(set:$foo to 'bar')(cycling-link:'(print:$foo)','(set:$foo to \"baz\")qux')");
expect(p.find('tw-link').text()).toBe('bar');
p.find('tw-link').click();
expect(p.find('tw-link').text()).toBe('qux');
p.find('tw-link').click();
expect(p.find('tw-link').text()).toBe('baz');
});
it("can't begin with an empty string", function() {
expect("(cycling-link:'','baz')").markupToError();
});
it("upon reaching an empty string, the link disappears", function() {
var p = runPassage("(cycling-link:'bar','baz','')");
expect(p.find('tw-link').text()).toBe('bar');
p.find('tw-link').click();
expect(p.find('tw-link').text()).toBe('baz');
p.find('tw-link').click();
expect(p.find('tw-link').length).toBe(0);
});
it("will be replaced with markup error messages if they're encountered", function() {
var p = runPassage("(cycling-link:'bar','(print: 2 + true)')");
expect(p.find('tw-link').text()).toBe('bar');
p.find('tw-link').click();
expect(p.find('tw-error').length).toBe(1);
});
describe("when given a bound variable", function() {
it("when clicked, sets the variable to the string label", function() {
var p = runPassage("(cycling-link: bind $foo, 'bar','baz', 'qux')");
expect("$foo").markupToPrint('bar');
p = runPassage("(cycling-link: bind $foo, 'bar','baz', 'qux')");
p.find('tw-link').click();
expect("$foo").markupToPrint('baz');
p = runPassage("(cycling-link: bind $foo, 'bar','baz', 'qux')");
p.find('tw-link').click();
p.find('tw-link').click();
expect("$foo").markupToPrint('qux');
});
it("errors if the bind is invalid", function() {
expect("(set:$foo to 1)(cycling-link: bind $foo's 1st, 'bar','baz', 'qux')").markupToError();
});
it("errors if the bind is no longer valid when it cycles", function() {
var p = runPassage("(set:$foo to (a:))(cycling-link: bind $foo's 1st, 'bar','baz','qux')(set:$foo to 2)");
p.find('tw-link').click();
expect(p.find('tw-error').length).toBe(1);
p = runPassage("(set:$foo to (dm:'garply',2))(cycling-link: bind $foo's 'garply', 'bar','baz','qux')(set:$foo to (a:))");
p.find('tw-link').click();
expect(p.find('tw-error').length).toBe(1);
// The error message for this one, while existent, is extremely poor.
p = runPassage("(set:$foo to (a:))(cycling-link: bind $foo's 1st, 'bar','baz','qux')(set:$foo to (dm:'garply',2))");
p.find('tw-link').click();
expect(p.find('tw-error').length).toBe(1);
p = runPassage("(set:$foo to (a:(a:)))(cycling-link: bind $foo's 1st's 1st, 'bar','baz','qux')(set:$foo's 1st to (dm:'garply',2))");
p.find('tw-link').click();
expect(p.find('tw-error').length).toBe(1);
});
});
});
});
......@@ -37,6 +37,12 @@ describe("link macros", function() {
p.find('tw-link').click();
expect(p.text()).toBe("Hello");
});
it("can't be clicked if its text contains an error", function() {
var p = runPassage("(link-replace:'(print:2+true)')[B]");
expect(p.find('tw-link tw-error').length).toBe(1);
p.find('tw-link').click();
expect(p.text()).not.toBe("B");
});
});
describe("(link-reveal:)", function() {
it("accepts exactly 1 non-empty string", function() {
......@@ -132,6 +138,12 @@ describe("link macros", function() {
expect(link.tag()).toBe("tw-broken-link");
expect(link.html()).toBe("mire");
});
it("still becomes a <tw-broken-link> if its text contains an error", function() {
var link = runPassage("(" + name + ":'(print:2+true)','mire')"+hook).find('tw-broken-link');
expect(link.parent().is(hook ? 'tw-hook' : 'tw-expression')).toBe(true);
expect(link.tag()).toBe("tw-broken-link");
});
it("renders markup in the link text, and ignores it for discerning the passage name", function() {
createPassage("","mire");
var p = runPassage("("+name+":'//glower//','//mire//')"+hook);
......@@ -143,7 +155,8 @@ describe("link macros", function() {
if (hook) {
it("runs the hook when clicked, before going to the passage", function() {
createPassage("<p>$foo</p>","mire");
var link = runPassage("("+name+":'mire')[(set:$foo to 'garply')]").find('tw-link');
var link = runPassage("(set:$foo to 'grault')("+name+":'mire')[(set:$foo to 'garply')]").find('tw-link');
expect(link.length).toBe(1);
link.click();
expect($('tw-passage p').text()).toBe("garply");
});
......@@ -186,6 +199,13 @@ describe("link macros", function() {
link.trigger($.Event('keydown', { which: 13 }));
expect($('tw-passage p').text()).toBe("garply");
});
it("can't be clicked if its text contains an error", function() {
createPassage("<p>garply</p>","mire");
var p = runPassage("(" + name + ":'(print:2+true)','mire')"+hook);
expect(p.find('tw-link tw-error').length).toBe(1);
p.find('tw-link').click();
expect(p.text()).not.toBe("garply");
});
});
});
describe("(link-undo:)", function() {
......
......@@ -225,7 +225,7 @@ describe("style changer macros", function() {
it("changes the passage transitions of (" + name2 + ":)", function(done) {
runPassage("foo","grault");
var p = runPassage("foo("+name+":'pulse')("+name2+":"+arg+")");
p.find('tw-link, .enchantment-link')[interaction]();
p.find('tw-link, .enchantment-link, .enchantment-mouseover')[interaction]();
setTimeout(function() {
expect($('tw-story tw-transition-container[data-t8n="pulse"]').length).toBe(1);
done();
......@@ -276,7 +276,7 @@ describe("style changer macros", function() {
it("on (" + name2 + ":)", function(done) {
runPassage("foo","grault");
var p = runPassage("foo(t8n-depart:'dissolve')+(t8n-arrive:'pulse')("+name2+":"+arg+")");
p.find('tw-link, .enchantment-link')[interaction]();
p.find('tw-link, .enchantment-link, .enchantment-mouseover')[interaction]();
setTimeout(function() {
expect($('tw-story tw-transition-container.transition-out[data-t8n="dissolve"]').length).toBe(1);
expect($('tw-story tw-transition-container.transition-in[data-t8n="pulse"]').length).toBe(1);
......
......@@ -330,6 +330,7 @@ afterAll(function() {
<script src="spec/changer_macros/hooks.js"></script>
<script src="spec/changer_macros/enchantment.js"></script>
<script src="spec/changer_macros/interaction.js"></script>
<script src="spec/changer_macros/interface.js"></script>
<script src="spec/changer_macros/revision.js"></script>
<script src="spec/command_macros/basics.js"></script>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment