react-lib-tools 0.0.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.
@@ -0,0 +1,31 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { extname, join } from "node:path";
3
+
4
+ export async function getFilesWithExtensions(
5
+ directory: string,
6
+ extensions: string[],
7
+ filter?: ((path: string) => boolean) | undefined
8
+ ) {
9
+ const files: string[] = [];
10
+
11
+ const entries = await readdir(directory, { withFileTypes: true });
12
+
13
+ for (const entry of entries) {
14
+ const fullPath = join(directory, entry.name);
15
+
16
+ if (entry.isDirectory()) {
17
+ files.push(
18
+ ...(await getFilesWithExtensions(fullPath, extensions, filter))
19
+ );
20
+ } else if (entry.isFile()) {
21
+ const fileExtension = extname(entry.name);
22
+ if (extensions.includes(fileExtension)) {
23
+ if (typeof filter !== "function" || filter(fullPath)) {
24
+ files.push(fullPath);
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ return files;
31
+ }
@@ -0,0 +1,34 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { cwd } from "node:process";
4
+ import { getFilesWithExtensions } from "./getFilesWithExtensions.ts";
5
+ import { rmFilesWithExtensions } from "./rmFilesWithExtensions.ts";
6
+
7
+ export async function initialize({
8
+ fileExtensions,
9
+ fileFilter,
10
+ inputPath,
11
+ outputDirName
12
+ }: {
13
+ fileExtensions: string[];
14
+ fileFilter?: ((path: string) => boolean) | undefined;
15
+ inputPath: string[];
16
+ outputDirName: string;
17
+ }) {
18
+ const inputDir = join(cwd(), ...inputPath);
19
+ const outputDir = join(cwd(), "public", "generated", outputDirName);
20
+ await mkdir(outputDir, { recursive: true });
21
+ await rmFilesWithExtensions(outputDir, [".json"]);
22
+
23
+ const files = await getFilesWithExtensions(
24
+ inputDir,
25
+ fileExtensions,
26
+ fileFilter
27
+ );
28
+
29
+ return {
30
+ files,
31
+ inputDir,
32
+ outputDir
33
+ };
34
+ }
@@ -0,0 +1,13 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { getFilesWithExtensions } from "./getFilesWithExtensions.ts";
3
+
4
+ export async function rmFilesWithExtensions(
5
+ directory: string,
6
+ extensions: string[]
7
+ ) {
8
+ const files = await getFilesWithExtensions(directory, extensions);
9
+
10
+ for (const file of files) {
11
+ await rm(file);
12
+ }
13
+ }
@@ -0,0 +1,255 @@
1
+ import { htmlLanguage } from "@codemirror/lang-html";
2
+ import {
3
+ jsxLanguage,
4
+ tsxLanguage,
5
+ typescriptLanguage
6
+ } from "@codemirror/lang-javascript";
7
+ import { ensureSyntaxTree, LRLanguage } from "@codemirror/language";
8
+ import { EditorState } from "@codemirror/state";
9
+ import { classHighlighter, highlightTree } from "@lezer/highlight";
10
+ import { type BuiltInParserName } from "prettier";
11
+
12
+ type TokenType = string;
13
+ type Token = {
14
+ columnIndex: number;
15
+ type: TokenType | null;
16
+ value: string;
17
+ };
18
+
19
+ export type Language = "HTML" | "JS" | "JSX" | "TS" | "TSX";
20
+
21
+ type State = {
22
+ parsedTokens: Token[];
23
+ rawString: string;
24
+ };
25
+
26
+ export const DEFAULT_MAX_CHARACTERS = 500000;
27
+ export const DEFAULT_MAX_TIME = 5000;
28
+
29
+ export async function syntaxHighlight(code: string, language: Language) {
30
+ let extension: LRLanguage;
31
+ let prettierParser: BuiltInParserName;
32
+ switch (language) {
33
+ case "HTML": {
34
+ extension = htmlLanguage.configure({ dialect: "selfClosing" });
35
+ prettierParser = "html";
36
+ break;
37
+ }
38
+ case "JS":
39
+ case "JSX": {
40
+ extension = jsxLanguage;
41
+ prettierParser = "babel";
42
+ break;
43
+ }
44
+ case "TS": {
45
+ extension = typescriptLanguage;
46
+ prettierParser = "typescript";
47
+ break;
48
+ }
49
+ case "TSX": {
50
+ extension = tsxLanguage;
51
+ prettierParser = "typescript";
52
+ break;
53
+ }
54
+ }
55
+ if (!extension) {
56
+ console.error("Unsupported language %o", language);
57
+ }
58
+
59
+ const tokens = await parser(code, extension, prettierParser);
60
+
61
+ return tokens.map(parsedTokensToHtml).join("\n");
62
+ }
63
+
64
+ async function parser(
65
+ code: string,
66
+ languageExtension: LRLanguage,
67
+
68
+ // @ts-expect-error TS6133
69
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
70
+ prettierParser: BuiltInParserName
71
+ ) {
72
+ const parsedTokens: Token[][] = [];
73
+ const currentLineState: State = {
74
+ parsedTokens: [],
75
+ rawString: ""
76
+ };
77
+
78
+ // The logic below to trim code sections only works with "\n"
79
+ code = code.replace(/\r\n?|\n|\u2028|\u2029/g, "\n");
80
+ if (code.length > DEFAULT_MAX_CHARACTERS) {
81
+ let index = DEFAULT_MAX_CHARACTERS - 1;
82
+ while (index > 0 && code.charAt(index) !== "\n") {
83
+ index--;
84
+ }
85
+ if (index === 0) {
86
+ while (index < code.length && code.charAt(index) !== "\n") {
87
+ index++;
88
+ }
89
+ }
90
+ code = code.slice(0, index + 1);
91
+ }
92
+
93
+ // In the future I may want to (re)format code examples for different screen widths
94
+ // code = await format(code, {
95
+ // parser: prettierParser,
96
+ // plugins: ["prettier-plugin-tailwindcss"],
97
+ // printWidth: 100,
98
+ // proseWrap: "always",
99
+ // semi: true
100
+ // });
101
+
102
+ const state = EditorState.create({
103
+ doc: code,
104
+ extensions: [languageExtension]
105
+ });
106
+
107
+ const tree = ensureSyntaxTree(
108
+ state,
109
+ DEFAULT_MAX_CHARACTERS,
110
+ DEFAULT_MAX_TIME
111
+ );
112
+
113
+ if (tree === null) {
114
+ return [];
115
+ }
116
+
117
+ let characterIndex = 0;
118
+ let parsedCharacterIndex = 0;
119
+ highlightTree(
120
+ tree,
121
+ classHighlighter,
122
+ (from: number, to: number, className: string) => {
123
+ if (from > characterIndex) {
124
+ // No style applied to the token between position and from.
125
+ // This typically indicates white space or newline characters.
126
+ processSection(
127
+ currentLineState,
128
+ parsedTokens,
129
+ code.slice(characterIndex, from),
130
+ ""
131
+ );
132
+ }
133
+ processSection(
134
+ currentLineState,
135
+ parsedTokens,
136
+ code.slice(from, to),
137
+ className
138
+ );
139
+ characterIndex = to;
140
+ }
141
+ );
142
+
143
+ const maxPosition = code.length;
144
+
145
+ if (characterIndex < maxPosition) {
146
+ // No style applied on the trailing text.
147
+ // This typically indicates white space or newline characters.
148
+ processSection(
149
+ currentLineState,
150
+ parsedTokens,
151
+ code.slice(characterIndex, maxPosition),
152
+ ""
153
+ );
154
+ }
155
+ if (currentLineState.parsedTokens.length) {
156
+ parsedTokens.push(currentLineState.parsedTokens);
157
+ }
158
+
159
+ parsedCharacterIndex += characterIndex + 1;
160
+
161
+ // Anything that's left should de-opt to plain text.
162
+ if (parsedCharacterIndex < code.length) {
163
+ let nextIndex = code.indexOf("\n", parsedCharacterIndex);
164
+ let parsedLineTokens = [];
165
+ while (true) {
166
+ const line =
167
+ nextIndex >= 0
168
+ ? code.substring(parsedCharacterIndex, nextIndex)
169
+ : code.substring(parsedCharacterIndex);
170
+ parsedLineTokens.push({
171
+ columnIndex: 0,
172
+ type: null,
173
+ value: line
174
+ });
175
+ if (nextIndex >= 0) {
176
+ parsedTokens.push(parsedLineTokens);
177
+ parsedLineTokens = [];
178
+ } else if (nextIndex === -1) {
179
+ break;
180
+ }
181
+ parsedCharacterIndex = nextIndex + 1;
182
+ nextIndex = code.indexOf("\n", parsedCharacterIndex);
183
+ }
184
+ if (parsedLineTokens.length) {
185
+ parsedTokens.push(parsedLineTokens);
186
+ }
187
+ }
188
+
189
+ return parsedTokens;
190
+ }
191
+
192
+ function processSection(
193
+ currentLineState: State,
194
+ parsedTokens: Token[][],
195
+ section: string,
196
+ className: string
197
+ ) {
198
+ // Remove "tok-" prefix;
199
+ const tokenType =
200
+ className === null || className === void 0
201
+ ? null
202
+ : className.substring(4) || null;
203
+
204
+ let index = 0;
205
+ let nextIndex = section.indexOf("\n");
206
+ while (true) {
207
+ const substring =
208
+ nextIndex >= 0
209
+ ? section.substring(index, nextIndex)
210
+ : section.substring(index);
211
+ const token: Token = {
212
+ columnIndex: currentLineState.rawString.length,
213
+ type: tokenType,
214
+ value: substring
215
+ };
216
+ currentLineState.parsedTokens.push(token);
217
+ currentLineState.rawString += substring;
218
+ if (nextIndex === -1) {
219
+ break;
220
+ }
221
+ if (nextIndex >= 0) {
222
+ parsedTokens.push(currentLineState.parsedTokens);
223
+ currentLineState.parsedTokens = [];
224
+ currentLineState.rawString = "";
225
+ }
226
+ index = nextIndex + 1;
227
+ nextIndex = section.indexOf("\n", index);
228
+ }
229
+ }
230
+
231
+ function parsedTokensToHtml(tokens: Token[]) {
232
+ const htmlStrings = tokens.map((token) => {
233
+ const className = token.type ? `tok-${token.type}` : "";
234
+ const escapedValue = escapeHtmlEntities(token.value);
235
+
236
+ return `<span class="${className}">${escapedValue}</span>`;
237
+ });
238
+
239
+ // Edge case to avoid empty line
240
+ let htmlString = htmlStrings.join("");
241
+ if (tokens.length <= 1) {
242
+ if (!tokens[0].value) {
243
+ htmlString = "&nbsp;";
244
+ }
245
+ }
246
+
247
+ return `<div>${htmlString}</div>`;
248
+ }
249
+
250
+ function escapeHtmlEntities(rawString: string) {
251
+ return rawString.replace(
252
+ /[\u00A0-\u9999<>&]/g,
253
+ (substring) => "&#" + substring.charCodeAt(0) + ";"
254
+ );
255
+ }
package/styles.css ADDED
@@ -0,0 +1,127 @@
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+ font-size: 12px;
6
+
7
+ @media only screen and (max-width: 600px) {
8
+ font-size: 16px;
9
+ }
10
+
11
+ color-scheme: dark;
12
+ font-synthesis: none;
13
+ text-rendering: optimizeLegibility;
14
+ -webkit-font-smoothing: antialiased;
15
+ -moz-osx-font-smoothing: grayscale;
16
+ }
17
+
18
+ @keyframes background-gradient-animation {
19
+ 0% {
20
+ background-position: 0% 50%;
21
+ }
22
+ 50% {
23
+ background-position: 100% 50%;
24
+ }
25
+ 100% {
26
+ background-position: 0% 50%;
27
+ }
28
+ }
29
+
30
+ #root {
31
+ width: 100dvw;
32
+ height: 100dvh;
33
+ overflow: auto;
34
+ background: linear-gradient(
35
+ -45deg,
36
+ var(--template-gradient-color-1),
37
+ var(--template-gradient-color-2),
38
+ var(--template-gradient-color-3)
39
+ );
40
+ background-size: 400% 400%;
41
+ animation: background-gradient-animation 20s ease infinite;
42
+ }
43
+
44
+ * {
45
+ scrollbar-width: thin;
46
+ scrollbar-color: var(--color-slate-600) transparent;
47
+ outline: none;
48
+ transition:
49
+ color 0.25s ease,
50
+ background-color 0.25s ease,
51
+ border-color 0.25s ease,
52
+ opacity 0.25s ease,
53
+ outline-color 0.25s ease;
54
+ }
55
+
56
+ main {
57
+ [data-link],
58
+ a {
59
+ cursor: pointer;
60
+ color: var(--template-focus-color-400);
61
+ &:hover {
62
+ color: var(--template-focus-color-300);
63
+ }
64
+ }
65
+ }
66
+
67
+ *[data-focus] {
68
+ border: 2px solid transparent;
69
+ &:focus {
70
+ border: 2px solid var(--template-focus-color-300);
71
+ }
72
+ }
73
+
74
+ *[data-focus-within] {
75
+ border: 2px solid transparent;
76
+ &:focus-within {
77
+ border: 2px solid var(--template-focus-color-300);
78
+ }
79
+
80
+ &[data-focus-within="bold"] {
81
+ border-color: var(--template-focus-color-600);
82
+ &:focus-within {
83
+ border: 2px solid var(--template-focus-color-300);
84
+ }
85
+ }
86
+ }
87
+
88
+ code {
89
+ color: rgba(255, 255, 255, 0.8);
90
+ }
91
+
92
+ .tok-comment {
93
+ color: var(--color-slate-500);
94
+ }
95
+ .tok-definition {
96
+ }
97
+ .tok-local {
98
+ }
99
+ .tok-keyword {
100
+ color: var(--color-pink-400);
101
+ }
102
+ .tok-meta {
103
+ }
104
+ .tok-number {
105
+ }
106
+ .tok-operator {
107
+ color: var(--color-slate-400);
108
+ }
109
+ .tok-propertyName {
110
+ color: var(--color-sky-300);
111
+ }
112
+ .tok-punctuation {
113
+ color: var(--color-slate-400);
114
+ }
115
+ .tok-string {
116
+ color: var(--color-teal-300);
117
+ }
118
+ .tok-string2 {
119
+ color: var(--color-sky-300);
120
+ }
121
+ .tok-typeName {
122
+ color: var(--color-pink-400);
123
+ }
124
+ .tok-variableName:not(.tok-definition) {
125
+ }
126
+ .tok-variableName2 {
127
+ }