" };
46 | siblings.splice(
47 | lastChildIndex,
48 | 0,
49 | { type: "text", value: "\n" },
50 | { type: "jsx", value: "
" }
51 | );
52 | for (let prei = 1; prei < pres.length; prei++) {
53 | const siblingi = siblings.indexOf(pres[prei]);
54 | siblings.splice(
55 | siblingi,
56 | 1,
57 | { type: "jsx", value: "" },
58 | { type: "text", value: "\n" },
59 | { type: "jsx", value: "" }
60 | );
61 | }
62 |
63 | // parse the codeblocks into input steps
64 | const steps = pres.map(readStepFromElement);
65 |
66 | // parse the input steps
67 | const lang = steps[0].lang;
68 | if (!Prism.languages[lang]) {
69 | require(`prismjs/components/prism-${lang}`);
70 | }
71 |
72 | const s = parseSteps(steps);
73 |
74 | // pass the parsed steps prop to CodeWave
75 | node.value = node.value.replace(
76 | ">",
77 | ` parsedSteps={${JSON.stringify(s)}}>`
78 | );
79 | }
80 | });
81 | }
82 |
83 | function isOpenTag(value) {
84 | return /^\s*<([^\/>]*)>\s*$/.test(value);
85 | }
86 |
87 | function isCloseTag(value) {
88 | return /^\s*<\/([^\/>]*)>\s*$/.test(value);
89 | }
90 |
--------------------------------------------------------------------------------
/rehype-waves/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rehype-waves",
3 | "version": "0.1.6",
4 | "author": "Rodrigo Pombo",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/pomber/gatsby-theme-waves.git"
8 | },
9 | "license": "MIT",
10 | "devDependencies": {
11 | "@mdx-js/mdx": "^1.4.0",
12 | "jest": "^24.9.0",
13 | "jest-file-snapshot": "^0.3.7"
14 | },
15 | "scripts": {
16 | "test": "jest",
17 | "watch": "jest --watch -u"
18 | },
19 | "jest": {
20 | "watchPathIgnorePatterns": [
21 | "__file_snapshots__"
22 | ]
23 | },
24 | "dependencies": {
25 | "@code-surfer/step-parser": "3.1.1",
26 | "shell-quote": "^1.7.2",
27 | "unist-util-visit": "^2.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/rehype-waves/parser.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function _interopDefault(ex) {
4 | return ex && typeof ex === "object" && "default" in ex ? ex["default"] : ex;
5 | }
6 |
7 | var diff = require("diff");
8 | var React = _interopDefault(require("react"));
9 | var Prism = _interopDefault(require("prismjs"));
10 | var flat = _interopDefault(require("array.prototype.flat"));
11 |
12 | function _extends() {
13 | _extends =
14 | Object.assign ||
15 | function(target) {
16 | for (var i = 1; i < arguments.length; i++) {
17 | var source = arguments[i];
18 |
19 | for (var key in source) {
20 | if (Object.prototype.hasOwnProperty.call(source, key)) {
21 | target[key] = source[key];
22 | }
23 | }
24 | }
25 |
26 | return target;
27 | };
28 |
29 | return _extends.apply(this, arguments);
30 | }
31 |
32 | function _objectWithoutPropertiesLoose(source, excluded) {
33 | if (source == null) return {};
34 | var target = {};
35 | var sourceKeys = Object.keys(source);
36 | var key, i;
37 |
38 | for (i = 0; i < sourceKeys.length; i++) {
39 | key = sourceKeys[i];
40 | if (excluded.indexOf(key) >= 0) continue;
41 | target[key] = source[key];
42 | }
43 |
44 | return target;
45 | }
46 |
47 | function grammarNotFound(_ref2) {
48 | var lang = _ref2.lang;
49 | return {
50 | element: React.createElement(ErrorBox, {
51 | header: "Oops, there's a problem",
52 | body: React.createElement(
53 | React.Fragment,
54 | null,
55 | "Syntax highlighter for ",
56 | React.createElement(Mark, null, '"', lang, '"'),
57 | " not found.",
58 | React.createElement(
59 | "p",
60 | null,
61 | "You can try importing it from prismjs with: ",
62 | React.createElement("br", null),
63 | React.createElement(
64 | Mark,
65 | null,
66 | 'import "prismjs/components/prism-',
67 | lang,
68 | '"'
69 | )
70 | ),
71 | "(See",
72 | " ",
73 | React.createElement(
74 | "a",
75 | {
76 | href: "https://prismjs.com/#supported-languages",
77 | style: {
78 | color: "grey"
79 | }
80 | },
81 | "all the supported languages"
82 | ),
83 | ")"
84 | )
85 | })
86 | };
87 | }
88 | function invalidFocusNumber(n) {
89 | return {
90 | withFocusString: function withFocusString(focusString) {
91 | return {
92 | withStepIndex: function withStepIndex(stepIndex) {
93 | return {
94 | element: React.createElement(ErrorBox, {
95 | header: React.createElement(StepErrorHeader, {
96 | stepIndex: stepIndex
97 | }),
98 | body: React.createElement(
99 | React.Fragment,
100 | null,
101 | React.createElement(Mark, null, '"', n, '"'),
102 | " isn't a valid number",
103 | " ",
104 | n != focusString &&
105 | React.createElement(Mark, null, ' (in "', focusString, '")')
106 | )
107 | })
108 | };
109 | }
110 | };
111 | }
112 | };
113 | }
114 | function invalidLineOrColumnNumber() {
115 | return {
116 | withFocusString: function withFocusString(focusString) {
117 | return {
118 | withStepIndex: function withStepIndex(stepIndex) {
119 | return {
120 | element: React.createElement(ErrorBox, {
121 | header: React.createElement(StepErrorHeader, {
122 | stepIndex: stepIndex
123 | }),
124 | body: React.createElement(
125 | React.Fragment,
126 | null,
127 | 'Are you using "0" as a line or column number',
128 | " ",
129 | React.createElement(Mark, null, 'in "', focusString, '"'),
130 | "?",
131 | React.createElement("br", null),
132 | "(Line and column numbers should start at 1, not 0) ",
133 | React.createElement("br", null)
134 | )
135 | })
136 | };
137 | }
138 | };
139 | }
140 | };
141 | }
142 |
143 | function ErrorBox(_ref3) {
144 | var header = _ref3.header,
145 | body = _ref3.body;
146 | return React.createElement(
147 | "div",
148 | {
149 | style: {
150 | background: "#290000",
151 | color: "#b96f70",
152 | border: "2px solid #b96f70",
153 | padding: "10px 30px",
154 | maxWidth: "90vw",
155 | margin: "0 auto",
156 | fontFamily: "monospace",
157 | fontSize: "1rem"
158 | }
159 | },
160 | React.createElement("h3", null, header),
161 | React.createElement("p", null, body)
162 | );
163 | }
164 |
165 | function StepErrorHeader(_ref4) {
166 | var stepIndex = _ref4.stepIndex;
167 | return React.createElement(
168 | React.Fragment,
169 | null,
170 | "Oops, there's a problem with the",
171 | " ",
172 | React.createElement(
173 | Mark,
174 | null,
175 | stepIndex + 1,
176 | React.createElement("sup", null, ordinal(stepIndex + 1)),
177 | " step"
178 | )
179 | );
180 | }
181 |
182 | function Mark(_ref5) {
183 | var children = _ref5.children;
184 | return React.createElement(
185 | "mark",
186 | {
187 | style: {
188 | background: "none",
189 | color: "pink",
190 | fontWeight: "bolder"
191 | }
192 | },
193 | children
194 | );
195 | }
196 |
197 | function ordinal(i) {
198 | var j = i % 10,
199 | k = i % 100;
200 |
201 | if (j == 1 && k != 11) {
202 | return "st";
203 | }
204 |
205 | if (j == 2 && k != 12) {
206 | return "nd";
207 | }
208 |
209 | if (j == 3 && k != 13) {
210 | return "rd";
211 | }
212 |
213 | return "th";
214 | }
215 |
216 | var newlineRe = /\r\n|\r|\n/; // Take a list of nested tokens
217 | // (token.content may contain an array of tokens)
218 | // and flatten it so content is always a string
219 | // and type the type of the leaf
220 |
221 | function flattenTokens(tokens) {
222 | var flatList = [];
223 | tokens.forEach(function(token) {
224 | var type = token.type,
225 | content = token.content;
226 |
227 | if (Array.isArray(content)) {
228 | flatList.push.apply(flatList, flattenTokens(content));
229 | } else {
230 | flatList.push({
231 | type: type,
232 | content: content
233 | });
234 | }
235 | });
236 | return flatList;
237 | }
238 |
239 | function wrapToken(prismToken, parentType) {
240 | if (parentType === void 0) {
241 | parentType = "plain";
242 | }
243 |
244 | if (typeof prismToken === "string") {
245 | return {
246 | type: parentType,
247 | content: prismToken
248 | };
249 | }
250 |
251 | if (Array.isArray(prismToken.content)) {
252 | return {
253 | type: prismToken.type,
254 | content: tokenizeStrings(prismToken.content, prismToken.type)
255 | };
256 | }
257 |
258 | return wrapToken(prismToken.content, prismToken.type);
259 | } // Wrap strings in tokens
260 |
261 | function tokenizeStrings(prismTokens, parentType) {
262 | if (parentType === void 0) {
263 | parentType = "plain";
264 | }
265 |
266 | return prismTokens.map(function(prismToken) {
267 | return wrapToken(prismToken, parentType);
268 | });
269 | }
270 |
271 | function tokenize(code, language) {
272 | if (language === void 0) {
273 | language = "javascript";
274 | }
275 |
276 | var grammar = Prism.languages[language];
277 |
278 | if (!grammar) {
279 | throw grammarNotFound({
280 | lang: language
281 | });
282 | }
283 |
284 | var prismTokens = Prism.tokenize(code, Prism.languages[language]);
285 | var nestedTokens = tokenizeStrings(prismTokens);
286 | var tokens = flattenTokens(nestedTokens);
287 | var currentLine = [];
288 | var lines = [currentLine];
289 | tokens.forEach(function(token) {
290 | var contentLines = token.content.split(newlineRe);
291 | var firstContent = contentLines.shift();
292 |
293 | if (firstContent !== undefined && firstContent !== "") {
294 | currentLine.push({
295 | type: token.type,
296 | content: firstContent
297 | });
298 | }
299 |
300 | contentLines.forEach(function(content) {
301 | currentLine = [];
302 | lines.push(currentLine);
303 |
304 | if (content !== "") {
305 | currentLine.push({
306 | type: token.type,
307 | content: content
308 | });
309 | }
310 | });
311 | });
312 | return lines;
313 | }
314 |
315 | var newlineRe$1 = /\r\n|\r|\n/;
316 |
317 | function myDiff(oldCode, newCode) {
318 | var changes = diff.diffLines(oldCode || "", newCode);
319 | var oldIndex = -1;
320 | return changes.map(function(_ref) {
321 | var value = _ref.value,
322 | count = _ref.count,
323 | removed = _ref.removed,
324 | added = _ref.added;
325 | var lines = value.split(newlineRe$1); // check if last line is empty, if it is, remove it
326 |
327 | var lastLine = lines.pop();
328 |
329 | if (lastLine) {
330 | lines.push(lastLine);
331 | }
332 |
333 | var result = {
334 | oldIndex: oldIndex,
335 | lines: lines,
336 | count: count,
337 | removed: removed,
338 | added: added
339 | };
340 |
341 | if (!added) {
342 | oldIndex += count || 0;
343 | }
344 |
345 | return result;
346 | });
347 | }
348 |
349 | function insert(array, index, elements) {
350 | return array.splice.apply(array, [index, 0].concat(elements));
351 | }
352 |
353 | function slideDiff(lines, codes, slideIndex, language) {
354 | var prevLines = lines.filter(function(l) {
355 | return l.slides.includes(slideIndex - 1);
356 | });
357 | var prevCode = codes[slideIndex - 1] || "";
358 | var currCode = codes[slideIndex];
359 | var changes = myDiff(prevCode, currCode);
360 | changes.forEach(function(change) {
361 | if (change.added) {
362 | var prevLine = prevLines[change.oldIndex];
363 | var addAtIndex = lines.indexOf(prevLine) + 1;
364 | var addLines = change.lines.map(function(content) {
365 | return {
366 | content: content,
367 | slides: [slideIndex],
368 | tokens: []
369 | };
370 | });
371 | insert(lines, addAtIndex, addLines);
372 | } else if (!change.removed) {
373 | for (var j = 1; j <= (change.count || 0); j++) {
374 | prevLines[change.oldIndex + j].slides.push(slideIndex);
375 | }
376 | }
377 | });
378 | var tokenLines = tokenize(currCode, language);
379 | var currLines = lines.filter(function(l) {
380 | return l.slides.includes(slideIndex);
381 | });
382 | currLines.forEach(function(line, index) {
383 | return (line.tokens = tokenLines[index]);
384 | });
385 | }
386 |
387 | function parseLines(codes, language) {
388 | var lines = [];
389 |
390 | for (var slideIndex = 0; slideIndex < codes.length; slideIndex++) {
391 | slideDiff(lines, codes, slideIndex, language);
392 | }
393 |
394 | return lines;
395 | }
396 | function getSlides(codes, language) {
397 | // codes are in reverse cronological order
398 | var lines = parseLines(codes, language); // console.log("lines", lines);
399 |
400 | return codes.map(function(_, slideIndex) {
401 | return lines
402 | .map(function(line, lineIndex) {
403 | return {
404 | content: line.content,
405 | tokens: line.tokens,
406 | isNew: !line.slides.includes(slideIndex + 1),
407 | show: line.slides.includes(slideIndex),
408 | key: lineIndex
409 | };
410 | })
411 | .filter(function(line) {
412 | return line.show;
413 | });
414 | });
415 | }
416 | function getCodes(rawSteps) {
417 | var codes = [];
418 | rawSteps.forEach(function(s, i) {
419 | if (s.lang === "diff" && i > 0) {
420 | codes[i] = diff.applyPatch(codes[i - 1], s.code);
421 | } else {
422 | codes[i] = s.code;
423 | }
424 | });
425 | return codes;
426 | }
427 |
428 | function parseFocus(focus) {
429 | if (!focus) {
430 | throw new Error("Focus cannot be empty");
431 | }
432 |
433 | try {
434 | var parts = focus.split(/,(?![^\[]*\])/g).map(parsePart);
435 | return new Map(flat(parts));
436 | } catch (error) {
437 | if (error.withFocusString) {
438 | throw error.withFocusString(focus);
439 | } else {
440 | throw error;
441 | }
442 | }
443 | }
444 |
445 | function parsePart(part) {
446 | // a part could be
447 | // - a line number: "2"
448 | // - a line range: "5:9"
449 | // - a line number with a column selector: "2[1,3:5,9]"
450 | var columnsMatch = part.match(/(\d+)\[(.+)\]/);
451 |
452 | if (columnsMatch) {
453 | var line = columnsMatch[1],
454 | columns = columnsMatch[2];
455 | var columnsList = columns.split(",").map(expandString);
456 | var lineIndex = Number(line) - 1;
457 | var columnIndexes = flat(columnsList).map(function(c) {
458 | return c - 1;
459 | });
460 | return [[lineIndex, columnIndexes]];
461 | } else {
462 | return expandString(part).map(function(lineNumber) {
463 | return [lineNumber - 1, true];
464 | });
465 | }
466 | }
467 |
468 | function expandString(part) {
469 | // Transforms something like
470 | // - "1:3" to [1,2,3]
471 | // - "4" to [4]
472 | var _part$split = part.split(":"),
473 | start = _part$split[0],
474 | end = _part$split[1]; // todo check if start is 0, line numbers and column numbers start at 1
475 |
476 | if (!isNaturalNumber(start)) {
477 | throw invalidFocusNumber(start);
478 | }
479 |
480 | var startNumber = Number(start);
481 |
482 | if (startNumber < 1) {
483 | throw invalidLineOrColumnNumber();
484 | }
485 |
486 | if (!end) {
487 | return [startNumber];
488 | } else {
489 | if (!isNaturalNumber(end)) {
490 | throw invalidFocusNumber(end);
491 | }
492 |
493 | var list = [];
494 |
495 | for (var i = startNumber; i <= +end; i++) {
496 | list.push(i);
497 | }
498 |
499 | return list;
500 | }
501 | }
502 |
503 | function isNaturalNumber(n) {
504 | n = n.toString(); // force the value in case it is not
505 |
506 | var n1 = Math.abs(n),
507 | n2 = parseInt(n, 10);
508 | return !isNaN(n1) && n2 === n1 && n1.toString() === n;
509 | }
510 |
511 | function parseSteps(rawSteps, lang) {
512 | var codes = getCodes(rawSteps);
513 | var stepsLines = getSlides(codes.reverse(), lang).reverse();
514 | var steps = rawSteps.map(function(step, i) {
515 | var lines = stepsLines[i];
516 |
517 | try {
518 | return parseStep(step, lines);
519 | } catch (e) {
520 | if (e.withStepIndex) {
521 | throw e.withStepIndex(i);
522 | } else {
523 | throw e;
524 | }
525 | }
526 | });
527 | steps.forEach(function(step) {
528 | var lines = step.lines,
529 | focusMap = step.focusMap;
530 | lines.forEach(function(line, index) {
531 | line.focus = focusMap.has(index);
532 | var columnFocus = focusMap.get(index);
533 | line.focusPerToken = Array.isArray(columnFocus);
534 |
535 | if (Array.isArray(columnFocus)) {
536 | // this mutates the tokens array in order to change it to the same line in other steps
537 | splitTokensToColumns(line.tokens);
538 | line.tokens = setTokenFocus(line.tokens, columnFocus);
539 | }
540 | });
541 | });
542 | return steps;
543 | }
544 |
545 | function parseStep(step, lines) {
546 | var focus = step.focus,
547 | rest = _objectWithoutPropertiesLoose(step, ["focus"]);
548 |
549 | var focusMap = focus ? parseFocus(focus) : getDefaultFocus(lines);
550 | var focusIndexes = Array.from(focusMap.keys());
551 | var focusStart = Math.min.apply(Math, focusIndexes);
552 | var focusEnd = Math.max.apply(Math, focusIndexes);
553 | return _extends(
554 | {
555 | lines: lines,
556 | focusMap: focusMap,
557 | focusStart: focusStart,
558 | focusEnd: focusEnd,
559 | focusCenter: (focusStart + focusEnd + 1) / 2,
560 | focusCount: focusEnd - focusStart + 1
561 | },
562 | rest
563 | );
564 | }
565 |
566 | function getDefaultFocus(lines) {
567 | var indexes = lines
568 | .map(function(line, index) {
569 | return line.isNew ? index : -1;
570 | })
571 | .filter(function(index) {
572 | return index !== -1;
573 | });
574 | return new Map(
575 | indexes.map(function(i) {
576 | return [i, true];
577 | })
578 | );
579 | }
580 |
581 | function splitTokensToColumns(tokenArray) {
582 | var tokens = Array.from(tokenArray);
583 | var key = 0;
584 | tokenArray.splice(0, tokenArray.length);
585 | tokens.forEach(function(token) {
586 | var chars = Array.from(token.content);
587 | chars.forEach(function(_char) {
588 | return tokenArray.push(
589 | _extends({}, token, {
590 | content: _char,
591 | key: key++
592 | })
593 | );
594 | });
595 | });
596 | }
597 |
598 | function setTokenFocus(tokens, focusColumns) {
599 | // Assumes that tokens are already splitted in columns
600 | // Return new token objects to avoid changing other steps tokens
601 | return tokens.map(function(token, i) {
602 | return _extends({}, token, {
603 | focus: focusColumns.includes(i)
604 | });
605 | });
606 | }
607 |
608 | exports.parseSteps = parseSteps;
609 | //# sourceMappingURL=parser.cjs.development.js.map
610 |
--------------------------------------------------------------------------------
/rehype-waves/readme.md:
--------------------------------------------------------------------------------
1 | Rehype plugin for gatsby-theme-waves
2 |
3 | Features:
4 |
5 | - precalculate steps for
6 |
--------------------------------------------------------------------------------
/rehype-waves/step-reader.js:
--------------------------------------------------------------------------------
1 | const { parse } = require("shell-quote");
2 |
3 | module.exports.readStepFromElement = function(pre) {
4 | if (!pre.children || !pre.children[0]) {
5 | return null;
6 | }
7 |
8 | const codeElement = pre.children[0];
9 |
10 | const { className, metastring } = codeElement.properties;
11 | const code = codeElement.children[0].value;
12 |
13 | return {
14 | code,
15 | lang: className[0].substring("language-".length),
16 | ...parseMetastring(metastring)
17 | };
18 | };
19 |
20 | function parseMetastring(metastring) {
21 | if (!metastring) {
22 | return {};
23 | }
24 |
25 | const argv = parse(metastring);
26 |
27 | const result = {};
28 | argv.forEach(arg => {
29 | if (!arg.includes("=")) {
30 | result.focus = arg;
31 | } else {
32 | const [key, value] = arg.split(/=(.*)/);
33 | result[key] = value;
34 | }
35 | });
36 | return result;
37 | }
38 |
--------------------------------------------------------------------------------
/rehype-waves/test.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const { toMatchFile } = require("jest-file-snapshot");
3 | const compile = require("@mdx-js/mdx");
4 | const plugin = require("./index");
5 |
6 | expect.extend({ toMatchFile });
7 |
8 | fs.readdirSync("__fixtures__/").forEach(filename => {
9 | test(filename, () => {
10 | const mdx = fs.readFileSync("__fixtures__/" + filename, "utf8");
11 | expect(compile.sync(mdx, { rehypePlugins: [plugin] })).toMatchFile();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/theme/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # dotenv environment variables file
55 | .env
56 |
57 | # gatsby files
58 | .cache/
59 | public
60 |
61 | # Mac files
62 | .DS_Store
63 |
64 | # Yarn
65 | yarn-error.log
66 | .pnp/
67 | .pnp.js
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
--------------------------------------------------------------------------------
/theme/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "singleQuote": false,
5 | "tabWidth": 2,
6 | "trailingComma": "es5"
7 | }
8 |
--------------------------------------------------------------------------------
/theme/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ["gatsby-plugin-theme-ui"],
3 | }
4 |
--------------------------------------------------------------------------------
/theme/gatsby-node.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pomber/gatsby-waves/43b088d266b079078a6d5c854493ba62931c6f13/theme/gatsby-node.js
--------------------------------------------------------------------------------
/theme/index.js:
--------------------------------------------------------------------------------
1 | import * as codeThemes from "@code-surfer/standalone"
2 | export { default as CodeWave } from "./src/components/code-wave"
3 | export { default as ImageWave } from "./src/components/image-wave"
4 | export { codeThemes }
5 |
--------------------------------------------------------------------------------
/theme/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Rodrigo Pombo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/theme/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-theme-waves",
3 | "description": "Bring scrollytelling to your mdx.",
4 | "author": "Rodrigo Pombo",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/pomber/gatsby-theme-waves.git"
8 | },
9 | "version": "0.1.6",
10 | "main": "index.js",
11 | "license": "MIT",
12 | "peerDependencies": {
13 | "@emotion/core": "^10.0.14",
14 | "gatsby": "^2.13.24",
15 | "gatsby-plugin-theme-ui": "^0.2.2",
16 | "react": "^16.8.6",
17 | "react-dom": "^16.8.6",
18 | "theme-ui": "^0.2.2"
19 | },
20 | "devDependencies": {
21 | "gatsby": "^2.13.24",
22 | "react": "^16.8.6",
23 | "react-dom": "^16.8.6"
24 | },
25 | "dependencies": {
26 | "@code-surfer/standalone": "3.1.1",
27 | "@mdx-js/react": "^1.0.21",
28 | "rebound": "^0.1.0",
29 | "shell-quote": "^1.6.1",
30 | "use-spring": "^0.2.2"
31 | },
32 | "keywords": [
33 | "gatsby",
34 | "gatsby-theme",
35 | "gatsby-plugin",
36 | "code",
37 | "scrollytelling",
38 | "mdx",
39 | "blog"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/theme/readme.md:
--------------------------------------------------------------------------------
1 | # Gatsby Theme Waves
2 |
3 | > Still experimental but you can give it a try
4 |
5 | Bring scrollytelling to your mdx. Animate code, images, charts, maps and more as you scroll.
6 |
7 |
13 |
14 | The MDX looks like this:
15 |
16 | ````md
17 | import { CodeWave } from "gatsby-theme-waves"
18 |
19 |
20 |
21 | ```py
22 | # some code
23 | ```
24 |
25 | # Some markdown
26 |
27 | ```py
28 | # more code
29 | ```
30 |
31 | More markdown
32 |
33 | > and more
34 |
35 | ```py
36 | # and more
37 | ```
38 |
39 | - ok
40 | - that's enough
41 |
42 |
43 | ````
44 |
45 | ## Installation
46 |
47 | You need a Gatsby site with MDX. For example, this is how you add gatsby-theme-waves to a site that uses [gatsby-theme-blog](https://www.npmjs.com/package/gatsby-theme-blog):
48 |
49 | 1. Install the theme (and `deepmerge` for merging the theme styles)
50 |
51 | ```sh
52 | npm install --save gatsby-theme-waves deepmerge
53 | ```
54 |
55 | 2. Add the theme to your `gatsby-config.js` (at the end of the plugin list just in case)
56 |
57 | ```js
58 | module.exports = {
59 | plugins: [
60 | "gatsby-theme-blog",
61 | "gatsby-theme-waves", // <-- add this
62 | ],
63 | }
64 | ```
65 |
66 | 3. Merge the styles: create or edit `src/gatsby-plugin-theme-ui/index.js`
67 |
68 | ```js
69 | import wavesTheme from "gatsby-theme-waves/src/gatsby-plugin-theme-ui/index"
70 | import blogTheme from "gatsby-theme-blog/src/gatsby-plugin-theme-ui/index"
71 | import merge from "deepmerge"
72 |
73 | export default merge(blogTheme, wavesTheme)
74 | ```
75 |
76 | 4) Import `CodeWave` and use it in any MDX file
77 |
78 | ````md
79 | import { CodeWave } from "gatsby-theme-waves"
80 |
81 |
82 |
83 | ```py
84 | # some code
85 | ```
86 |
87 | # Some markdown
88 |
89 | ```py
90 | # more code
91 | ```
92 |
93 | More markdown
94 |
95 | > and more
96 |
97 | ```py
98 | # and more
99 | ```
100 |
101 | - ok
102 | - that's enough
103 |
104 |
105 | ````
106 |
107 | Your set up should look like [this example](https://github.com/pomber/gatsby-theme-waves/tree/master/blog-demo).
108 |
109 | ### Code Blocks
110 |
111 | By default the lines that changed between two consecutive code blocks will be highlighted. You can change it to highlihgt the line (and columns) you want:
112 |
113 | ````md
114 | ```js 1:3,6
115 | // highlihgts line 1,2,3 and 6
116 | ```
117 |
118 | ```js 5[1,3:6],8
119 | // highlihgts:
120 | // columns 1,3,4,5 and 6 from line 5
121 | // and line 8
122 | ```
123 | ````
124 |
125 | ## Coming Soon
126 |
127 | - Import code from files
128 | - Better custom code syntax highligthing using theme-ui
129 | - More waves
130 | - More docs
131 |
--------------------------------------------------------------------------------
/theme/src/components/bar-scroller.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from "theme-ui"
3 | import { useSpring } from "use-spring"
4 |
5 | function Scroller({ steps, currentStep, progress, variant }) {
6 | const [fasterProgress] = useSpring(currentStep, {
7 | decimals: 3,
8 | stiffness: 52,
9 | damping: 14,
10 | mass: 0.1,
11 | })
12 |
13 | const startBorder = Math.min(fasterProgress, progress)
14 | const endBorder = Math.max(fasterProgress, progress)
15 |
16 | const progressStyles = steps.map((_, i) => {
17 | const from = Math.max(startBorder - i, 0)
18 | const to = Math.min(endBorder + 1 - i, 1)
19 |
20 | if (to <= from) {
21 | return { top: "0%", bottom: "100%" }
22 | } else {
23 | const width = 3 / (1 + endBorder - startBorder)
24 | return {
25 | top: from * 100 + "%",
26 | bottom: 100 - to * 100 + "%",
27 | width,
28 | }
29 | }
30 | })
31 | return (
32 |
36 | {steps.map((step, i) => (
37 |
49 | ))}
50 |
51 | )
52 | }
53 |
54 | export default Scroller
55 |
--------------------------------------------------------------------------------
/theme/src/components/code-sticker.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from "theme-ui"
3 | import React from "react"
4 | import { CodeSurfer } from "@code-surfer/standalone"
5 | import { readStepFromElement } from "../stuff/step-reader"
6 |
7 | function CodeSticker({ steps: stepElements, progress, variant, parsedSteps }) {
8 | const steps = React.useMemo(
9 | () =>
10 | parsedSteps
11 | ? undefined
12 | : stepElements.map(element => {
13 | const parsedStep = readStepFromElement(element)
14 | return parsedStep
15 | }),
16 | []
17 | )
18 |
19 | return (
20 |
40 | )
41 | }
42 |
43 | export default CodeSticker
44 |
--------------------------------------------------------------------------------
/theme/src/components/code-wave.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from "theme-ui"
3 | import React from "react"
4 | import BarScroller from "./bar-scroller"
5 | import CodeSticker from "./code-sticker"
6 | import Wave from "./wave"
7 |
8 | /**
9 | *
10 | * There are two ways to use
in MDX:
11 | *
12 | *
13 | *
14 | *
15 | * ```js 1:2
16 | * // some code
17 | * ```
18 | *
19 | * ## some
20 | *
21 | * ## markdown
22 | *
23 | * ```js
24 | * // more code
25 | * ```
26 | *
27 | * - more
28 | * - markdown
29 | *
30 | *
31 | *
32 | *
33 | * Or, using the output of rehype-waves:
34 | *
35 | *
36 | *
37 | *
38 | *
39 | *
40 | * ## some
41 | *
42 | * ## markdown
43 | *
44 | *
45 | *
46 | *
47 | *
48 | * - more
49 | * - markdown
50 | *
51 | *
52 | *
53 | *
54 | *
55 | *
56 | *
57 | */
58 |
59 | function CodeWave(props) {
60 | const { parsedSteps } = props
61 |
62 | const childrenToColumns = children => {
63 | const kids = React.Children.toArray(children)
64 | if (parsedSteps) {
65 | return [[], React.Children.toArray(children)]
66 | } else {
67 | const columnCount = 2
68 | return toColumns(kids, columnCount)
69 | }
70 | }
71 |
72 | return (
73 |
78 | )
79 | }
80 |
81 | function toColumns(items, columnCount) {
82 | const columns = Array(columnCount)
83 | .fill()
84 | .map(() => [])
85 |
86 | items.forEach((item, i) => {
87 | const isCode = item.props && item.props.mdxType === "pre"
88 | if (isCode) {
89 | columns[0].push(item)
90 | columns[1].push(React.createElement("div", {}, []))
91 | } else {
92 | const step = columns[0].length - 1
93 | columns[1][step].props.children.push(item)
94 | }
95 | })
96 |
97 | return columns
98 | }
99 |
100 | export default CodeWave
101 |
--------------------------------------------------------------------------------
/theme/src/components/image-sticker.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from "theme-ui"
3 |
4 | function ImageSticker({ progress, steps, variant }) {
5 | const currentStep = Math.round(progress)
6 | const prev = steps[currentStep - 1]
7 | const curr = steps[currentStep]
8 | const next = steps[currentStep + 1]
9 |
10 | return (
11 |
12 |
13 |
21 | {prev && (
22 |
27 | {prev}
28 |
29 | )}
30 |
35 | {curr}
36 |
37 | {next && (
38 |
43 | {next}
44 |
45 | )}
46 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default ImageSticker
60 |
--------------------------------------------------------------------------------
/theme/src/components/image-wave.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from "theme-ui"
3 | import React from "react"
4 | import OpacityScroller from "./opacity-scroller"
5 | import Wave from "./wave"
6 | import ImageSticker from "./image-sticker"
7 |
8 | function toColumns(items, columnCount) {
9 | const columns = Array(columnCount)
10 | .fill()
11 | .map(() => [])
12 |
13 | items.forEach((item, i) => {
14 | const isImg =
15 | item.props &&
16 | item.props.mdxType === "p" &&
17 | item.props.children &&
18 | item.props.children.props &&
19 | item.props.children.props.className === "gatsby-resp-image-wrapper"
20 | // console.log("item props", item.props, isImg)
21 | if (isImg) {
22 | const img = React.cloneElement(
23 | item.props.children.props.children[1].props.children[3],
24 | { style: { width: "100%", height: "100%", objectFit: "cover" } }
25 | )
26 | columns[0].push(img)
27 | columns[1].push(React.createElement("div", {}, []))
28 | } else {
29 | const step = columns[0].length - 1
30 | columns[1][step].props.children.push(item)
31 | }
32 | })
33 |
34 | return columns
35 | }
36 |
37 | function ImageWave(props) {
38 | const childrenToColumns = children => {
39 | const items = React.Children.map(children, child => [child])
40 | const columnCount = 2
41 | const columns = toColumns(items, columnCount)
42 | return columns
43 | }
44 |
45 | return (
46 |
51 | )
52 | }
53 |
54 | export default ImageWave
55 |
--------------------------------------------------------------------------------
/theme/src/components/opacity-scroller.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from "theme-ui"
3 |
4 | function Scroller({ steps, progress, variant }) {
5 | return (
6 |
12 | {steps.map((step, i) => (
13 |
22 | {step}
23 |
24 | ))}
25 |
26 | )
27 | }
28 |
29 | export default Scroller
30 |
--------------------------------------------------------------------------------
/theme/src/components/wave.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx, useThemeUI } from "theme-ui"
3 | import React from "react"
4 | import { useSpring } from "use-spring"
5 |
6 | function getProgress(scroller, focusPoint) {
7 | const children = scroller.childNodes
8 | const middle = window.innerHeight * focusPoint
9 | let prevBottom = children[0].getBoundingClientRect().bottom
10 | for (let i = 1; i < children.length; i++) {
11 | const { top, bottom } = children[i].getBoundingClientRect()
12 | const breakpoint = (prevBottom + top) / 2
13 | if (middle < breakpoint) {
14 | return i - 1
15 | }
16 | prevBottom = bottom
17 | }
18 | return children.length - 1
19 | }
20 |
21 | function useFocusPoint(variant) {
22 | if (typeof window === "undefined") return false
23 | //TODO keep focus point in ref and update on window resize
24 | const { theme } = useThemeUI()
25 | const focus = theme.styles.waves[variant].focus || [0.7, 0.5]
26 | //TODO find out how to get default breakpoints from theme-ui
27 | const breakpoint = theme.breakpoints ? theme.breakpoints[0] : "40em"
28 | let mql = window.matchMedia(`(min-width: ${breakpoint})`)
29 | return mql.matches ? focus[1] : focus[0]
30 | }
31 |
32 | function useCurrentStep(ref, variant) {
33 | const [progress, setProgress] = React.useState(0)
34 | const focusPoint = useFocusPoint(variant)
35 |
36 | React.useEffect(() => {
37 | const scroller = ref.current.querySelector(".scroller")
38 | function onScroll() {
39 | const newProgress = getProgress(scroller, focusPoint)
40 | setProgress(newProgress)
41 | }
42 | document.addEventListener("scroll", onScroll)
43 | return () => {
44 | document.removeEventListener("scroll", onScroll)
45 | }
46 | }, [])
47 |
48 | return progress
49 | }
50 |
51 | function Wave({
52 | children,
53 | variant = "default",
54 | columnComponents = [],
55 | childrenToStepColumns,
56 | ...rest
57 | }) {
58 | const ref = React.useRef()
59 | const currentStep = useCurrentStep(ref, variant)
60 |
61 | const [progress] = useSpring(currentStep, {
62 | decimals: 3,
63 | stiffness: 80,
64 | damping: 48,
65 | mass: 8,
66 | })
67 |
68 | const columns = React.useMemo(() => {
69 | return childrenToStepColumns(children)
70 | }, [])
71 |
72 | return (
73 |
74 | {columns.map((columnSteps, columnIndex) => {
75 | const Component = columnComponents[columnIndex]
76 | //TODO rename currentStep to currentStepIndex
77 | return (
78 |
86 | )
87 | })}
88 |
89 | )
90 | }
91 |
92 | export default Wave
93 |
--------------------------------------------------------------------------------
/theme/src/gatsby-plugin-theme-ui/index.js:
--------------------------------------------------------------------------------
1 | import waves from "./waves"
2 |
3 | /**
4 | * This theme uses `theme-ui` under the hood.
5 | * @see https://theme-ui.com/
6 | * @see https://theme-ui.com/gatsby-plugin/
7 | */
8 | export default {
9 | styles: {
10 | waves,
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/theme/src/gatsby-plugin-theme-ui/waves.js:
--------------------------------------------------------------------------------
1 | export default {
2 | default: {
3 | Wave: {
4 | width: ["100%", "960px"],
5 | marginTop: "40px",
6 | marginLeft: [0, "calc(50% - 480px)"],
7 | marginBottom: "40px",
8 | position: "relative",
9 | display: ["block", "flex"],
10 | },
11 | ScrollerContainer: {
12 | flex: 1,
13 | paddingLeft: [0, "50px"],
14 | paddingTop: ["50px", 0],
15 | },
16 | ScrollerStep: {
17 | position: "relative",
18 | padding: [0, "0 10px"],
19 | minHeight: "250px",
20 | display: "flex",
21 | alignItems: "center",
22 | borderLeft: ["none", "3px solid transparent"],
23 | },
24 | ScrollerProgress: {
25 | position: "absolute",
26 | left: ["-12px", "-3px"],
27 | backgroundColor: "primary",
28 | },
29 | StickerContainer: {
30 | width: ["100vw", "50%"],
31 | marginLeft: ["calc(50% - 50vw)", 0],
32 | position: ["sticky", "static"],
33 | top: [0, "auto"],
34 | zIndex: [1, "auto"],
35 | height: ["50vh", "auto"],
36 | },
37 | Sticker: {
38 | position: ["static", "sticky"],
39 | width: "100%",
40 | height: ["100%", "60vh"],
41 | top: ["auto", "20vh"],
42 | border: ["none", "1px solid"],
43 | borderColor: "secondary",
44 | },
45 | // this is used to select the active scroller step
46 | // 0.5 selects the step that is at half the screen height
47 | // 0.7 the step that is at 70% the screen height
48 | focus: [0.7, 0.5],
49 | },
50 | }
51 |
--------------------------------------------------------------------------------
/theme/src/stuff/step-reader.js:
--------------------------------------------------------------------------------
1 | import { parse } from "shell-quote"
2 |
3 | export function readStepFromElement(element) {
4 | if (!element.props.children || !element.props.children.props) {
5 | return null
6 | }
7 | const { props } = element.props.children
8 | const className = props.className
9 | return {
10 | code: props.children,
11 | lang: className.substring("language-".length),
12 | ...parseMetastring(props.metastring),
13 | }
14 | }
15 |
16 | function parseMetastring(metastring) {
17 | if (!metastring) {
18 | return {}
19 | }
20 |
21 | const argv = parse(metastring)
22 |
23 | const result = {}
24 | argv.forEach(arg => {
25 | if (!arg.includes("=")) {
26 | result.focus = arg
27 | } else {
28 | const [key, value] = arg.split(/=(.*)/)
29 | result[key] = value
30 | }
31 | })
32 | return result
33 | }
34 |
--------------------------------------------------------------------------------