@gpichot/spectacle-deck 1.1.7 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-dev.log +4 -0
  3. package/dist/SlideWrapper.d.ts +7 -0
  4. package/dist/colors.d.ts +14 -0
  5. package/dist/components/QRCode.d.ts +6 -0
  6. package/dist/components/Timeline.styled.d.ts +7 -0
  7. package/{components → dist/components}/map.d.ts +2 -0
  8. package/dist/components/styled.d.ts +12 -0
  9. package/dist/context.d.ts +11 -0
  10. package/{index.cjs → dist/index.cjs} +321 -200
  11. package/{index.d.ts → dist/index.d.ts} +30 -7
  12. package/{index.mjs → dist/index.mjs} +312 -191
  13. package/{layouts → dist/layouts}/BaseLayout.d.ts +1 -1
  14. package/dist/layouts/SectionLayout.d.ts +2 -0
  15. package/{layouts → dist/layouts}/index.d.ts +1 -1
  16. package/dist/layouts/styled.d.ts +2 -0
  17. package/package.json +29 -23
  18. package/publish.sh +7 -0
  19. package/scripts/bundle.ts +84 -0
  20. package/src/SlideWrapper.tsx +25 -0
  21. package/src/colors.ts +42 -0
  22. package/src/components/CodeStepper/CodeStepper.tsx +228 -0
  23. package/src/components/CodeStepper/code-directives.test.ts +58 -0
  24. package/src/components/CodeStepper/code-directives.ts +129 -0
  25. package/src/components/DocumentationItem.tsx +85 -0
  26. package/src/components/FilePane.tsx +18 -0
  27. package/src/components/HorizontalList.tsx +141 -0
  28. package/src/components/IconBox.tsx +31 -0
  29. package/src/components/Image.tsx +39 -0
  30. package/src/components/ItemsColumn.tsx +60 -0
  31. package/src/components/QRCode.tsx +55 -0
  32. package/src/components/Timeline.styled.tsx +24 -0
  33. package/src/components/Timeline.tsx +159 -0
  34. package/src/components/map.tsx +128 -0
  35. package/src/components/styled.tsx +73 -0
  36. package/src/context.tsx +33 -0
  37. package/src/front.png +0 -0
  38. package/src/index.tsx +127 -0
  39. package/src/layouts/BaseLayout.tsx +52 -0
  40. package/src/layouts/CenteredLayout.tsx +40 -0
  41. package/src/layouts/Default3Layout.tsx +159 -0
  42. package/src/layouts/MainSectionLayout.tsx +31 -0
  43. package/src/layouts/QuoteLayout.tsx +107 -0
  44. package/src/layouts/SectionLayout.tsx +14 -0
  45. package/src/layouts/SideCodeLayout.tsx +44 -0
  46. package/src/layouts/SideImageLayout.tsx +82 -0
  47. package/src/layouts/SideLayout.tsx +31 -0
  48. package/src/layouts/columns.tsx +56 -0
  49. package/src/layouts/index.tsx +19 -0
  50. package/src/layouts/styled.ts +7 -0
  51. package/src/layouts/utils.ts +66 -0
  52. package/src/node.d.ts +5 -0
  53. package/src/style.d.ts +10 -0
  54. package/src/template.tsx +25 -0
  55. package/src/theme.ts +28 -0
  56. package/tsconfig.json +29 -0
  57. package/components/Timeline.styled.d.ts +0 -7
  58. package/components/styled.d.ts +0 -12
  59. package/layouts/SectionLayout.d.ts +0 -2
  60. package/layouts/styled.d.ts +0 -2
  61. /package/{components → dist/components}/CodeStepper/CodeStepper.d.ts +0 -0
  62. /package/{components → dist/components}/CodeStepper/code-directives.d.ts +0 -0
  63. /package/{components → dist/components}/DocumentationItem.d.ts +0 -0
  64. /package/{components → dist/components}/FilePane.d.ts +0 -0
  65. /package/{components → dist/components}/HorizontalList.d.ts +0 -0
  66. /package/{components → dist/components}/IconBox.d.ts +0 -0
  67. /package/{components → dist/components}/Image.d.ts +0 -0
  68. /package/{components → dist/components}/ItemsColumn.d.ts +0 -0
  69. /package/{components → dist/components}/Timeline.d.ts +0 -0
  70. /package/{layouts → dist/layouts}/CenteredLayout.d.ts +0 -0
  71. /package/{layouts → dist/layouts}/Default3Layout.d.ts +0 -0
  72. /package/{layouts → dist/layouts}/MainSectionLayout.d.ts +0 -0
  73. /package/{layouts → dist/layouts}/QuoteLayout.d.ts +0 -0
  74. /package/{layouts → dist/layouts}/SideCodeLayout.d.ts +0 -0
  75. /package/{layouts → dist/layouts}/SideImageLayout.d.ts +0 -0
  76. /package/{layouts → dist/layouts}/SideLayout.d.ts +0 -0
  77. /package/{layouts → dist/layouts}/columns.d.ts +0 -0
  78. /package/{layouts → dist/layouts}/utils.d.ts +0 -0
  79. /package/{template.d.ts → dist/template.d.ts} +0 -0
  80. /package/{theme.d.ts → dist/theme.d.ts} +0 -0
@@ -2,4 +2,4 @@ import React from "react";
2
2
  export declare const BaseLayout: ({ children, title, ...otherProps }: {
3
3
  children: React.ReactNode;
4
4
  title?: React.ReactNode;
5
- } & Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "title">) => React.JSX.Element;
5
+ } & Omit<React.ComponentPropsWithoutRef<"div">, "title">) => React.JSX.Element;
@@ -0,0 +1,2 @@
1
+ /// <reference types="react" />
2
+ export declare const SectionLayout: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
@@ -21,6 +21,6 @@ declare const _default: {
21
21
  height?: number;
22
22
  }) => import("react").JSX.Element;
23
23
  side: typeof SideLayout;
24
- section: import("styled-components").IStyledComponent<"web", import("styled-components/dist/types").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>>;
24
+ section: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
25
25
  };
26
26
  export default _default;
@@ -0,0 +1,2 @@
1
+ /// <reference types="react" />
2
+ export declare const SVGObject: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ObjectHTMLAttributes<HTMLObjectElement>, HTMLObjectElement>, never>> & string;
package/package.json CHANGED
@@ -1,30 +1,36 @@
1
1
  {
2
2
  "name": "@gpichot/spectacle-deck",
3
- "version": "1.1.7",
4
- "license": "MIT",
5
- "type": "module",
6
- "main": "index.cjs",
7
- "types": "index.d.ts",
8
- "module": "index.mjs",
9
- "exports": {
10
- ".": {
11
- "types": "./index.d.ts",
12
- "require": "./index.cjs",
13
- "import": "./index.mjs"
14
- }
15
- },
16
- "keywords": [
17
- "spectacle"
18
- ],
3
+ "version": "1.2.1",
4
+ "description": "",
5
+ "module": "src/index.tsx",
6
+ "types": "src/index.d.ts",
7
+ "keywords": [],
8
+ "author": "",
9
+ "license": "ISC",
19
10
  "dependencies": {
20
- "@fontsource/bitter": "^5.0.16",
21
- "@mdx-js/react": "^3.0.0",
22
- "react": "^18.2.0",
23
- "react-dom": "^18.2.0",
24
- "react-is": "^18.2.0",
11
+ "@fontsource/bitter": "^5.0.18",
12
+ "@mdx-js/react": "^3.0.1",
13
+ "qr-creator": "^1.0.0",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1",
16
+ "react-is": "^18.3.1",
25
17
  "react-spring": "^9.7.3",
26
18
  "react-syntax-highlighter": "^15.5.0",
27
- "spectacle": "^10.1.7",
28
- "styled-components": "^6.1.8"
19
+ "spectacle": "^10.1.8",
20
+ "styled-components": "^6.1.11"
21
+ },
22
+ "devDependencies": {
23
+ "@arnaud-barre/tnode": "^0.19.2",
24
+ "@types/react": "^18.3.2",
25
+ "@types/react-dom": "^18.3.0",
26
+ "@types/react-is": "^18.3.0",
27
+ "@types/react-syntax-highlighter": "^15.5.13",
28
+ "esbuild": "^0.21.3",
29
+ "typescript": "^5.4.5"
30
+ },
31
+ "scripts": {
32
+ "test": "echo \"Error: no test specified\" && exit 1",
33
+ "dev": "tnode scripts/bundle.ts",
34
+ "build": "tnode scripts/bundle.ts"
29
35
  }
30
36
  }
package/publish.sh ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ pnpm run build
4
+
5
+ cd dist
6
+
7
+ pnpm publish --no-git-checks
@@ -0,0 +1,84 @@
1
+ import { rmSync, writeFileSync, copyFileSync } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import { build, BuildOptions, context } from "esbuild";
4
+
5
+ import packageJSON from "../package.json";
6
+
7
+ const dev = process.argv.includes("--dev");
8
+
9
+ rmSync("dist", { force: true, recursive: true });
10
+
11
+ const serverOptions: BuildOptions = {
12
+ bundle: true,
13
+ platform: "node",
14
+ target: "node14",
15
+ legalComments: "inline",
16
+ loader: {
17
+ ".png": "dataurl",
18
+ },
19
+ external: Object.keys(packageJSON.peerDependencies || {}).concat(
20
+ Object.keys(packageJSON.dependencies || {}),
21
+ ),
22
+ };
23
+
24
+ const buildOrWatch = async (options: BuildOptions) => {
25
+ if (!dev) return build(options);
26
+ const ctx = await context(options);
27
+ await ctx.watch();
28
+ await ctx.rebuild();
29
+ };
30
+
31
+ Promise.all([
32
+ buildOrWatch({
33
+ ...serverOptions,
34
+ stdin: {
35
+ contents: `import * as mod from "./src";
36
+ module.exports = mod;
37
+ `,
38
+ resolveDir: ".",
39
+ },
40
+ outfile: "dist/index.cjs",
41
+ logOverride: { "empty-import-meta": "silent" },
42
+ }),
43
+ buildOrWatch({
44
+ ...serverOptions,
45
+ entryPoints: ["src/index.tsx"],
46
+ format: "esm",
47
+ outfile: "dist/index.mjs",
48
+ }),
49
+ ]).then(() => {
50
+ // copyFileSync("LICENSE", "dist/LICENSE");
51
+ // copyFileSync("README.md", "dist/README.md");
52
+
53
+ execSync(
54
+ "tsc src/index.tsx --declaration --jsx react --emitDeclarationOnly --outDir dist --module preserve --target es2020 --allowSyntheticDefaultImports --skipLibCheck --moduleResolution bundler --types ./src/node.d.ts,./src/style.d.ts",
55
+ { stdio: "inherit" },
56
+ );
57
+
58
+ writeFileSync(
59
+ "dist/package.json",
60
+ JSON.stringify(
61
+ {
62
+ name: "@gpichot/spectacle-deck",
63
+ version: packageJSON.version,
64
+ license: "MIT",
65
+ type: "module",
66
+ main: "index.cjs",
67
+ types: "index.d.ts",
68
+ module: "index.mjs",
69
+ exports: {
70
+ ".": {
71
+ types: "./index.d.ts",
72
+ require: "./index.cjs",
73
+ import: "./index.mjs",
74
+ },
75
+ },
76
+ keywords: ["spectacle"],
77
+ peerDependencies: packageJSON.peerDependencies,
78
+ dependencies: packageJSON.dependencies,
79
+ },
80
+ null,
81
+ 2,
82
+ ),
83
+ );
84
+ });
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ import { usePestacle } from "./context";
3
+
4
+ export function SlideWrapper({
5
+ children,
6
+ frontmatter,
7
+ }: {
8
+ children: React.ReactNode;
9
+ frontmatter: { layout?: string };
10
+ }) {
11
+ const { layouts } = usePestacle();
12
+ const layout = frontmatter?.layout || "default";
13
+ console.log(layouts, layout);
14
+ const Layout = layout in layouts ? layouts[layout] : null;
15
+
16
+ if (layout && !Layout) {
17
+ console.warn(`Layout ${layout} not found`);
18
+ }
19
+
20
+ if (Layout) {
21
+ return <Layout {...frontmatter}>{children}</Layout>;
22
+ }
23
+
24
+ return <>{children}</>;
25
+ }
package/src/colors.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Extract colors from `rgb(...) or rgba(...)` string or #hex string
3
+ */
4
+ export function extractColors(color: string): {
5
+ r: number;
6
+ g: number;
7
+ b: number;
8
+ } {
9
+ if (color.startsWith("rgb")) {
10
+ const [r, g, b] = color
11
+ .replace("rgb(", "")
12
+ .replace("rgba(", "")
13
+ .replace(")", "")
14
+ .split(",")
15
+ .map((c) => parseInt(c.trim(), 10));
16
+ return { r, g, b };
17
+ } else if (color.startsWith("#")) {
18
+ const hex = color.replace("#", "");
19
+ const r = parseInt(hex.substring(0, 2), 16);
20
+ const g = parseInt(hex.substring(2, 4), 16);
21
+ const b = parseInt(hex.substring(4, 6), 16);
22
+ return { r, g, b };
23
+ }
24
+ throw new Error(`Invalid color format: ${color}`);
25
+ }
26
+
27
+ /**
28
+ * Create vars for css colors
29
+ */
30
+ export function createCssVariables(colors: { [key: string]: string }) {
31
+ const base = Object.entries(colors)
32
+ .map(([key, value]) => `--color-${key}: ${value};`)
33
+ .join("\n");
34
+ const rgbs = Object.entries(colors)
35
+ .map(([key, value]) => {
36
+ const { r, g, b } = extractColors(value);
37
+ return `--color-${key}-rgb: ${r}, ${g}, ${b};`;
38
+ })
39
+ .join("\n");
40
+
41
+ return `${base}\n${rgbs}`;
42
+ }
@@ -0,0 +1,228 @@
1
+ import React from "react";
2
+ import ReactIs from "react-is";
3
+ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
4
+ import { gruvboxDark } from "react-syntax-highlighter/dist/esm/styles/prism";
5
+ import { Stepper } from "spectacle";
6
+ import styled from "styled-components";
7
+
8
+ import { parseStepDirectives, Step } from "./code-directives";
9
+
10
+ const Highlighter = SyntaxHighlighter as unknown as React.ElementType;
11
+
12
+ const CodeContainer = styled.div`
13
+ pre {
14
+ padding: 1rem 0rem !important;
15
+ background-color: transparent !important;
16
+ }
17
+ .linenumber {
18
+ min-width: 2rem !important;
19
+ }
20
+ [data-highlight-line="true"] {
21
+ &:before {
22
+ content: " ";
23
+ position: absolute;
24
+ background-color: rgba(var(--color-primary-rgb), 0.5);
25
+ }
26
+
27
+ &[data-step-active="true"]:before {
28
+ background-color: var(--color-secondary);
29
+ }
30
+ }
31
+ `;
32
+
33
+ function useCodeSteps(code: string) {
34
+ return React.useMemo(() => {
35
+ const prefixes = code.match(/(?:\/\/|<!--) @.*\n/g) || ([] as string[]);
36
+ const prefixesLength = prefixes.reduce(
37
+ (acc, prefix) => acc + prefix.length,
38
+ 0,
39
+ );
40
+
41
+ const codeWithoutPrefixes = code.slice(prefixesLength);
42
+
43
+ const hasDirectives = prefixes.length > 0;
44
+ const allDirectives = hasDirectives ? [...prefixes] : [];
45
+ const steps = parseStepDirectives(allDirectives);
46
+
47
+ return {
48
+ steps,
49
+ code: codeWithoutPrefixes,
50
+ prefixes,
51
+ hasSteps: Boolean(steps.length),
52
+ hasName: steps.some((step) => step.name),
53
+ };
54
+ }, [code]);
55
+ }
56
+
57
+ function getCodeDetails(children: React.ReactNode) {
58
+ const child = React.Children.toArray(children)[0];
59
+
60
+ if (!React.isValidElement(child)) {
61
+ return {
62
+ language: "",
63
+ code: ReactIs.isFragment(child) ? "" : String(child || ""),
64
+ };
65
+ }
66
+
67
+ const result = {
68
+ language: (String(child.props.className) || "").replace("language-", ""),
69
+ code: (child.props.children as string).trim(),
70
+ };
71
+
72
+ return result;
73
+ }
74
+
75
+ function CodeWrapper({
76
+ name,
77
+ stepName,
78
+ hasName,
79
+ children,
80
+ }: {
81
+ name?: string;
82
+ stepName?: string;
83
+ hasName?: boolean;
84
+ children: React.ReactNode;
85
+ }) {
86
+ return (
87
+ <div
88
+ style={{
89
+ boxSizing: "border-box",
90
+ margin: "0.5rem 1rem",
91
+ backgroundColor: "rgb(38,39,40)",
92
+ borderRadius: "4px",
93
+ }}
94
+ >
95
+ {name && (
96
+ <span
97
+ style={{
98
+ fontFamily: 'Consolas, Monaco, "Andale Mono", monospace',
99
+ fontSize: "1rem",
100
+ color: "#ffffffbb",
101
+ backgroundColor: "#33333388",
102
+ display: "inline-block",
103
+ padding: "8px 8px",
104
+ width: "100%",
105
+ boxSizing: "border-box",
106
+ }}
107
+ >
108
+ {name}
109
+ </span>
110
+ )}
111
+ {children}
112
+ {hasName && (
113
+ <span
114
+ style={{
115
+ fontFamily: 'Consolas, Monaco, "Andale Mono", monospace',
116
+ fontSize: "0.8rem",
117
+ color: "#ffffffaa",
118
+ backgroundColor: "#33333388",
119
+ display: "inline-block",
120
+ padding: "4px 8px",
121
+ width: "100%",
122
+ boxSizing: "border-box",
123
+ fontStyle: "italic",
124
+ }}
125
+ >
126
+ {stepName || <span style={{ visibility: "hidden" }}>Step</span>}
127
+ </span>
128
+ )}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ export default function CodeStepper({
134
+ priority,
135
+ name,
136
+ ...props
137
+ }: React.ComponentProps<"pre"> & {
138
+ priority?: number;
139
+ name?: string;
140
+ }) {
141
+ const { language, code } = React.useMemo(() => {
142
+ return getCodeDetails(props.children);
143
+ }, [props.children]);
144
+
145
+ const {
146
+ steps,
147
+ code: codeNormalized,
148
+ prefixes,
149
+ hasSteps,
150
+ hasName,
151
+ } = useCodeSteps(code);
152
+ return (
153
+ <CodeContainer>
154
+ {import.meta.env.DEV && false && Boolean(prefixes?.length) && (
155
+ <div style={{ position: "absolute", top: 0, opacity: 0.5, left: 0 }}>
156
+ <Highlighter language={language} style={gruvboxDark}>
157
+ {prefixes.join("")}
158
+ </Highlighter>
159
+ </div>
160
+ )}
161
+ <Stepper
162
+ values={steps}
163
+ alwaysVisible={!hasSteps}
164
+ priority={priority ? priority + 1 : undefined}
165
+ >
166
+ {(step, _, isActive) => {
167
+ console.log({ step, isActive });
168
+ return (
169
+ <CodeWrapper
170
+ name={name}
171
+ stepName={(step as Step | null)?.name}
172
+ hasName={hasName}
173
+ >
174
+ <Highlighter
175
+ language={language}
176
+ wrapLines
177
+ showLineNumbers
178
+ style={gruvboxDark}
179
+ lineNumberStyle={(lineNumber: number) => {
180
+ const { highlight = [] } = (step as Step) || {};
181
+ const isHighlighted = highlight.includes(lineNumber);
182
+
183
+ return {
184
+ fontWeight: isHighlighted ? "bold" : "normal",
185
+ };
186
+ }}
187
+ lineProps={(lineNumber: number) => {
188
+ const { hiddenLines = [], highlight = [] } =
189
+ (step as Step) || {};
190
+ const isVisible =
191
+ hasSteps && isActive
192
+ ? !hiddenLines.includes(lineNumber)
193
+ : isActive || !hasSteps;
194
+ const isHighlighted = highlight.includes(lineNumber);
195
+ const getOpacity = () => {
196
+ if (!isVisible) return 0;
197
+ if (isHighlighted || !highlight.length) return 1;
198
+ return 0.8;
199
+ };
200
+
201
+ return {
202
+ ...(isHighlighted && {
203
+ "data-highlight-line": isHighlighted,
204
+ "data-step-active": isActive,
205
+ }),
206
+ style: {
207
+ opacity: getOpacity(),
208
+ transition: "all 0.3s ease",
209
+ display: "block",
210
+ width: "100%",
211
+ backgroundColor: isHighlighted
212
+ ? "rgba(var(--color-secondary-rgb), 0.13)"
213
+ : "",
214
+ },
215
+ };
216
+ }}
217
+ >
218
+ {codeNormalized}
219
+ </Highlighter>
220
+ </CodeWrapper>
221
+ );
222
+ }}
223
+ </Stepper>
224
+ </CodeContainer>
225
+ );
226
+ }
227
+
228
+ CodeStepper.mdxType = "CodeStepper";
@@ -0,0 +1,58 @@
1
+ import { parseStepDirectives } from './code-directives';
2
+
3
+ function splitDirectives(str: string): string[] {
4
+ return str.trim().split('\n');
5
+ }
6
+
7
+ describe('parseStepDirectives', () => {
8
+ it('should reduce properly with showLines', () => {
9
+ const directives = splitDirectives(`
10
+ // @step showLines(1-3) highlight(1-3)
11
+ // @step showLines(4-6) highlight(4-6)
12
+ `);
13
+
14
+ const result = parseStepDirectives(directives);
15
+ expect(result[0]).toEqual({ hiddenLines: [4, 5, 6], highlight: [1, 2, 3] });
16
+ expect(result[1]).toEqual({ hiddenLines: [], highlight: [4, 5, 6] });
17
+ });
18
+
19
+ it('should reduce properly with highlight', () => {
20
+ const directives = splitDirectives(`
21
+ // @step highlight(3)
22
+ // @step showLines(4) highlight(4)
23
+ `);
24
+
25
+ const result = parseStepDirectives(directives);
26
+ expect(result).toHaveLength(2);
27
+ expect(result[0]).toEqual({ hiddenLines: [4], highlight: [3] });
28
+ expect(result[1]).toEqual({ hiddenLines: [], highlight: [4] });
29
+ });
30
+
31
+ it('parse only highlights', () => {
32
+ const directives = splitDirectives(`
33
+ // @step highlight(6)
34
+ // @step highlight(7-9)
35
+ // @step highlight(10)
36
+ `);
37
+
38
+ const result = parseStepDirectives(directives);
39
+ expect(result).toHaveLength(3);
40
+ expect(result[0]).toEqual({ hiddenLines: [], highlight: [6] });
41
+ expect(result[1]).toEqual({ hiddenLines: [], highlight: [7, 8, 9] });
42
+ expect(result[2]).toEqual({ hiddenLines: [], highlight: [10] });
43
+ });
44
+
45
+ it('parses typescript lines', () => {
46
+ const directives = splitDirectives(`
47
+ // @step highlight(3) name("(string|number)[]")
48
+ `);
49
+
50
+ const result = parseStepDirectives(directives);
51
+ expect(result).toHaveLength(1);
52
+ expect(result[0]).toEqual({
53
+ hiddenLines: [],
54
+ highlight: [3],
55
+ name: '(string|number)[]',
56
+ });
57
+ });
58
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The Code Stepper component is a component that allows you to step through
3
+ * the code using familiar directives.
4
+ *
5
+ * @example "@step showLines(1-3)" will show lines 1-3
6
+ * @example "@step highlight(1-3)" will highlight lines 1-3
7
+ *
8
+ */
9
+
10
+ function range(start: number, end: number) {
11
+ return Array.from({ length: end - start + 1 }, (_, i) => i + start);
12
+ }
13
+
14
+ /**
15
+ * Function parse ranges list
16
+ */
17
+ function parseRangeList(str: string): number[] {
18
+ return str.split(',').reduce((acc, line) => {
19
+ if (!line.includes('-')) return [...acc, parseInt(line, 10)];
20
+
21
+ const [start, end] = line.split('-').map(Number);
22
+ return [...acc, ...range(start, end)];
23
+ }, [] as number[]);
24
+ }
25
+
26
+ /**
27
+ * Parse showLines fn
28
+ *
29
+ * "showLines(1-3)" => [1, 2, 3]
30
+ * "showLines(1,3)" => [1,3]
31
+ * "showLines(1-2,4-5)" => [1,2,4,5]
32
+ */
33
+ function parseShowLines(directive: string): { showLines: number[] } | null {
34
+ const match = directive.match(/showLines\((.*)\)/);
35
+ if (!match) return null;
36
+
37
+ const lines = parseRangeList(match[1]);
38
+
39
+ return { showLines: lines };
40
+ }
41
+
42
+ /**
43
+ * Parse highlight fn
44
+ *
45
+ * "highlight(1-3)" => [1,2,3]
46
+ * etc.
47
+ */
48
+ function parseHighlight(directive: string): { highlight: number[] } | null {
49
+ const match = directive.match(/highlight\((.*)\)/);
50
+ if (!match) return null;
51
+
52
+ const lines = parseRangeList(match[1]);
53
+
54
+ return { highlight: lines };
55
+ }
56
+
57
+ type StepDirective = {
58
+ showLines?: number[];
59
+ highlight?: number[];
60
+ name?: string;
61
+ };
62
+
63
+ /**
64
+ * Parse name fn
65
+ *
66
+ * "name("my name")" => "my name"
67
+ */
68
+ function parseName(directive: string): { name: string } | null {
69
+ const match = directive.match(/name\("(.*)"\)/);
70
+ if (!match) return null;
71
+
72
+ return { name: match[1] };
73
+ }
74
+
75
+ /**
76
+ * Parse step directive
77
+ *
78
+ * @example "@step showLines(1-3) highlight(1-3)"
79
+ * @example "@step showLines(1-3)"
80
+ * @example "@step highlight(1-3)"
81
+ */
82
+ function parseStepDirective(directive: string): StepDirective {
83
+ // Should match a showLines(1-3) or highlight(1-3) directive
84
+ // Should match name("(string)")
85
+ const regex = /showLines\([^)]+\)|highlight\([^)]+\)|name\("(.*)"\)/g;
86
+
87
+ const directives = directive.match(regex) || [];
88
+
89
+ const name = directives.map(parseName).find(Boolean);
90
+ const showLines = directives.map(parseShowLines).find(Boolean);
91
+ const highlight = directives.map(parseHighlight).find(Boolean);
92
+
93
+ return { ...showLines, ...highlight, ...name };
94
+ }
95
+
96
+ export type Step = {
97
+ hiddenLines: number[];
98
+ highlight: number[];
99
+ name?: string;
100
+ };
101
+
102
+ /**
103
+ * Reduce steps directives
104
+ */
105
+ export function combineStepDirectives(directives: StepDirective[]): Step[] {
106
+ let hiddenLines = directives.reduce((acc, { showLines }) => {
107
+ if (!showLines) return acc;
108
+
109
+ return [...acc, ...showLines];
110
+ }, [] as number[]);
111
+
112
+ return directives.map(({ highlight, showLines, name }) => {
113
+ hiddenLines = hiddenLines.filter((line) => !showLines?.includes(line));
114
+ return {
115
+ highlight: highlight || [],
116
+ hiddenLines,
117
+ name,
118
+ };
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Parse step directives
124
+ */
125
+ export function parseStepDirectives(directives: string[]): Step[] {
126
+ const parsedDirectives = directives.map(parseStepDirective);
127
+
128
+ return combineStepDirectives(parsedDirectives);
129
+ }