Commit dd5b2c30 authored by Leon Arnott's avatar Leon Arnott
Browse files

Added (p-not:), a pattern macro corresponding to the RegExp (?!) operator.

Also fixed a bug related to CodeHook parsing.
parent 34c6fe2af64e
......@@ -159,6 +159,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
* `(p-opt:)` is a variation of `(p:)` that optionally matches the sequence - it matches strings that match the sequence, or are empty.
* `(p-many:)` is a variation of `(p:)` that matches strings that match the sequence many times. You can specify the minimum and/or maximum amount of times the string can match the sequence.
* `(p-ins:)` is a case-insensitive version of `(p:)`, which also treats any sub-patterns given to it as case-insensitive.
* `(p-not:)` matches any character except those that match the given single-character strings, or datatypes corresponding to single characters.
* You can also use the preceding macros with `(unpack:)`. For instance, `(unpack: "Dr. Iris Cornea" into (p: (p-opt:"Dr. "), (p: str-type _firstName, whitespace, str-type _lastName)-type _fullName))` creates three variables, _firstName, _lastName and _fullName, from a single string.
* Added `(datatype:)` and `(datapattern)` macros. `(datatype:)` produces the datatype that matches the given value, if it exists. `(datapattern)` is a variation that, when given arrays or datamaps, produces a data pattern that can be used with the `matches` operator, among other things, to check if the structure of one value matches the other. `(datapattern: (passage:))` produces `(dm:"name",str,"source",str,"tags",(a:str))`.
......
......@@ -2558,7 +2558,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
(icon-reload:)
Added in: 1.0.0
#url
#navigation
*/
("reload",
noop,
......
......@@ -385,6 +385,57 @@ define(['jquery', 'macros', 'utils', 'utils/operationutils', 'datatypes/datatype
makeRegExpString: subargs => "(?:" + subargs.join('') + ")?"
}),
PatternSignature)
/*d:
(p-not: ...String or Datatype) -> Datatype
Also known as: (pattern-not:)
Given any number of string characters or non-spread datatypes, this creates a string pattern that matches any one character that doesn't
match any of those values.
Example usage:
* `(p-not: digit, ".")` matches any one string character except digits (matched by the `digit` datatype) or a "." character.
* `(p-not:..."aeiou")` matches any one string character except a lowercase vowel. Note that using the spread `...` syntax to spread strings into their individual characters
is recommended when using this macro.
* `(p:"[", (p-many:(p-not:"]")), "]")` matches "[" followed by any number of characters except "]", followed by a closing "]".
Details:
This is part of a suite of string pattern macros. Consult the (p:) article to learn more about string patterns, special user-created datatypes
that can match very precise kinds of strings.
Unlike many pattern datatype macros, this will error if given any datatype that could match 0 or 2+ characters. So, passing `str`, `empty`, any spread datatype like `...digit`,
or any string with more or less than 1 character, will produce an error.
When you use this in (unpack:), such as `(unpack: "-" into (p-many:(p-not:'s'))-type _isLord`, and the optional pattern doesn't match,
the variable will be set to the empty string "".
While you can use this as the datatype of a TypedVar, you can't nest TypedVars inside this.
See also:
(p-not-before:)
Added in: 3.2.0.
#patterns
*/
(["p-not","pattern-not"],
(_, ...fullArgs) => {
const wrong = fullArgs.find(e =>
/*
This must exclude the following datatypes: patterns, strings of length != 1,
and basic datatypes representing multiple or zero string characters.
*/
typeof e === "string" ? [...e].length !== 1 : e.rest || e.regExp || ['str','empty'].includes(e.name));
if (wrong) {
return TwineError.create("datatype", "(p-not:) should only be given single character");
}
return createPattern({
name: "p-not", fullArgs, canContainTypedVars: false,
makeRegExpString: subargs => "[^" + subargs.join('') + "]"
});
},
PatternSignature)
/*d:
(p-many: [Number], [Number], ...String or Datatype) -> Datatype
Also known as: (pattern-many:)
......
......@@ -134,7 +134,11 @@
/*
The contents of macro tags - expressions and other macros.
*/
macroMode = [];
macroMode = [],
/*
The contents of strings - just escaped characters and closing quote marks.
*/
stringMode = [];
/*
These rules objects contain each ordered category of rules.
......@@ -340,12 +344,6 @@
hookFront: "hook",
},
cannotCross: ["verbatimOpener"],
/*
The "hookFront" and "hookBack" tokens (well, actually just "hookBack" once
they're folded together) set the mode back to 'markup' even when inside a macro.
This explicit innerMode setting causes this.
*/
innerMode: 'markup',
}),
},
......@@ -563,6 +561,7 @@
singleStringOpener:
"string",
},
innerMode: stringMode,
}),
},
doubleStringOpener: {
......@@ -572,6 +571,7 @@
doubleStringOpener:
"string",
},
innerMode: stringMode,
}),
},
......@@ -718,6 +718,16 @@
return a;
},{})
));
/*
TBW
*/
const stringRules = setupRules(stringMode, {
singleStringCloser: macroRules.singleStringOpener,
doubleStringCloser: macroRules.doubleStringOpener,
escapedStringChar: macroRules.escapedStringChar,
});
/*
Now that all of the rule categories have been defined, the modes can be
defined as selections of these categories.
......@@ -740,10 +750,12 @@
macroMode.push( ...keys(expressionRules),
...keys(macroRules));
stringMode.push( ...keys(stringRules));
/*
Merge all of the categories together.
*/
const allRules = assign({}, blockRules, inlineRules, expressionRules, macroRules);
const allRules = assign({}, blockRules, inlineRules, expressionRules, macroRules, stringRules);
/*
Add the 'pattern' property to each rule
......@@ -780,16 +792,19 @@
}
});
assign(Lexer.rules, allRules);
const {modes} = Lexer;
/*
Declare that the starting mode for lexing, before any
tokens are appraised, is...
*/
Lexer.modes.start = Lexer.modes.markup = markupMode;
modes.start = modes.markup = markupMode;
/*
But macroMode is also exposed in order for certain consumers
(such as the documentation) to be able to lex in that context.
*/
Lexer.modes.macro = macroMode;
modes.macro = macroMode;
modes.string = stringMode;
return Lexer;
}
......
......@@ -1033,6 +1033,11 @@
singleStringOpener: "'",
doubleStringOpener: '"',
/*
This doubled-up name is used for the special closers inside the "string" mode.
*/
singleStringCloser: "'",
doubleStringCloser: '"',
/*
Macro operators
......
Introduction 2: Welcome to the world of 3.2.0
Dear fellow author of hypertext fiction: this version of Harlowe is most unlike others before it - despite a commitment to backwards-compatibility letting it be numbered a "minor" release, its wealth of added features is truly incredible. Let me explain in brief how this came to be. In April 2020, as the COVID-19 pandemic death toll was steadily rising worldwide, I came to something of a quiet realisation that it was very possible I would not live to the end of this year, and, furthermore, it was probable that many Harlowe users would not live to the end of this year. I thought about Harlowe, and about how so much functionality I'd always envisioned for it - custom macros, string patterns, fullscreen support - was still unimplemented, and came to a decision that I absolutely had to finally get all of it in, this year.
Dear fellow author of hypertext fiction: this version of Harlowe is most unlike others before it - despite a commitment to backwards-compatibility letting it be numbered a "minor" release, its wealth of added features is truly incredible. Let me explain in brief how this came to be. In April 2020, as the COVID-19 pandemic death toll (and the even greater permanent disability toll) was steadily rising worldwide, I came to something of a quiet realisation... that it was very possible I would not live to the end of this year, and, furthermore, it was probable that many Harlowe users would not live to the end of this year. I thought about Harlowe, and about how so much functionality I'd always envisioned for it - custom macros, string patterns, fullscreen support - was still unimplemented, and came to a decision that I absolutely had to finally get all of it in, this year.
So, I worked diligently on Harlowe for 9 months. In retrospect, it feels like my life lost all meaning except for Harlowe. But at the end, Harlowe finally became, for the first time, a hypertext fiction coding language I could be proud of. Even now, there's still some extra features I wish I'd been able to fit in, but this version feels like the first version where Harlowe is "complete", as my original vision for the story format in 2014 had resembled.
......
......@@ -58,6 +58,9 @@ describe("twinescript literals", function() {
expect('(print: "A\nB")').markupToPrint("A\nB");
expect("(print: 'A\nB')").markupToPrint("A\nB");
});
it("can contain opening hook markup", function() {
expect('(print: "[" + \'[\')').markupToPrint("[[");
});
it("can contain C-style backslash escapes", function() {
expect('(print: "A\\B")').markupToPrint("AB");
expect("(print: 'A\\B')").markupToPrint("AB");
......
......@@ -348,13 +348,26 @@ describe("patterns", function() {
expect("(print: (p-ins:'A',lowercase,'E') matches 'aBe' and 'AbE')").markupToPrint('true');
});
});
/*describe("(p-not-before:)", function() {
basicTest("p-not-before", '"black"),(p:" green"', false);
it("when used in (p:), it matches if the sequence does not appear at that spot", function() {
expect("(print: (p:'red',(p-not-before:whitespace,'blue'),string) matches 'redblue green' and 'redgreen' and 'red')").markupToPrint('true');
expect("(print: (p:'red',(p-not-before:whitespace,'blue'),string) does not match 'red bluegreen')").markupToPrint('true');
describe("(p-not:)", function() {
it("accepts multiple single-character strings or string datatypes", function() {
['alphanumeric','lowercase','uppercase','whitespace'].forEach(function(type) {
expect("(print: (p-not:" + type + "))").not.markupToError();
expect("(print: (p-not:" + type + ",'w','S','b'))").not.markupToError();
});
expect("(print: (p-not:...'aeiouy'))").not.markupToError();
expect("(print: (p-not:num))").markupToError();
expect("(print: (p-not:...num))").markupToError()
expect("(print: (p-not:str))").markupToError();
expect("(print: (p-not:(p:'a')))").markupToError();
expect("(print: (p-not:))").markupToError();
});
it("when used in (p:), it matches any character that doesn't match the given patterns", function() {
expect("(print: (p:'red',(p-not:...'1234'),string) matches 'red5' and 'red6green' and 'red7')").markupToPrint('true');
expect("(print: (p:'red',(p-not:...'1234'),string) does not match 'red3')").markupToPrint('true');
expect("(print: (p:'red',(p-not:digit),string) does not match 'red3')").markupToPrint('true');
expect("(print: (p:'red',(p-not:digit),string) does not match 'red6green')").markupToPrint('true');
});
});*/
});
});
describe("(datatype:)", function() {
it("takes most kinds of data, and produces a datatype that matches it", 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