Sugarcube with Underscore templating patched in.

I like the look and feel of Sugarcube, but I prefer the scripting environment of Snowman.

So I patched underscore templating into Sugarcube, just to see if it would work. Ended up being pretty easy, just one line of code changed, lol.

Now I am a happy internet-person. If this sounds interesting to you, too, I put the patched story format up at echohollow.net/sugarcube_underscore/ , along with instructions on usage and how to roll your own.

Currently only have State API exported, but it seems to be working. Will probably work on making it more feature complete as I need to.

FWIW, I have exported the rest of the Sugarcube APIs to the template processor, and added a config option and tag to compress multiple paragraph breaks into a single one (to remove blank lines resulting from code sections off by themselves and such). Same link as above, minimal documentation in README.php.

This is probably all I’ll do with it for a while, as it seems to be adequate for my purposes and it’s time to work on my game instead. But please let me know if you decide to use it for something or find any bugs or have any comments.

Well, I think I figured out how to do this without patching the story format, so I will probably stop working on this patch and delete it from my repos.

This other way replaces the processText method on the passage before it is passed off to the wikifier. This seems like a really messy way to do it, but if there is some other way then I am too dumb to see it. :stuck_out_tongue:

Even if it is a little messy, I think it’s still less messy than trying to maintain a patch against upstream sugarcube and do releases.

In the story javascript:

[code]//The underscore library will probably have to be pasted verbatim into
//the story javascript for release, but this seems to work for testing
//and development.
$.getScript(‘file:///D:/path/to/underscore.js’);

$(document).on(
‘:passagestart’,
function(ev) {
// Don’t try to process if underscore hasn’t been loaded yet.
if (_.template) {
ev.passage.processText = function() {
// Evaluate <% %> blocks.
let processed = _.template(this.text)({
Dialog : Dialog,
Engine : Engine,
LoadScreen : LoadScreen,
Macro : Macro,
Save : Save,
Setting : Setting,
State : State,
Story : Story,
UI : UI,
UIBar : UIBar
});
// Strip leading and trailing newlines, and compress
// multiple paragraph breaks into a single paragraph break.
processed = processed.replace(/\n\n+/g, ‘\n\n’).replace(
/^\n+|\n+$/g, ‘’);

				// Everything past this was copied verbatim from the
				// original processText method.
				
				// Handle image passage transclusion.
				if (this.tags.includes('Twine.image')) {
					processed = `[img[${processed}]]`;
				}
				return processed;
			};
		}
	});[/code]

You have to start the game on a “fake” passage, that does nothing but <> for a very short time, to give the underscore library time to load, and then <> the real start passage. A delay of even as little as 1ms seems to work (at least when using a local file with getScript; I haven’t tried loading over http), but I’ve been using 100ms just in case. It’s still pretty much instantaneous.

I don’t buy the pitch—I think you’re overselling things a bit—but it doesn’t make me any difference what you use, so get your template on I guess.

I do, however, have some suggestions on how to monkey patch Underscore.js templates into the render pipeline. Both methods require some configuration—they’re also untested.

[size=125]Adding Underscore.js, via an external file (local or online), and patching in its template system: (goes in Story JavaScript or the equivalent)[/size]

[spoiler][code]
/*
This code is asynchronous, thus requires some special finagling.
Underscore.js templates may be used anywhere except the StoryInit
special passage.
*/
(function () {
// Set the URL to Underscore.js here. Local or remote files are acceptable.
var libUrl = ‘PASTE UNDERSCORE.JS URL HERE’;

// Grab a lock on the loading screen.
var lockId = LoadScreen.lock();

// Set up a utility function to release our lock on the loading
// screen and reshow the the starting passage with Underscore.js
// templates enabled.
var unlockAndShow = function () {
	LoadScreen.unlock(lockId);
	Engine.show();
};

// Import Underscore.js and patch 
importScripts(libUrl).then(function () {
	// Monkey patch in our customized `<Passage>.processText()`.
	Passage.prototype.processText = function () {
		let processed = this.text;

		// Handle image passage transclusion.
		if (this.tags.includes('Twine.image')) {
			processed = '[img[' + processed + ']]';
		}

		// Normal passage handling.
		else {
			// Process <% %> blocks, remove all leading & trailing
			// newlines, and compact all internal sequences of
			// newlines into two newlines.
			processed = _.template(processed)({
				Dialog     : Dialog,
				Engine     : Engine,
				LoadScreen : LoadScreen,
				Macro      : Macro,
				Save       : Save,
				Setting    : Setting,
				State      : State,
				Story      : Story,
				UI         : UI,
				UIBar      : UIBar
			})
				.replace(/^\n+|\n+$/g, '')
				.replace(/\n\n+/g, '\n\n');

			// Handle `Config.passages.nobr` and the `nobr` tag.
			if (Config.passages.nobr || this.tags.includes('nobr')) {
				// Compact all internal sequences of newlines into single spaces.
				processed = processed.replace(/\n+/g, ' ');
			}
		}

		return processed;
	};

	// The starting passage has already been played.
	if (Engine.lastPlay !== null) {
		unlockAndShow();
	}

	// The starting passage is currently rendering.
	else if (Engine.isRendering()) {
		postdisplay['#monkey-patch-underscore.js'] = function (taskName) {
			delete postdisplay[taskName]; // single-use task
			unlockAndShow();
		};
	}

	// The starting passage's play state is unknown.
	else {
		setTimeout(unlockAndShow, 250);
	}
}, function () {
	// Release our lock on the loading screen.
	LoadScreen.unlock(lockId);

	// The library failed to load, so we complain, though there's
	// likely already a error waiting for the player.
	throw new Error('Import of Underscore.js failed (url: "' + libUrl + '").');
});

})();
[/code][/spoiler]
[size=125]Adding Underscore.js, directly via a wrapper, and patching in its template system: (goes in Story JavaScript or the equivalent)[/size]

[spoiler][code]
/*
This code is synchronous, thus doesn’t require any special finagling.
Underscore.js templates may be used anywhere.
/
(function (define, exports) {
/
PASTE UNDERSCORE.JS LIBRARY HERE */
}).call(window);
// Monkey patch in our customized <Passage>.processText().
Passage.prototype.processText = function () {
let processed = this.text;

// Handle image passage transclusion.
if (this.tags.includes('Twine.image')) {
	processed = '[img[' + processed + ']]';
}

// Normal passage handling.
else {
	// Process <% %> blocks, remove all leading & trailing
	// newlines, and compact all internal sequences of
	// newlines into two newlines.
	processed = _.template(processed)({
		Dialog     : Dialog,
		Engine     : Engine,
		LoadScreen : LoadScreen,
		Macro      : Macro,
		Save       : Save,
		Setting    : Setting,
		State      : State,
		Story      : Story,
		UI         : UI,
		UIBar      : UIBar
	})
		.replace(/^\n+|\n+$/g, '')
		.replace(/\n\n+/g, '\n\n');

	// Handle `Config.passages.nobr` and the `nobr` tag.
	if (Config.passages.nobr || this.tags.includes('nobr')) {
		// Compact all internal sequences of newlines into single spaces.
		processed = processed.replace(/\n+/g, ' ');
	}
}

return processed;

};
[/code][/spoiler]

Excellent. Thank you very much.