Skip to content

Commit

Permalink
feat: add nextUntil subroutine (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Sep 2, 2017
1 parent 5f5387b commit 6fe7aec
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Have you got suggestions for improvement? [I am all ears](https://github.com/gaj
* [`cheerio` evaluator](#cheerio-evaluator)
* [Subroutines](#subroutines)
* [Built-in subroutines](#built-in-subroutines)
* [`nextUntil` subroutine](#nextuntil-subroutine)
* [`select` subroutine](#select-subroutine)
* [Quantifier expression](#quantifier-expression)
* [`read` subroutine](#read-subroutine)
Expand Down Expand Up @@ -169,6 +170,15 @@ There are two types of subroutines:

The following subroutines are available out of the box.

#### `nextUntil` subroutine

`nextUntil` subroutine is used to select all following siblings of each element up to but not including the element matched by the selector.

|Parameter name|Description|Default|
|---|---|---|
|selector expression|A string containing a selector expression to indicate where to stop matching following sibling elements.|N/A|
|filter expression|A string containing a selector expression to match elements against.|

#### `select` subroutine

`select` subroutine is used to select the elements in the document using an [evaluator](#evaluators).
Expand Down
5 changes: 5 additions & 0 deletions src/evaluators/browserEvaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ export default (): EvaluatorType => {
return [].slice.apply(node.querySelectorAll(selector));
};

const nextUntil = (node: HTMLElement, selector: string, filter?: string) => {
throw new Error('Not implemented.');
};

return {
getAttributeValue,
getPropertyValue,
isElement,
nextUntil,
parseDocument,
querySelectorAll
};
Expand Down
10 changes: 10 additions & 0 deletions src/evaluators/cheerioEvaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,20 @@ export default (): EvaluatorType => {
});
};

const nextUntil = (node: Object, selector: string, filter?: string): Array<Object> => {
return node
.nextUntil(selector, filter)
.toArray()
.map((element) => {
return cheerio(element);
});
};

return {
getAttributeValue,
getPropertyValue,
isElement,
nextUntil,
parseDocument,
querySelectorAll
};
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
InvalidValueSentinel
} from './sentinels';
import {
nextUntilSubroutine,
readSubroutine,
selectSubroutine,
testSubroutine
Expand All @@ -43,6 +44,7 @@ export {
};

const builtInSubroutines = {
nextUntil: nextUntilSubroutine,
read: readSubroutine,
select: selectSubroutine,
test: testSubroutine
Expand Down
4 changes: 4 additions & 0 deletions src/subroutineAliasPreset.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {
nextUntil,
readSubroutine,
selectSubroutine,
testSubroutine
} from './subroutines';

export default {
nu: (subject, values, bindle) => {
return nextUntil(subject, values, bindle);
},
ra: (subject, values, bindle) => {
return readSubroutine(subject, ['attribute'].concat(values), bindle);
},
Expand Down
1 change: 1 addition & 0 deletions src/subroutines/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow

export {default as nextUntilSubroutine} from './nextUntilSubroutine';
export {default as readSubroutine} from './readSubroutine';
export {default as selectSubroutine} from './selectSubroutine';
export {default as testSubroutine} from './testSubroutine';
37 changes: 37 additions & 0 deletions src/subroutines/nextUntilSubroutine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// @flow

import {
FinalResultSentinel
} from 'pianola';
import {
createDebug
} from '../utilities';
import {
parseQuantifierExpression
} from '../parsers';
import {
SelectSubroutineUnexpectedResultCountError,
SurgeonError
} from '../errors';
import type {
SelectSubroutineQuantifierType,
SubroutineType
} from '../types';

const debug = createDebug('subroutine:nextUntil');

const selectSubroutine: SubroutineType = (subject, [selectorExpression, filterExpression], {evaluator}) => {
debug('selecting following siblings matching "%s" until "%s"', filterExpression, selectorExpression);

if (!evaluator.isElement(subject)) {
throw new SurgeonError('Unexpected value. Value must be an element.');
}

const matches = evaluator.nextUntil(subject, selectorExpression, filterExpression);

debug('matched %d node(s)', matches.length);

return matches;
};

export default selectSubroutine;
1 change: 1 addition & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type EvaluatorType = {|
+getPropertyValue: (element: Object, name: string) => mixed,

+isElement: (maybeElement: mixed) => boolean,
+nextUntil: (element: Object, selector: string, filter?: string) => Array<Object>,

// eslint-disable-next-line flowtype/no-weak-types
+parseDocument: (subject: string) => Object,
Expand Down
49 changes: 49 additions & 0 deletions test/surgeon/queries/multiple-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,55 @@ test('extracts multiple values', (t): void => {
]);
});

test('extracts multiple nodes nextUntil', (t): void => {
const x = surgeon();

const subject = `
<div class="foo">foo0</div>
<div class="bar">bar0</div>
<div class="bar">bar1</div>
<div class="bar">bar2</div>
<div class="foo">foo1</div>
<div class="bar">bar3</div>
`;

const query: DenormalizedQueryType = [
'select .foo {0,}[0]',
'nextUntil .foo',
'read property textContent'
];

t.deepEqual(x(query, subject), [
'bar0',
'bar1',
'bar2'
]);
});

test('extracts multiple nodes nextUntil (with filter)', (t): void => {
const x = surgeon();

const subject = `
<div class="foo">foo0</div>
<div class="bar">bar0</div>
<div class="bar">bar1</div>
<div class="bar">bar2</div>
<div class="foo">foo1</div>
<div class="bar">bar3</div>
`;

const query: DenormalizedQueryType = [
'select .foo {0,}[0]',
'nextUntil ".foo" ":not(:nth-child(4))"',
'read property textContent'
];

t.deepEqual(x(query, subject), [
'bar0',
'bar1'
]);
});

test('throws error if too few nodes are matched', (t): void => {
const x = surgeon();

Expand Down
24 changes: 24 additions & 0 deletions test/surgeon/subroutines/nextUntilSubroutine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @flow

import test from 'ava';
import sinon from 'sinon';
import {
FinalResultSentinel
} from 'pianola';
import nextUntilSubroutine from '../../../src/subroutines/nextUntilSubroutine';

test('returns array when expecting multiple results', (t): void => {
const isElement = sinon.stub().returns(true);
const nextUntil = sinon.stub().returns(['foo', 'bar']);

const evaluator = {
isElement,
nextUntil
};

const results = nextUntilSubroutine(null, ['.foo', '.bar'], {evaluator});

t.true(nextUntil.calledWith(null, '.foo', '.bar'));

t.deepEqual(results, ['foo', 'bar']);
});

0 comments on commit 6fe7aec

Please sign in to comment.