mirror of
https://gitlab.com/nightlycommit/twing.git
synced 2025-01-18 08:46:50 +02:00
Resolve issue #595
This commit is contained in:
parent
6353ead10c
commit
faeab75abe
15
README.md
15
README.md
@ -68,13 +68,14 @@ Twing's strategy here is to stick strictly to Semantic Versioning rules and *nev
|
||||
|
||||
Here is the compatibility chart between minor versions of Twing and Twig specifications levels, along with a summary of notable features provided by each Twig specifications level. Note that Twig minor versions don't always provide new language-related features (because of Twig's team perpetuating the confusion between Twig and their reference implementation, TwigPHP).
|
||||
|
||||
|Twing version|Twig specifications level|Notable features|
|
||||
|:---:|:---:|---|
|
||||
|3.0|2.11|[Macros scoping](https://twig.symfony.com/doc/2.x/tags/macro.html#macros-scoping)|
|
||||
|2.3|2.10|`spaceless`, `column`, `filter`, `map` and `reduce` filters, `apply` tag, `line whitespace trimming` whitespace control modifier|
|
||||
|2.2|2.6|`deprecated` tag|
|
||||
|1.3|2.5|`spaceless` and `block`-related deprecations|
|
||||
|1.0|2.4| |
|
||||
| Twing version | Twig specifications level | Notable features |
|
||||
|:-------------:|:-------------------------:|----------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 5.2 | 2.14 | `spaceship` operator, `sort` filter comparator`, hash “short” syntax |
|
||||
| 3.0 | 2.11 | [Macros scoping](https://twig.symfony.com/doc/2.x/tags/macro.html#macros-scoping) |
|
||||
| 2.3 | 2.10 | `spaceless`, `column`, `filter`, `map` and `reduce` filters, `apply` tag, `line whitespace trimming` whitespace control modifier |
|
||||
| 2.2 | 2.6 | `deprecated` tag |
|
||||
| 1.3 | 2.5 | `spaceless` and `block`-related deprecations |
|
||||
| 1.0 | 2.4 | |
|
||||
|
||||
It is highly recommended to always use the latest version of Twing available as bug fixes will always target the latest version.
|
||||
|
||||
|
@ -127,6 +127,7 @@ import {TwingNodeVisitorMacroAutoImport} from "../node-visitor/macro-auto-import
|
||||
import {TwingTokenParserLine} from "../token-parser/line";
|
||||
import {extname, basename} from "path";
|
||||
import {TwingEscapingStrategyResolver} from "../environment";
|
||||
import {TwingNodeExpressionBinarySpaceship} from "../node/expression/binary/spaceship";
|
||||
|
||||
export class TwingExtensionCore extends TwingExtension {
|
||||
private dateFormats: Array<string> = ['F j, Y H:i', '%d days'];
|
||||
@ -432,7 +433,10 @@ export class TwingExtensionCore extends TwingExtension {
|
||||
{name: 'length', defaultValue: null},
|
||||
{name: 'preserve_keys', defaultValue: false}
|
||||
]),
|
||||
new TwingFilter('sort', sort, []),
|
||||
new TwingFilter('sort', sort, [{
|
||||
name: 'arrow',
|
||||
defaultValue: null
|
||||
}]),
|
||||
new TwingFilter('spaceless', spaceless, [], {
|
||||
is_safe: ['html']
|
||||
}),
|
||||
@ -571,6 +575,9 @@ export class TwingExtensionCore extends TwingExtension {
|
||||
new TwingOperator('!=', TwingOperatorType.BINARY, 20, function (operands: [TwingNode, TwingNode], lineno: number, columnno: number) {
|
||||
return new TwingNodeExpressionBinaryNotEqual(operands, lineno, columnno);
|
||||
}),
|
||||
new TwingOperator('<=>', TwingOperatorType.BINARY, 20, (operands: [TwingNode, TwingNode], line: number, column: number) => {
|
||||
return new TwingNodeExpressionBinarySpaceship(operands, line, column);
|
||||
}),
|
||||
new TwingOperator('<', TwingOperatorType.BINARY, 20, function (operands: [TwingNode, TwingNode], lineno: number, columnno: number) {
|
||||
return new TwingNodeExpressionBinaryLess(operands, lineno, columnno);
|
||||
}),
|
||||
|
@ -10,14 +10,17 @@ import {asort} from "../../../helpers/asort";
|
||||
*
|
||||
* @returns {Promise<Map<any, any>>}
|
||||
*/
|
||||
export function sort(iterable: Map<any, any>): Promise<Map<any, any>> {
|
||||
export async function sort(
|
||||
iterable: Map<any, any>,
|
||||
arrow: ((a: any, b: any) => Promise<-1 | 0 | 1>) | null
|
||||
): Promise<Map<any, any>> {
|
||||
if (!isTraversable(iterable)) {
|
||||
throw new TwingErrorRuntime(`The sort filter only works with iterables, got "${typeof iterable}".`);
|
||||
}
|
||||
|
||||
let map = iteratorToMap(iterable);
|
||||
|
||||
asort(map);
|
||||
await asort(map, arrow || undefined);
|
||||
|
||||
return Promise.resolve(map);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import {sortAsynchronously} from "./sort";
|
||||
|
||||
/**
|
||||
* Sort a map and maintain index association.
|
||||
*
|
||||
@ -5,25 +7,33 @@
|
||||
* @param {Function} handler
|
||||
* @returns {Map<* ,*>}
|
||||
*/
|
||||
export function asort(map: Map<any, any>, handler: any = undefined) {
|
||||
let sortedMap = new Map();
|
||||
export async function asort(map: Map<any, any>, compareFunction?: (a: any, b: any) => Promise<-1 | 0 | 1>) {
|
||||
const sortedMap = new Map();
|
||||
const keys: Array<any> = ([] as Array<any>).fill(null, 0, map.size);
|
||||
const values = [...map.values()];
|
||||
|
||||
let keys: Array<any> = [].fill(null, 0, map.size);
|
||||
let sortedValues = [...map.values()].sort(handler);
|
||||
let sortedValues: Array<any>;
|
||||
|
||||
for (let [key, value] of map) {
|
||||
let index = sortedValues.indexOf(value);
|
||||
if (compareFunction) {
|
||||
sortedValues = await sortAsynchronously(values, compareFunction);
|
||||
}
|
||||
else {
|
||||
sortedValues = values.sort();
|
||||
}
|
||||
|
||||
for (const [key, value] of map) {
|
||||
const index = sortedValues.indexOf(value);
|
||||
|
||||
keys[index] = key;
|
||||
}
|
||||
|
||||
for (let key of keys) {
|
||||
for (const key of keys) {
|
||||
sortedMap.set(key, map.get(key));
|
||||
}
|
||||
|
||||
map.clear();
|
||||
|
||||
for (let [key, value] of sortedMap) {
|
||||
for (const [key, value] of sortedMap) {
|
||||
map.set(key, value);
|
||||
}
|
||||
}
|
||||
|
77
src/lib/helpers/sort.ts
Normal file
77
src/lib/helpers/sort.ts
Normal file
@ -0,0 +1,77 @@
|
||||
type Comparator = (a: any, b: any) => Promise<-1 | 0 | 1>;
|
||||
|
||||
export const sortAsynchronously = (array: Array<any>, comparator: Comparator) => {
|
||||
/**
|
||||
* return the median value among x, y, and z
|
||||
*/
|
||||
const getPivot = async (x: any, y: any, z: any, comparator: Comparator): Promise<any> => {
|
||||
if (await comparator(x, y) < 0) {
|
||||
if (await comparator(y, z) < 0) {
|
||||
return y;
|
||||
}
|
||||
else if (await comparator(z, x) < 0) {
|
||||
return x;
|
||||
}
|
||||
else {
|
||||
return z;
|
||||
}
|
||||
}
|
||||
else if (await comparator(y, z) > 0) {
|
||||
return y;
|
||||
}
|
||||
else if (await comparator(z, x) > 0) {
|
||||
return x;
|
||||
}
|
||||
else {
|
||||
return z;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronous quick sort.
|
||||
*
|
||||
* @see https://gist.github.com/kimamula/fa34190db624239111bbe0deba72a6ab
|
||||
*
|
||||
* @param array The array to sort
|
||||
* @param comparator The comparator function
|
||||
* @param left The index where the range of elements to be sorted starts
|
||||
* @param right The index where the range of elements to be sorted ends
|
||||
*/
|
||||
const quickSort = async (array: Array<any>, comparator: Comparator, left = 0, right = array.length - 1): Promise<Array<any>> => {
|
||||
if (left < right) {
|
||||
let i = left;
|
||||
let j = right;
|
||||
let tmp;
|
||||
|
||||
const pivot = await getPivot(array[i], array[i + Math.floor((j - i) / 2)], array[j], comparator);
|
||||
|
||||
while (true) {
|
||||
while (await comparator(array[i], pivot) < 0) {
|
||||
i++;
|
||||
}
|
||||
|
||||
while (await comparator(pivot, array[j]) < 0) {
|
||||
j--;
|
||||
}
|
||||
|
||||
if (i >= j) {
|
||||
break;
|
||||
}
|
||||
|
||||
tmp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = tmp;
|
||||
|
||||
i++;
|
||||
j--;
|
||||
}
|
||||
|
||||
await quickSort(array, comparator, left, i - 1);
|
||||
await quickSort(array, comparator, j + 1, right);
|
||||
}
|
||||
|
||||
return array;
|
||||
};
|
||||
|
||||
return quickSort(array, comparator);
|
||||
};
|
25
src/lib/node/expression/binary/spaceship.ts
Normal file
25
src/lib/node/expression/binary/spaceship.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {TwingNodeExpressionBinary} from "../binary";
|
||||
import {TwingCompiler} from "../../../compiler";
|
||||
import {TwingNodeType} from "../../../node-type";
|
||||
|
||||
export const type = new TwingNodeType('expression_spaceship');
|
||||
|
||||
export class TwingNodeExpressionBinarySpaceship extends TwingNodeExpressionBinary {
|
||||
get type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
compile(compiler: TwingCompiler) {
|
||||
compiler
|
||||
.raw('this.compare(')
|
||||
.subcompile(this.getNode('left'))
|
||||
.raw(', ')
|
||||
.subcompile(this.getNode('right'))
|
||||
.raw(') ? 0 : (')
|
||||
.subcompile(this.getNode('left'))
|
||||
.raw(' < ')
|
||||
.subcompile(this.getNode('right'))
|
||||
.raw(' ? -1 : 1)')
|
||||
;
|
||||
}
|
||||
}
|
@ -851,7 +851,17 @@ export class TwingParser {
|
||||
let token;
|
||||
let key;
|
||||
|
||||
if ((token = stream.nextIf(TokenType.STRING)) || (token = stream.nextIf(TokenType.NAME)) || (token = stream.nextIf(TokenType.NUMBER))) {
|
||||
if (token = stream.nextIf(TokenType.NAME)) {
|
||||
key = new TwingNodeExpressionConstant(token.value, token.line, token.column);
|
||||
|
||||
// {a} is a shortcut for {a:a}
|
||||
if (stream.test(TokenType.PUNCTUATION, [',', '}'])) {
|
||||
node.addElement(new TwingNodeExpressionName(token.value, token.line, token.column), key);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if ((token = stream.nextIf(TokenType.STRING)) || (token = stream.nextIf(TokenType.NUMBER))) {
|
||||
key = new TwingNodeExpressionConstant(token.value, token.line, token.column);
|
||||
} else if (stream.test(TokenType.PUNCTUATION, '(')) {
|
||||
key = this.parseExpression();
|
||||
|
@ -0,0 +1,21 @@
|
||||
import TestBase from "../../TestBase";
|
||||
|
||||
export default class extends TestBase {
|
||||
getDescription() {
|
||||
return 'Hash key can be omitted if it is the same as the variable name';
|
||||
}
|
||||
|
||||
getTemplates() {
|
||||
return {
|
||||
'index.twig': `
|
||||
{% set foo = "foo" %}
|
||||
{% set hash = { foo, bar: "bar" } %}
|
||||
{{ hash|join }}`
|
||||
};
|
||||
}
|
||||
|
||||
getExpected() {
|
||||
return `
|
||||
foobar`;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import TestBase from "../../TestBase";
|
||||
|
||||
export default class extends TestBase {
|
||||
getDescription() {
|
||||
return '"sort" filter with arrow parameter';
|
||||
}
|
||||
|
||||
getTemplates() {
|
||||
return {
|
||||
'index.twig': `{{ [5, 3, 4]|sort((a, b) => b <=> a)|join() }}`
|
||||
};
|
||||
}
|
||||
|
||||
getExpected() {
|
||||
return `543`;
|
||||
}
|
||||
}
|
124
test/tests/integration/fixtures/operators/spaceship.ts
Normal file
124
test/tests/integration/fixtures/operators/spaceship.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import TestBase from "../../TestBase";
|
||||
|
||||
export default class extends TestBase {
|
||||
getDescription() {
|
||||
return '"spaceship" operator';
|
||||
}
|
||||
|
||||
getTemplates() {
|
||||
return {
|
||||
'index.twig': `
|
||||
{% set nullVar = null %}
|
||||
{% set emptyArray = [] %}
|
||||
|
||||
{{ false <=> 0 }}
|
||||
{{ false <=> '' }}
|
||||
{{ false <=> '0' }}
|
||||
{{ false <=> nullVar }}
|
||||
{{ false <=> emptyArray }}
|
||||
|
||||
1
|
||||
{{ 1 <=> true }}
|
||||
{{ 1 <=> '1' }}
|
||||
|
||||
0
|
||||
{{ 0 <=> false }}
|
||||
{{ 0 <=> '0' }}
|
||||
{{ 0 <=> null }}
|
||||
|
||||
-1
|
||||
{{ -1 <=> true }}
|
||||
{{ -1 <=> '-1' }}
|
||||
|
||||
"1"
|
||||
{{ '1' <=> true }}
|
||||
{{ '1' <=> 1 }}
|
||||
|
||||
"0"
|
||||
{{ '0' <=> false }}
|
||||
{{ '0' <=> 0 }}
|
||||
|
||||
"-1"
|
||||
{{ '-1' <=> true }}
|
||||
{{ '-1' <=> -1 }}
|
||||
|
||||
null
|
||||
{{ nullVar <=> false }}
|
||||
{{ nullVar <=> 0 }}
|
||||
{{ nullVar <=> emptyArray }}
|
||||
{{ nullVar <=> '' }}
|
||||
|
||||
[]
|
||||
{{ emptyArray <=> false }}
|
||||
{{ emptyArray <=> nullVar }}
|
||||
|
||||
"php"
|
||||
{{ 'php' <=> true }}
|
||||
{{ 'php' <=> 0 }}
|
||||
|
||||
""
|
||||
{{ '' <=> false }}
|
||||
{{ '' <=> 0 }}
|
||||
{{ '' <=> nullVar }}
|
||||
|
||||
{{ 1 <=> 2 }}
|
||||
`
|
||||
};
|
||||
}
|
||||
|
||||
getExpected() {
|
||||
return `
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
|
||||
1
|
||||
0
|
||||
0
|
||||
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
|
||||
-1
|
||||
0
|
||||
0
|
||||
|
||||
"1"
|
||||
0
|
||||
0
|
||||
|
||||
"0"
|
||||
0
|
||||
0
|
||||
|
||||
"-1"
|
||||
0
|
||||
0
|
||||
|
||||
null
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
|
||||
[]
|
||||
0
|
||||
0
|
||||
|
||||
"php"
|
||||
0
|
||||
0
|
||||
|
||||
""
|
||||
0
|
||||
0
|
||||
0
|
||||
|
||||
-1
|
||||
`;
|
||||
}
|
||||
}
|
@ -3,11 +3,10 @@ import {sort} from "../../../../../../../../src/lib/extension/core/filters/sort"
|
||||
|
||||
tape('sort', async (test) => {
|
||||
try {
|
||||
await sort(5 as any);
|
||||
await sort(5 as any, () => Promise.resolve(-1));
|
||||
|
||||
test.fail();
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
test.same(e.message, 'The sort filter only works with iterables, got "number".');
|
||||
}
|
||||
|
||||
|
@ -74,10 +74,14 @@ tape('TwingExtensionCore', (test) => {
|
||||
* @param {TwingFunction} f
|
||||
* @param fixture
|
||||
*/
|
||||
const testAcceptedArguments = (test: tape.Test, name: string, f: TwingFunction, fixture: { name: string, arguments: TwingCallableArgument[] }) => {
|
||||
const testAcceptedArguments = (test: tape.Test, name: string, f: TwingFunction, fixture: {
|
||||
name: string,
|
||||
arguments: TwingCallableArgument[]
|
||||
}) => {
|
||||
if (!fixture) {
|
||||
test.fail(`${name} function has no registered fixture`);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
test.same(f.getAcceptedArgments(), fixture.arguments, `${name} function accepted arguments are as expected`);
|
||||
}
|
||||
};
|
||||
@ -167,10 +171,14 @@ tape('TwingExtensionCore', (test) => {
|
||||
* @param {TwingFilter} f
|
||||
* @param fixture
|
||||
*/
|
||||
const testAcceptedArguments = (test: tape.Test, name: string, f: TwingFilter, fixture: { name: string, arguments: TwingCallableArgument[] }) => {
|
||||
const testAcceptedArguments = (test: tape.Test, name: string, f: TwingFilter, fixture: {
|
||||
name: string,
|
||||
arguments: TwingCallableArgument[]
|
||||
}) => {
|
||||
if (!fixture) {
|
||||
test.fail(`${name} filter has no registered fixture`);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
test.same(f.getAcceptedArgments(), fixture.arguments, `${name} filter accepted arguments are as expected`);
|
||||
}
|
||||
};
|
||||
@ -344,7 +352,9 @@ tape('TwingExtensionCore', (test) => {
|
||||
},
|
||||
{
|
||||
name: 'sort',
|
||||
arguments: []
|
||||
arguments: [
|
||||
{name: 'arrow', defaultValue: null}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'spaceless',
|
||||
|
@ -16,7 +16,7 @@ tape('asort', (test) => {
|
||||
let map = new Map([[1, 'foo'], [0, 'bar']]);
|
||||
|
||||
asort(map, (a: any, b: any) => {
|
||||
return (a > b) ? -1 : 1;
|
||||
return Promise.resolve((a > b) ? -1 : 1);
|
||||
});
|
||||
|
||||
test.same([...map.values()], ['foo', 'bar']);
|
||||
|
Loading…
Reference in New Issue
Block a user