Skip to content

Commit

Permalink
feat: Helpers for implementing improved frontmatter in helix-pipe
Browse files Browse the repository at this point in the history
feat(string.js): New helper library for working with strings
feat(sequence.js): dict() used for casting values to es6 maps
feat(sequence.js): obj() used for casting values to k/v objects
chore(sequence.js): Refactor trySkip to use range0(..) instead range(0, ...)
feat(sequence.js): reject() opposite of filter()
feat(sequence.js): take() throwing version of tryTake()
feat(sequence.js): zipLeast2() curryable version of zipLeast()
feat(sequence.js): zip2() curryable version of zip()
feat(sequence.js): zipLongest2() curryable version of zipLongest()
feat(sequence.js): slidingWindow()
feat(sequence.js): trySlidingWindow()
feat(sequence.js): lookahead()
feat(sequence.js): mod() Transform value as a sequence then cast back
  to original type
feat(sequence.js): union() Combine set or map-like collections and cast
  back to original type
feat(sequence.js): union2() curryable version of union()
  • Loading branch information
koraa committed Mar 27, 2019
1 parent f79fa59 commit d5544d5
Show file tree
Hide file tree
Showing 5 changed files with 403 additions and 34 deletions.
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const HelixConfig = require('./HelixConfig.js');
const Strain = require('./Strain.js');
const Logger = require('./Logger.js');
const sequence = require('./sequence.js');
const string = require('./string.js');
const dom = require('./dom.js');

module.exports = {
Expand All @@ -22,5 +23,6 @@ module.exports = {
Strain,
Logger,
sequence,
string,
dom,
};
206 changes: 188 additions & 18 deletions src/sequence.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ const type = v => (isdef(v) ? v.constructor : v);
* This is useful as a replacement for val.constructor.name,
* since this can deal with null and undefined.
*/
const typename = (t) => isdef(t) ? t.name : `${t}`
const typename = t => (isdef(t) ? t.name : `${t}`);

/**
* Immediately execute the given function.
Expand Down Expand Up @@ -606,6 +606,30 @@ const list = seq => Array.from(iter(seq));
*/
const uniq = seq => new Set(iter(seq));

/**
* Turns any sequence into an es6 map
* This is particularly useful for constructing es7 maps from objects...
*/
const dict = (seq) => {
const r = new Map();
each(seq, (pair) => {
const [key, value] = iter(pair);
r.set(key, value);
});
return r;
};

/** Turns any sequence into an object */
const obj = (seq) => {
const r = {};
each(seq, (pair) => {
const [key, value] = iter(pair);
r[key] = value;
});
return r;
};


/**
* Convert each element from a sequence into a string
* and join them with the given separator.
Expand Down Expand Up @@ -704,22 +728,8 @@ into.impl = new Map();
into.impl.set(Array, list);
into.impl.set(String, join(''));
into.impl.set(Set, uniq);
into.impl.set(Map, (seq) => {
const r = new Map();
each(seq, (pair) => {
const [key, value] = iter(pair);
r.set(key, value);
});
return r;
});
into.impl.set(Object, (seq) => {
const r = {};
each(seq, (pair) => {
const [key, value] = iter(pair);
r[key] = value;
});
return r;
});
into.impl.set(Map, dict);
into.impl.set(Object, obj);

class IntoNotImplemented extends TraitNotImplemented {}

Expand Down Expand Up @@ -822,6 +832,12 @@ const filter = curry('filter', function* filter(seq, fn) {
}
});

/**
* Opposite of filter: Removes values from the sequence if the function
* returns true.
*/
const reject = curry('reject', (seq, fn) => filter(seq, v => !fn(v)));

/**
* Reverse a given sequence
* @param {Sequence} seq Any sequence for which iter() is defined
Expand Down Expand Up @@ -859,7 +875,7 @@ function* enumerate(seq) {
*/
const trySkip = curry('trySkip', (seq, no) => {
const it = iter(seq);
each(range(0, no), () => it.next());
each(range0(no), () => it.next());
return it;
});

Expand Down Expand Up @@ -917,6 +933,19 @@ const tryTake = curry('tryTake', function* tryTake(seq, no) {
}
});

/**
* Version of tryTake that will throw IteratorEnded
* if the given iterable is too short.
* @returns {Array}
*/
const take = curry('take', (seq, no) => {
const r = list(tryTake(seq, no));
if (size(r) < no) {
throw new IteratorEnded();
}
return r;
});

/**
* Cut off the sequence at the first point where the given condition is no
* longer met.
Expand Down Expand Up @@ -1038,6 +1067,9 @@ const zipLeast = seqs => pipe(
map(list),
);

/** Curryable version of zipLeast */
const zipLeast2 = curry('zipLeast2', (a, b) => zipLeast([a, b]));

/**
* Zip multiple sequences.
* Puts all the first values from sequences into one sublist;
Expand All @@ -1059,6 +1091,9 @@ function* zip(seqs) {
}
}

/** Curryable version of zip */
const zip2 = curry('zip2', (a, b) => zip([a, b]));

/**
* Zip multiple sequences.
* Puts all the first values from sequences into one sublist;
Expand All @@ -1077,7 +1112,129 @@ const zipLongest = curry('zipLongest', (seqs, fallback) => pipe(
map(list),
));

/** Curryable version of zipLongest */
const zipLongest2 = curry('zipLongest2', (a, b, fallback) => zipLongest([a, b], fallback));

/**
* Forms a sliding window on the underlying iterator.
*
* `slidingWindow([1,2,3,4,5], 3)`
* yields `[[1,2,3], [2,3,4], [3,4,5]]`
*
* Will throw IteratorEnded if the sequence is shorter than
* the given window.
*
* @param {Sequence} seq A sequence of sequences
* @returns {Iterator} Iterator of lists
*/
const slidingWindow = curry('slidingWindow', (seq, no) => {
const it = iter(seq);
const cache = [];
each(range0(no), () => cache.push(next(it)));

// By making just this part a generator we make
// sure that an error while filling the cache
// above is thrown early
function* slidingWindowImpl() {
yield list(cache);
for (const v of it) {
cache.shift();
cache.push(v);
yield list(cache);
}
}

return slidingWindowImpl();
});

/**
* Like slidingWindow, but returns an empty sequence if the given
* sequence is too short.
*/
const trySlidingWindow = curry('trySlidingWindow', function* trySlidingWindow(seq, no) {
const it = iter(seq);
const cache = [];
for (let idx = 0; idx < no; idx += 1) {
const { value, done } = it.next();
if (done) {
return;
}
cache.push(value);
}

yield list(cache);
for (const v of it) {
cache.shift();
cache.push(v);
yield list(cache);
}
});

/**
* Almost like trySlidingWindow, but makes sure that
* every element from the sequence gets it's own subarray,
* even the last element. The arrays at the end are filled
* with the filler value to make sure they have the correct
* length.
*
* ```
* lookahead([], 3, null) # => []
* lookahead([42], 3, null) # => [[42, null, null, null]]
* lookahead([42, 23], 3, null) # => [[42, 23, null, null], [23, null, null, null]]
* lookahead([42, 23], 0, null) # => [[42], [23]]
* ```
*
* Try sliding window would yield an empty array in each of the examples
* above.
*/
const lookahead = curry('lookahead', (seq, no, filler) => {
const filled = concat(seq, take(repeat(filler), no));
return trySlidingWindow(filled, no + 1);
});

// TRANSFORMING NON SEQUENCES ////////////////////////////////

/**
* Modify/Transform the given value.
*
* Applys the given value to the given function; after the return
* value is known, that return value is converted into the type
* of the given parameter.
*
* ```
* const s = new Set([1,2,3,4]);
* const z = mod1(s, map(plus(1))); # => new Set([2,3,4,5]),
* assert(z.constructor === Set)
* ```
*
* @param {Any} v The value to transform
* @param {Function} Fn The transformation function
* @returns {typeof v}
*/
const mod = curry('mod', (v, fn) => into(type(v))(fn(v)));

/**
* Combine multiple map/set like objects.
*
* The return type is always the type of the first value.
* Internally this just concatenates the values from all
* parameters and then uses into to convert the values back
* to the original type.
*
* `union({a: 42, b: 23}, new Map([['b', 99]]))` => `{a: 42, b: 99}`
* `union(new Set(1,2,3,4), [4,6,99])` => `new Set([1,2,3,4,6,99])`AA
*
* Takes any number of values to combine.
*/
const union = (fst, ...args) => into(type(fst))(concat(fst, ...args));

/** Curryable version of union */
const union2 = curry('union2', (a, b) => union(a, b));

module.exports = {
isdef,
type,
typename,
exec,
identity,
compose,
Expand Down Expand Up @@ -1119,6 +1276,8 @@ module.exports = {
list,
uniq,
join,
dict,
obj,
into,
IntoNotImplemented,
foldl,
Expand All @@ -1129,12 +1288,14 @@ module.exports = {
product,
map,
filter,
reject,
reverse,
enumerate,
trySkip,
skip,
skipWhile,
tryTake,
take,
takeWhile,
takeUntilVal,
takeDef,
Expand All @@ -1146,4 +1307,13 @@ module.exports = {
zipLeast,
zip,
zipLongest,
zipLeast2,
zip2,
zipLongest2,
slidingWindow,
trySlidingWindow,
lookahead,
mod,
union,
union2,
};
72 changes: 72 additions & 0 deletions src/string.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2018 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

const {
pipe, filter, map, count, foldl, join, size,
} = require('./sequence.js');

/**
* Helpers for working with strings.
*/

/**
* This is a helper for declaring multiline strings.
*
* ```
* const s = multiline(`
* Foo
* Bar
* Baz
*
* Hello
*
* Bang
* `);
* ```
*
* The function basically just takes a string and then
* strips the first & last lines if they are empty.
*
* In order to remove indentation, we determine the common
* whitespace prefix length (number of space 0x20 characters
* at the start of the line). This prefix is simply removed
* from each line...
*/
const multiline = (str) => {
// Discard the leading & trailing line
const lines = str.split('\n');

// Strip the first and the last line
if (lines[0].match(/^\s*$/)) {
lines.shift();
}
if (size(lines) > 0 && lines[size(lines) - 1].match(/^\s*$/)) {
lines.pop();
}

// Find the prefix length
const prefixLen = pipe(
lines,
filter(l => !l.match(/^\s*$/)), // Disregarding empty lines
map(l => l.match(/^ */)[0]), // Extract prefixes
map(count), // calculate length
foldl(Infinity, (a, b) => Math.min(a, b)),
); // minimum

return pipe(
lines,
map(l => l.slice(prefixLen)), // discard prefixes
join('\n'),
);
};

module.exports = { multiline };
Loading

0 comments on commit d5544d5

Please sign in to comment.