Skip to content

Commit

Permalink
v0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
ChapelR committed Jan 25, 2020
1 parent bf7d59d commit ba1ae8a
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 30 deletions.
117 changes: 106 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,30 @@ Including the `changer` argument causes a changer macro to be created. Omitting
#### Arguments

- `name` ( *`string`* ) The name to give the macro. For example, the name `"blue"` would create a macro called like this: `(blue:)`. Valid macro names should generally consist of only lowercase Latin letters.
- `handler` ( *`function`* ) The handler function. For changer macros it is run when the macro is executed, before rendering, and can be used for tasks that need to happen immediately. Not every changer macro will require a handler, pass an empty function `funciton () {}` is you don't need it. The arguments passed to the macro are passed to the function as arguments. There is also a *macro content* similar (superficially) to SugarCube's macro API that you may access (see below).
- `handler` ( *`function`* ) The handler function. For changer macros it is run when the macro is executed, before rendering, and can be used for tasks that need to happen immediately. Not every changer macro will require a handler, pass an empty function `funciton () {}` if you don't need it. The arguments passed to the macro are passed to the function as arguments. There is also a *macro content* similar (superficially) to SugarCube's macro API that you may access (see below).
- `changer` ( *`function`* ) ( optional ) The changer function, which is run during the rendering process. Including this argument creates a changer macro, while omitting it creates a command macro. You can access the hook content (called a *descriptor*) from the macro context. Like handlers, macro arguments are passed through.

#### Context
#### Context Properties

You can use the JavaScript `this` keyword to access the *macro context* from within handler and changer functions. The following properties are available in the macro context:
You can use the JavaScript `this` keyword to access the *`MacroContext`* instance from within handler and changer functions. The following properties are available in the macro context:

- `this.name` ( *`string`* ) The name of the macro.
- `this.args` ( *`array`* ) An array of arguments passed to the macro invocation--an empty array if no arguments are provided.
- `this.instance` ( *`a ChangerCommand instance`* ) This is the instance of the changer command created by a changer macro. This property is only available to the handler (not the changer function) of a changer macro.
- `this.descriptor` ( *`a ChangerDescriptor instance`* ) This is the descriptor (which represents the attached hook) of a changer macro. This property is only available to the changer function of changer macros.

#### Context Methods

In addition to the above properties, the macro context also has the following methods.

- `this.error(message [, alert])` This method can be used to generate an error. The error object will be returned so that you can catch or throw it. The error message will be logged to the console, and optionally can generate an alert. Do not generate an alert if you intend to throw the error.
- `message` ( *`string`* ) The message to pair with the error.
- `alert` ( *`boolean`* ) If truthy, creates an alert with the error message.
- **Returns**: An `Error` instance.
- `this.typeCheck(typeList)` This method allows you to quickly check arguments against a list of types. If there is a mismatch, an `Error` instance is returned, prefilled with all the issues as a message.
- `typeList` ( *`string array`* ) An array of string types, like `"string"` or `"number"`. You can use the special keyword `"any"` to accept any type, and check for one of a list of types using the pipe to separate them, e.g. `"number|boolean"` would check for a number or boolean. The array of types should be provided in the same order as the arguments are expected.
- **Returns**: An `Error` instance if any errors are found or `undefined` if no errors are found.

**Example contexts:**

```javascript
Expand Down Expand Up @@ -121,6 +133,29 @@ Harlowe.macro('greet', function (name) {
});
```

You can use the context methods described above to generate errors:

```javascript
Harlowe.macro('greet', function (name) {
if (typeof name !== 'string' || !name.trim()) {
throw this.error('The "name" parameter should be a non-empty string!');
}
return 'Hey, ' + name + '!';
});
```

You can use the `context#typeCheck()` method to simplify type checking arguments to be even simpler, though you can only check for types, not for non-empty strings. You also cannot check *instances* like arrays.

```javascript
Harlowe.macro('greet', function (name) {
var err = this.typeCheck(['string']);
if (err) { // if no errors are found, nothing (e.g., undefined) is returned
throw err;
}
return 'Hey, ' + name + '!';
});
```

Macros do not need to return anything in specific and can also be used to simply run any arbitrary JavaScript code.

```javascript
Expand All @@ -135,8 +170,8 @@ You can access arguments via the macro context (via `this`) instead of the funct

```javascript
Harlowe.macro('log', function () {
if (this.content !== undefined) {
console.log(this.content);
if (this.args[0] !== undefined) { // this.args[0] is the first argument
console.log(this.args[0]);
}
});
```
Expand All @@ -145,8 +180,8 @@ You can also access the macro's name through the macro context:

```javascript
Harlowe.macro('log', function () {
if (this.content !== undefined) {
console.log('The macro (' + this.name + ':) says:', this.content);
if (this.args[0] !== undefined) {
console.log('The macro (' + this.name + ':) says:', this.args[0]);
}
});
```
Expand All @@ -155,13 +190,73 @@ Harlowe.macro('log', function () {

Changer macros are significantly more complex than command macros. Fortunately, most of what applies to command macros also applies to these macros. The biggest difference is that you can't return anything from the handlers of changer macros, and that changer macros have an additional changer function argument that handles most of the actual logic.

TODO
Let's look at an incredibly basic changer macro, a macro that simply suppresses content.

```javascript
Harlowe.macro('silently', function () {}, function () {
this.descriptor.enabled = false;
});
```

#### Descriptors
The above code suppressed output *and execution*:

When you act on descriptors in your changer macros, you'll need to know what it's parameters do, and what particular options you have built-in at your finger tips. This section will go over some basic and common use-cases for descriptors.
```
This content is visible. (set: $num to 1)
TODO
$num <!-- 1 -->
(silently:)[You won't see this disabled content! (set: $b to 3)]
$num <!-- 1 -->
```

You have access to the descriptor source, which lets you alter it:

```javascript
Harlowe.macro('p', function () {}, function () {
this.descriptor.source = '<p class="p-macro">' + this.descriptor.source + '</p>';
});
```

The `(p:)` macro above wraps it's source content in a `<p>` element with the class `.p-macro`. Easy!

How about altering styles?

```javascript
Harlowe.macro('red', function () {}, function () {
this.descriptor.attr.push( {
style : function () {
return 'color: red;';
}
});
});
```

A more advanced color macro (Harlowe doesn't need one, but still):

```javascript
Harlowe.macro('magiccolor', function () {
var err = this.typeCheck(['string']);
if (err) throw err;
}, function (color) {
this.descriptor.attr.push( {
style : function () {
return 'color: ' + color + ';';
}
});
});
```

You aren't limited to just doing things the Harlowe way, though. The descriptor also has a `target` property that contains a jQuery instance of the `<tw-hook>` element your changer macro is working on.

```javascript
Harlowe.macro('classy', function () {
var err = this.typeCheck(['string']);
if (err) throw err;
}, function (cls) {
this.descriptor.target.addClass(cls);
});
```

## Other APIs

Expand Down
5 changes: 3 additions & 2 deletions build.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// jshint esversion: 9
// jshint environment: node, esversion: 9

const jetpack = require('fs-jetpack'),
uglify = require('uglify-js'),
zip = require('node-zip')(),
fs = require('fs'),
version = require('./package.json').version,
scriptName = 'Harlowe Macro API',
file = 'macro.js',
dist = 'dist/macro.min.js',
Expand All @@ -19,7 +20,7 @@ function build (path, output) {

console.log(result.error);

ret = '// ' + scriptName + ', for Harlowe, by Chapel\n;' + result.code + '\n// end ' + scriptName;
ret = '// ' + scriptName + ', by Chapel; version ' + version + '\n;' + result.code + '\n// end ' + scriptName;

jetpack.write(output, ret, {atomic : true});

Expand Down
42 changes: 40 additions & 2 deletions installation-guide.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
# TODO
# Installation Guide

Installation guide for end users.
This is a guide for installing the custom macro API. The custom macro API itself does **not** include any custom macros, it is instead a resource to allow for the creation of custom macros. If you want to create your own custom macros, refer to the [documentation](README.md).

## Step 1: Get the Code

Head over to the [releases page](https://github.com/ChapelR/harlowe-macro-api/releases) and find the latest release. Under `assets`, find the `harlowe-macro-api.zip` file and click on it to download it. On your computer, extract the files from the zip archive.

## Step 2: Copy the Code

Open the `macro.js` file in a **text editor** (like Notepad or TextEdit). It is very important you do not use a word processor. Select all the code and copy it (`CTRL + A` followed by `CTRL + C` should work fine).

## Step 3: Paste the Code

Open Twine 2, either the application on your computer, or on the web. Open your story, or start a new one. In the bottom left is an up arrow next to your story's name, click this and select the `Edit Story JavaScript` option.

![Story JavaScript](https://i.imgur.com/lY52aWU.jpg)

In the resulting editor window, paste the code you copied in Step 2. If there is already code in your JavaScript section, or you plan to add more, make sure this code is first.

Since macros you write or install will *always* depend on this code, you always want to make sure you paste your custom macros in *under* the code we just pasted in now.

## But I'm Not Using Twine 2!

Refer to the docs of whatever compiler you are using for instructions on how to add the code to your project. With compilers like Tweego or Extwee, installation is as simple as copying the `macro.js` file itself over into your project directory. Just make sure that this file loads **first** you can do that in Tweego and Extwee by changing the file name so that it appears first in the directory, e.g. by changing it to `a-macro.js`.

If your compiler does not support multiple scripts or doesn't allow you to control their order, make sure this script and all the custom macros that depend on it are in the same place and that this script is first.

## FAQ

### Should I credit you?

This project is free and dedicated to the public domain. You do not have to provide credit or attribution. If you decide to do so, you may credit me as Chapel, but please don't imply I personally worked on your game, as that can make it seem like I support or endorse whatever it is.

### I'm having trouble...

[Open an issue](https://github.com/ChapelR/harlowe-macro-api/issues) and ask me for help, I'll do what I can. You can also ask in any one of the various Twine communities and I'll try to help.

### Is there a list of custom macros somewhere?

Not yet. If people start making a decent amount I'll consider compiling some sort of list.
129 changes: 115 additions & 14 deletions macro.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,144 @@
// jshint environment: browser, esversion: 5
(function () {
'use strict';

// get the APIs: we have to do this in a blocking manner--failure to do so will cause order of operations problems later
var _macros = require('macros');
var _state = require('state');
var _engine = require('engine');
var _changer = require('datatypes/changercommand');

// context prototype, private use only
function MacroContext (name, args, data) {
if (!(this instanceof MacroContext)) {
return new MacroContext(name, args, data);
}
this.name = name || 'unknown';
this.args = args || [];
this.data = data || {};
this.type = (data && data.type) || 'command';
this.fn = (data && data.fn) || 'handler';

if (this.type === 'changer') {
if (this.fn === 'handler') {
this.instance = (data && data.instance) || null;
} else {
this.descriptor = (data && data.descriptor) || null;
}
}
}

MacroContext.create = function (name, args, data) {
if (!name || typeof name !== 'string' || !name.trim()) {
throw new TypeError('Invalid macro name.');
}
if (!args || !(args instanceof Array)) {
args = [];
}
if (!data || typeof data !== 'object') {
data = { type : 'command', fn : 'handler' };
}
return new MacroContext(name, args, data);
};

Object.assign(MacroContext.prototype, {
clone : function () {
return MacroContext.create(this.name, this.args, this.data);
},
// return syntax string
syntax : function () {
return '(' + this.name + ':)';
},
// throw targeted errors from inside macros
error : function (message, warn) {
var msg = 'Error in the ' + this.syntax() + ' macro: ' + message;
if (warn) {
alert(msg);
}
console.warn('HARLOWE CUSTOM MACRO ERROR -> ', msg);
return new Error(message);
},
// check types of args: `this.typeCheck(['string|number', 'any'])`
typeCheck : function (types) {
if (!types || !(types instanceof Array)) {
types = [].slice.call(arguments);
}
var self = this;
var check = [];
types.forEach( function (type, idx) {
var thisIsArg = idx + 1, list = [];
if (typeof type !== 'string') {
return;
}
if (type.includes('|')) {
list = type.split('|').map( function (t) {
return t.trim().toLowerCase();
});
} else {
list = [ type.trim().toLowerCase() ];
}
if (list[0] === 'any' || list.some( function (t) {
return (typeof self.args[idx]) === t;
})) {
return;
} else {
check.push('argument ' + thisIsArg + ' should be a(n) ' + list.join(' or '));
}
});
if (check.length) {
return self.error(check.join('; '));
}
}
});

function simpleCommandMacro (name, cb) {
// a command macro can not have a hook
_macros.add(name, function () {

var arr = [].slice.call(arguments).slice(1);
var result = cb.apply({
name : name,
args : arr
}, arr);
return (result == undefined) ? '' : result;

var context = MacroContext.create(name, arr, {
type : 'command',
fn : 'handler'
});

var result = cb.apply(context, arr);

return (result == null) ? '' : result;

}, _macros.TypeSignature.zeroOrMore(_macros.TypeSignature.Any));

}

function simpleChangerMacro (name, cb, changer) {
// a simplified changer macro
_macros.addChanger(name, function () {

var arr = [].slice.call(arguments).slice(1);
var changer = _changer.create(name, arr);
cb.apply({
name : name,
args : arr,

var context = MacroContext.create(name, arr, {
type : 'changer',
fn : 'handler',
instance : changer
}, arr);
});

cb.apply(context, arr);
return changer;

}, function () {

var arr = [].slice.call(arguments);
var d = arr.shift();
changer.apply({
name : name,
args : arr,

var context = MacroContext.create(name, arr, {
type : 'changer',
fn : 'changer',
descriptor : d
}, arr);
});

changer.apply(context, arr);

}, _macros.TypeSignature.zeroOrMore(_macros.TypeSignature.Any));
}

Expand All @@ -55,7 +156,7 @@
}
}

// functions
// helper functions
function isSerialisable (variable) {
return (typeof variable === "number"||
typeof variable === "boolean" ||
Expand Down
Loading

0 comments on commit ba1ae8a

Please sign in to comment.