diff --git a/.prettierrc b/.prettierrc
index c35a73b5..8581ce3c 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -7,7 +7,7 @@
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
- "jsxBracketSameLine": false,
+ "bracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e29f3d2d..fe110897 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -82,6 +82,10 @@ Please report any problems you've had with the setup instructions!
## Code Style
To make sure the code changes only where it should, we are using prettier to unify the code style.
+
+- You can format all files at once by running `pnpm prettier` from the project root
+- Run `pnpm format-check` from the project root to check if all files are well formatted
+
If you use VSCode, you can
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
@@ -89,6 +93,24 @@ If you use VSCode, you can
3. Choose "Configure Default Formatter..."
4. Select prettier
+## ESLint
+
+To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
+
+- You can check for lint errors by running `pnpm lint`
+
+There are also eslint extensions / plugins for most editors.
+
+## Running Tests
+
+- Run all tests with `pnpm test`
+- Run all tests with UI using `pnpm test-ui`
+
+## Running all CI Checks
+
+When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
+You can run the same check with `pnpm check`
+
## Package Workflow
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
diff --git a/my-patterns/README.md b/my-patterns/README.md
index b7f416c6..1cec5e57 100644
--- a/my-patterns/README.md
+++ b/my-patterns/README.md
@@ -20,11 +20,8 @@ Example:
### 6. edit `website/astro.config.mjs` to use site: `https://.github.io` and base `/strudel`, like this
```js
-export default defineConfig({
- /* ... rest of config ... */
- site: 'https://.github.io',
- base: '/strudel',
-});
+const site = 'https://.github.io';
+const base = '/strudel';
```
### 7. commit & push the changes
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 4e230240..c07045ff 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see .
*/
-import { Pattern, sequence } from './pattern.mjs';
+import { Pattern, sequence, registerControl } from './pattern.mjs';
const controls = {};
const generic_params = [
@@ -828,26 +828,26 @@ const generic_params = [
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
-const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
-
-const _setter = (func, name) =>
- function (...pats) {
+const makeControl = function (name) {
+ const func = (...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
+ const setter = function (...pats) {
if (!pats.length) {
return this.fmap((value) => ({ [name]: value }));
}
return this.set(func(...pats));
};
+ Pattern.prototype[name] = setter;
+ registerControl(name, func);
+ return func;
+};
generic_params.forEach(([type, name, description]) => {
- controls[name] = (...pats) => _name(name, ...pats);
- Pattern.prototype[name] = _setter(controls[name], name);
+ controls[name] = makeControl(name);
});
// create custom param
controls.createParam = (name) => {
- const func = (...pats) => _name(name, ...pats);
- Pattern.prototype[name] = _setter(func, name);
- return (...pats) => _name(name, ...pats);
+ return makeControl(name);
};
controls.createParams = (...names) =>
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index b9c9e24c..1184e1a1 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -21,6 +21,141 @@ let stringParser;
// intended to use with mini to automatically interpret all strings as mini notation
export const setStringParser = (parser) => (stringParser = parser);
+const alignments = ['in', 'out', 'mix', 'squeeze', 'squeezeout', 'trig', 'trigzero'];
+
+const methodRegistry = [];
+const getterRegistry = [];
+const controlRegistry = [];
+const controlSubscribers = [];
+const composifiedRegistry = [];
+
+//////////////////////////////////////////////////////////////////////
+// Magic for supporting higher order composition of method chains
+
+// Dresses the given (unary) function with methods for composition chaining, so e.g.
+// `fast(2).iter(4)` composes to pattern functions into a new one.
+function composify(func) {
+ if (!func.__composified) {
+ for (const [name, method] of methodRegistry) {
+ func[name] = method;
+ }
+ for (const [name, getter] of getterRegistry) {
+ Object.defineProperty(func, name, getter);
+ }
+ func.__composified = true;
+ composifiedRegistry.push(func);
+ } else {
+ console.log('Warning: attempt at composifying a function more than once');
+ }
+ return func;
+}
+
+export function registerMethod(name, addAlignments = false, addControls = false) {
+ if (addAlignments || addControls) {
+ // This method needs to make its 'this' object available to chained alignments and/or
+ // control parameters, so it has to be implemented as a getter
+ const getter = {
+ get: function () {
+ const func = this;
+ const wrapped = function (...args) {
+ const composed = (pat) => func(pat)[name](...args);
+ return composify(composed);
+ };
+
+ if (addAlignments) {
+ for (const alignment of alignments) {
+ wrapped[alignment] = function (...args) {
+ const composed = (pat) => func(pat)[name][alignment](...args);
+ return composify(composed);
+ };
+ for (const [controlname, controlfunc] of controlRegistry) {
+ wrapped[alignment][controlname] = function (...args) {
+ const composed = (pat) => func(pat)[name][alignment](controlfunc(...args));
+ return composify(composed);
+ };
+ }
+ }
+ }
+ if (addControls) {
+ for (const [controlname, controlfunc] of controlRegistry) {
+ wrapped[controlname] = function (...args) {
+ const composed = (pat) => func(pat)[name](controlfunc(...args));
+ return composify(composed);
+ };
+ }
+ }
+ return wrapped;
+ },
+ };
+
+ getterRegistry.push([name, getter]);
+
+ // Add to functions already 'composified'
+ for (const composified of composifiedRegistry) {
+ Object.defineProperty(composified, name, getter);
+ }
+ } else {
+ // No chained alignments/controls needed, so we can just add as a plain method,
+ // probably more efficient this way?
+ const method = function (...args) {
+ const func = this;
+ const composed = (pat) => func(pat)[name](...args);
+ return composify(composed);
+ };
+
+ methodRegistry.push([name, method]);
+
+ // Add to functions already 'composified'
+ for (const composified of composifiedRegistry) {
+ composified[name] = method;
+ }
+ }
+}
+
+export function registerControl(controlname, controlfunc) {
+ registerMethod(controlname);
+ controlRegistry.push([controlname, controlfunc]);
+ for (const subscriber of controlSubscribers) {
+ subscriber(controlname, controlfunc);
+ }
+}
+
+export function withControls(func) {
+ for (const [controlname, controlfunc] of controlRegistry) {
+ func(controlname, controlfunc);
+ }
+ controlSubscribers.push(func);
+}
+
+export function addToPrototype(name, func) {
+ Pattern.prototype[name] = func;
+ registerMethod(name);
+}
+
+export function curryPattern(func, arity = func.length) {
+ const fn = function curried(...args) {
+ if (args.length >= arity) {
+ return func.apply(this, args);
+ }
+
+ const partial = function (...args2) {
+ return curried.apply(this, args.concat(args2));
+ };
+ if (args.length == arity - 1) {
+ return composify(partial);
+ }
+
+ return partial;
+ };
+ if (arity == 1) {
+ composify(fn);
+ }
+ return fn;
+}
+
+//////////////////////////////////////////////////////////////////////
+// The core Pattern class
+
/** @class Class representing a pattern. */
export class Pattern {
/**
@@ -643,7 +778,9 @@ export class Pattern {
* @noAutocomplete
*/
get firstCycleValues() {
- return this.firstCycle().map((hap) => hap.value);
+ return this.sortHapsByPart()
+ .firstCycle()
+ .map((hap) => hap.value);
}
/**
@@ -693,7 +830,7 @@ export class Pattern {
const otherPat = reify(other);
return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin();
}
- _opSqueezeOut(other, func) {
+ _opSqueezeout(other, func) {
const thisPat = this;
const otherPat = reify(other);
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
@@ -860,11 +997,11 @@ function groupHapsBy(eq, haps) {
const congruent = (a, b) => a.spanEquals(b);
// Pattern> -> Pattern>
// returned pattern contains arrays of congruent haps
-Pattern.prototype.collect = function () {
+addToPrototype('collect', function () {
return this.withHaps((haps) =>
groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})),
);
-};
+});
/**
* Selects indices in in stacked notes.
@@ -872,12 +1009,12 @@ Pattern.prototype.collect = function () {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arpWith(haps => haps[2])
* */
-Pattern.prototype.arpWith = function (func) {
+addToPrototype('arpWith', function (func) {
return this.collect()
.fmap((v) => reify(func(v)))
.innerJoin()
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
-};
+});
/**
* Selects indices in in stacked notes.
@@ -885,9 +1022,9 @@ Pattern.prototype.arpWith = function (func) {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]").slow(2)
* */
-Pattern.prototype.arp = function (pat) {
+addToPrototype('arp', function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
-};
+});
//////////////////////////////////////////////////////////////////////
// compose matrix functions
@@ -985,15 +1122,15 @@ function _composeOp(a, b, func) {
func: [(a, b) => b(a)],
};
- const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero'];
+ const hows = alignments.map((x) => x.charAt(0).toUpperCase() + x.slice(1));
// generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) {
// make plain version, e.g. pat._add(value) adds that plain value
// to all the values in pat
- Pattern.prototype['_' + what] = function (value) {
+ addToPrototype('_' + what, function (value) {
return this.fmap((x) => op(x, value));
- };
+ });
// make patternified monster version
Object.defineProperty(Pattern.prototype, what, {
@@ -1007,7 +1144,7 @@ function _composeOp(a, b, func) {
// add methods to that function for each behaviour
for (const how of hows) {
- wrapper[how.toLowerCase()] = function (...other) {
+ const howfunc = function (...other) {
var howpat = pat;
other = sequence(other);
if (preprocess) {
@@ -1025,19 +1162,41 @@ function _composeOp(a, b, func) {
}
return result;
};
+
+ for (const [controlname, controlfunc] of controlRegistry) {
+ howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
+ }
+ wrapper[how.toLowerCase()] = howfunc;
}
wrapper.squeezein = wrapper.squeeze;
+ for (const [controlname, controlfunc] of controlRegistry) {
+ wrapper[controlname] = (...args) => wrapper.in(controlfunc(...args));
+ }
+
return wrapper;
},
});
- // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
- for (const how of hows) {
- Pattern.prototype[how.toLowerCase()] = function (...args) {
- return this.set[how.toLowerCase()](args);
- };
- }
+ registerMethod(what, true, true);
+ }
+
+ // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
+ for (const howLower of alignments) {
+ // Using a 'get'ted function so that all the controls are added
+ Object.defineProperty(Pattern.prototype, howLower, {
+ get: function () {
+ const pat = this;
+ const howfunc = function (...args) {
+ return pat.set[howLower](args);
+ };
+ for (const [controlname, controlfunc] of controlRegistry) {
+ howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
+ }
+ return howfunc;
+ },
+ });
+ registerMethod(howLower, false, true);
}
// binary composers
@@ -1049,36 +1208,36 @@ function _composeOp(a, b, func) {
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
* .slow(4)
*/
- Pattern.prototype.struct = function (...args) {
+ addToPrototype('struct', function (...args) {
return this.keepif.out(...args);
- };
- Pattern.prototype.structAll = function (...args) {
+ });
+ addToPrototype('structAll', function (...args) {
return this.keep.out(...args);
- };
+ });
/**
* Returns silence when mask is 0 or "~"
*
* @example
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
*/
- Pattern.prototype.mask = function (...args) {
+ addToPrototype('mask', function (...args) {
return this.keepif.in(...args);
- };
- Pattern.prototype.maskAll = function (...args) {
+ });
+ addToPrototype('maskAll', function (...args) {
return this.keep.in(...args);
- };
+ });
/**
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
*
* @example
* s(" sd, hh*4").reset("")
*/
- Pattern.prototype.reset = function (...args) {
+ addToPrototype('reset', function (...args) {
return this.keepif.trig(...args);
- };
- Pattern.prototype.resetAll = function (...args) {
+ });
+ addToPrototype('resetAll', function (...args) {
return this.keep.trig(...args);
- };
+ });
/**
* Restarts the pattern for each onset of the restart pattern.
* While reset will only reset the current cycle, restart will start from cycle 0.
@@ -1086,12 +1245,12 @@ function _composeOp(a, b, func) {
* @example
* s(" sd, hh*4").restart("")
*/
- Pattern.prototype.restart = function (...args) {
+ addToPrototype('restart', function (...args) {
return this.keepif.trigzero(...args);
- };
- Pattern.prototype.restartAll = function (...args) {
+ });
+ addToPrototype('restartAll', function (...args) {
return this.keep.trigzero(...args);
- };
+ });
})();
// aliases
@@ -1336,36 +1495,68 @@ export function pm(...args) {
polymeter(...args);
}
-export const mask = curry((a, b) => reify(b).mask(a));
-export const struct = curry((a, b) => reify(b).struct(a));
-export const superimpose = curry((a, b) => reify(b).superimpose(...a));
+export const mask = curryPattern((a, b) => reify(b).mask(a));
+export const struct = curryPattern((a, b) => reify(b).struct(a));
+export const superimpose = curryPattern((a, b) => reify(b).superimpose(...a));
+
+const methodToFunction = function (name, addAlignments = false) {
+ const func = curryPattern((a, b) => reify(b)[name](a));
+
+ withControls((controlname, controlfunc) => {
+ func[controlname] = function (...pats) {
+ return func(controlfunc(...pats));
+ };
+ });
+
+ if (addAlignments) {
+ for (const alignment of alignments) {
+ func[alignment] = curryPattern((a, b) => reify(b)[name][alignment](a));
+ withControls((controlname, controlfunc) => {
+ func[alignment][controlname] = function (...pats) {
+ return func[alignment](controlfunc(...pats));
+ };
+ });
+ }
+ }
+
+ return func;
+};
// operators
-export const set = curry((a, b) => reify(b).set(a));
-export const keep = curry((a, b) => reify(b).keep(a));
-export const keepif = curry((a, b) => reify(b).keepif(a));
-export const add = curry((a, b) => reify(b).add(a));
-export const sub = curry((a, b) => reify(b).sub(a));
-export const mul = curry((a, b) => reify(b).mul(a));
-export const div = curry((a, b) => reify(b).div(a));
-export const mod = curry((a, b) => reify(b).mod(a));
-export const pow = curry((a, b) => reify(b).pow(a));
-export const band = curry((a, b) => reify(b).band(a));
-export const bor = curry((a, b) => reify(b).bor(a));
-export const bxor = curry((a, b) => reify(b).bxor(a));
-export const blshift = curry((a, b) => reify(b).blshift(a));
-export const brshift = curry((a, b) => reify(b).brshift(a));
-export const lt = curry((a, b) => reify(b).lt(a));
-export const gt = curry((a, b) => reify(b).gt(a));
-export const lte = curry((a, b) => reify(b).lte(a));
-export const gte = curry((a, b) => reify(b).gte(a));
-export const eq = curry((a, b) => reify(b).eq(a));
-export const eqt = curry((a, b) => reify(b).eqt(a));
-export const ne = curry((a, b) => reify(b).ne(a));
-export const net = curry((a, b) => reify(b).net(a));
-export const and = curry((a, b) => reify(b).and(a));
-export const or = curry((a, b) => reify(b).or(a));
-export const func = curry((a, b) => reify(b).func(a));
+export const set = methodToFunction('set', true);
+export const keep = methodToFunction('keep', true);
+export const keepif = methodToFunction('keepif', true);
+export const add = methodToFunction('add', true);
+export const sub = methodToFunction('sub', true);
+export const mul = methodToFunction('mul', true);
+export const div = methodToFunction('div', true);
+export const mod = methodToFunction('mod', true);
+export const pow = methodToFunction('pow', true);
+export const band = methodToFunction('band', true);
+export const bor = methodToFunction('bor', true);
+export const bxor = methodToFunction('bxor', true);
+export const blshift = methodToFunction('blshift', true);
+export const brshift = methodToFunction('brshift', true);
+export const lt = methodToFunction('lt', true);
+export const gt = methodToFunction('gt', true);
+export const lte = methodToFunction('lte', true);
+export const gte = methodToFunction('gte', true);
+export const eq = methodToFunction('eq', true);
+export const eqt = methodToFunction('eqt', true);
+export const ne = methodToFunction('ne', true);
+export const net = methodToFunction('net', true);
+export const and = methodToFunction('and', true);
+export const or = methodToFunction('or', true);
+export const func = methodToFunction('func', true);
+
+// alignments
+// export const in = methodToFunction('in'); // reserved word :(
+export const out = methodToFunction('out');
+export const mix = methodToFunction('mix');
+export const squeeze = methodToFunction('squeeze');
+export const squeezeout = methodToFunction('squeezeout');
+export const trig = methodToFunction('trig');
+export const trigzero = methodToFunction('trigzero');
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
@@ -1384,9 +1575,10 @@ export function register(name, func) {
return result;
}
const arity = func.length;
- var pfunc; // the patternified function
- pfunc = function (...args) {
+ registerMethod(name);
+
+ const pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
@@ -1402,8 +1594,12 @@ export function register(name, func) {
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
};
- mapFn = curry(mapFn, null, arity - 1);
- return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
+ mapFn = curryPattern(mapFn, arity - 1);
+
+ const app = (acc, p) => acc.appLeft(p);
+ const start = left.fmap(mapFn);
+
+ return right.reduce(app, start).innerJoin();
};
Pattern.prototype[name] = function (...args) {
@@ -1428,7 +1624,7 @@ export function register(name, func) {
// toplevel functions get curried as well as patternified
// because pfunc uses spread args, we need to state the arity explicitly!
- return curry(pfunc, null, arity);
+ return curryPattern(pfunc, arity);
}
//////////////////////////////////////////////////////////////////////
diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs
index 08638d18..2142242d 100644
--- a/packages/core/test/pattern.test.mjs
+++ b/packages/core/test/pattern.test.mjs
@@ -45,6 +45,8 @@ import {
rev,
time,
run,
+ hitch,
+ set,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -204,7 +206,7 @@ describe('Pattern', () => {
),
);
});
- it('can SqueezeOut() structure', () => {
+ it('can squeezeout() structure', () => {
sameFirst(
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
@@ -252,7 +254,7 @@ describe('Pattern', () => {
),
);
});
- it('can SqueezeOut() structure', () => {
+ it('can squeezeout() structure', () => {
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
});
});
@@ -294,7 +296,7 @@ describe('Pattern', () => {
),
);
});
- it('can SqueezeOut() structure', () => {
+ it('can squeezeout() structure', () => {
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
});
});
@@ -929,6 +931,14 @@ describe('Pattern', () => {
});
});
describe('alignments', () => {
+ it('Can combine controls', () => {
+ sameFirst(s('bd').set.in.n(3), s('bd').n(3));
+ sameFirst(s('bd').set.squeeze.n(3, 4), sequence(s('bd').n(3), s('bd').n(4)));
+ });
+ it('Can combine functions with alignmed controls', () => {
+ sameFirst(s('bd').apply(fast(2).set(n(3))), s('bd').fast(2).set.in.n(3));
+ sameFirst(s('bd').apply(fast(2).set.in.n(3)), s('bd').fast(2).set.in.n(3));
+ });
it('Can squeeze arguments', () => {
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
});
@@ -959,4 +969,16 @@ describe('Pattern', () => {
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
});
});
+ describe('composable functions', () => {
+ it('Can compose functions', () => {
+ sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
+ });
+ it('Can compose by method chaining operators with controls', () => {
+ sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
+ });
+ it('Can compose by method chaining operators and alignments with controls', () => {
+ sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
+ // sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
+ });
+ });
});
diff --git a/packages/core/util.mjs b/packages/core/util.mjs
index 3127e0d1..6ba8f397 100644
--- a/packages/core/util.mjs
+++ b/packages/core/util.mjs
@@ -139,7 +139,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
export const flatten = (arr) => [].concat(...arr);
export const id = (a) => a;
-export const constant = (a, b) => a;
+export const constant = curry((a, b) => a);
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f61104a6..97aa6c81 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -387,6 +387,7 @@ importers:
react-dom: ^18.2.0
rehype-autolink-headings: ^6.1.1
rehype-slug: ^5.0.1
+ rehype-urls: ^1.1.1
remark-toc: ^8.0.1
tailwindcss: ^3.2.4
vite-plugin-pwa: ^0.14.1
@@ -428,6 +429,7 @@ importers:
react-dom: 18.2.0_react@18.2.0
rehype-autolink-headings: 6.1.1
rehype-slug: 5.1.0
+ rehype-urls: 1.1.1
remark-toc: 8.0.1
tailwindcss: 3.2.4
devDependencies:
@@ -7432,6 +7434,10 @@ packages:
web-namespaces: 2.0.1
dev: false
+ /hast-util-has-property/1.0.4:
+ resolution: {integrity: sha512-ghHup2voGfgFoHMGnaLHOjbYFACKrRh9KFttdCzMCbFoBMJXiNi2+XTrPP8+q6cDJM/RSqlCfVWrjp1H201rZg==}
+ dev: false
+
/hast-util-has-property/2.0.1:
resolution: {integrity: sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==}
dev: false
@@ -7775,6 +7781,10 @@ packages:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true
+ /is-arrayish/0.3.2:
+ resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
+ dev: false
+
/is-bigint/1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies:
@@ -11110,6 +11120,14 @@ packages:
unified: 10.1.2
dev: false
+ /rehype-urls/1.1.1:
+ resolution: {integrity: sha512-ct9Kb/nAL6oe/O5fDc0xjiqm8Z9xgXdorOdDhZAWx7awucyiuYXU7Dax+23Gu24nnGwtdaCW6zslKAYzlEW1lw==}
+ dependencies:
+ hast-util-has-property: 1.0.4
+ stdopt: 2.2.0
+ unist-util-visit: 1.4.1
+ dev: false
+
/rehype/12.0.1:
resolution: {integrity: sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==}
dependencies:
@@ -11839,6 +11857,12 @@ packages:
tslib: 2.5.0
dev: false
+ /stdopt/2.2.0:
+ resolution: {integrity: sha512-D/p41NgXOkcj1SeGhfXOwv9z1K6EV3sjAUY5aeepVbgEHv7DpKWLTjhjScyzMWAQCAgUQys1mjH0eArm4cjRGw==}
+ dependencies:
+ is-arrayish: 0.3.2
+ dev: false
+
/stream-connect/1.0.2:
resolution: {integrity: sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ==}
engines: {node: '>=0.10.0'}
@@ -12707,6 +12731,10 @@ packages:
resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==}
dev: false
+ /unist-util-is/3.0.0:
+ resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==}
+ dev: false
+
/unist-util-is/5.2.0:
resolution: {integrity: sha512-Glt17jWwZeyqrFqOK0pF1Ded5U3yzJnFr8CG1GMjCWTp9zDo2p+cmD6pWbZU8AgM5WU3IzRv6+rBwhzsGh6hBQ==}
dev: false
@@ -12755,6 +12783,12 @@ packages:
'@types/unist': 2.0.6
dev: false
+ /unist-util-visit-parents/2.1.2:
+ resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==}
+ dependencies:
+ unist-util-is: 3.0.0
+ dev: false
+
/unist-util-visit-parents/5.1.3:
resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==}
dependencies:
@@ -12762,6 +12796,12 @@ packages:
unist-util-is: 5.2.0
dev: false
+ /unist-util-visit/1.4.1:
+ resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==}
+ dependencies:
+ unist-util-visit-parents: 2.1.2
+ dev: false
+
/unist-util-visit/4.1.2:
resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==}
dependencies:
diff --git a/website/astro.config.mjs b/website/astro.config.mjs
index f8f345b8..a90ce0f6 100644
--- a/website/astro.config.mjs
+++ b/website/astro.config.mjs
@@ -1,24 +1,47 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
-
import mdx from '@astrojs/mdx';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
+import rehypeUrls from 'rehype-urls';
import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro';
// import { visualizer } from 'rollup-plugin-visualizer';
+const site = `https://strudel.tidalcycles.org`; // root url without a path
+const base = '/'; // base path of the strudel site
+
+// this rehype plugin converts relative anchor links to absolute ones
+// it wokrs by prepending the absolute page path to anchor links
+// example: #gain -> /learn/effects/#gain
+// this is necessary when using a base href like
+// in this setup, relative anchor links will always link to base, instead of the current page
+function absoluteAnchors() {
+ return (tree, file) => {
+ const chunks = file.history[0].split('/src/pages/'); // file.history[0] is the file path
+ const path = chunks[chunks.length - 1].slice(0, -4); // only path inside src/pages, without .mdx
+ return rehypeUrls((url) => {
+ if (!url.href.startsWith('#')) {
+ return;
+ }
+ const baseWithSlash = base.endsWith('/') ? base : base + '/';
+ const absoluteUrl = baseWithSlash + path + url.href;
+ // console.log(url.href + ' -> ', absoluteUrl);
+ return absoluteUrl;
+ })(tree);
+ };
+}
const options = {
// See https://mdxjs.com/advanced/plugins
remarkPlugins: [
remarkToc,
// E.g. `remark-frontmatter`
],
- rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
+ rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append' }], absoluteAnchors],
};
// https://astro.build/config
@@ -97,8 +120,8 @@ export default defineConfig({
},
}),
],
- site: `https://strudel.tidalcycles.org`,
- base: '/',
+ site,
+ base,
vite: {
ssr: {
// Example: Force a broken package to skip SSR processing, if needed
@@ -106,13 +129,3 @@ export default defineConfig({
},
},
});
-
-/*
- build: {
- outDir: '../out',
- sourcemap: true,
- rollupOptions: {
- plugins: [visualizer({ template: 'treemap' })],
- },
- },
- */
diff --git a/website/package.json b/website/package.json
index 5393d1b8..f8c466d3 100644
--- a/website/package.json
+++ b/website/package.json
@@ -48,6 +48,7 @@
"react-dom": "^18.2.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
+ "rehype-urls": "^1.1.1",
"remark-toc": "^8.0.1",
"tailwindcss": "^3.2.4"
},
diff --git a/website/src/components/RightSidebar/RightSidebar.astro b/website/src/components/RightSidebar/RightSidebar.astro
index ce16b9c0..e6031e4d 100644
--- a/website/src/components/RightSidebar/RightSidebar.astro
+++ b/website/src/components/RightSidebar/RightSidebar.astro
@@ -9,7 +9,9 @@ type Props = {
};
const { headings, githubEditUrl } = Astro.props as Props;
-const currentPage = Astro.url.pathname;
+let currentPage = Astro.url.pathname;
+// remove slash before #
+currentPage = currentPage.endsWith('/') ? currentPage.slice(0, -1) : currentPage;
---