Commit 25950d99 authored by Leon Arnott's avatar Leon Arnott
Browse files

Implemented (link-reveal-goto:), which functions as a combination of (link-reveal:) and (goto:).

I'd previously been unwilling to implement the old Twine 1 [[Link|Passage][x = 1]] link syntax for a few reasons, most notably that using = for assignment, while technically supported in Harlowe, is nonetheless heavily frowned upon, and there's no comfortably way to incorporate "to" or "into" in it without "set" or "put". Additionally, I felt adding another feature to the link syntax which didn't naturally arise from other syntactic structures was adding unpleasant complexity. I'd toyed with adding this as the macro (link-set-goto:), but I was uncomfortably with the syntax of a lone VariableToValue "$a to 2" in the middle of unrelated params, and additionally disliked how this looked like an "emergent concatenation" of (link:), (set:) and (goto:) by themselves, comparable to (mouseover-replace:) and its ilk, and had no model for explaining how this would make sense.

What changed? I decided that it does make sense for (link-reveal:) to be concatenated with (goto:) to make (link-reveal-goto:), because the (set:) could be included in the attached hook, rather than crudely incorporated into the call itself. Moreover, by using a hook, any other arbitrary instant-effect macro could be used with it, like (alert:), or a hypothetical audio macro. Since this is only the concatenation of two macros - (link-reveal:) and (goto:) - rather than three, it's in keeping with (mouseover-replace:)'s example.

I intend to add link syntax sugar for this, but am currently debating its exact form. Certainly it won't just resemble the Twine 1 form.
parent 4a431f886235
......@@ -132,6 +132,13 @@ define(['jquery', 'utils'], ($, {impossible, escape}) => {
});
errorElement.append(explanationButton).append(explanationElement);
/*
Storing this TwineError object on the element is currently only required for
macros that evaluate TwineMarkup strings passed into them to examine their text content,
such as (link-reveal-goto:).
*/
errorElement.data('TwineError', this);
return errorElement;
},
......
......@@ -513,7 +513,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
See also:
(link:), (link-reveal:), (link-repeat:), (mouseover:), (mouseout:), (replace:), (click-replace:)
#links 5
#links 7
*/
{
name: "click",
......@@ -550,7 +550,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
See also:
(link:), (link-reveal:), (link-repeat:), (click:), (mouseout:), (replace:), (mouseover-replace:), (hover-style:)
#links 9
#links 11
*/
{
name: "mouseover",
......@@ -588,7 +588,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
See also:
(link:), (link-reveal:), (link-repeat:), (click:), (mouseover:), (replace:), (mouseout-replace:), (hover-style:)
#links 13
#links 15
*/
{
name: "mouseout",
......@@ -623,7 +623,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
See also:
(click-prepend:), (click-append:)
#links 6
#links 8
*/
/*d:
(click-append: HookName or String) -> Changer
......@@ -641,7 +641,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
See also:
(click-replace:), (click-prepend:)
#links 7
#links 9
*/
/*d:
(click-prepend: HookName or String) -> Changer
......@@ -659,7 +659,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
See also:
(click-replace:), (click-append:)
#links 8
#links 10
*/
/*d:
(mouseover-replace: HookName or String) -> Changer
......@@ -667,7 +667,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
This is similar to (click-replace:), but uses the (mouseover:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-replace:).
#links 10
#links 12
*/
/*d:
(mouseover-append: HookName or String) -> Changer
......@@ -675,7 +675,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
This is similar to (click-append:), but uses the (mouseover:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-append:).
#links 11
#links 13
*/
/*d:
(mouseover-prepend: HookName or String) -> Changer
......@@ -683,7 +683,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
This is similar to (click-prepend:), but uses the (mouseover:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-prepend:).
#links 12
#links 14
*/
/*d:
(mouseout-replace: HookName or String) -> Changer
......@@ -691,7 +691,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
This is similar to (click-replace:), but uses the (mouseout:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-replace:).
#links 14
#links 16
*/
/*d:
(mouseout-append: HookName or String) -> Changer
......@@ -699,7 +699,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
This is similar to (click-append:), but uses the (mouseout:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-append:).
#links 15
#links 17
*/
/*d:
(mouseout-prepend: HookName or String) -> Changer
......@@ -707,7 +707,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'macros',
This is similar to (click-prepend:), but uses the (mouseout:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-prepend:).
#links 16
#links 18
*/
revisionTypes.forEach((revisionType) => {
interactionTypes.forEach((interactionType) => {
......
......@@ -63,7 +63,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
(link: String) -> Changer
Also known as: (link-replace:)
Makes a command to create a special link that can be used to show a hook.
Makes a changer to create a special link that can be used to show a hook.
Example usage:
`(link: "Stake")[The dracula crumbles to dust.]` will create a link reading "Stake"
......@@ -91,7 +91,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
/*d:
(link-reveal: String) -> Changer
Makes a command to create a special link that shows a hook, keeping the link's
Makes a changer to create a special link that shows a hook, keeping the link's
text visible after clicking.
Example usage:
......@@ -120,7 +120,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
/*d:
(link-repeat: String) -> Changer
Makes a command to create a special link that shows a hook, and, when clicked again,
Makes a changer to create a special link that shows a hook, and, when clicked again,
re-runs the hook, appending its contents again.
Example usage:
......@@ -168,8 +168,6 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
*/
desc.append = arr[0] === "link" ? "replace" : "append";
desc.data.clickEvent = (link) => {
desc.source = desc.innerSource;
desc.section.renderInto(desc.innerSource + "", null, desc);
/*
Only (link-reveal:) turns the link into plain text:
the others either remove it (via the above) or leave it be.
......@@ -177,6 +175,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);
};
},
[String]
......@@ -232,8 +232,8 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
}
return {
TwineScript_TypeName: "a (link-goto: "
+ Utils.toJSLiteral(text) + ", "
+ Utils.toJSLiteral(passage) + ") command",
+ Utils.toJSLiteral(text)
+ (passage ? ", " + Utils.toJSLiteral(passage) : "") + ") command",
TwineScript_ObjectName: "a (link-goto:) command",
TwineScript_Print() {
......@@ -270,8 +270,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
TODO: Maybe this should be an error as well??
*/
return '<tw-broken-link passage-name="' + Utils.escape(passageName) + '">'
+ (text || passage)
+ '</tw-broken-link>';
+ text + '</tw-broken-link>';
}
/*
Previously visited passages may be styled differently compared
......@@ -286,7 +285,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
return '<tw-link tabindex=0 ' + (visited > 0 ? 'class="visited" ' : '')
// Always remember to Utils.escape() any strings that must become raw HTML attributes.
+ 'passage-name="' + Utils.escape(passageName)
+ '">' + (text || passage) + '</tw-link>';
+ '">' + text + '</tw-link>';
}
};
},
......@@ -301,7 +300,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
This command should not be attached to a hook.
Example usage:
`(link-undo:"Retreat")` behaves the same as `(link:"Retreat")[(undo:)]`.
`(link-undo:"Retreat")` behaves the same as `(link:"Retreat")[(undo: )]`.
Rationale:
The ability to undo the player's last turn, as an alternative to (go-to:), is explained in the documentation
......@@ -319,7 +318,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
See also:
(undo:), (link-goto:)
#links 5
#links 6
*/
("link-undo", (section, text) => {
if (!text) {
......@@ -345,4 +344,94 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
};
},
[String]);
/*d:
(link-reveal-goto: String, [String]) -> Changer
TBW
Example usage:
* `(link-reveal-goto: "Study English", "Afternoon 1")[(set:$eng to it + 1)]` will create a link reading "Study English"
which, when clicked, adds 1 to the $eng variable using (set:), and then goes to the passage "Afternoon 1".
* `(link-reveal-goto: "Fight the King of England", "Death")[(alert:"You asked for it!")]` will create a link reading
"Fight the King of England" which, when clicked, displays an alert using (alert:), and then goes to the passage "Death".
Rationale:
TBW
Details:
TBW
See also:
(link-reveal:), (link:), (link-goto:), (click:)
#links 5
*/
Macros.addChanger(["link-reveal-goto"],
(section, text, passage) => {
if (!text) {
return TwineError.create("macrocall", emptyLinkTextError);
}
/*
Being a variant of (link-goto:), this uses the same rules for passage name computation
from TwineMarkup, as described in that macro.
*/
const passageName = section.evaluateTwineMarkup(Utils.unescape(passage || text));
/*
If TwineMarkup rendering produced an error, return it.
<tw-error> elements have the original TwineError object stashed on them, which we
can retrieve now.
*/
if (passageName instanceof $) {
return passageName.data('TwineError');
}
/*
We could check for the passage's existence here, but because this is a desugaring of
the link syntax, like (link-goto:), we should create a broken link instead, when
the changer is attached.
*/
return ChangerCommand.create("link-reveal-goto", [text || passage, passageName]);
},
(desc, text, passageName) => {
/*
As explained above, we create the broken link now, and dispose of
whatever the contained hook had.
*/
if (!Passages.has(passageName)) {
desc.source = '<tw-broken-link passage-name="' + Utils.escape(passageName) + '">'
+ text + '</tw-broken-link>';
return;
}
/*
All of the following assigned properties are those assigned by (link-reveal:).
*/
if (!desc.innerSource) {
desc.innerSource = desc.source;
}
/*
Previously visited passages may be styled differently compared
to unvisited passages.
*/
const visited = (State.passageNameVisited(passageName));
desc.source = '<tw-link tabindex=0 ' + (visited > 0 ? 'class="visited" ' : '') + '>' + text + '</tw-link>';
desc.append = "append";
desc.data.clickEvent = (link) => {
desc.source = desc.innerSource;
/*
It may seem pointless to unwrap the link, now that we're going to
somewhere else, but this change could be observed if a modal (alert:)
was displayed by the innerSource.
*/
link.contents().unwrap();
desc.section.renderInto(desc.innerSource + "", null, desc);
/*
Having revealed, we now go-to.
*/
Engine.goToPassage(passageName);
};
},
[String, optional(String)]
)
});
......@@ -98,60 +98,75 @@ describe("link macros", function() {
});
});
/*
Though these are not changers, they are similar enough to the above in terms of API.
Though (link-goto:) and (link-undo:) are not changers, they are similar enough to the above in terms of API.
*/
describe("(link-goto:)", function() {
it("renders to a <tw-link> element if the linked passage exists", function() {
createPassage("","mire");
var link = runPassage("(link-goto:'mire')").find('tw-link');
expect(link.parent().is('tw-expression')).toBe(true);
expect(link.tag()).toBe("tw-link");
expect(link.attr("passage-name")).toBe("mire");
});
it("becomes a <tw-broken-link> if the linked passage is absent", function() {
var link = runPassage("(link-goto: 'mire')").find('tw-broken-link');
expect(link.parent().is('tw-expression')).toBe(true);
expect(link.tag()).toBe("tw-broken-link");
expect(link.html()).toBe("mire");
});
it("accepts 1 or 2 non-empty strings", function() {
expect("(link-goto:)").markupToError();
expect("(link-goto:'')").markupToError();
expect("(link-goto:2)").markupToError();
expect("(link-goto:true)").markupToError();
['link-goto', 'link-reveal-goto'].forEach(function(name) {
var hook = name === "link-reveal-goto" ? "[]" : "";
expect("(link-goto:'s')").not.markupToError();
expect("(link-goto:'s','s')").not.markupToError();
expect("(link-goto:'s','s','s')").markupToError();
});
it("renders markup in the link text, and ignores it for discerning the passage name", function() {
createPassage("","mire");
var p = runPassage("(link-goto:'//glower//','//mire//')");
expect(p.find('i').text()).toBe("glower");
expect(p.find('tw-link').attr("passage-name")).toBe("mire");
describe("("+name+":)", function() {
it("accepts 1 or 2 non-empty strings" + (hook && ", and attaches to a hook"), function() {
expect("("+name+":)"+hook).markupToError();
expect("("+name+":'')"+hook).markupToError();
expect("("+name+":2)"+hook).markupToError();
expect("("+name+":true)"+hook).markupToError();
p = runPassage("(link-goto:'//mire//')");
expect(p.find('i').text()).toBe("mire");
expect(p.find('tw-link').attr("passage-name")).toBe("mire");
});
it("goes to the passage when clicked", function() {
createPassage("<p>garply</p>","mire");
var link = runPassage("(link-goto:'mire')").find('tw-link');
link.click();
expect($('tw-passage p').text()).toBe("garply");
});
it("can be focused", function() {
createPassage("","mire");
var link = runPassage("(link-goto:'mire')").find('tw-link');
expect(link.attr("tabindex")).toBe("0");
});
it("behaves as if clicked when the enter key is pressed while it is focused", function() {
createPassage("<p>garply</p>","mire");
var link = runPassage("(link-goto:'mire')").find('tw-link');
link.trigger($.Event('keydown', { which: 13 }));
expect($('tw-passage p').text()).toBe("garply");
expect("("+name+":'s')"+hook).not.markupToError();
expect("("+name+":'s','s')"+hook).not.markupToError();
expect("("+name+":'s','s','s')"+hook).markupToError();
if (hook) {
expect("("+name+":'s','s')").markupToError();
}
});
it("renders to a <tw-link> element if the linked passage exists", function() {
createPassage("","mire");
var link = runPassage("("+name+":'mire')"+hook).find('tw-link');
expect(link.parent().is(hook ? 'tw-hook' : 'tw-expression')).toBe(true);
expect(link.tag()).toBe("tw-link");
});
it("becomes a <tw-broken-link> if the linked passage is absent", function() {
var link = runPassage("("+name+": 'mire')"+hook).find('tw-broken-link');
expect(link.parent().is(hook ? 'tw-hook' : 'tw-expression')).toBe(true);
expect(link.tag()).toBe("tw-broken-link");
expect(link.html()).toBe("mire");
});
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);
expect(p.find('i').text()).toBe("glower");
p = runPassage("("+name+":'//mire//')"+hook);
expect(p.find('i').text()).toBe("mire");
});
if (hook) {
it("runs the hook when clicked, before going to the passage", function() {
createPassage("<p>$foo</p>","mire");
var link = runPassage("(link-reveal-goto:'mire')[(set:$foo to 'garply')]").find('tw-link');
link.click();
expect($('tw-passage p').text()).toBe("garply");
});
}
else {
it("goes to the passage when clicked", function() {
createPassage("<p>garply</p>","mire");
var link = runPassage("(link-goto:'mire')").find('tw-link');
link.click();
expect($('tw-passage p').text()).toBe("garply");
});
}
it("can be focused", function() {
createPassage("","mire");
var link = runPassage("("+name+":'mire')"+hook).find('tw-link');
expect(link.attr("tabindex")).toBe("0");
});
it("behaves as if clicked when the enter key is pressed while it is focused", function() {
createPassage("<p>garply</p>","mire");
var link = runPassage("("+name+":'mire')"+hook).find('tw-link');
link.trigger($.Event('keydown', { which: 13 }));
expect($('tw-passage p').text()).toBe("garply");
});
});
});
describe("(link-undo:)", function() {
......
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